Initial support for XEP-0221

Stephen Paul Weber created

Works for images delivered over https only

Change summary

cheogram.doap                                                         |  8 
src/cheogram/res/layout/command_result_field.xml                      | 13 
src/main/java/eu/siacs/conversations/entities/Conversation.java       | 43 
src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java  |  8 
src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java | 14 
src/main/java/eu/siacs/conversations/persistance/FileBackend.java     | 10 
6 files changed, 88 insertions(+), 8 deletions(-)

Detailed changes

cheogram.doap 🔗

@@ -60,6 +60,14 @@
 	<implements rdf:resource="https://xmpp.org/rfcs/rfc6122.html"/>
 	<implements rdf:resource="https://xmpp.org/rfcs/rfc7590.html"/>
 
+	<implements>
+		<xmpp:SupportedXep>
+			<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0221.html"/>
+			<xmpp:status>partial</xmpp:status>
+			<xmpp:version>1.0</xmpp:version>
+			<xmpp:note xml:lang='en'>When used with XEP-0050, images over HTTPS only</xmpp:note>
+		</xmpp:SupportedXep>
+	</implements>
 	<implements>
 		<xmpp:SupportedXep>
 			<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0224.html"/>

src/cheogram/res/layout/command_result_field.xml 🔗

@@ -16,6 +16,19 @@
             android:paddingBottom="4dp"
             android:textAppearance="@style/TextAppearance.Conversations.Caption" />
 
+        <ImageView
+            android:id="@+id/media_image"
+            android:visibility="gone"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginBottom="4dp"
+            android:layout_marginTop="8dp"
+            android:layout_gravity="center"
+            android:adjustViewBounds="true"
+            android:background="@color/black87"
+            android:longClickable="false"
+            android:scaleType="centerCrop"/>
+
         <ListView
             android:id="@+id/values"
             android:layout_width="fill_parent"

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

@@ -7,6 +7,7 @@ import android.content.Intent;
 import android.database.Cursor;
 import android.database.DataSetObserver;
 import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
 import android.graphics.Bitmap;
 import android.graphics.Canvas;
 import android.graphics.Rect;
@@ -41,6 +42,7 @@ import android.webkit.WebView;
 import android.webkit.WebViewClient;
 import android.webkit.WebChromeClient;
 import android.util.DisplayMetrics;
+import android.util.LruCache;
 import android.util.Pair;
 import android.util.SparseArray;
 
@@ -112,6 +114,7 @@ import eu.siacs.conversations.databinding.CommandSpinnerFieldBinding;
 import eu.siacs.conversations.databinding.CommandTextFieldBinding;
 import eu.siacs.conversations.databinding.CommandWebviewBinding;
 import eu.siacs.conversations.databinding.DialogQuickeditBinding;
+import eu.siacs.conversations.http.HttpConnectionManager;
 import eu.siacs.conversations.persistance.DatabaseBackend;
 import eu.siacs.conversations.services.AvatarService;
 import eu.siacs.conversations.services.QuickConversationsService;
@@ -1742,6 +1745,42 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
                     setTextOrHide(binding.label, field.getLabel());
                     setTextOrHide(binding.desc, field.getDesc());
 
+                    Element media = field.el.findChild("media", "urn:xmpp:media-element");
+                    if (media == null) {
+                        binding.mediaImage.setVisibility(View.GONE);
+                    } else {
+                        final LruCache<String, Drawable> cache = xmppConnectionService.getDrawableCache();
+                        final HttpConnectionManager httpManager = xmppConnectionService.getHttpConnectionManager();
+                        for (Element uriEl : media.getChildren()) {
+                            if (!"uri".equals(uriEl.getName())) continue;
+                            if (!"urn:xmpp:media-element".equals(uriEl.getNamespace())) continue;
+                            String mimeType = uriEl.getAttribute("type");
+                            String uriS = uriEl.getContent();
+                            if (mimeType == null || uriS == null) continue;
+                            Uri uri = Uri.parse(uriS);
+                            if (mimeType.startsWith("image/") && "https".equals(uri.getScheme())) {
+                                final Drawable d = cache.get(uri.toString());
+                                if (d == null) {
+                                    int size = (int)(xmppConnectionService.getResources().getDisplayMetrics().density * 288);
+                                    Message dummy = new Message(Conversation.this, uri.toString(), Message.ENCRYPTION_NONE);
+                                    dummy.setFileParams(new Message.FileParams(uri.toString()));
+                                    httpManager.createNewDownloadConnection(dummy, true, (file) -> {
+                                        if (file == null) {
+                                            dummy.getTransferable().start();
+                                        } else {
+                                            try {
+                                                xmppConnectionService.getFileBackend().getThumbnail(file, xmppConnectionService.getResources(), size, false, uri.toString());
+                                            } catch (final Exception e) { }
+                                        }
+                                    });
+                                } else {
+                                    binding.mediaImage.setImageDrawable(d);
+                                    binding.mediaImage.setVisibility(View.VISIBLE);
+                                }
+                            }
+                        }
+                    }
+
                     Element validate = field.el.findChild("validate", "http://jabber.org/protocol/xdata-validate");
                     String datatype = validate == null ? null : validate.getAttribute("datatype");
 
@@ -2987,7 +3026,9 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
                 return false;
             }
 
