Implement experimental WebXDC "realtime" API

This commit is contained in:
Stephen Paul Weber 2024-08-14 18:21:47 +02:00 committed by Arne
parent 64b969a4bf
commit 359767212d
4 changed files with 91 additions and 14 deletions

View file

@ -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;
@ -37,6 +38,7 @@ import androidx.core.graphics.drawable.IconCompat;
import androidx.core.util.Consumer;
import androidx.databinding.DataBindingUtil;
import com.google.android.material.color.MaterialColors;
import com.google.common.io.ByteStreams;
import io.ipfs.cid.Cid;
@ -148,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);
@ -301,7 +310,7 @@ public class WebxdcPage implements ConversationPage {
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)));
tv.setBackgroundColor(MaterialColors.harmonizeWithPrimary(activity.get(),UIHelper.getColorForName(getItem(position))));
return v;
}
});
@ -323,6 +332,7 @@ public class WebxdcPage implements ConversationPage {
}
ShortcutManagerCompat.requestPinShortcut(xmppConnectionService, builder.build(), null);
} else {
binding.webview.loadUrl("about:blank");
remover.accept(WebxdcPage.this);
}
});
@ -336,9 +346,11 @@ public class WebxdcPage implements ConversationPage {
}
public void refresh() {
if (binding != null && binding.webview != null) {
binding.webview.post(() -> binding.webview.loadUrl("javascript:__webxdcUpdate();"));
}
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() {
@ -353,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(), "@/+");
}
@ -462,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);
}
}
}

View file

@ -1702,6 +1702,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);
}
@ -1835,6 +1839,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);

View file

@ -682,16 +682,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();
}

View file

@ -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(),
@ -112,5 +117,21 @@ window.webxdc = (() => {
return Promise.reject(errorMsg);
}
},
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;
}
};
},
};
})();