Implement experimenal WebXDC "realtime" API

Stephen Paul Weber created

Change summary

src/cheogram/java/com/cheogram/android/WebxdcPage.java          | 33 +++
src/cheogram/res/raw/webxdc.js                                  | 20 +
src/main/java/eu/siacs/conversations/entities/Conversation.java | 16 +
src/main/java/eu/siacs/conversations/parser/MessageParser.java  | 28 +
4 files changed, 87 insertions(+), 10 deletions(-)

Detailed changes

src/cheogram/java/com/cheogram/android/WebxdcPage.java 🔗

@@ -18,6 +18,7 @@ import android.view.LayoutInflater;
 import android.view.Gravity;
 import android.view.View;
 import android.view.ViewGroup;
+import android.util.Base64;
 import android.webkit.JavascriptInterface;
 import android.webkit.ValueCallback;
 import android.webkit.WebChromeClient;
@@ -149,6 +150,13 @@ public class WebxdcPage implements ConversationPage {
 		return "webxdc\0" + source.getUuid();
 	}
 
+	public boolean threadMatches(final Element thread) {
+		if (thread == null) return false;
+		if (thread.getContent() == null) return false;
+		if (source.getThread() == null) return false;
+		return thread.getContent().equals(source.getThread().getContent());
+	}
+
 	public boolean openUri(Uri uri) {
 		Intent intent = new Intent(Intent.ACTION_VIEW, uri);
 		intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
@@ -324,6 +332,7 @@ public class WebxdcPage implements ConversationPage {
 				}
 				ShortcutManagerCompat.requestPinShortcut(xmppConnectionService, builder.build(), null);
 			} else {
+				binding.webview.loadUrl("about:blank");
 				remover.accept(WebxdcPage.this);
 			}
 		});
@@ -340,6 +349,10 @@ public class WebxdcPage implements ConversationPage {
 		binding.webview.post(() -> binding.webview.loadUrl("javascript:__webxdcUpdate();"));
 	}
 
+	public void realtimeData(String base64) {
+		binding.webview.post(() -> binding.webview.loadUrl("javascript:__webxdcRealtimeData('" + base64.replace("'", "").replace("\\", "").replace("+", "%2B") + "');"));
+	}
+
 	protected Jid selfJid() {
 		final Conversation conversation = (Conversation) source.getConversation();
 		if (conversation.getMode() == Conversation.MODE_MULTI && !conversation.getMucOptions().nonanonymous()) {
@@ -352,6 +365,11 @@ public class WebxdcPage implements ConversationPage {
 	protected class InternalJSApi {
 		@JavascriptInterface
 		public String selfAddr() {
+			final Conversation conversation = (Conversation) source.getConversation();
+			if (conversation.getMode() == Conversation.MODE_MULTI && !conversation.getMucOptions().nonanonymous()) {
+				final var occupantId = conversation.getMucOptions().getSelf().getOccupantId();
+				if (occupantId != null) return occupantId;
+			}
 			return "xmpp:" + Uri.encode(selfJid().toEscapedString(), "@/+");
 		}
 
@@ -461,5 +479,20 @@ public class WebxdcPage implements ConversationPage {
 				return e.toString();
 			}
 		}
+
+		@JavascriptInterface
+		public void sendRealtime(byte[] data) {
+			Message message = new Message(source.getConversation(), null, Message.ENCRYPTION_NONE);
+			message.addPayload(new Element("no-store", "urn:xmpp:hints"));
+			Element webxdc = new Element("x", "urn:xmpp:webxdc:0");
+			message.addPayload(webxdc);
+			webxdc.addChild("data").setContent(Base64.encodeToString(data, Base64.NO_WRAP));
+			message.setThread(source.getThread());
+			if (source.isPrivateMessage()) {
+				Message.configurePrivateMessage(message, source.getCounterpart());
+			}
+			message.setBody((String) null);
+			xmppConnectionService.sendMessage(message);
+		}
 	}
 }

src/cheogram/res/raw/webxdc.js 🔗

@@ -5,6 +5,7 @@ window.webxdc = (() => {
 	let setUpdateListenerPromise = null
 	var update_listener = () => {};
 	var last_serial = 0;
+	var realtime_listener = (data) => {};
 
 	window.__webxdcUpdate = () => {
 		var updates = JSON.parse(InternalJSApi.getStatusUpdates(last_serial));
@@ -18,6 +19,10 @@ window.webxdc = (() => {
 		}
 	};
 
+	window.__webxdcRealtimeData = (data) => {
+		realtime_listener(Uint8Array.from(atob(data), c => c.charCodeAt(0)));
+	};
+
 	return {
 		selfAddr: InternalJSApi.selfAddr(),
 
@@ -113,5 +118,20 @@ window.webxdc = (() => {
 			}
 		},
 
+		joinRealtimeChannel: () => {
+			return {
+				leave: () => {},
+				send: (data) => {
+					if (!(data instanceof Uint8Array)) {
+						throw new Error('realtime listener data must be a Uint8Array')
+					}
+					InternalJSApi.sendRealtime(data);
+				},
+				setListener: (listener) => {
+					realtime_listener = listener;
+				}
+			};
+		},
+
 	};
 })();

src/main/java/eu/siacs/conversations/entities/Conversation.java 🔗

@@ -1440,6 +1440,10 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
         pagerAdapter.startWebxdc(page);
     }
 
+    public void webxdcRealtimeData(final Element thread, final String base64) {
+        pagerAdapter.webxdcRealtimeData(thread, base64);
+    }
+
     public void startCommand(Element command, XmppConnectionService xmppConnectionService) {
         pagerAdapter.startCommand(command, xmppConnectionService);
     }
@@ -1570,6 +1574,18 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
             }
         }
 
+        public void webxdcRealtimeData(final Element thread, final String base64) {
+            if (sessions == null) return;
+
+            for (ConversationPage session : sessions) {
+                if (session instanceof WebxdcPage) {
+                    if (((WebxdcPage) session).threadMatches(thread)) {
+                        ((WebxdcPage) session).realtimeData(base64);
+                    }
+                }
+            }
+        }
+
         public void startWebxdc(WebxdcPage page) {
             show();
             sessions.add(page);

src/main/java/eu/siacs/conversations/parser/MessageParser.java 🔗

@@ -583,16 +583,24 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
                     webxdcSender = counterpart;
                 }
             }
-            mXmppConnectionService.insertWebxdcUpdate(new WebxdcUpdate(
-                conversation,
-                remoteMsgId,
-                counterpart,
-                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")
-            ));
+            final var document = webxdc.findChildContent("document", "urn:xmpp:webxdc:0");
+            final var summary = webxdc.findChildContent("summary", "urn:xmpp:webxdc:0");
+            final var payload = webxdc.findChildContent("json", "urn:xmpp:json:0");
+            if (document != null || summary != null || payload != null) {
+                mXmppConnectionService.insertWebxdcUpdate(new WebxdcUpdate(
+                    conversation,
+                    remoteMsgId,
+                    counterpart,
+                    thread,
+                    body == null ? null : body.content,
+                    document,
+                    summary,
+                    payload
+                ));
+            }
+
+            final var realtime = webxdc.findChildContent("data", "urn:xmpp:webxdc:0");
+            if (realtime != null) conversation.webxdcRealtimeData(thread, realtime);
 
             mXmppConnectionService.updateConversationUi();
         }