-            public void refresh() { }
+            public void refresh() {
+                notifyDataSetChanged();
+            }
 
             protected void loading() {
                 View v = getView();

src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java 🔗

@@ -27,9 +27,11 @@ import javax.net.ssl.X509TrustManager;
 import eu.siacs.conversations.BuildConfig;
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.DownloadableFile;
 import eu.siacs.conversations.entities.Message;
 import eu.siacs.conversations.services.AbstractConnectionManager;
 import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.utils.Consumer;
 import eu.siacs.conversations.utils.TLSSocketFactory;
 import okhttp3.HttpUrl;
 import okhttp3.OkHttpClient;
@@ -85,6 +87,10 @@ public class HttpConnectionManager extends AbstractConnectionManager {
     }
 
     public void createNewDownloadConnection(final Message message, boolean interactive) {
+        createNewDownloadConnection(message, interactive, null);
+    }
+
+    public void createNewDownloadConnection(final Message message, boolean interactive, Consumer<DownloadableFile> cb) {
         synchronized (this.downloadConnections) {
             for (HttpDownloadConnection connection : this.downloadConnections) {
                 if (connection.getMessage() == message) {
@@ -92,7 +98,7 @@ public class HttpConnectionManager extends AbstractConnectionManager {
                     return;
                 }
             }
-            final HttpDownloadConnection connection = new HttpDownloadConnection(message, this);
+            final HttpDownloadConnection connection = new HttpDownloadConnection(message, this, cb);
             connection.init(interactive);
             this.downloadConnections.add(connection);
         }

src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java 🔗

@@ -24,6 +24,7 @@ import eu.siacs.conversations.entities.Transferable;
 import eu.siacs.conversations.persistance.FileBackend;
 import eu.siacs.conversations.services.AbstractConnectionManager;
 import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.utils.Consumer;
 import eu.siacs.conversations.utils.CryptoHelper;
 import eu.siacs.conversations.utils.FileWriterException;
 import eu.siacs.conversations.utils.MimeUtils;
@@ -46,11 +47,13 @@ public class HttpDownloadConnection implements Transferable {
     private boolean acceptedAutomatically = false;
     private int mProgress = 0;
     private Call mostRecentCall;
+    final private Consumer<DownloadableFile> cb;
 
-    HttpDownloadConnection(Message message, HttpConnectionManager manager) {
+    HttpDownloadConnection(Message message, HttpConnectionManager manager, Consumer<DownloadableFile> cb) {
         this.message = message;
         this.mHttpConnectionManager = manager;
         this.mXmppConnectionService = manager.getXmppConnectionService();
+        this.cb = cb;
     }
 
     @Override
@@ -188,7 +191,7 @@ public class HttpDownloadConnection implements Transferable {
     }
 
     private void finish() {
-        boolean notify = acceptedAutomatically && !message.isRead();
+        boolean notify = acceptedAutomatically && !message.isRead() && cb == null;
         if (message.getEncryption() == Message.ENCRYPTION_PGP) {
             notify = message.getConversation().getAccount().getPgpDecryptionService().decrypt(message, notify);
         }
@@ -210,6 +213,7 @@ public class HttpDownloadConnection implements Transferable {
             message.setDeleted(true);
         }
         message.setTransferable(null);
+        cb.accept(file);
         mXmppConnectionService.updateMessage(message);
         mHttpConnectionManager.finishConnection(this);
         final boolean notifyAfterScan = notify;
@@ -325,7 +329,11 @@ public class HttpDownloadConnection implements Transferable {
             } else {
                 changeStatus(STATUS_OFFER);
                 HttpDownloadConnection.this.acceptedAutomatically = false;
-                HttpDownloadConnection.this.mXmppConnectionService.getNotificationService().push(message);
+                if (cb == null) {
+                    HttpDownloadConnection.this.mXmppConnectionService.getNotificationService().push(message);
+                } else {
+                    cb.accept(null);
+                }
             }
         }
 

src/main/java/eu/siacs/conversations/persistance/FileBackend.java 🔗

@@ -1164,11 +1164,15 @@ public class FileBackend {
     }
 
     public Drawable getThumbnail(DownloadableFile file, Resources res, int size, boolean cacheOnly) throws IOException {
+        return getThumbnail(file, res, size, cacheOnly, file.getAbsolutePath());
+    }
+
+    public Drawable getThumbnail(DownloadableFile file, Resources res, int size, boolean cacheOnly, String cacheKey) throws IOException {
         final LruCache<String, Drawable> cache = mXmppConnectionService.getDrawableCache();
-        Drawable thumbnail = cache.get(file.getAbsolutePath());
+        Drawable thumbnail = cache.get(cacheKey);
         if ((thumbnail == null) && (!cacheOnly)) {
             synchronized (THUMBNAIL_LOCK) {
-                thumbnail = cache.get(file.getAbsolutePath());
+                thumbnail = cache.get(cacheKey);
                 if (thumbnail != null) {
                     return thumbnail;
                 }
@@ -1183,7 +1187,7 @@ public class FileBackend {
                         throw new FileNotFoundException();
                     }
                 }
-                cache.put(file.getAbsolutePath(), thumbnail);
+                cache.put(cacheKey, thumbnail);
             }
         }
         return thumbnail;