Play animated gifs inline

Stephen Paul Weber created

On Android 9 or above, older devices will fall back to the GIF overlay.

Change summary

src/main/java/eu/siacs/conversations/persistance/FileBackend.java        | 104 
src/main/java/eu/siacs/conversations/services/NotificationService.java   |   2 
src/main/java/eu/siacs/conversations/services/XmppConnectionService.java |  19 
src/main/java/eu/siacs/conversations/ui/XmppActivity.java                |  25 
4 files changed, 111 insertions(+), 39 deletions(-)

Detailed changes

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

@@ -2,11 +2,15 @@ package eu.siacs.conversations.persistance;
 
 import android.content.ContentResolver;
 import android.content.Context;
+import android.content.res.Resources;
 import android.database.Cursor;
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
 import android.graphics.Canvas;
 import android.graphics.Color;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.ImageDecoder;
 import android.graphics.Matrix;
 import android.graphics.Paint;
 import android.graphics.RectF;
@@ -914,10 +918,10 @@ public class FileBackend {
         }
     }
 
-    public Bitmap getThumbnail(Message message, int size, boolean cacheOnly) throws IOException {
+    public Drawable getThumbnail(Message message, Resources res, int size, boolean cacheOnly) throws IOException {
         final String uuid = message.getUuid();
-        final LruCache<String, Bitmap> cache = mXmppConnectionService.getBitmapCache();
-        Bitmap thumbnail = cache.get(uuid);
+        final LruCache<String, Drawable> cache = mXmppConnectionService.getDrawableCache();
+        Drawable thumbnail = cache.get(uuid);
         if ((thumbnail == null) && (!cacheOnly)) {
             synchronized (THUMBNAIL_LOCK) {
                 thumbnail = cache.get(uuid);
@@ -927,27 +931,14 @@ public class FileBackend {
                 DownloadableFile file = getFile(message);
                 final String mime = file.getMimeType();
                 if ("application/pdf".equals(mime) && Compatibility.runsTwentyOne()) {
-                    thumbnail = getPdfDocumentPreview(file, size);
+                    thumbnail = new BitmapDrawable(res, getPdfDocumentPreview(file, size));
                 } else if (mime.startsWith("video/")) {
-                    thumbnail = getVideoPreview(file, size);
+                    thumbnail = new BitmapDrawable(res, getVideoPreview(file, size));
                 } else {
-                    final Bitmap fullSize = getFullSizeImagePreview(file, size);
-                    if (fullSize == null) {
+                    thumbnail = getImagePreview(file, res, size, mime);
+                    if (thumbnail == null) {
                         throw new FileNotFoundException();
                     }
-                    thumbnail = resize(fullSize, size);
-                    thumbnail = rotate(thumbnail, getRotation(file));
-                    if (mime.equals("image/gif")) {
-                        Bitmap withGifOverlay = thumbnail.copy(Bitmap.Config.ARGB_8888, true);
-                        drawOverlay(
-                                withGifOverlay,
-                                paintOverlayBlack(withGifOverlay)
-                                        ? R.drawable.play_gif_black
-                                        : R.drawable.play_gif_white,
-                                1.0f);
-                        thumbnail.recycle();
-                        thumbnail = withGifOverlay;
-                    }
                 }
                 cache.put(uuid, thumbnail);
             }
@@ -955,17 +946,74 @@ public class FileBackend {
         return thumbnail;
     }
 
-    private Bitmap getFullSizeImagePreview(File file, int size) {
-        BitmapFactory.Options options = new BitmapFactory.Options();
-        options.inSampleSize = calcSampleSize(file, size);
-        try {
-            return BitmapFactory.decodeFile(file.getAbsolutePath(), options);
-        } catch (OutOfMemoryError e) {
-            options.inSampleSize *= 2;
-            return BitmapFactory.decodeFile(file.getAbsolutePath(), options);
+    public Bitmap getThumbnailBitmap(Message message, Resources res, int size, boolean cacheOnly) throws IOException {
+        final String uuid = message.getUuid();
+        final LruCache<String, Bitmap> cache = mXmppConnectionService.getBitmapCache();
+        Bitmap thumbnail = cache.get(uuid);
+        if ((thumbnail == null) && (!cacheOnly)) {
+          final Drawable drawable = getThumbnail(message, res, size, cacheOnly);
+          if (drawable != null) {
+              thumbnail = drawDrawable(drawable);
+              cache.put(uuid, thumbnail);
+          }
+        }
+        return thumbnail;
+    }
+
+    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);
+            });
+        } else {
+            BitmapFactory.Options options = new BitmapFactory.Options();
+            options.inSampleSize = calcSampleSize(file, size);
+            Bitmap bitmap = null;
+            try {
+                bitmap = BitmapFactory.decodeFile(file.getAbsolutePath(), options);
+            } catch (OutOfMemoryError e) {
+                options.inSampleSize *= 2;
+                bitmap = BitmapFactory.decodeFile(file.getAbsolutePath(), options);
+            }
+            bitmap = resize(bitmap, size);
+            bitmap = rotate(bitmap, getRotation(file));
+            if (mime.equals("image/gif")) {
+                Bitmap withGifOverlay = bitmap.copy(Bitmap.Config.ARGB_8888, true);
+                drawOverlay(withGifOverlay, paintOverlayBlack(withGifOverlay) ? R.drawable.play_gif_black : R.drawable.play_gif_white, 1.0f);
+                bitmap.recycle();
+                bitmap = withGifOverlay;
+            }
+            return new BitmapDrawable(res, bitmap);
         }
     }
 
+    protected Bitmap drawDrawable(Drawable drawable) {
+        Bitmap bitmap = null;
+
+        if (drawable instanceof BitmapDrawable) {
+            bitmap = ((BitmapDrawable) drawable).getBitmap();
+            if (bitmap != null) return bitmap;
+        }
+
+        bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
+        Canvas canvas = new Canvas(bitmap);
+        drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
+        drawable.draw(canvas);
+        return bitmap;
+    }
+
     private void drawOverlay(Bitmap bitmap, int resource, float factor) {
         Bitmap overlay =
                 BitmapFactory.decodeResource(mXmppConnectionService.getResources(), resource);

src/main/java/eu/siacs/conversations/services/NotificationService.java 🔗

@@ -1062,7 +1062,7 @@ public class NotificationService {
 
     private void modifyForImage(final Builder builder, final Message message, final ArrayList<Message> messages) {
         try {
-            final Bitmap bitmap = mXmppConnectionService.getFileBackend().getThumbnail(message, getPixel(288), false);
+            final Bitmap bitmap = mXmppConnectionService.getFileBackend().getThumbnailBitmap(message, mXmppConnectionService.getResources(), getPixel(288), false);
             final ArrayList<Message> tmp = new ArrayList<>();
             for (final Message msg : messages) {
                 if (msg.getType() == Message.TYPE_TEXT

src/main/java/eu/siacs/conversations/services/XmppConnectionService.java 🔗

@@ -18,6 +18,8 @@ import android.content.SharedPreferences;
 import android.content.pm.PackageManager;
 import android.database.ContentObserver;
 import android.graphics.Bitmap;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
 import android.media.AudioManager;
 import android.net.ConnectivityManager;
 import android.net.Network;
@@ -486,6 +488,7 @@ public class XmppConnectionService extends Service {
     private PgpEngine mPgpEngine = null;
     private WakeLock wakeLock;
     private LruCache<String, Bitmap> mBitmapCache;
+    private LruCache<String, Drawable> mDrawableCache;
     private final BroadcastReceiver mInternalEventReceiver = new InternalEventReceiver();
     private final BroadcastReceiver mInternalScreenEventReceiver = new InternalEventReceiver();
 
@@ -1151,13 +1154,23 @@ public class XmppConnectionService extends Service {
         this.mRandom = new SecureRandom();
         updateMemorizingTrustmanager();
         final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
-        final int cacheSize = maxMemory / 8;
+        final int cacheSize = maxMemory / 9;
         this.mBitmapCache = new LruCache<String, Bitmap>(cacheSize) {
             @Override
             protected int sizeOf(final String key, final Bitmap bitmap) {
                 return bitmap.getByteCount() / 1024;
             }
         };
+        this.mDrawableCache = new LruCache<String, Drawable>(cacheSize) {
+            @Override
+            protected int sizeOf(final String key, final Drawable drawable) {
+                if (drawable instanceof BitmapDrawable) {
+                    return ((BitmapDrawable) drawable).getBitmap().getByteCount() / 1024;
+                } else {
+                    return drawable.getIntrinsicWidth() * drawable.getIntrinsicHeight() * 40 / 1024;
+                }
+            }
+        };
         if (mLastActivity == 0) {
             mLastActivity = getPreferences().getLong(SETTING_LAST_ACTIVITY_TS, System.currentTimeMillis());
         }
@@ -4327,6 +4340,10 @@ public class XmppConnectionService extends Service {
         return this.mBitmapCache;
     }
 
+    public LruCache<String, Drawable> getDrawableCache() {
+        return this.mDrawableCache;
+    }
+
     public Collection<String> getKnownHosts() {
         final Set<String> hosts = new HashSet<>();
         for (final Account account : getAccounts()) {

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

@@ -22,6 +22,7 @@ import android.content.res.TypedArray;
 import android.graphics.Bitmap;
 import android.graphics.Color;
 import android.graphics.Point;
+import android.graphics.drawable.AnimatedImageDrawable;
 import android.graphics.drawable.BitmapDrawable;
 import android.graphics.drawable.Drawable;
 import android.net.ConnectivityManager;
@@ -876,16 +877,19 @@ public abstract class XmppActivity extends ActionBarActivity {
     }
 
     public void loadBitmap(Message message, ImageView imageView) {
-        Bitmap bm;
+        Drawable bm;
         try {
-            bm = xmppConnectionService.getFileBackend().getThumbnail(message, (int) (metrics.density * 288), true);
+            bm = xmppConnectionService.getFileBackend().getThumbnail(message, getResources(), (int) (metrics.density * 288), true);
         } catch (IOException e) {
             bm = null;
         }
         if (bm != null) {
             cancelPotentialWork(message, imageView);
-            imageView.setImageBitmap(bm);
+            imageView.setImageDrawable(bm);
             imageView.setBackgroundColor(0x00000000);
+            if (Build.VERSION.SDK_INT >= 28 && bm instanceof AnimatedImageDrawable) {
+                ((AnimatedImageDrawable) bm).start();
+            }
         } else {
             if (cancelPotentialWork(message, imageView)) {
                 imageView.setBackgroundColor(0xff333333);
@@ -939,7 +943,7 @@ public abstract class XmppActivity extends ActionBarActivity {
         }
     }
 
-    static class BitmapWorkerTask extends AsyncTask<Message, Void, Bitmap> {
+    static class BitmapWorkerTask extends AsyncTask<Message, Void, Drawable> {
         private final WeakReference<ImageView> imageViewReference;
         private Message message = null;
 
@@ -948,7 +952,7 @@ public abstract class XmppActivity extends ActionBarActivity {
         }
 
         @Override
-        protected Bitmap doInBackground(Message... params) {
+        protected Drawable doInBackground(Message... params) {
             if (isCancelled()) {
                 return null;
             }
@@ -956,7 +960,7 @@ public abstract class XmppActivity extends ActionBarActivity {
             try {
                 final XmppActivity activity = find(imageViewReference);
                 if (activity != null && activity.xmppConnectionService != null) {
-                    return activity.xmppConnectionService.getFileBackend().getThumbnail(message, (int) (activity.metrics.density * 288), false);
+                    return activity.xmppConnectionService.getFileBackend().getThumbnail(message, imageViewReference.get().getContext().getResources(), (int) (activity.metrics.density * 288), false);
                 } else {
                     return null;
                 }
@@ -966,12 +970,15 @@ public abstract class XmppActivity extends ActionBarActivity {
         }
 
         @Override
-        protected void onPostExecute(final Bitmap bitmap) {
+        protected void onPostExecute(final Drawable drawable) {
             if (!isCancelled()) {
                 final ImageView imageView = imageViewReference.get();
                 if (imageView != null) {
-                    imageView.setImageBitmap(bitmap);
-                    imageView.setBackgroundColor(bitmap == null ? 0xff333333 : 0x00000000);
+                    imageView.setImageDrawable(drawable);
+                    imageView.setBackgroundColor(drawable == null ? 0xff333333 : 0x00000000);
+                    if (Build.VERSION.SDK_INT >= 28 && drawable instanceof AnimatedImageDrawable) {
+                        ((AnimatedImageDrawable) drawable).start();
+                    }
                 }
             }
         }