From 758c85a1c9d85600de84b961d246a784511b1916 Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Sun, 21 Aug 2022 21:40:33 -0500 Subject: [PATCH 1/4] Preserve Text Nodes in XML The XML model that was being used was not able to represent elements with text nodes and element children interspersed (such as in "markup like" XML: abc def ghi). This switches from a single content String to a list of child Node. Content is pulled from all children recursively. A list of Element children is also maintained since this list is frequently traversed so it saves runtime checks in all of those loops at the expense of a small amount of memory. Since the children and childNode lists must be kept in sync, they are both made private to avoid a child class trying to mutate one of them without a safe helper. --- .../eu/siacs/conversations/xml/Element.java | 60 +++++++++++++------ .../conversations/xml/LocalizedContent.java | 2 +- .../java/eu/siacs/conversations/xml/Node.java | 5 ++ .../eu/siacs/conversations/xml/TextNode.java | 20 +++++++ .../eu/siacs/conversations/xml/XmlReader.java | 11 +--- .../siacs/conversations/xmpp/forms/Data.java | 9 +-- .../siacs/conversations/xmpp/forms/Field.java | 17 ++---- .../xmpp/jingle/stanzas/Group.java | 2 +- .../xmpp/jingle/stanzas/Propose.java | 2 +- .../xmpp/jingle/stanzas/RtpDescription.java | 24 ++------ .../xmpp/stanzas/MessagePacket.java | 10 ++-- 11 files changed, 87 insertions(+), 75 deletions(-) create mode 100644 src/main/java/eu/siacs/conversations/xml/Node.java create mode 100644 src/main/java/eu/siacs/conversations/xml/TextNode.java diff --git a/src/main/java/eu/siacs/conversations/xml/Element.java b/src/main/java/eu/siacs/conversations/xml/Element.java index 4d53a17b723f3b179bf8446d82829b7ebc0b3348..d70e45ef6093e330bf8d352eb8cd562ba194c102 100644 --- a/src/main/java/eu/siacs/conversations/xml/Element.java +++ b/src/main/java/eu/siacs/conversations/xml/Element.java @@ -3,19 +3,21 @@ package eu.siacs.conversations.xml; import org.jetbrains.annotations.NotNull; import java.util.ArrayList; +import java.util.Collection; import java.util.Hashtable; import java.util.List; +import java.util.stream.Collectors; import eu.siacs.conversations.utils.XmlHelper; import eu.siacs.conversations.xmpp.InvalidJid; import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.stanzas.MessagePacket; -public class Element { +public class Element implements Node { private final String name; private Hashtable attributes = new Hashtable<>(); - private String content; - protected List children = new ArrayList<>(); + private List children = new ArrayList<>(); + private List childNodes = new ArrayList<>(); public Element(String name) { this.name = name; @@ -26,30 +28,52 @@ public class Element { this.setAttribute("xmlns", xmlns); } - public Element addChild(Element child) { - this.content = null; - children.add(child); + public Node prependChild(Node child) { + childNodes.add(0, child); + if (child instanceof Element) children.add(0, (Element) child); + return child; + } + + public Node addChild(Node child) { + childNodes.add(child); + if (child instanceof Element) children.add((Element) child); return child; } public Element addChild(String name) { - this.content = null; Element child = new Element(name); + childNodes.add(child); children.add(child); return child; } public Element addChild(String name, String xmlns) { - this.content = null; Element child = new Element(name); child.setAttribute("xmlns", xmlns); + childNodes.add(child); children.add(child); return child; } + public void addChildren(final Collection children) { + if (children == null) return; + + this.childNodes.addAll(children); + for (Node node : children) { + if (node instanceof Element) { + this.children.add((Element) node); + } + } + } + + public void removeChild(Node child) { + this.childNodes.remove(child); + if (child instanceof Element) this.children.remove(child); + } + public Element setContent(String content) { - this.content = content; - this.children.clear(); + clearChildren(); + if (content != null) this.childNodes.add(new TextNode(content)); return this; } @@ -106,17 +130,18 @@ public class Element { return findChild(name, xmlns) != null; } - public List getChildren() { + public final List getChildren() { return this.children; } public Element setChildren(List children) { + this.childNodes = new ArrayList(children); this.children = children; return this; } public final String getContent() { - return content; + return this.childNodes.stream().map(Node::getContent).filter(c -> c != null).collect(Collectors.joining()); } public Element setAttribute(String name, String value) { @@ -170,7 +195,7 @@ public class Element { @NotNull public String toString() { final StringBuilder elementOutput = new StringBuilder(); - if ((content == null) && (children.size() == 0)) { + if (childNodes.size() == 0) { Tag emptyTag = Tag.empty(name); emptyTag.setAtttributes(this.attributes); elementOutput.append(emptyTag.toString()); @@ -178,12 +203,8 @@ public class Element { Tag startTag = Tag.start(name); startTag.setAtttributes(this.attributes); elementOutput.append(startTag); - if (content != null) { - elementOutput.append(XmlHelper.encodeEntities(content)); - } else { - for (Element child : children) { - elementOutput.append(child.toString()); - } + for (Node child : childNodes) { + elementOutput.append(child.toString()); } Tag endTag = Tag.end(name); elementOutput.append(endTag); @@ -197,6 +218,7 @@ public class Element { public void clearChildren() { this.children.clear(); + this.childNodes.clear(); } public void setAttribute(String name, long value) { diff --git a/src/main/java/eu/siacs/conversations/xml/LocalizedContent.java b/src/main/java/eu/siacs/conversations/xml/LocalizedContent.java index fac5099a70f8efed7c8c0b73bbefb6ffbf5d4638..6fa71e9625e95891a9c868f94b13fff82b5c781e 100644 --- a/src/main/java/eu/siacs/conversations/xml/LocalizedContent.java +++ b/src/main/java/eu/siacs/conversations/xml/LocalizedContent.java @@ -23,7 +23,7 @@ public class LocalizedContent { public static LocalizedContent get(final Element element, String name) { final HashMap contents = new HashMap<>(); final String parentLanguage = element.getAttribute("xml:lang"); - for(Element child : element.children) { + for(Element child : element.getChildren()) { if (name.equals(child.getName())) { final String namespace = child.getNamespace(); final String childLanguage = child.getAttribute("xml:lang"); diff --git a/src/main/java/eu/siacs/conversations/xml/Node.java b/src/main/java/eu/siacs/conversations/xml/Node.java new file mode 100644 index 0000000000000000000000000000000000000000..dbe18d252ddc981f9b0d6c3e41d6f97fc211c8b0 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xml/Node.java @@ -0,0 +1,5 @@ +package eu.siacs.conversations.xml; + +public interface Node { + public String getContent(); +} diff --git a/src/main/java/eu/siacs/conversations/xml/TextNode.java b/src/main/java/eu/siacs/conversations/xml/TextNode.java new file mode 100644 index 0000000000000000000000000000000000000000..edbc1d312a6e282470d56f2ba279cd054b9ee52c --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xml/TextNode.java @@ -0,0 +1,20 @@ +package eu.siacs.conversations.xml; + +import eu.siacs.conversations.utils.XmlHelper; + +public class TextNode implements Node { + protected String content; + + public TextNode(final String content) { + if (content == null) throw new IllegalArgumentException("null TextNode is not allowed"); + this.content = content; + } + + public String getContent() { + return content; + } + + public String toString() { + return XmlHelper.encodeEntities(content); + } +} diff --git a/src/main/java/eu/siacs/conversations/xml/XmlReader.java b/src/main/java/eu/siacs/conversations/xml/XmlReader.java index 240b92b7ae9ae11ab7cbbdfbca51403f8231d356..c775fd34c67804e00f21dbedbd26109dec3f9773 100644 --- a/src/main/java/eu/siacs/conversations/xml/XmlReader.java +++ b/src/main/java/eu/siacs/conversations/xml/XmlReader.java @@ -94,15 +94,10 @@ public class XmlReader implements Closeable { if (nextTag == null) { throw new IOException("interrupted mid tag"); } - if (nextTag.isNo()) { - element.setContent(nextTag.getName()); - nextTag = this.readTag(); - if (nextTag == null) { - throw new IOException("interrupted mid tag"); - } - } while (!nextTag.isEnd(element.getName())) { - if (!nextTag.isNo()) { + if (nextTag.isNo()) { + element.addChild(new TextNode(nextTag.getName())); + } else { Element child = this.readElement(nextTag); element.addChild(child); } diff --git a/src/main/java/eu/siacs/conversations/xmpp/forms/Data.java b/src/main/java/eu/siacs/conversations/xmpp/forms/Data.java index e3bd9eb7481229914bd0fdc144373036420b7a53..16578967586c3f98a95074274b7950c48d6b8388 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/forms/Data.java +++ b/src/main/java/eu/siacs/conversations/xmpp/forms/Data.java @@ -4,8 +4,8 @@ import android.os.Bundle; import java.util.ArrayList; import java.util.Collection; -import java.util.Iterator; import java.util.List; +import java.util.stream.Collectors; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Namespace; @@ -77,12 +77,7 @@ public class Data extends Element { } private void removeUnnecessaryChildren() { - for(Iterator iterator = this.children.iterator(); iterator.hasNext();) { - Element element = iterator.next(); - if (!element.getName().equals("field") && !element.getName().equals("title")) { - iterator.remove(); - } - } + setChildren(getChildren().stream().filter(element -> element.getName().equals("field") || element.getName().equals("title")).collect(Collectors.toList())); } public static Data parse(Element element) { diff --git a/src/main/java/eu/siacs/conversations/xmpp/forms/Field.java b/src/main/java/eu/siacs/conversations/xmpp/forms/Field.java index d6ac14707b12dfff17b3937a51546790acf2a9e9..6e97b552c70c2070434cf3bfd170343e62052a5c 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/forms/Field.java +++ b/src/main/java/eu/siacs/conversations/xmpp/forms/Field.java @@ -2,8 +2,8 @@ package eu.siacs.conversations.xmpp.forms; import java.util.ArrayList; import java.util.Collection; -import java.util.Iterator; import java.util.List; +import java.util.stream.Collectors; import eu.siacs.conversations.xml.Element; @@ -23,24 +23,15 @@ public class Field extends Element { } public void setValue(String value) { - this.children.clear(); - this.addChild("value").setContent(value); + setChildren(List.of(new Element("value").setContent(value))); } public void setValues(Collection values) { - this.children.clear(); - for(String value : values) { - this.addChild("value").setContent(value); - } + setChildren(values.stream().map(val -> new Element("value").setContent(val)).collect(Collectors.toList())); } public void removeNonValueChildren() { - for(Iterator iterator = this.children.iterator(); iterator.hasNext();) { - Element element = iterator.next(); - if (!element.getName().equals("value")) { - iterator.remove(); - } - } + setChildren(getChildren().stream().filter(element -> element.getName().equals("value")).collect(Collectors.toList())); } public static Field parse(Element element) { diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Group.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Group.java index eb5c32252bf181d5037a02cbe1aaebf7ee8cd4f9..2f7873a860fc2d23dcabc75251578c333fc27b01 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Group.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Group.java @@ -29,7 +29,7 @@ public class Group extends Element { public List getIdentificationTags() { final ImmutableList.Builder builder = new ImmutableList.Builder<>(); - for (final Element child : this.children) { + for (final Element child : getChildren()) { if ("content".equals(child.getName())) { final String name = child.getAttribute("name"); if (name != null) { diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Propose.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Propose.java index da3a93da301dde0d4b0dc19f3f972629d3d7af8e..f72162be8e5896e22184ee41f699c1caafa0e13a 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Propose.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Propose.java @@ -15,7 +15,7 @@ public class Propose extends Element { public List getDescriptions() { final ImmutableList.Builder builder = new ImmutableList.Builder<>(); - for (final Element child : this.children) { + for (final Element child : getChildren()) { if ("description".equals(child.getName())) { final String namespace = child.getNamespace(); if (FileTransferDescription.NAMESPACES.contains(namespace)) { diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/RtpDescription.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/RtpDescription.java index 650c26bef07af1eb1876641c4c4eb6c95cef3784..b1cb59a34d630230ffbcf0dcfd5b20979b971ac2 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/RtpDescription.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/RtpDescription.java @@ -66,7 +66,7 @@ public class RtpDescription extends GenericDescription { public List getSources() { final ImmutableList.Builder builder = new ImmutableList.Builder<>(); - for (final Element child : this.children) { + for (final Element child : getChildren()) { if ("source".equals(child.getName()) && Namespace.JINGLE_RTP_SOURCE_SPECIFIC_MEDIA_ATTRIBUTES.equals(child.getNamespace())) { builder.add(Source.upgrade(child)); } @@ -76,7 +76,7 @@ public class RtpDescription extends GenericDescription { public List getSourceGroups() { final ImmutableList.Builder builder = new ImmutableList.Builder<>(); - for (final Element child : this.children) { + for (final Element child : getChildren()) { if ("ssrc-group".equals(child.getName()) && Namespace.JINGLE_RTP_SOURCE_SPECIFIC_MEDIA_ATTRIBUTES.equals(child.getNamespace())) { builder.add(SourceGroup.upgrade(child)); } @@ -326,16 +326,8 @@ public class RtpDescription extends GenericDescription { return null; } - public void addChildren(final List children) { - if (children != null) { - this.children.addAll(children); - } - } - public void addParameters(List parameters) { - if (parameters != null) { - this.children.addAll(parameters); - } + addChildren(parameters); } } @@ -442,7 +434,7 @@ public class RtpDescription extends GenericDescription { public List getParameters() { ImmutableList.Builder builder = new ImmutableList.Builder<>(); - for (Element child : this.children) { + for (Element child : getChildren()) { if ("parameter".equals(child.getName())) { builder.add(Parameter.upgrade(child)); } @@ -512,7 +504,7 @@ public class RtpDescription extends GenericDescription { public List getSsrcs() { ImmutableList.Builder builder = new ImmutableList.Builder<>(); - for (Element child : this.children) { + for (Element child : getChildren()) { if ("source".equals(child.getName())) { final String ssrc = child.getAttribute("ssrc"); if (ssrc != null) { @@ -610,10 +602,4 @@ public class RtpDescription extends GenericDescription { } return rtpDescription; } - - private void addChildren(List elements) { - if (elements != null) { - this.children.addAll(elements); - } - } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/stanzas/MessagePacket.java b/src/main/java/eu/siacs/conversations/xmpp/stanzas/MessagePacket.java index 86068bf774efe9ed255c1def48812d0286ec8827..bac76adfc7f419c4f5bf10c953d43743772888be 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/stanzas/MessagePacket.java +++ b/src/main/java/eu/siacs/conversations/xmpp/stanzas/MessagePacket.java @@ -22,15 +22,13 @@ public class MessagePacket extends AbstractAcknowledgeableStanza { } public void setBody(String text) { - this.children.remove(findChild("body")); - Element body = new Element("body"); - body.setContent(text); - this.children.add(0, body); + removeChild(findChild("body")); + prependChild(new Element("body").setContent(text)); } public void setAxolotlMessage(Element axolotlMessage) { - this.children.remove(findChild("body")); - this.children.add(0, axolotlMessage); + removeChild(findChild("body")); + prependChild(axolotlMessage); } public void setType(int type) { From 87ddf94b2e884dbbc03a484b36519187d397d6ff Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Mon, 3 Oct 2022 13:50:48 -0500 Subject: [PATCH 2/4] Support for storing and displaying XHTML-IM Only supports images with XEP-0231 compatible cid: URIs, otherwise images render as a fallback image. This commit doesn't yet support fetching the images at all, but can ask a passed-in thumbnailer to figure out getting the image for a certain Cid. --- .../com/cheogram/android/BobTransfer.java | 1 + .../cheogram/android/GetThumbnailForCid.java | 9 +++ .../siacs/conversations/entities/Message.java | 57 ++++++++++++++++++- .../generator/AbstractGenerator.java | 1 + .../conversations/parser/MessageParser.java | 12 +++- 5 files changed, 75 insertions(+), 5 deletions(-) create mode 100644 src/cheogram/java/com/cheogram/android/GetThumbnailForCid.java diff --git a/src/cheogram/java/com/cheogram/android/BobTransfer.java b/src/cheogram/java/com/cheogram/android/BobTransfer.java index 1099b143081ea1f5dfa7c6a58e1c3d5fbe4ed60a..680c2bdfed1b0beab3c0fb4fd886bbd62af2f2a1 100644 --- a/src/cheogram/java/com/cheogram/android/BobTransfer.java +++ b/src/cheogram/java/com/cheogram/android/BobTransfer.java @@ -33,6 +33,7 @@ public class BobTransfer implements Transferable { protected XmppConnectionService xmppConnectionService; public static Cid cid(URI uri) { + if (!uri.getScheme().equals("cid")) return null; String bobCid = uri.getSchemeSpecificPart(); if (!bobCid.contains("@") || !bobCid.contains("+")) return null; String[] cidParts = bobCid.split("@")[0].split("\\+"); diff --git a/src/cheogram/java/com/cheogram/android/GetThumbnailForCid.java b/src/cheogram/java/com/cheogram/android/GetThumbnailForCid.java new file mode 100644 index 0000000000000000000000000000000000000000..17e7d9db6129e4268fdce2c2ca5fb667e57098db --- /dev/null +++ b/src/cheogram/java/com/cheogram/android/GetThumbnailForCid.java @@ -0,0 +1,9 @@ +package com.cheogram.android; + +import android.graphics.drawable.Drawable; + +import io.ipfs.cid.Cid; + +public interface GetThumbnailForCid { + public Drawable getThumbnail(Cid cid); +} diff --git a/src/main/java/eu/siacs/conversations/entities/Message.java b/src/main/java/eu/siacs/conversations/entities/Message.java index db7229afaa579387d9533e73f8c25ad7587fbdca..5d84e743416e1a55d49c98081d1bce3880a1776d 100644 --- a/src/main/java/eu/siacs/conversations/entities/Message.java +++ b/src/main/java/eu/siacs/conversations/entities/Message.java @@ -2,10 +2,15 @@ package eu.siacs.conversations.entities; import android.content.ContentValues; import android.database.Cursor; +import android.graphics.drawable.Drawable; import android.graphics.Color; +import android.text.Html; import android.text.SpannableStringBuilder; import android.util.Log; +import com.cheogram.android.BobTransfer; +import com.cheogram.android.GetThumbnailForCid; + import com.google.common.io.ByteSource; import com.google.common.base.Strings; import com.google.common.collect.ImmutableSet; @@ -25,6 +30,8 @@ import java.util.Set; import java.util.stream.Collectors; import java.util.concurrent.CopyOnWriteArraySet; +import io.ipfs.cid.Cid; + import eu.siacs.conversations.Config; import eu.siacs.conversations.crypto.axolotl.AxolotlService; import eu.siacs.conversations.crypto.axolotl.FingerprintStatus; @@ -762,8 +769,42 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable public static class MergeSeparator { } + public SpannableStringBuilder getSpannableBody(GetThumbnailForCid thumbnailer, Drawable fallbackImg) { + final Element html = getHtml(); + if (html == null) { + return new SpannableStringBuilder(MessageUtils.filterLtrRtl(getBody()).trim()); + } else { + SpannableStringBuilder spannable = new SpannableStringBuilder(Html.fromHtml( + MessageUtils.filterLtrRtl(html.toString()).trim(), + Html.FROM_HTML_MODE_COMPACT, + (source) -> { + try { + if (thumbnailer == null) return fallbackImg; + Cid cid = BobTransfer.cid(new URI(source)); + if (cid == null) return fallbackImg; + Drawable thumbnail = thumbnailer.getThumbnail(cid); + if (thumbnail == null) return fallbackImg; + return thumbnail; + } catch (final URISyntaxException e) { + return fallbackImg; + } + }, + (opening, tag, output, xmlReader) -> {} + )); + + // https://stackoverflow.com/a/10187511/8611 + int i = spannable.length(); + while(--i >= 0 && Character.isWhitespace(spannable.charAt(i))) { } + return (SpannableStringBuilder) spannable.subSequence(0, i+1); + } + } + public SpannableStringBuilder getMergedBody() { - SpannableStringBuilder body = new SpannableStringBuilder(MessageUtils.filterLtrRtl(getBody()).trim()); + return getMergedBody(null, null); + } + + public SpannableStringBuilder getMergedBody(GetThumbnailForCid thumbnailer, Drawable fallbackImg) { + SpannableStringBuilder body = getSpannableBody(thumbnailer, fallbackImg); Message current = this; while (current.mergeable(current.next())) { current = current.next(); @@ -773,7 +814,7 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable body.append("\n\n"); body.setSpan(new MergeSeparator(), body.length() - 2, body.length(), SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE); - body.append(MessageUtils.filterLtrRtl(current.getBody()).trim()); + body.append(current.getSpannableBody(thumbnailer, fallbackImg)); } return body; } @@ -868,6 +909,18 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable this.payloads.add(el); } + public Element getHtml() { + if (this.payloads == null) return null; + + for (Element el : this.payloads) { + if (el.getName().equals("html") && el.getNamespace().equals("http://jabber.org/protocol/xhtml-im")) { + return el.getChildren().get(0); + } + } + + return null; + } + public List getCommands() { if (this.payloads == null) return null; diff --git a/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java b/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java index 706b50043a4b1dc2e39b420c496b7ec1af2c1865..3d1440384903f9b26d6de5e9349f0103d778d2a3 100644 --- a/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java @@ -112,6 +112,7 @@ public abstract class AbstractGenerator { public List getFeatures(Account account) { final XmppConnection connection = account.getXmppConnection(); final ArrayList features = new ArrayList<>(Arrays.asList(FEATURES)); + features.add("http://jabber.org/protocol/xhtml-im"); if (mXmppConnectionService.confirmMessages()) { features.addAll(Arrays.asList(MESSAGE_CONFIRMATION_FEATURES)); } diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java index 0945e31a059233e79892b56963c5d75b922f5574..1613f037283e8a4d21a15fb2dbfc5edcdaa83b88 100644 --- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java +++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java @@ -434,6 +434,11 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece } boolean notify = false; + Element html = original.findChild("html", "http://jabber.org/protocol/xhtml-im"); + if (html != null && html.findChild("body", "http://www.w3.org/1999/xhtml") == null) { + html = null; + } + if (from == null || !InvalidJid.isValid(from) || !InvalidJid.isValid(to)) { Log.e(Config.LOGTAG, "encountered invalid message from='" + from + "' to='" + to + "'"); return; @@ -472,7 +477,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece } } - if ((body != null || pgpEncrypted != null || (axolotlEncrypted != null && axolotlEncrypted.hasChild("payload")) || oobUrl != null) && !isMucStatusMessage) { + if ((body != null || pgpEncrypted != null || (axolotlEncrypted != null && axolotlEncrypted.hasChild("payload")) || oobUrl != null || 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); final boolean conversationMultiMode = conversation.getMode() == Conversation.MODE_MULTI; @@ -577,12 +582,13 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece message.setEncryption(Message.ENCRYPTION_DECRYPTED); } } else { - message = new Message(conversation, body.content, Message.ENCRYPTION_NONE, status); - if (body.count > 1) { + message = new Message(conversation, body == null ? "HTML-only message" : body.content, Message.ENCRYPTION_NONE, status); + if (body != null && body.count > 1) { message.setBodyLanguage(body.language); } } + if (html != null) message.addPayload(html); message.setSubject(original.findChildContent("subject")); message.setCounterpart(counterpart); message.setRemoteMsgId(remoteMsgId); From 4f4a5bb2a925b0f4c9fd62187cf90a4651af7ff7 Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Mon, 3 Oct 2022 13:54:52 -0500 Subject: [PATCH 3/4] Actually display images we already have inline in XHTML-IM If we already have the image for the Cid downloaded, then do create the thumbnail and show it inline. --- .../persistance/DatabaseBackend.java | 7 +-- .../persistance/FileBackend.java | 41 +++++++++------ .../services/XmppConnectionService.java | 3 +- .../ui/adapter/MessageAdapter.java | 50 ++++++++++++++++++- .../siacs/conversations/utils/UIHelper.java | 6 ++- 5 files changed, 86 insertions(+), 21 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java index bf69e87613035244c94d62fdf3c827fa7690d924..4ab3b02d3c0824432fe027eac3848916789ff6a4 100644 --- a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java @@ -48,6 +48,7 @@ import eu.siacs.conversations.crypto.axolotl.SQLiteAxolotlStore; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.DownloadableFile; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.PresenceTemplate; import eu.siacs.conversations.entities.Roster; @@ -733,12 +734,12 @@ public class DatabaseBackend extends SQLiteOpenHelper { cursor.close(); } - public File getFileForCid(Cid cid) { + public DownloadableFile getFileForCid(Cid cid) { SQLiteDatabase db = this.getReadableDatabase(); Cursor cursor = db.query("cheogram.cids", new String[]{"path"}, "cid=?", new String[]{cid.toString()}, null, null, null); - File f = null; + DownloadableFile f = null; if (cursor.moveToNext()) { - f = new File(cursor.getString(0)); + f = new DownloadableFile(cursor.getString(0)); } cursor.close(); return f; diff --git a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java index c2edaa3a15da9b4155e162762c37283e99f8402d..13118252c08e574289c96c414e285bb18c0ccefd 100644 --- a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java @@ -13,6 +13,7 @@ import android.graphics.drawable.Drawable; import android.graphics.ImageDecoder; import android.graphics.Matrix; import android.graphics.Paint; +import android.graphics.Rect; import android.graphics.RectF; import android.graphics.pdf.PdfRenderer; import android.media.MediaMetadataRetriever; @@ -995,16 +996,18 @@ public class FileBackend { } public Drawable getThumbnail(Message message, Resources res, int size, boolean cacheOnly) throws IOException { - final String uuid = message.getUuid(); + return getThumbnail(getFile(message), res, size, cacheOnly); + } + + public Drawable getThumbnail(DownloadableFile file, Resources res, int size, boolean cacheOnly) throws IOException { final LruCache cache = mXmppConnectionService.getDrawableCache(); - Drawable thumbnail = cache.get(uuid); + Drawable thumbnail = cache.get(file.getAbsolutePath()); if ((thumbnail == null) && (!cacheOnly)) { synchronized (THUMBNAIL_LOCK) { - thumbnail = cache.get(uuid); + thumbnail = cache.get(file.getAbsolutePath()); if (thumbnail != null) { return thumbnail; } - DownloadableFile file = getFile(message); final String mime = file.getMimeType(); if ("application/pdf".equals(mime)) { thumbnail = new BitmapDrawable(res, getPdfDocumentPreview(file, size)); @@ -1016,7 +1019,7 @@ public class FileBackend { throw new FileNotFoundException(); } } - cache.put(uuid, thumbnail); + cache.put(file.getAbsolutePath(), thumbnail); } } return thumbnail; @@ -1028,22 +1031,30 @@ public class FileBackend { return drawDrawable(drawable); } + public static Rect rectForSize(int w, int h, int size) { + int scalledW; + int scalledH; + if (w <= h) { + scalledW = Math.max((int) (w / ((double) h / size)), 1); + scalledH = size; + } else { + scalledW = size; + scalledH = Math.max((int) (h / ((double) w / size)), 1); + } + + if (scalledW > w || scalledH > h) return new Rect(0, 0, w, h); + + return new Rect(0, 0, scalledW, scalledH); + } + private Drawable getImagePreview(File file, Resources res, int size, final String mime) throws IOException { if (android.os.Build.VERSION.SDK_INT >= 28) { ImageDecoder.Source source = ImageDecoder.createSource(file); return ImageDecoder.decodeDrawable(source, (decoder, info, src) -> { int w = info.getSize().getWidth(); int h = info.getSize().getHeight(); - int scalledW; - int scalledH; - if (w <= h) { - scalledW = Math.max((int) (w / ((double) h / size)), 1); - scalledH = size; - } else { - scalledW = size; - scalledH = Math.max((int) (h / ((double) w / size)), 1); - } - decoder.setTargetSize(scalledW, scalledH); + Rect r = rectForSize(w, h, size); + decoder.setTargetSize(r.width(), r.height()); }); } else { BitmapFactory.Options options = new BitmapFactory.Options(); diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index f8ca42db270c5916f52bcdd59c56c8a2eec4449d..3f4838eb5d6058beee104a08bdff8df9fbd8b293 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -103,6 +103,7 @@ import eu.siacs.conversations.entities.Bookmark; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Conversational; +import eu.siacs.conversations.entities.DownloadableFile; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.MucOptions; import eu.siacs.conversations.entities.MucOptions.OnRenameListener; @@ -549,7 +550,7 @@ public class XmppConnectionService extends Service { return this.fileBackend; } - public File getFileForCid(Cid cid) { + public DownloadableFile getFileForCid(Cid cid) { return this.databaseBackend.getFileForCid(cid); } diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java index f56c2a8e66c588b914d5cfbf8341be011dc635a6..efb5e9b1373d6854036030c4ae172221e8e4753e 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java @@ -6,8 +6,10 @@ import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.graphics.PorterDuff; +import android.graphics.drawable.Drawable; import android.graphics.Typeface; import android.net.Uri; +import android.os.AsyncTask; import android.preference.PreferenceManager; import android.text.Spannable; import android.text.SpannableString; @@ -32,15 +34,19 @@ import android.widget.Toast; import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; +import androidx.core.content.res.ResourcesCompat; import com.google.common.base.Strings; +import java.io.IOException; import java.net.URI; import java.util.List; import java.util.Locale; import java.util.regex.Matcher; import java.util.regex.Pattern; +import io.ipfs.cid.Cid; + import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.crypto.axolotl.FingerprintStatus; @@ -439,7 +445,25 @@ public class MessageAdapter extends ArrayAdapter { if (message.getBody() != null && !message.getBody().equals("")) { viewHolder.messageBody.setVisibility(View.VISIBLE); final String nick = UIHelper.getMessageDisplayName(message); - SpannableStringBuilder body = message.getMergedBody(); + Drawable fallbackImg = ResourcesCompat.getDrawable(activity.getResources(), activity.getThemeResource(R.attr.ic_attach_photo, R.drawable.ic_attach_photo), null); + fallbackImg.setBounds(FileBackend.rectForSize(fallbackImg.getIntrinsicWidth(), fallbackImg.getIntrinsicHeight(), (int) (metrics.density * 32))); + SpannableStringBuilder body = message.getMergedBody((cid) -> { + try { + DownloadableFile f = activity.xmppConnectionService.getFileForCid(cid); + if (f == null) return null; + + Drawable d = activity.xmppConnectionService.getFileBackend().getThumbnail(f, activity.getResources(), (int) (metrics.density * 288), true); + if (d == null) { + new ThumbnailTask().execute(f); + } else { + d = d.getConstantState().newDrawable(); + d.setBounds(FileBackend.rectForSize(d.getIntrinsicWidth(), d.getIntrinsicHeight(), (int) (metrics.density * 32))); + } + return d; + } catch (final IOException e) { + return fallbackImg; + } + }, fallbackImg); boolean hasMeCommand = message.hasMeCommand(); if (hasMeCommand) { body = body.replace(0, Message.ME_COMMAND.length(), nick + " "); @@ -959,4 +983,28 @@ public class MessageAdapter extends ArrayAdapter { protected TextView encryption; protected ListView commands_list; } + + class ThumbnailTask extends AsyncTask { + @Override + protected Drawable[] doInBackground(DownloadableFile... params) { + if (isCancelled()) return null; + + Drawable[] d = new Drawable[params.length]; + for (int i = 0; i < params.length; i++) { + try { + d[i] = activity.xmppConnectionService.getFileBackend().getThumbnail(params[i], activity.getResources(), (int) (metrics.density * 288), false); + } catch (final IOException e) { + d[i] = null; + } + } + + return d; + } + + @Override + protected void onPostExecute(final Drawable[] d) { + if (isCancelled()) return; + activity.xmppConnectionService.updateConversationUi(); + } + } } diff --git a/src/main/java/eu/siacs/conversations/utils/UIHelper.java b/src/main/java/eu/siacs/conversations/utils/UIHelper.java index 578bb2c89af778fcdce6318832222aedc941f2dc..7692138be38c01d2268ee43e7626cdae00719e86 100644 --- a/src/main/java/eu/siacs/conversations/utils/UIHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/UIHelper.java @@ -1,12 +1,14 @@ package eu.siacs.conversations.utils; import android.content.Context; +import android.graphics.drawable.Drawable; import android.text.SpannableStringBuilder; import android.text.format.DateFormat; import android.text.format.DateUtils; import android.util.Pair; import androidx.annotation.ColorInt; +import androidx.core.content.res.ResourcesCompat; import com.google.common.base.Strings; @@ -319,7 +321,9 @@ public class UIHelper { return new Pair<>(context.getString(R.string.x_file_offered_for_download, getFileDescriptionString(context, message)), true); } else { - SpannableStringBuilder styledBody = new SpannableStringBuilder(body); + Drawable fallbackImg = ResourcesCompat.getDrawable(context.getResources(), R.drawable.ic_attach_photo, null); + fallbackImg.setBounds(0, 0, fallbackImg.getIntrinsicWidth(), fallbackImg.getIntrinsicHeight()); + SpannableStringBuilder styledBody = message.getSpannableBody(null, fallbackImg); if (textColor != 0) { StylingHelper.format(styledBody, 0, styledBody.length() - 1, textColor); } From a8e57ce1c9a9603dbb0f22b8dd6d924016364609 Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Mon, 3 Oct 2022 15:21:48 -0500 Subject: [PATCH 4/4] Fetch XEP-0231 inline images from trusted contacts If a contact is trusted or already likely to know our presence (due to having sent them a message) then auto-fetch XHTML-IM inline images via XEP-0231 from them. --- .../com/cheogram/android/BobTransfer.java | 72 +++++++++++++------ .../siacs/conversations/entities/Contact.java | 4 ++ .../conversations/entities/Conversation.java | 6 ++ .../entities/Conversational.java | 2 + .../entities/StubConversation.java | 6 ++ .../conversations/parser/MessageParser.java | 2 +- .../persistance/FileBackend.java | 19 +++-- .../ui/ConversationFragment.java | 2 +- .../ui/adapter/MessageAdapter.java | 13 +++- .../conversations/utils/CryptoHelper.java | 13 ++++ 10 files changed, 107 insertions(+), 32 deletions(-) diff --git a/src/cheogram/java/com/cheogram/android/BobTransfer.java b/src/cheogram/java/com/cheogram/android/BobTransfer.java index 680c2bdfed1b0beab3c0fb4fd886bbd62af2f2a1..cbe30a302674891129c61620f970d0cb215d2625 100644 --- a/src/cheogram/java/com/cheogram/android/BobTransfer.java +++ b/src/cheogram/java/com/cheogram/android/BobTransfer.java @@ -15,6 +15,7 @@ import io.ipfs.cid.Cid; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; +import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.DownloadableFile; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.Transferable; @@ -24,12 +25,14 @@ import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.utils.CryptoHelper; import eu.siacs.conversations.utils.MimeUtils; import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.stanzas.IqPacket; public class BobTransfer implements Transferable { protected int status = Transferable.STATUS_OFFER; - protected Message message; protected URI uri; + protected Account account; + protected Jid to; protected XmppConnectionService xmppConnectionService; public static Cid cid(URI uri) { @@ -44,10 +47,15 @@ public class BobTransfer implements Transferable { } } - public BobTransfer(Message message, XmppConnectionService xmppConnectionService) throws URISyntaxException { - this.message = message; + public static URI uri(Cid cid) throws NoSuchAlgorithmException, URISyntaxException { + return new URI("cid", CryptoHelper.multihashAlgo(cid.getType()) + "+" + CryptoHelper.bytesToHex(cid.getHash()) + "@bob.xmpp.org", null); + } + + public BobTransfer(URI uri, Account account, Jid to, XmppConnectionService xmppConnectionService) { this.xmppConnectionService = xmppConnectionService; - this.uri = new URI(message.getFileParams().url); + this.uri = uri; + this.to = to; + this.account = account; } @Override @@ -56,10 +64,7 @@ public class BobTransfer implements Transferable { File f = xmppConnectionService.getFileForCid(cid(uri)); if (f != null && f.canRead()) { - message.setRelativeFilePath(f.getAbsolutePath()); - finish(); - message.setTransferable(null); - xmppConnectionService.updateConversationUi(); + finish(f); return true; } @@ -67,13 +72,14 @@ public class BobTransfer implements Transferable { changeStatus(Transferable.STATUS_DOWNLOADING); IqPacket request = new IqPacket(IqPacket.TYPE.GET); - request.setTo(message.getCounterpart()); + request.setTo(to); final Element dataq = request.addChild("data", "urn:xmpp:bob"); dataq.setAttribute("cid", uri.getSchemeSpecificPart()); - xmppConnectionService.sendIqPacket(message.getConversation().getAccount(), request, (acct, packet) -> { + xmppConnectionService.sendIqPacket(account, request, (acct, packet) -> { final Element data = packet.findChild("data", "urn:xmpp:bob"); if (packet.getType() == IqPacket.TYPE.ERROR || data == null) { Log.d(Config.LOGTAG, "BobTransfer failed: " + packet); + finish(null); xmppConnectionService.showErrorToastInUi(R.string.download_failed_file_not_found); } else { final String contentType = data.getAttribute("type"); @@ -85,25 +91,23 @@ public class BobTransfer implements Transferable { try { final byte[] bytes = Base64.decode(data.getContent(), Base64.DEFAULT); - xmppConnectionService.getFileBackend().setupRelativeFilePath(message, new ByteArrayInputStream(bytes), fileExtension); - DownloadableFile file = xmppConnectionService.getFileBackend().getFile(message); + File file = xmppConnectionService.getFileBackend().getStorageLocation(new ByteArrayInputStream(bytes), fileExtension); file.getParentFile().mkdirs(); if (!file.exists() && !file.createNewFile()) { throw new IOException(file.getAbsolutePath()); } - final OutputStream outputStream = AbstractConnectionManager.createOutputStream(file, false, false); + final OutputStream outputStream = AbstractConnectionManager.createOutputStream(new DownloadableFile(file.getAbsolutePath()), false, false); outputStream.write(bytes); outputStream.flush(); outputStream.close(); - finish(); + finish(file); } catch (IOException e) { + finish(null); xmppConnectionService.showErrorToastInUi(R.string.download_failed_could_not_write_file); } } - message.setTransferable(null); - xmppConnectionService.updateConversationUi(); }); return true; } else { @@ -130,7 +134,6 @@ public class BobTransfer implements Transferable { public void cancel() { // No real way to cancel an iq in process... changeStatus(Transferable.STATUS_CANCELLED); - message.setTransferable(null); } protected void changeStatus(int newStatus) { @@ -138,10 +141,35 @@ public class BobTransfer implements Transferable { xmppConnectionService.updateConversationUi(); } - protected void finish() { - final boolean privateMessage = message.isPrivateMessage(); - message.setType(privateMessage ? Message.TYPE_PRIVATE_FILE : Message.TYPE_FILE); - xmppConnectionService.getFileBackend().updateFileParams(message, uri.toString(), false); - xmppConnectionService.updateMessage(message); + protected void finish(File f) { + if (f != null) xmppConnectionService.updateConversationUi(); + } + + public static class ForMessage extends BobTransfer { + protected Message message; + + public ForMessage(Message message, XmppConnectionService xmppConnectionService) throws URISyntaxException { + super(new URI(message.getFileParams().url), message.getConversation().getAccount(), message.getCounterpart(), xmppConnectionService); + this.message = message; + } + + @Override + public void cancel() { + super.cancel(); + message.setTransferable(null); + } + + @Override + protected void finish(File f) { + if (f != null) { + message.setRelativeFilePath(f.getAbsolutePath()); + final boolean privateMessage = message.isPrivateMessage(); + message.setType(privateMessage ? Message.TYPE_PRIVATE_FILE : Message.TYPE_FILE); + xmppConnectionService.getFileBackend().updateFileParams(message, uri.toString(), false); + xmppConnectionService.updateMessage(message); + } + message.setTransferable(null); + super.finish(f); + } } } diff --git a/src/main/java/eu/siacs/conversations/entities/Contact.java b/src/main/java/eu/siacs/conversations/entities/Contact.java index 42232c560f784d1b3cc13c9c325586eee82613ae..6da9283f982613af3a9673cc3a8ab76d9c4fdbb3 100644 --- a/src/main/java/eu/siacs/conversations/entities/Contact.java +++ b/src/main/java/eu/siacs/conversations/entities/Contact.java @@ -414,6 +414,10 @@ public class Contact implements ListItem, Blockable { return ((this.subscription & (1 << option)) != 0); } + public boolean canInferPresence() { + return showInContactList() || isSelf(); + } + public boolean showInRoster() { return (this.getOption(Contact.Options.IN_ROSTER) && (!this .getOption(Contact.Options.DIRTY_DELETE))) diff --git a/src/main/java/eu/siacs/conversations/entities/Conversation.java b/src/main/java/eu/siacs/conversations/entities/Conversation.java index 8367f7cba0da69838484bd194b9f5ba20f7417a3..05537efd27ac3855b425d2130ad6961558855288 100644 --- a/src/main/java/eu/siacs/conversations/entities/Conversation.java +++ b/src/main/java/eu/siacs/conversations/entities/Conversation.java @@ -1130,6 +1130,12 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl return count; } + public boolean canInferPresence() { + final Contact contact = getContact(); + if (contact != null && contact.canInferPresence()) return true; + return sentMessagesCount() > 0; + } + public boolean isWithStranger() { final Contact contact = getContact(); return mode == MODE_SINGLE diff --git a/src/main/java/eu/siacs/conversations/entities/Conversational.java b/src/main/java/eu/siacs/conversations/entities/Conversational.java index 58af42213c570a95c54494a721cc64e582fc25dd..9d5aa8e2b91d4103f357a7fb769720c9157d5290 100644 --- a/src/main/java/eu/siacs/conversations/entities/Conversational.java +++ b/src/main/java/eu/siacs/conversations/entities/Conversational.java @@ -45,4 +45,6 @@ public interface Conversational { int getMode(); String getUuid(); + + boolean canInferPresence(); } diff --git a/src/main/java/eu/siacs/conversations/entities/StubConversation.java b/src/main/java/eu/siacs/conversations/entities/StubConversation.java index 79f4e80b4a65a60b7dc6f8881169d7955435036c..42f98e740233cc1be7f28e0a1dba44c6e4c22cc0 100644 --- a/src/main/java/eu/siacs/conversations/entities/StubConversation.java +++ b/src/main/java/eu/siacs/conversations/entities/StubConversation.java @@ -70,4 +70,10 @@ public class StubConversation implements Conversational { public String getUuid() { return uuid; } + + @Override + public boolean canInferPresence() { + final Contact contact = getContact(); + return contact != null && contact.canInferPresence(); + } } diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java index 1613f037283e8a4d21a15fb2dbfc5edcdaa83b88..d6bd5ef3e91ae550218fa0e32d192fb10241bcb4 100644 --- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java +++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java @@ -765,7 +765,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece if (message.trusted() && message.treatAsDownloadable() && manager.getAutoAcceptFileSize() > 0) { if (message.getOob() != null && message.getOob().getScheme().equalsIgnoreCase("cid")) { try { - BobTransfer transfer = new BobTransfer(message, mXmppConnectionService); + BobTransfer transfer = new BobTransfer.ForMessage(message, mXmppConnectionService); message.setTransferable(transfer); transfer.start(); } catch (URISyntaxException e) { diff --git a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java index 13118252c08e574289c96c414e285bb18c0ccefd..4a5f4d3aed5bf9339aba259c85802bd8fa6c2c12 100644 --- a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java @@ -887,13 +887,7 @@ public class FileBackend { } public void setupRelativeFilePath(final Message message, final InputStream is, final String extension) throws IOException { - Cid[] cids = calculateCids(is); - - setupRelativeFilePath(message, String.format("%s.%s", cids[0], extension)); - File file = getFile(message); - for (int i = 0; i < cids.length; i++) { - mXmppConnectionService.saveCid(cids[i], file); - } + message.setRelativeFilePath(getStorageLocation(is, extension).getAbsolutePath()); } public void setupRelativeFilePath(final Message message, final String filename) { @@ -902,6 +896,17 @@ public class FileBackend { setupRelativeFilePath(message, filename, mime); } + public File getStorageLocation(final InputStream is, final String extension) throws IOException { + final String mime = MimeUtils.guessMimeTypeFromExtension(extension); + Cid[] cids = calculateCids(is); + + File file = getStorageLocation(String.format("%s.%s", cids[0], extension), mime); + for (int i = 0; i < cids.length; i++) { + mXmppConnectionService.saveCid(cids[i], file); + } + return file; + } + public File getStorageLocation(final String filename, final String mime) { final File parentDirectory; if (Strings.isNullOrEmpty(mime)) { diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index 6359e363c5b7300bcf4180210e9b834a7bec2a97..73bf6a556930429e866181d475fda3f68aad6483 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -1946,7 +1946,7 @@ public class ConversationFragment extends XmppFragment } if (message.getOob() != null && message.getOob().getScheme().equalsIgnoreCase("cid")) { try { - BobTransfer transfer = new BobTransfer(message, activity.xmppConnectionService); + BobTransfer transfer = new BobTransfer.ForMessage(message, activity.xmppConnectionService); message.setTransferable(transfer); transfer.start(); } catch (URISyntaxException e) { diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java index efb5e9b1373d6854036030c4ae172221e8e4753e..7b57c0dc27c529f72c096e79d41bf9f3d86e840c 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java @@ -36,10 +36,14 @@ import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; import androidx.core.content.res.ResourcesCompat; +import com.cheogram.android.BobTransfer; + import com.google.common.base.Strings; import java.io.IOException; import java.net.URI; +import java.net.URISyntaxException; +import java.security.NoSuchAlgorithmException; import java.util.List; import java.util.Locale; import java.util.regex.Matcher; @@ -450,7 +454,14 @@ public class MessageAdapter extends ArrayAdapter { SpannableStringBuilder body = message.getMergedBody((cid) -> { try { DownloadableFile f = activity.xmppConnectionService.getFileForCid(cid); - if (f == null) return null; + if (f == null || !f.canRead()) { + if (!message.trusted() && !message.getConversation().canInferPresence()) return null; + + try { + new BobTransfer(BobTransfer.uri(cid), message.getConversation().getAccount(), message.getCounterpart(), activity.xmppConnectionService).start(); + } catch (final NoSuchAlgorithmException | URISyntaxException e) { } + return null; + } Drawable d = activity.xmppConnectionService.getFileBackend().getThumbnail(f, activity.getResources(), (int) (metrics.density * 288), true); if (d == null) { diff --git a/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java b/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java index c2d47ebe3c8ddbbe0092eaf285b56fa8676e3bdd..410a43838e80781838691618a332a5e06214e8d1 100644 --- a/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java @@ -288,6 +288,19 @@ public final class CryptoHelper { return !u.contains(" ") && (u.startsWith("https://") || u.startsWith("http://") || u.startsWith("p1s3://")) && u.endsWith(".pgp"); } + public static String multihashAlgo(Multihash.Type type) throws NoSuchAlgorithmException { + switch(type) { + case sha1: + return "sha1"; + case sha2_256: + return "sha-256"; + case sha2_512: + return "sha-512"; + default: + throw new NoSuchAlgorithmException("" + type); + } + } + public static Multihash.Type multihashType(String algo) throws NoSuchAlgorithmException { if (algo.equals("SHA-1") || algo.equals("sha-1") || algo.equals("sha1")) { return Multihash.Type.sha1;