Support for XEP-0428 v0.2.0

Stephen Paul Weber created

Store `fallback` elements in payloads.  If there is a `fallback` for something
we support (currently extended addressing and oob) then use the data provided to
snip the relevant portion out of the body.

Generate `fallback` element for oob we send, since we do send a fallback body.

Change summary

src/main/java/eu/siacs/conversations/entities/Message.java           | 44 
src/main/java/eu/siacs/conversations/generator/MessageGenerator.java |  4 
src/main/java/eu/siacs/conversations/parser/MessageParser.java       |  3 
3 files changed, 47 insertions(+), 4 deletions(-)

Detailed changes

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

@@ -9,6 +9,7 @@ import android.text.Html;
 import android.text.SpannableStringBuilder;
 import android.util.Base64;
 import android.util.Log;
+import android.util.Pair;
 
 import com.cheogram.android.BobTransfer;
 import com.cheogram.android.GetThumbnailForCid;
@@ -409,10 +410,29 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
     }
 
     public String getBody() {
-        if (getOob() != null) {
-            return body.replace(getOob().toString(), "");
+        StringBuilder body = new StringBuilder(this.body);
+
+        List<Element> fallbacks = getFallbacks();
+        List<Pair<Integer, Integer>> spans = new ArrayList<>();
+        for (Element fallback : fallbacks) {
+            for (Element span : fallback.getChildren()) {
+                if (!span.getName().equals("body") && !span.getNamespace().equals("urn:xmpp:fallback:0")) continue;
+                if (span.getAttribute("start") == null || span.getAttribute("end") == null) return "";
+                spans.add(new Pair(parseInt(span.getAttribute("start")), parseInt(span.getAttribute("end"))));
+            }
+        }
+        // Do them in reverse order so that span deletions don't affect the indexes of other spans
+        spans.sort((x, y) -> y.first.compareTo(x.first));
+        try {
+            for (Pair<Integer, Integer> span : spans) {
+                body.delete(span.first, span.second);
+            }
+        } catch (final StringIndexOutOfBoundsException e) { spans.clear(); }
+
+        if (spans.isEmpty() && getOob() != null) {
+            return body.toString().replace(getOob().toString(), "");
         } else {
-            return body;
+            return body.toString();
         }
     }
 
@@ -953,6 +973,24 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
        return new ArrayList<>(this.payloads);
     }
 
+    public List<Element> getFallbacks() {
+        List<Element> fallbacks = new ArrayList<>();
+
+        if (this.payloads == null) return fallbacks;
+
+        for (Element el : this.payloads) {
+            if (el.getName().equals("fallback") && el.getNamespace().equals("urn:xmpp:fallback:0")) {
+                final String fallbackFor = el.getAttribute("for");
+                if (fallbackFor == null) continue;
+                if (fallbackFor.equals("http://jabber.org/protocol/address") || fallbackFor.equals(Namespace.OOB)) {
+                    fallbacks.add(el);
+                }
+            }
+        }
+
+        return fallbacks;
+    }
+
     public Element getHtml() {
         if (this.payloads == null) return null;
 

src/main/java/eu/siacs/conversations/generator/MessageGenerator.java 🔗

@@ -107,6 +107,8 @@ public class MessageGenerator extends AbstractGenerator {
             final Message.FileParams fileParams = message.getFileParams();
             content = fileParams.url;
             packet.addChild("x", Namespace.OOB).addChild("url").setContent(content);
+            packet.addChild("fallback", "urn:xmpp:fallback:0").setAttribute("for", Namespace.OOB)
+                  .addChild("body", "urn:xmpp:fallback:0");
         } else {
             content = message.getBody();
         }
@@ -121,6 +123,8 @@ public class MessageGenerator extends AbstractGenerator {
             final String url = fileParams.url;
             packet.setBody(url);
             packet.addChild("x", Namespace.OOB).addChild("url").setContent(url);
+            packet.addChild("fallback", "urn:xmpp:fallback:0").setAttribute("for", Namespace.OOB)
+                  .addChild("body", "urn:xmpp:fallback:0");
         } else {
             if (Config.supportUnencrypted()) {
                 packet.setBody(PGP_FALLBACK_MESSAGE);

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

@@ -633,7 +633,8 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
             }
             message.markable = packet.hasChild("markable", "urn:xmpp:chat-markers:0");
             for (Element el : packet.getChildren()) {
-                if (el.getName().equals("query") && el.getNamespace().equals("http://jabber.org/protocol/disco#items") && el.getAttribute("node").equals("http://jabber.org/protocol/commands")) {
+                if ((el.getName().equals("query") && el.getNamespace().equals("http://jabber.org/protocol/disco#items") && el.getAttribute("node").equals("http://jabber.org/protocol/commands")) ||
+                    (el.getName().equals("fallback") && el.getNamespace().equals("urn:xmpp:fallback:0"))) {
                     message.addPayload(el);
                 }
                 if (el.getName().equals("thread") && (el.getNamespace() == null || el.getNamespace().equals("jabber:client"))) {