Actually display images we already have inline in XHTML-IM

Stephen Paul Weber created

If we already have the image for the Cid downloaded, then do create the
thumbnail and show it inline.

Change summary

src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java    |  7 
src/main/java/eu/siacs/conversations/persistance/FileBackend.java        | 41 
src/main/java/eu/siacs/conversations/services/XmppConnectionService.java |  3 
src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java      | 50 
src/main/java/eu/siacs/conversations/utils/UIHelper.java                 |  6 
5 files changed, 86 insertions(+), 21 deletions(-)

Detailed changes

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;

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<String, Drawable> 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();

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);
     }
 

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<Message> {
         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<Message> {
         protected TextView encryption;
         protected ListView commands_list;
     }
+
+    class ThumbnailTask extends AsyncTask<DownloadableFile, Void, Drawable[]> {
+        @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();
+        }
+    }
 }

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);
                 }