Show fallback thumbnail if there is no image

Stephen Paul Weber created

Some thumbnails are just as good as scaling down the real image, but
some (especially blurhash) are not.  So if nothing that's "good enough" to go in
the main cache is present, try the fallback.

If there is a thumbnail we can render right now (as of this commit, just
blurhash) then render that alongside the download button.  Tapping the image
starts the download just like the button.

Change summary

build.gradle                                                        |  1 
src/main/java/eu/siacs/conversations/persistance/FileBackend.java   | 38 
src/main/java/eu/siacs/conversations/ui/XmppActivity.java           | 10 
src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java | 56 
4 files changed, 91 insertions(+), 14 deletions(-)

Detailed changes

build.gradle 🔗

@@ -100,6 +100,7 @@ dependencies {
     implementation 'me.saket:better-link-movement-method:2.2.0'
     implementation 'com.github.singpolyma:android-identicons:master-SNAPSHOT'
     implementation 'org.snikket:webrtc-android:107.0.0'
+    implementation 'com.github.woltapp:blurhash:master'
     // INSERT
 }
 

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

@@ -41,6 +41,8 @@ import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableList;
 import com.google.common.io.ByteStreams;
 
+import com.wolt.blurhashkt.BlurHashDecoder;
+
 import java.io.ByteArrayOutputStream;
 import java.io.Closeable;
 import java.io.File;
@@ -78,6 +80,7 @@ import eu.siacs.conversations.utils.FileUtils;
 import eu.siacs.conversations.utils.FileWriterException;
 import eu.siacs.conversations.utils.MimeUtils;
 import eu.siacs.conversations.xmpp.pep.Avatar;
+import eu.siacs.conversations.xml.Element;
 
 public class FileBackend {
 
@@ -1000,6 +1003,41 @@ public class FileBackend {
         }
     }
 
+    public BitmapDrawable getFallbackThumbnail(final Message message, int size) {
+        List<Element> thumbs = message.getFileParams() != null ? message.getFileParams().getThumbnails() : null;
+        if (thumbs != null && !thumbs.isEmpty()) {
+            for (Element thumb : thumbs) {
+                Uri uri = Uri.parse(thumb.getAttribute("uri"));
+                if (uri.getScheme().equals("data")) {
+                    String[] parts = uri.getSchemeSpecificPart().split(",", 2);
+                    if (parts[0].equals("image/blurhash")) {
+                        final LruCache<String, Drawable> cache = mXmppConnectionService.getDrawableCache();
+                        BitmapDrawable cached = (BitmapDrawable) cache.get(parts[1]);
+                        if (cached != null) return cached;
+
+                        int width = message.getFileParams().width;
+                        if (width < 1 && thumb.getAttribute("width") != null) width = Integer.parseInt(thumb.getAttribute("width"));
+                        if (width < 1) width = 1920;
+
+                        int height = message.getFileParams().height;
+                        if (height < 1 && thumb.getAttribute("height") != null) height = Integer.parseInt(thumb.getAttribute("height"));
+                        if (height < 1) height = 1080;
+                        Rect r = rectForSize(width, height, size);
+
+                        Bitmap blurhash = BlurHashDecoder.INSTANCE.decode(parts[1], r.width(), r.height(), 1.0f, false);
+                        if (blurhash != null) {
+                            cached = new BitmapDrawable(blurhash);
+                            cache.put(parts[1], cached);
+                            return cached;
+                        }
+                    }
+                }
+            }
+         }
+
+        return null;
+    }
+
     public Drawable getThumbnail(Message message, Resources res, int size, boolean cacheOnly) throws IOException {
         return getThumbnail(getFile(message), res, size, cacheOnly);
     }

src/main/java/eu/siacs/conversations/ui/XmppActivity.java 🔗

