A src/cheogram/java/com/cheogram/android/ConversationPage.java => src/cheogram/java/com/cheogram/android/ConversationPage.java +14 -0
@@ 0,0 1,14 @@
+package com.cheogram.android;
+
+import android.content.Context;
+import android.view.View;
+
+import eu.siacs.conversations.utils.Consumer;
+
+public interface ConversationPage {
+ public String getTitle();
+ public String getNode();
+ public View inflateUi(Context context, Consumer<ConversationPage> remover);
+ public View getView();
+ public void refresh();
+}
A src/cheogram/java/com/cheogram/android/WebxdcPage.java => src/cheogram/java/com/cheogram/android/WebxdcPage.java +321 -0
@@ 0,0 1,321 @@
+// Based on GPLv3 code from deltachat-android
+// https://github.com/deltachat/deltachat-android/blob/master/src/org/thoughtcrime/securesms/WebViewActivity.java
+// https://github.com/deltachat/deltachat-android/blob/master/src/org/thoughtcrime/securesms/WebxdcActivity.java
+package com.cheogram.android;
+
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Build;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.webkit.JavascriptInterface;
+import android.webkit.WebResourceRequest;
+import android.webkit.WebResourceResponse;
+import android.webkit.WebSettings;
+import android.webkit.WebView;
+import android.webkit.WebViewClient;
+import android.widget.TextView;
+
+import androidx.annotation.RequiresApi;
+import androidx.core.content.ContextCompat;
+import androidx.databinding.DataBindingUtil;
+
+import io.ipfs.cid.Cid;
+
+import java.lang.ref.WeakReference;
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+
+import org.json.JSONObject;
+import org.json.JSONException;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.databinding.WebxdcPageBinding;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.utils.Consumer;
+import eu.siacs.conversations.utils.MimeUtils;
+import eu.siacs.conversations.utils.UIHelper;
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xmpp.Jid;
+
+public class WebxdcPage implements ConversationPage {
+ protected XmppConnectionService xmppConnectionService;
+ protected WebxdcPageBinding binding = null;
+ protected ZipFile zip = null;
+ protected String baseUrl;
+ protected Message source;
+
+ public WebxdcPage(Cid cid, Message source, XmppConnectionService xmppConnectionService) {
+ this.xmppConnectionService = xmppConnectionService;
+ this.source = source;
+ File f = xmppConnectionService.getFileForCid(cid);
+ try {
+ if (f != null) zip = new ZipFile(xmppConnectionService.getFileForCid(cid));
+ } catch (final IOException e) {
+ Log.w(Config.LOGTAG, "WebxdcPage: " + e);
+ }
+
+ // ids in the subdomain makes sure, different apps using same files do not share the same cache entry
+ // (WebView may use a global cache shared across objects).
+ // (a random-id would also work, but would need maintenance and does not add benefits as we regard the file-part interceptRequest() only,
+ // also a random-id is not that useful for debugging)
+ baseUrl = "https://" + source.getUuid() + ".localhost";
+ }
+
+ public String getTitle() {
+ return "WebXDC";
+ }
+
+ public String getNode() {
+ return "webxdc\0" + source.getUuid();
+ }
+
+ public boolean openUri(Uri uri) {
+ Intent intent = new Intent(Intent.ACTION_VIEW, uri);
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ xmppConnectionService.startActivity(intent);
+ return true;
+ }
+
+ protected WebResourceResponse interceptRequest(String rawUrl) {
+ Log.i(Config.LOGTAG, "interceptRequest: " + rawUrl);
+ WebResourceResponse res = null;
+ try {
+ if (zip == null) {
+ throw new Exception("no zip found");
+ }
+ if (rawUrl == null) {
+ throw new Exception("no url specified");
+ }
+ String path = Uri.parse(rawUrl).getPath();
+ if (path.equalsIgnoreCase("/webxdc.js")) {
+ InputStream targetStream = xmppConnectionService.getResources().openRawResource(R.raw.webxdc);
+ res = new WebResourceResponse("text/javascript", "UTF-8", targetStream);
+ } else {
+ ZipEntry entry = zip.getEntry(path.substring(1));
+ if (entry == null) {
+ throw new Exception("\"" + path + "\" not found");
+ }
+ String mimeType = MimeUtils.guessFromPath(path);
+ String encoding = mimeType.startsWith("text/") ? "UTF-8" : null;
+ res = new WebResourceResponse(mimeType, encoding, zip.getInputStream(entry));
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ InputStream targetStream = new ByteArrayInputStream(("Webxdc Request Error: " + e.getMessage()).getBytes());
+ res = new WebResourceResponse("text/plain", "UTF-8", targetStream);
+ }
+
+ if (res != null) {
+ Map<String, String> headers = new HashMap<>();
+ headers.put("Content-Security-Policy",
+ "default-src 'self'; "
+ + "style-src 'self' 'unsafe-inline' blob: ; "
+ + "font-src 'self' data: blob: ; "
+ + "script-src 'self' 'unsafe-inline' 'unsafe-eval' blob: ; "
+ + "connect-src 'self' data: blob: ; "
+ + "img-src 'self' data: blob: ; "
+ + "webrtc 'block' ; "
+ );
+ headers.put("X-DNS-Prefetch-Control", "off");
+ res.setResponseHeaders(headers);
+ }
+ return res;
+ }
+
+ public View inflateUi(Context context, Consumer<ConversationPage> remover) {
+ if (binding != null) {
+ binding.webview.loadUrl("javascript:__webxdcUpdate();");
+ return getView();
+ }
+
+ binding = DataBindingUtil.inflate(LayoutInflater.from(context), R.layout.webxdc_page, null, false);
+ binding.webview.setWebViewClient(new WebViewClient() {
+ // `shouldOverrideUrlLoading()` is called when the user clicks a URL,
+ // returning `true` causes the WebView to abort loading the URL,
+ // returning `false` causes the WebView to continue loading the URL as usual.
+ // the method is not called for POST request nor for on-page-links.
+ //
+ // nb: from API 24, `shouldOverrideUrlLoading(String)` is deprecated and
+ // `shouldOverrideUrlLoading(WebResourceRequest)` shall be used.
+ // the new one has the same functionality, and the old one still exist,
+ // so, to support all systems, for now, using the old one seems to be the simplest way.
+ @Override
+ public boolean shouldOverrideUrlLoading(WebView view, String url) {
+ if (url != null) {
+ Uri uri = Uri.parse(url);
+ switch (uri.getScheme()) {
+ case "http":
+ case "https":
+ case "mailto":
+ case "xmpp":
+ return openUri(uri);
+ }
+ }
+ // by returning `true`, we also abort loading other URLs in our WebView;
+ // eg. that might be weird or internal protocols.
+ // if we come over really useful things, we should allow that explicitly.
+ return true;
+ }
+
+ @Override
+ @SuppressWarnings("deprecation")
+ public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
+ WebResourceResponse res = interceptRequest(url);
+ if (res!=null) {
+ return res;
+ }
+ return super.shouldInterceptRequest(view, url);
+ }
+
+ @Override
+ @RequiresApi(21)
+ public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
+ WebResourceResponse res = interceptRequest(request.getUrl().toString());
+ if (res!=null) {
+ return res;
+ }
+ return super.shouldInterceptRequest(view, request);
+ }
+ });
+
+ // disable "safe browsing" as this has privacy issues,
+ // eg. at least false positives are sent to the "Safe Browsing Lookup API".
+ // as all URLs opened in the WebView are local anyway,
+ // "safe browsing" will never be able to report issues, so it can be disabled.
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ binding.webview.getSettings().setSafeBrowsingEnabled(false);
+ }
+
+ WebSettings webSettings = binding.webview.getSettings();
+ webSettings.setJavaScriptEnabled(true);
+ webSettings.setAllowFileAccess(false);
+ webSettings.setBlockNetworkLoads(true);
+ webSettings.setAllowContentAccess(false);
+ webSettings.setGeolocationEnabled(false);
+ webSettings.setAllowFileAccessFromFileURLs(false);
+ webSettings.setAllowUniversalAccessFromFileURLs(false);
+ webSettings.setDatabaseEnabled(true);
+ webSettings.setDomStorageEnabled(true);
+ binding.webview.setNetworkAvailable(false); // this does not block network but sets `window.navigator.isOnline` in js land
+ binding.webview.addJavascriptInterface(new InternalJSApi(), "InternalJSApi");
+
+ binding.webview.loadUrl(baseUrl + "/index.html");
+
+ binding.actions.setAdapter(new ArrayAdapter<String>(context, R.layout.simple_list_item, new String[]{"Close"}) {
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ View v = super.getView(position, convertView, parent);
+ TextView tv = (TextView) v.findViewById(android.R.id.text1);
+ tv.setGravity(Gravity.CENTER);
+ tv.setTextColor(ContextCompat.getColor(context, R.color.white));
+ tv.setBackgroundColor(UIHelper.getColorForName(getItem(position)));
+ return v;
+ }
+ });
+ binding.actions.setOnItemClickListener((parent, v, pos, id) -> {
+ remover.accept(WebxdcPage.this);
+ });
+
+ return getView();
+ }
+
+ public View getView() {
+ if (binding == null) return null;
+ return binding.getRoot();
+ }
+
+ public void refresh() {
+ binding.webview.post(() -> binding.webview.loadUrl("javascript:__webxdcUpdate();"));
+ }
+
+ protected Jid selfJid() {
+ Conversation conversation = (Conversation) source.getConversation();
+ if (conversation.getMode() == Conversation.MODE_MULTI && !conversation.getMucOptions().nonanonymous()) {
+ return conversation.getMucOptions().getSelf().getFullJid();
+ } else {
+ return source.getConversation().getAccount().getJid().asBareJid();
+ }
+ }
+
+ protected class InternalJSApi {
+ @JavascriptInterface
+ public String selfAddr() {
+ return "xmpp:" + Uri.encode(selfJid().toEscapedString(), "@/+");
+ }
+
+ @JavascriptInterface
+ public String selfName() {
+ return source.getConversation().getAccount().getDisplayName();
+ }
+
+ @JavascriptInterface
+ public boolean sendStatusUpdate(String paramS, String descr) {
+ JSONObject params = new JSONObject();
+ try {
+ params = new JSONObject(paramS);
+ } catch (final JSONException e) {
+ Log.w(Config.LOGTAG, "WebxdcPage sendStatusUpdate invalid JSON: " + e);
+ }
+ String payload = null;
+ Message message = new Message(source.getConversation(), descr, source.getEncryption());
+ message.addPayload(new Element("store", "urn:xmpp:hints"));
+ Element webxdc = new Element("x", "urn:xmpp:webxdc:0");
+ message.addPayload(webxdc);
+ if (params.has("payload")) {
+ payload = JSONObject.wrap(params.opt("payload")).toString();
+ webxdc.addChild("json", "urn:xmpp:json:0").setContent(payload);
+ }
+ if (params.has("document")) {
+ webxdc.addChild("document").setContent(params.optString("document", null));
+ }
+ if (params.has("summary")) {
+ webxdc.addChild("summary").setContent(params.optString("summary", null));
+ }
+ message.setBody(params.optString("info", null));
+ message.setThread(source.getThread());
+ if (source.isPrivateMessage()) {
+ Message.configurePrivateMessage(message, source.getCounterpart());
+ }
+ xmppConnectionService.sendMessage(message);
+ xmppConnectionService.insertWebxdcUpdate(new WebxdcUpdate(
+ (Conversation) message.getConversation(),
+ selfJid(),
+ message.getThread(),
+ params.optString("info", null),
+ params.optString("document", null),
+ params.optString("summary", null),
+ payload
+ ));
+ binding.webview.post(() -> binding.webview.loadUrl("javascript:__webxdcUpdate();"));
+ return true;
+ }
+
+ @JavascriptInterface
+ public String getStatusUpdates(long lastKnownSerial) {
+ StringBuilder builder = new StringBuilder("[");
+ String sep = "";
+ for (WebxdcUpdate update : xmppConnectionService.findWebxdcUpdates(source, lastKnownSerial)) {
+ builder.append(sep);
+ builder.append(update.toString());
+ sep = ",";
+ }
+ builder.append("]");
+ return builder.toString();
+ }
+ }
+}
A src/cheogram/java/com/cheogram/android/WebxdcUpdate.java => src/cheogram/java/com/cheogram/android/WebxdcUpdate.java +98 -0
@@ 0,0 1,98 @@
+package com.cheogram.android;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+
+import org.json.JSONObject;
+
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xmpp.Jid;
+
+public class WebxdcUpdate {
+ protected final Long serial;
+ protected final Long maxSerial;
+ protected final String conversationId;
+ protected final Jid sender;
+ protected final String thread;
+ protected final String threadParent;
+ protected final String info;
+ protected final String document;
+ protected final String summary;
+ protected final String payload;
+
+ public WebxdcUpdate(final Conversation conversation, final Jid sender, final Element thread, final String info, final String document, final String summary, final String payload) {
+ this.serial = null;
+ this.maxSerial = null;
+ this.conversationId = conversation.getUuid();
+ this.sender = sender;
+ this.thread = thread.getContent();
+ this.threadParent = thread.getAttribute("parent");
+ this.info = info;
+ this.document = document;
+ this.summary = summary;
+ this.payload = payload;
+ }
+
+ public WebxdcUpdate(final Cursor cursor, long maxSerial) {
+ this.maxSerial = maxSerial;
+ this.serial = cursor.getLong(cursor.getColumnIndex("serial"));
+ this.conversationId = cursor.getString(cursor.getColumnIndex(Message.CONVERSATION));
+ this.sender = Jid.of(cursor.getString(cursor.getColumnIndex("sender")));
+ this.thread = cursor.getString(cursor.getColumnIndex("thread"));
+ this.threadParent = cursor.getString(cursor.getColumnIndex("threadParent"));
+ this.info = cursor.getString(cursor.getColumnIndex("threadParent"));
+ this.document = cursor.getString(cursor.getColumnIndex("document"));
+ this.summary = cursor.getString(cursor.getColumnIndex("summary"));
+ this.payload = cursor.getString(cursor.getColumnIndex("payload"));
+ }
+
+ public String getSummary() {
+ return summary;
+ }
+
+ public ContentValues getContentValues() {
+ ContentValues cv = new ContentValues();
+ cv.put(Message.CONVERSATION, conversationId);
+ cv.put("sender", sender.toEscapedString());
+ cv.put("thread", thread);
+ cv.put("threadParent", threadParent);
+ if (info != null) cv.put("info", info);
+ if (document != null) cv.put("document", document);
+ if (summary != null) cv.put("summary", summary);
+ if (payload != null) cv.put("payload", payload);
+ return cv;
+ }
+
+ public String toString() {
+ StringBuilder body = new StringBuilder("{\"sender\":");
+ body.append(JSONObject.quote(sender.toEscapedString()));
+ if (serial != null) {
+ body.append(",\"serial\":");
+ body.append(serial.toString());
+ }
+ if (maxSerial != null) {
+ body.append(",\"max_serial\":");
+ body.append(maxSerial.toString());
+ }
+ if (info != null) {
+ body.append(",\"info\":");
+ body.append(JSONObject.quote(info));
+ }
+ if (document != null) {
+ body.append(",\"document\":");
+ body.append(JSONObject.quote(document));
+ }
+ if (summary != null) {
+ body.append(",\"summary\":");
+ body.append(JSONObject.quote(summary));
+ }
+ if (payload != null) {
+ body.append(",\"payload\":");
+ body.append(payload);
+ }
+ body.append("}");
+ return body.toString();
+ }
+}
A src/cheogram/res/layout/webxdc_page.xml => src/cheogram/res/layout/webxdc_page.xml +28 -0
@@ 0,0 1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto">
+ <RelativeLayout
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent">
+
+ <WebView
+ android:id="@+id/webview"
+ android:paddingTop="8dp"
+ android:layout_above="@+id/actions"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
+
+ <GridView
+ android:id="@+id/actions"
+ android:background="@color/perpy"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentStart="true"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentBottom="true"
+ android:horizontalSpacing="0dp"
+ android:verticalSpacing="0dp"
+ android:numColumns="1" />
+
+ </RelativeLayout>
+</layout>
A src/cheogram/res/raw/webxdc.js => src/cheogram/res/raw/webxdc.js +40 -0
@@ 0,0 1,40 @@
+// Based on GPLv3 code from deltachat-android
+// https://github.com/deltachat/deltachat-android/blob/master/res/raw/webxdc.js
+
+window.webxdc = (() => {
+ let setUpdateListenerPromise = null
+ var update_listener = () => {};
+ var last_serial = 0;
+
+ window.__webxdcUpdate = () => {
+ var updates = JSON.parse(InternalJSApi.getStatusUpdates(last_serial));
+ updates.forEach((update) => {
+ update_listener(update);
+ last_serial = update.serial;
+ });
+ if (setUpdateListenerPromise) {
+ setUpdateListenerPromise();
+ setUpdateListenerPromise = null;
+ }
+ };
+
+ return {
+ selfAddr: InternalJSApi.selfAddr(),
+
+ selfName: InternalJSApi.selfName(),
+
+ setUpdateListener: (cb, serial) => {
+ last_serial = typeof serial === "undefined" ? 0 : parseInt(serial);
+ update_listener = cb;
+ var promise = new Promise((res, _rej) => {
+ setUpdateListenerPromise = res;
+ });
+ window.__webxdcUpdate();
+ return promise;
+ },
+
+ sendUpdate: (payload, descr) => {
+ InternalJSApi.sendStatusUpdate(JSON.stringify(payload), descr);
+ },
+ };
+})();
M src/main/java/eu/siacs/conversations/entities/Conversation.java => src/main/java/eu/siacs/conversations/entities/Conversation.java +53 -14
@@ 57,12 57,17 @@ import androidx.viewpager.widget.ViewPager;
import com.caverock.androidsvg.SVG;
+import com.cheogram.android.ConversationPage;
+import com.cheogram.android.WebxdcPage;
+
import com.google.android.material.tabs.TabLayout;
import com.google.android.material.textfield.TextInputLayout;
import com.google.common.base.Optional;
import com.google.common.collect.ComparisonChain;
import com.google.common.collect.Lists;
+import io.ipfs.cid.Cid;
+
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
@@ 107,6 112,7 @@ import eu.siacs.conversations.ui.UriHandlerActivity;
import eu.siacs.conversations.ui.text.FixedURLSpan;
import eu.siacs.conversations.ui.util.ShareUtil;
import eu.siacs.conversations.ui.util.SoftKeyboardUtils;
+import eu.siacs.conversations.utils.Consumer;
import eu.siacs.conversations.utils.JidHelper;
import eu.siacs.conversations.utils.MessageUtils;
import eu.siacs.conversations.utils.UIHelper;
@@ 1299,6 1305,14 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
return 1;
}
+ public void refreshSessions() {
+ pagerAdapter.refreshSessions();
+ }
+
+ public void startWebxdc(Cid cid, Message message, XmppConnectionService xmppConnectionService) {
+ pagerAdapter.startWebxdc(cid, message, xmppConnectionService);
+ }
+
public void startCommand(Element command, XmppConnectionService xmppConnectionService) {
pagerAdapter.startCommand(command, xmppConnectionService);
}
@@ 1344,7 1358,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
public class ConversationPagerAdapter extends PagerAdapter {
protected ViewPager mPager = null;
protected TabLayout mTabs = null;
- ArrayList<CommandSession> sessions = null;
+ ArrayList<ConversationPage> sessions = null;
protected View page1 = null;
protected View page2 = null;
protected boolean mOnboarding = false;
@@ 1391,6 1405,21 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
notifyDataSetChanged();
}
+ public void refreshSessions() {
+ if (sessions == null) return;
+
+ for (ConversationPage session : sessions) {
+ session.refresh();
+ }
+ }
+
+ public void startWebxdc(Cid cid, Message message, XmppConnectionService xmppConnectionService) {
+ show();
+ sessions.add(new WebxdcPage(cid, message, xmppConnectionService));
+ notifyDataSetChanged();
+ if (mPager != null) mPager.setCurrentItem(getCount() - 1);
+ }
+
public void startCommand(Element command, XmppConnectionService xmppConnectionService) {
show();
CommandSession session = new CommandSession(command.getAttribute("name"), command.getAttribute("node"), xmppConnectionService);
@@ 1432,7 1461,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
if (mPager != null) mPager.setCurrentItem(getCount() - 1);
}
- public void removeSession(CommandSession session) {
+ public void removeSession(ConversationPage session) {
sessions.remove(session);
notifyDataSetChanged();
}
@@ 1441,8 1470,8 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
if (sessions == null) return false;
int i = 0;
- for (CommandSession session : sessions) {
- if (session.mNode.equals(node)) {
+ for (ConversationPage session : sessions) {
+ if (session.getNode().equals(node)) {
if (mPager != null) mPager.setCurrentItem(i + 2);
return true;
}
@@ 1464,10 1493,8 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
return page2;
}
- CommandSession session = sessions.get(position-2);
- CommandPageBinding binding = DataBindingUtil.inflate(LayoutInflater.from(container.getContext()), R.layout.command_page, container, false);
- container.addView(binding.getRoot());
- session.setBinding(binding);
+ ConversationPage session = sessions.get(position-2);
+ container.addView(session.inflateUi(container.getContext(), (s) -> removeSession(s)));
return session;
}
@@ 1478,7 1505,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
return;
}
- container.removeView(((CommandSession) o).getView());
+ container.removeView(((ConversationPage) o).getView());
}
@Override
@@ 1512,8 1539,8 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
public boolean isViewFromObject(@NonNull View view, @NonNull Object o) {
if (view == o) return true;
- if (o instanceof CommandSession) {
- return ((CommandSession) o).getView() == view;
+ if (o instanceof ConversationPage) {
+ return ((ConversationPage) o).getView() == view;
}
return false;
@@ 1528,13 1555,13 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
case 1:
return "Commands";
default:
- CommandSession session = sessions.get(position-2);
+ ConversationPage session = sessions.get(position-2);
if (session == null) return super.getPageTitle(position);
return session.getTitle();
}
}
- class CommandSession extends RecyclerView.Adapter<CommandSession.ViewHolder> {
+ class CommandSession extends RecyclerView.Adapter<CommandSession.ViewHolder> implements ConversationPage {
abstract class ViewHolder<T extends ViewDataBinding> extends RecyclerView.ViewHolder {
protected T binding;
@@ 2434,6 2461,10 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
return mTitle;
}
+ public String getNode() {
+ return mNode;
+ }
+
public void updateWithResponse(final IqPacket iq) {
if (getView() != null && getView().isAttachedToWindow()) {
getView().post(() -> updateWithResponseUiThread(iq));
@@ 2853,6 2884,8 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
return false;
}
+ public void refresh() { }
+
protected void loading() {
View v = getView();
loadingTimer.schedule(new TimerTask() {
@@ 2898,7 2931,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
return layoutManager;
}
- public void setBinding(CommandPageBinding b) {
+ protected void setBinding(CommandPageBinding b) {
mBinding = b;
// https://stackoverflow.com/a/32350474/8611
mBinding.form.addOnItemTouchListener(new RecyclerView.OnItemTouchListener() {
@@ 2951,6 2984,12 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
}
}
+ public View inflateUi(Context context, Consumer<ConversationPage> remover) {
+ CommandPageBinding binding = DataBindingUtil.inflate(LayoutInflater.from(context), R.layout.command_page, null, false);
+ setBinding(binding);
+ return binding.getRoot();
+ }
+
// https://stackoverflow.com/a/36037991/8611
private View findViewAt(ViewGroup viewGroup, float x, float y) {
for(int i = 0; i < viewGroup.getChildCount(); i++) {
M src/main/java/eu/siacs/conversations/parser/MessageParser.java => src/main/java/eu/siacs/conversations/parser/MessageParser.java +25 -0
@@ 4,6 4,7 @@ import android.util.Log;
import android.util.Pair;
import com.cheogram.android.BobTransfer;
+import com.cheogram.android.WebxdcUpdate;
import java.io.File;
import java.net.URISyntaxException;
@@ 521,6 522,30 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
}
}
+ final Element webxdc = packet.findChild("x", "urn:xmpp:webxdc:0");
+ if (webxdc != null) {
+ final Conversation conversation = mXmppConnectionService.find(account, counterpart.asBareJid());
+ Jid webxdcSender = counterpart.asBareJid();
+ if (conversation.getMode() == Conversation.MODE_MULTI) {
+ if(conversation.getMucOptions().nonanonymous()) {
+ webxdcSender = conversation.getMucOptions().getTrueCounterpart(counterpart);
+ } else {
+ webxdcSender = counterpart;
+ }
+ }
+ mXmppConnectionService.insertWebxdcUpdate(new WebxdcUpdate(
+ conversation,
+ counterpart,
+ packet.findChild("thread"),
+ body == null ? null : body.content,
+ webxdc.findChildContent("document", "urn:xmpp:webxdc:0"),
+ webxdc.findChildContent("summary", "urn:xmpp:webxdc:0"),
+ webxdc.findChildContent("json", "urn:xmpp:json:0")
+ ));
+
+ mXmppConnectionService.updateConversationUi();
+ }
+
if ((body != null || pgpEncrypted != null || (axolotlEncrypted != null && axolotlEncrypted.hasChild("payload")) || !attachments.isEmpty() || html != null) && !isMucStatusMessage) {
final boolean conversationIsProbablyMuc = isTypeGroupChat || mucUserElement != null || account.getXmppConnection().getMucServersWithholdAccount().contains(counterpart.getDomain().toEscapedString());
final Conversation conversation = mXmppConnectionService.findOrCreateConversation(account, counterpart.asBareJid(), conversationIsProbablyMuc, false, query, false);
M src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java => src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java +60 -0
@@ 11,6 11,8 @@ import android.os.SystemClock;
import android.util.Base64;
import android.util.Log;
+import com.cheogram.android.WebxdcUpdate;
+
import com.google.common.base.Stopwatch;
import org.json.JSONException;
@@ 291,6 293,24 @@ public class DatabaseBackend extends SQLiteOpenHelper {
db.execSQL("PRAGMA cheogram.user_version = 7");
}
+ if(cheogramVersion < 8) {
+ db.execSQL(
+ "CREATE TABLE cheogram.webxdc_updates (" +
+ "serial INTEGER PRIMARY KEY AUTOINCREMENT, " +
+ Message.CONVERSATION + " TEXT NOT NULL, " +
+ "sender TEXT NOT NULL, " +
+ "thread TEXT NOT NULL, " +
+ "threadParent TEXT, " +
+ "info TEXT, " +
+ "document TEXT, " +
+ "summary TEXT, " +
+ "payload TEXT" +
+ ")"
+ );
+ db.execSQL("CREATE INDEX cheogram.webxdc_index ON webxdc_updates (" + Message.CONVERSATION + ", thread)");
+ db.execSQL("PRAGMA cheogram.user_version = 8");
+ }
+
db.setTransactionSuccessful();
} finally {
db.endTransaction();
@@ 832,6 852,46 @@ public class DatabaseBackend extends SQLiteOpenHelper {
db.execSQL("DELETE FROM cheogram.blocked_media");
}
+ public void insertWebxdcUpdate(final WebxdcUpdate update) {
+ SQLiteDatabase db = this.getWritableDatabase();
+ db.insert("cheogram.webxdc_updates", null, update.getContentValues());
+ }
+
+ public WebxdcUpdate findLastWebxdcUpdate(Message message) {
+ SQLiteDatabase db = this.getReadableDatabase();
+ String[] selectionArgs = {message.getConversation().getUuid(), message.getThread().getContent()};
+ Cursor cursor = db.query("cheogram.webxdc_updates", null,
+ Message.CONVERSATION + "=? AND thread=?",
+ selectionArgs, null, null, null);
+ WebxdcUpdate update = null;
+ if (cursor.moveToLast()) {
+ update = new WebxdcUpdate(cursor, cursor.getLong(cursor.getColumnIndex("serial")));
+ }
+ cursor.close();
+ return update;
+ }
+
+ public List<WebxdcUpdate> findWebxdcUpdates(Message message, long serial) {
+ SQLiteDatabase db = this.getReadableDatabase();
+ String[] selectionArgs = {message.getConversation().getUuid(), message.getThread().getContent(), String.valueOf(serial)};
+ Cursor cursor = db.query("cheogram.webxdc_updates", null,
+ Message.CONVERSATION + "=? AND thread=? AND serial>?",
+ selectionArgs, null, null, null);
+ long maxSerial = 0;
+ if (cursor.moveToLast()) {
+ maxSerial = cursor.getLong(cursor.getColumnIndex("serial"));
+ }
+ cursor.moveToFirst();
+ cursor.moveToPrevious();
+
+ List<WebxdcUpdate> updates = new ArrayList<>();
+ while (cursor.moveToNext()) {
+ updates.add(new WebxdcUpdate(cursor, maxSerial));
+ }
+ cursor.close();
+ return updates;
+ }
+
public void createConversation(Conversation conversation) {
SQLiteDatabase db = this.getWritableDatabase();
db.insert(Conversation.TABLENAME, null, conversation.getContentValues());
M src/main/java/eu/siacs/conversations/services/XmppConnectionService.java => src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +14 -0
@@ 54,6 54,8 @@ import androidx.annotation.NonNull;
import androidx.core.app.RemoteInput;
import androidx.core.content.ContextCompat;
+import com.cheogram.android.WebxdcUpdate;
+
import com.google.common.base.Objects;
import com.google.common.base.Optional;
import com.google.common.base.Strings;
@@ 596,6 598,18 @@ public class XmppConnectionService extends Service {
this.databaseBackend.clearBlockedMedia();
}
+ public void insertWebxdcUpdate(final WebxdcUpdate update) {
+ this.databaseBackend.insertWebxdcUpdate(update);
+ }
+
+ public WebxdcUpdate findLastWebxdcUpdate(Message message) {
+ return this.databaseBackend.findLastWebxdcUpdate(message);
+ }
+
+ public List<WebxdcUpdate> findWebxdcUpdates(Message message, long serial) {
+ return this.databaseBackend.findWebxdcUpdates(message, serial);
+ }
+
public AvatarService getAvatarService() {
return this.mAvatarService;
}
M src/main/java/eu/siacs/conversations/ui/ConversationFragment.java => src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +10 -0
@@ 790,6 790,7 @@ public class ConversationFragment extends XmppFragment
if (conversation == null) {
return;
}
+ if (type == "application/xdc+zip") newSubThread();
final Toast prepareFileToast =
Toast.makeText(getActivity(), getText(R.string.preparing_file), Toast.LENGTH_LONG);
prepareFileToast.show();
@@ 2345,6 2346,14 @@ public class ConversationFragment extends XmppFragment
}
}
+ private void newSubThread() {
+ Element oldThread = conversation.getThread();
+ Element thread = new Element("thread", "jabber:client");
+ thread.setContent(UUID.randomUUID().toString());
+ if (oldThread != null) thread.setAttribute("parent", oldThread.getContent());
+ setThread(thread);
+ }
+
private void newThread() {
Element thread = new Element("thread", "jabber:client");
thread.setContent(UUID.randomUUID().toString());
@@ 3259,6 3268,7 @@ public class ConversationFragment extends XmppFragment
updateEditablity();
}
}
+ conversation.refreshSessions();
}
protected void messageSent() {
M src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java => src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java +22 -0
@@ 42,6 42,7 @@ import androidx.core.content.res.ResourcesCompat;
import com.cheogram.android.BobTransfer;
import com.cheogram.android.SwipeDetector;
+import com.cheogram.android.WebxdcUpdate;
import com.google.common.base.Strings;
@@ 673,6 674,25 @@ public class MessageAdapter extends ArrayAdapter<Message> {
viewHolder.download_button.setOnClickListener(v -> ConversationFragment.downloadFile(activity, message));
}
+ private void displayWebxdcMessage(ViewHolder viewHolder, final Message message, final boolean darkBackground, final int type) {
+ displayTextMessage(viewHolder, message, darkBackground, type);
+ viewHolder.image.setVisibility(View.GONE);
+ viewHolder.audioPlayer.setVisibility(View.GONE);
+ viewHolder.download_button.setVisibility(View.VISIBLE);
+ viewHolder.download_button.setText("Open ChatApp");
+ viewHolder.download_button.setOnClickListener(v -> {
+ Conversation conversation = (Conversation) message.getConversation();
+ if (!conversation.switchToSession("webxdc\0" + message.getUuid())) {
+ conversation.startWebxdc(message.getFileParams().getCids().get(0), message, activity.xmppConnectionService);
+ }
+ });
+ WebxdcUpdate lastUpdate = activity.xmppConnectionService.findLastWebxdcUpdate(message);
+ if (lastUpdate != null && lastUpdate.getSummary() != null) {
+ viewHolder.messageBody.setVisibility(View.VISIBLE);
+ viewHolder.messageBody.setText(lastUpdate.getSummary());
+ }
+ }
+
private void displayOpenableMessage(ViewHolder viewHolder, final Message message, final boolean darkBackground, final int type) {
displayTextMessage(viewHolder, message, darkBackground, type);
viewHolder.image.setVisibility(View.GONE);
@@ 982,6 1002,8 @@ public class MessageAdapter extends ArrayAdapter<Message> {
displayMediaPreviewMessage(viewHolder, message, darkBackground, type);
} else if (message.getFileParams().runtime > 0) {
displayAudioMessage(viewHolder, message, darkBackground, type);
+ } else if ("application/xdc+zip".equals(message.getFileParams().getMediaType()) && message.getConversation() instanceof Conversation) {
+ displayWebxdcMessage(viewHolder, message, darkBackground, type);
} else {
displayOpenableMessage(viewHolder, message, darkBackground, type);
}
M src/main/java/eu/siacs/conversations/utils/MimeUtils.java => src/main/java/eu/siacs/conversations/utils/MimeUtils.java +3 -1
@@ 233,6 233,7 @@ public final class MimeUtils {
add("application/x-x509-server-cert", "crt");
add("application/x-xcf", "xcf");
add("application/x-xfig", "fig");
+ add("application/xdc+zip", "xdc");
add("application/xhtml+xml", "xhtml");
add("video/3gpp", "3gpp");
add("video/3gpp", "3gp");
@@ 337,6 338,7 @@ public final class MimeUtils {
add("text/html", "html");
add("text/h323", "323");
add("text/iuls", "uls");
+ add("text/javascript", "js");
add("text/mathml", "mml");
// add ".txt" first so it will be the default for guessExtensionFromMimeType
add("text/plain", "txt");
@@ 589,7 591,7 @@ public final class MimeUtils {
return null;
}
- private static String guessFromPath(final String path) {
+ public static String guessFromPath(final String path) {
final int start = path.lastIndexOf('.') + 1;
if (start < path.length()) {
return MimeUtils.guessMimeTypeFromExtension(path.substring(start));