forked from mirror/monocles_chat_clean
Initial WebXDC prototype (Cheogram)
This commit is contained in:
parent
3896c221d1
commit
ac0558c5da
13 changed files with 710 additions and 14 deletions
|
@ -170,8 +170,8 @@ android {
|
|||
|
||||
compileOptions {
|
||||
coreLibraryDesugaringEnabled true
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
sourceCompatibility JavaVersion.VERSION_1_9
|
||||
targetCompatibility JavaVersion.VERSION_1_9
|
||||
}
|
||||
|
||||
flavorDimensions("distribution")
|
||||
|
|
14
src/main/java/de/monocles/chat/ConversationPage.java
Normal file
14
src/main/java/de/monocles/chat/ConversationPage.java
Normal file
|
@ -0,0 +1,14 @@
|
|||
package de.monocles.chat;
|
||||
|
||||
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();
|
||||
}
|
321
src/main/java/de/monocles/chat/WebxdcPage.java
Normal file
321
src/main/java/de/monocles/chat/WebxdcPage.java
Normal file
|
@ -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 de.monocles.chat;
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
98
src/main/java/de/monocles/chat/WebxdcUpdate.java
Normal file
98
src/main/java/de/monocles/chat/WebxdcUpdate.java
Normal file
|
@ -0,0 +1,98 @@
|
|||
package de.monocles.chat;
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
|
@ -54,6 +54,13 @@ import com.caverock.androidsvg.SVG;
|
|||
import com.google.android.material.tabs.TabLayout;
|
||||
import com.google.android.material.textfield.TextInputLayout;
|
||||
import com.google.common.base.Optional;
|
||||
|
||||
import de.monocles.chat.ConversationPage;
|
||||
import de.monocles.chat.WebxdcPage;
|
||||
|
||||
import eu.siacs.conversations.utils.Consumer;
|
||||
import io.ipfs.cid.Cid;
|
||||
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
import java.util.stream.Collectors;
|
||||
|
@ -297,6 +304,10 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
|
|||
return messagesLeftOnServer;
|
||||
}
|
||||
|
||||
public boolean switchToSession(final String node) {
|
||||
return pagerAdapter.switchToSession(node);
|
||||
}
|
||||
|
||||
public void setHasMessagesLeftOnServer(boolean value) {
|
||||
this.messagesLeftOnServer = value;
|
||||
}
|
||||
|
@ -1589,6 +1600,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);
|
||||
}
|
||||
|
@ -1642,7 +1661,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;
|
||||
|
||||
|
@ -1691,6 +1710,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);
|
||||
|
@ -1712,11 +1746,26 @@ 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();
|
||||
}
|
||||
|
||||
public boolean switchToSession(final String node) {
|
||||
if (sessions == null) return false;
|
||||
|
||||
int i = 0;
|
||||
for (ConversationPage session : sessions) {
|
||||
if (session.getNode().equals(node)) {
|
||||
if (mPager != null) mPager.setCurrentItem(i + 2);
|
||||
return true;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Object instantiateItem(@NonNull ViewGroup container, int position) {
|
||||
|
@ -1729,10 +1778,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;
|
||||
}
|
||||
|
||||
|
@ -1742,7 +1789,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
|
|||
container.removeView((View) o);
|
||||
return;
|
||||
}
|
||||
container.removeView(((CommandSession) o).getView());
|
||||
container.removeView(((ConversationPage) o).getView());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -1776,8 +1823,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;
|
||||
|
@ -1792,13 +1839,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;
|
||||
|
||||
|
@ -2728,6 +2775,10 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
|
|||
return mTitle;
|
||||
}
|
||||
|
||||
public String getNode() {
|
||||
return mNode;
|
||||
}
|
||||
|
||||
public void updateWithResponse(IqPacket iq) {
|
||||
this.loadingTimer.cancel();
|
||||
this.loadingTimer = new Timer();
|
||||
|
@ -3110,6 +3161,8 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
|
|||
return false;
|
||||
}
|
||||
|
||||
public void refresh() { }
|
||||
|
||||
protected void loading() {
|
||||
View v = getView();
|
||||
loadingTimer.schedule(new TimerTask() {
|
||||
|
@ -3157,7 +3210,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() {
|
||||
|
@ -3206,6 +3259,12 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
|
|||
actionsAdapter.notifyDataSetChanged();
|
||||
}
|
||||
|
||||
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++) {
|
||||
|
|
|
@ -6,6 +6,7 @@ import android.util.Pair;
|
|||
|
||||
|
||||
import de.monocles.chat.BobTransfer;
|
||||
import de.monocles.chat.WebxdcUpdate;
|
||||
import java.io.File;
|
||||
|
||||
import java.net.URISyntaxException;
|
||||
|
@ -617,6 +618,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);
|
||||
|
|
|
@ -15,6 +15,8 @@ import android.util.Log;
|
|||
|
||||
import com.google.common.base.Stopwatch;
|
||||
|
||||
import de.monocles.chat.WebxdcUpdate;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import org.whispersystems.libsignal.IdentityKey;
|
||||
|
@ -301,6 +303,25 @@ public class DatabaseBackend extends SQLiteOpenHelper {
|
|||
}
|
||||
|
||||
|
||||
if(monoclesVersion < 8) {
|
||||
db.execSQL(
|
||||
"CREATE TABLE monocles.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 monocles.webxdc_index ON webxdc_updates (" + Message.CONVERSATION + ", thread)");
|
||||
db.execSQL("PRAGMA monocles.user_version = 8");
|
||||
}
|
||||
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
|
@ -873,6 +894,46 @@ public class DatabaseBackend extends SQLiteOpenHelper {
|
|||
db.execSQL("DELETE FROM monocles.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());
|
||||
|
|
|
@ -88,6 +88,8 @@ import com.google.common.base.Objects;
|
|||
import com.google.common.base.Strings;
|
||||
import com.otaliastudios.transcoder.strategy.DefaultAudioStrategy;
|
||||
|
||||
import de.monocles.chat.WebxdcUpdate;
|
||||
|
||||
import org.conscrypt.Conscrypt;
|
||||
import org.openintents.openpgp.IOpenPgpService2;
|
||||
import org.openintents.openpgp.util.OpenPgpApi;
|
||||
|
@ -612,6 +614,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;
|
||||
}
|
||||
|
|
|
@ -828,6 +828,7 @@ public class ConversationFragment extends XmppFragment
|
|||
if (conversation == null) {
|
||||
return;
|
||||
}
|
||||
if (type == "application/xdc+zip") newSubThread();
|
||||
final Toast prepareFileToast = ToastCompat.makeText(getActivity(), getText(R.string.preparing_file), ToastCompat.LENGTH_SHORT);
|
||||
prepareFileToast.show();
|
||||
activity.delegateUriPermissionsToService(uri);
|
||||
|
@ -2556,6 +2557,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());
|
||||
|
@ -3554,6 +3563,7 @@ public class ConversationFragment extends XmppFragment
|
|||
updateEditablity();
|
||||
}
|
||||
}
|
||||
conversation.refreshSessions();
|
||||
}
|
||||
|
||||
protected void messageSent() {
|
||||
|
|
|
@ -88,6 +88,8 @@ import java.io.IOException;
|
|||
import java.net.URISyntaxException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
import de.monocles.chat.WebxdcUpdate;
|
||||
|
||||
import eu.siacs.conversations.entities.Contact;
|
||||
import eu.siacs.conversations.entities.Message;
|
||||
import eu.siacs.conversations.entities.Roster;
|
||||
|
@ -837,6 +839,26 @@ 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.download_button.setVisibility(View.VISIBLE);
|
||||
|
@ -1472,6 +1494,8 @@ public class MessageAdapter extends ArrayAdapter<Message> {
|
|||
displayMediaPreviewMessage(viewHolder, message, darkBackground, type);
|
||||
} else if (message.getFileParams().runtime > 0 && (message.getFileParams().width == 0 && message.getFileParams().height == 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);
|
||||
}
|
||||
|
|
|
@ -234,6 +234,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");
|
||||
|
@ -338,6 +339,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");
|
||||
|
|
28
src/main/res/layout/webxdc_page.xml
Normal file
28
src/main/res/layout/webxdc_page.xml
Normal file
|
@ -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/primary_monocles"
|
||||
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>
|
40
src/main/res/raw/webxdc.js
Normal file
40
src/main/res/raw/webxdc.js
Normal file
|
@ -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);
|
||||
},
|
||||
};
|
||||
})();
|
Loading…
Reference in a new issue