@@ -916,8 +916,9 @@ public abstract class XmppActivity extends ActionBarActivity {
                 imageView.setBackgroundColor(0xff333333);
                 imageView.setImageDrawable(null);
                 final BitmapWorkerTask task = new BitmapWorkerTask(imageView);
+                final BitmapDrawable fallbackThumb = xmppConnectionService.getFileBackend().getFallbackThumbnail(message, (int) (metrics.density * 288));
                 final AsyncDrawable asyncDrawable = new AsyncDrawable(
-                        getResources(), null, task);
+                        getResources(), fallbackThumb != null ? fallbackThumb.getBitmap() : null, task);
                 imageView.setImageDrawable(asyncDrawable);
                 try {
                     task.execute(message);
@@ -995,7 +996,12 @@ public abstract class XmppActivity extends ActionBarActivity {
             if (!isCancelled()) {
                 final ImageView imageView = imageViewReference.get();
                 if (imageView != null) {
-                    imageView.setImageDrawable(drawable);
+                    Drawable old = imageView.getDrawable();
+                    if (drawable == null && old instanceof AsyncDrawable) {
+                        imageView.setImageDrawable(new BitmapDrawable(((AsyncDrawable) old).getBitmap()));
+                    } else {
+                        imageView.setImageDrawable(drawable);
+                    }
                     imageView.setBackgroundColor(drawable == null ? 0xff333333 : 0x00000000);
                     if (Build.VERSION.SDK_INT >= 28 && drawable instanceof AnimatedImageDrawable) {
                         ((AnimatedImageDrawable) drawable).start();

src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java 🔗

@@ -586,6 +586,34 @@ public class MessageAdapter extends ArrayAdapter<Message> {
     private void displayDownloadableMessage(ViewHolder viewHolder, final Message message, String text, final boolean darkBackground, final int type) {
         displayTextMessage(viewHolder, message, darkBackground, type);
         viewHolder.image.setVisibility(View.GONE);
+        List<Element> thumbs = message.getFileParams() != null ? message.getFileParams().getThumbnails() : null;
+        if (thumbs != null && !thumbs.isEmpty()) {
+            for (Element thumb : thumbs) {
+                Uri uri = Uri.parse(thumb.getAttribute("uri"));
+                if (uri.getScheme().equals("data")) {
+                    String[] parts = uri.getSchemeSpecificPart().split(",", 2);
+                    parts = parts[0].split(";");
+                    if (!parts[0].equals("image/blurhash")) continue;
+                } else {
+                    continue;
+                }
+
+                int width = message.getFileParams().width;
+                if (width < 1 && thumb.getAttribute("width") != null) width = Integer.parseInt(thumb.getAttribute("width"));
+                if (width < 1) width = 1920;
+
+                int height = message.getFileParams().height;
+                if (height < 1 && thumb.getAttribute("height") != null) height = Integer.parseInt(thumb.getAttribute("height"));
+                if (height < 1) height = 1080;
+
+                viewHolder.image.setVisibility(View.VISIBLE);
+                imagePreviewLayout(width, height, viewHolder.image);
+                activity.loadBitmap(message, viewHolder.image);
+                viewHolder.image.setOnClickListener(v -> ConversationFragment.downloadFile(activity, message));
+
+                break;
+            }
+        }
         viewHolder.audioPlayer.setVisibility(View.GONE);
         viewHolder.download_button.setVisibility(View.VISIBLE);
         viewHolder.download_button.setText(text);
@@ -626,27 +654,31 @@ public class MessageAdapter extends ArrayAdapter<Message> {
         viewHolder.audioPlayer.setVisibility(View.GONE);
         viewHolder.image.setVisibility(View.VISIBLE);
         final FileParams params = message.getFileParams();
+        imagePreviewLayout(params.width, params.height, viewHolder.image);
+        activity.loadBitmap(message, viewHolder.image);
+        viewHolder.image.setOnClickListener(v -> openDownloadable(message));
+    }
+
+    private void imagePreviewLayout(int w, int h, ImageView image) {
         final float target = activity.getResources().getDimension(R.dimen.image_preview_width);
         final int scaledW;
         final int scaledH;
-        if (Math.max(params.height, params.width) * metrics.density <= target) {
-            scaledW = (int) (params.width * metrics.density);
-            scaledH = (int) (params.height * metrics.density);
-        } else if (Math.max(params.height, params.width) <= target) {
-            scaledW = params.width;
-            scaledH = params.height;
-        } else if (params.width <= params.height) {
-            scaledW = (int) (params.width / ((double) params.height / target));
+        if (Math.max(h, w) * metrics.density <= target) {
+            scaledW = (int) (w * metrics.density);
+            scaledH = (int) (h * metrics.density);
+        } else if (Math.max(h, w) <= target) {
+            scaledW = w;
+            scaledH = h;
+        } else if (w <= h) {
+            scaledW = (int) (w / ((double) h / target));
             scaledH = (int) target;
         } else {
             scaledW = (int) target;
-            scaledH = (int) (params.height / ((double) params.width / target));
+            scaledH = (int) (h / ((double) w / target));
         }
         final LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(scaledW, scaledH);
         layoutParams.setMargins(0, (int) (metrics.density * 4), 0, (int) (metrics.density * 4));
-        viewHolder.image.setLayoutParams(layoutParams);
-        activity.loadBitmap(message, viewHolder.image);
-        viewHolder.image.setOnClickListener(v -> openDownloadable(message));
+        image.setLayoutParams(layoutParams);
     }
 
     private void toggleWhisperInfo(ViewHolder viewHolder, final Message message, final boolean darkBackground) {