Support animated images in SVG

Stephen Paul Weber created

If they are the *only* thing in the SVG.

Change summary

src/main/java/eu/siacs/conversations/entities/Conversation.java          | 94 
src/main/java/eu/siacs/conversations/persistance/FileBackend.java        | 13 
src/main/java/eu/siacs/conversations/services/XmppConnectionService.java | 30 
src/main/java/eu/siacs/conversations/xmpp/forms/Option.java              | 11 
4 files changed, 65 insertions(+), 83 deletions(-)

Detailed changes

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

@@ -6,6 +6,7 @@ import android.content.DialogInterface;
 import android.content.Intent;
 import android.database.Cursor;
 import android.database.DataSetObserver;
+import android.graphics.drawable.AnimatedImageDrawable;
 import android.graphics.drawable.BitmapDrawable;
 import android.graphics.drawable.Drawable;
 import android.graphics.Bitmap;
@@ -61,7 +62,6 @@ import androidx.recyclerview.widget.GridLayoutManager;
 import androidx.viewpager.widget.ViewPager;
 
 import com.caverock.androidsvg.SVG;
-import com.caverock.androidsvg.SVGParseException;
 
 import com.cheogram.android.ConversationPage;
 import com.cheogram.android.Util;
@@ -1811,24 +1811,8 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
                             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) {
-                                    synchronized (CommandSession.this) {
-                                        waitingForRefresh = true;
-                                    }
-                                    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 {
+                                final Drawable d = getDrawableForUrl(uri.toString());
+                                if (d != null) {
                                     binding.mediaImage.setImageDrawable(d);
                                     binding.mediaImage.setVisibility(View.VISIBLE);
                                 }
@@ -2178,29 +2162,21 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
 
                             final SVG icon = getItem(position).getIcon();
                             if (icon != null) {
-                                 synchronized (CommandSession.this) {
-                                     waitingForRefresh = true;
-                                 }
+                                 final Element iconEl = getItem(position).getIconEl();
                                  if (height < 1) {
                                      v.measure(0, 0);
                                      height = v.getMeasuredHeight();
                                  }
                                  if (height < 1) return v;
-                                 icon.setDocumentPreserveAspectRatio(com.caverock.androidsvg.PreserveAspectRatio.LETTERBOX);
-                                 try {
-                                     icon.setDocumentWidth("100%");
-                                     icon.setDocumentHeight("100%");
-                                 } catch (final SVGParseException e) { }
                                  if (mediaSelector) {
-                                     Bitmap bitmap = Bitmap.createBitmap(height * 4, height * 4, Bitmap.Config.ARGB_8888);
-                                     Canvas bmcanvas = new Canvas(bitmap);
-                                     icon.renderToCanvas(bmcanvas);
-                                     v.setCompoundDrawablesRelativeWithIntrinsicBounds(null, new BitmapDrawable(bitmap), null, null);
+                                     final Drawable d = getDrawableForSVG(icon, iconEl, height * 4);
+                                     if (d != null) {
+                                         final int boundsHeight = 35 + (int)((height * 4) / xmppConnectionService.getResources().getDisplayMetrics().density);
+                                         d.setBounds(0, 0, d.getIntrinsicWidth(), boundsHeight);
+                                     }
+                                     v.setCompoundDrawables(null, d, null, null);
                                  } else {
-                                     Bitmap bitmap = Bitmap.createBitmap(height, height, Bitmap.Config.ARGB_8888);
-                                     Canvas bmcanvas = new Canvas(bitmap);
-                                     icon.renderToCanvas(bmcanvas);
-                                     v.setCompoundDrawablesRelativeWithIntrinsicBounds(new BitmapDrawable(bitmap), null, null, null);
+                                     v.setCompoundDrawablesRelativeWithIntrinsicBounds(getDrawableForSVG(icon, iconEl, height), null, null, null);
                                  }
                             }
 
@@ -2287,20 +2263,9 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
 
                         final SVG defaultIcon = defaultOption.getIcon();
                         if (defaultIcon != null) {
-                             synchronized (CommandSession.this) {
-                                 waitingForRefresh = true;
-                             }
-                             defaultIcon.setDocumentPreserveAspectRatio(com.caverock.androidsvg.PreserveAspectRatio.LETTERBOX);
-                             try {
-                                 defaultIcon.setDocumentWidth("100%");
-                                 defaultIcon.setDocumentHeight("100%");
-                             } catch (final SVGParseException e) { }
                              DisplayMetrics display = mPager.getContext().getResources().getDisplayMetrics();
-                             Bitmap bitmap = Bitmap.createBitmap((int)(display.heightPixels*display.density/4), (int)(display.heightPixels*display.density/4), Bitmap.Config.ARGB_8888);
-                             bitmap.setDensity(display.densityDpi);
-                             Canvas bmcanvas = new Canvas(bitmap);
-                             defaultIcon.renderToCanvas(bmcanvas);
-                             binding.defaultButton.setCompoundDrawablesRelativeWithIntrinsicBounds(null, new BitmapDrawable(bitmap), null, null);
+                             int height = (int)(display.heightPixels*display.density/4);
+                             binding.defaultButton.setCompoundDrawablesRelativeWithIntrinsicBounds(null, getDrawableForSVG(defaultIcon, defaultOption.getIconEl(), height), null, null);
                         }
 
                         binding.defaultButton.setText(defaultOption.toString());
@@ -3262,6 +3227,39 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
                 }
             }
 
+            private Drawable getDrawableForSVG(SVG svg, Element svgElement, int size) {
+               if (svgElement != null && svgElement.getChildren().size() == 1 && svgElement.getChildren().get(0).getName().equals("image"))  {
+                   return getDrawableForUrl(svgElement.getChildren().get(0).getAttribute("href"));
+               } else {
+                   return xmppConnectionService.getFileBackend().drawSVG(svg, size);
+               }
+            }
+
+            private Drawable getDrawableForUrl(final String url) {
+                final LruCache<String, Drawable> cache = xmppConnectionService.getDrawableCache();
+                final HttpConnectionManager httpManager = xmppConnectionService.getHttpConnectionManager();
+                final Drawable d = cache.get(url);
+                if (Build.VERSION.SDK_INT >= 28 && d instanceof AnimatedImageDrawable) ((AnimatedImageDrawable) d).start();
+                if (d == null) {
+                    synchronized (CommandSession.this) {
+                        waitingForRefresh = true;
+                    }
+                    int size = (int)(xmppConnectionService.getResources().getDisplayMetrics().density * 288);
+                    Message dummy = new Message(Conversation.this, url, Message.ENCRYPTION_NONE);
+                    dummy.setFileParams(new Message.FileParams(url));
+                    httpManager.createNewDownloadConnection(dummy, true, (file) -> {
+                        if (file == null) {
+                            dummy.getTransferable().start();
+                        } else {
+                            try {
+                                xmppConnectionService.getFileBackend().getThumbnail(file, xmppConnectionService.getResources(), size, false, url);
+                            } catch (final Exception e) { }
+                        }
+                    });
+                }
+                return d;
+            }
+
             public View inflateUi(Context context, Consumer<ConversationPage> remover) {
                 CommandPageBinding binding = DataBindingUtil.inflate(LayoutInflater.from(context), R.layout.command_page, null, false);
                 setBinding(binding);

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

@@ -2168,11 +2168,20 @@ public class FileBackend {
     public Drawable getSVG(File file, int size) {
         try {
             SVG svg = SVG.getFromInputStream(new FileInputStream(file));
+            return drawSVG(svg, size);
+        } catch (final IOException | SVGParseException | IllegalArgumentException e) {
+            Log.w(Config.LOGTAG, "Could not parse SVG: " + e);
+            return null;
+        }
+    }
+
+    public Drawable drawSVG(SVG svg, int size) {
+        try {
             svg.setDocumentPreserveAspectRatio(com.caverock.androidsvg.PreserveAspectRatio.LETTERBOX);
 
             float w = svg.getDocumentWidth();
             float h = svg.getDocumentHeight();
-            Rect r = rectForSize((int) w, (int) h, size);
+            Rect r = rectForSize(w < 1 ? size : (int) w, h < 1 ? size : (int) h, size);
             svg.setDocumentWidth("100%");
             svg.setDocumentHeight("100%");
 
@@ -2181,7 +2190,7 @@ public class FileBackend {
             svg.renderToCanvas(canvas);
 
             return new SVGDrawable(output);
-        } catch (final IOException | SVGParseException | IllegalArgumentException e) {
+        } catch (final SVGParseException e) {
             Log.w(Config.LOGTAG, "Could not parse SVG: " + e);
             return null;
         }

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

@@ -353,7 +353,6 @@ public class XmppConnectionService extends Service {
     private final Set<OnMucRosterUpdate> mOnMucRosterUpdate = Collections.newSetFromMap(new WeakHashMap<OnMucRosterUpdate, Boolean>());
     private final Set<OnKeyStatusUpdated> mOnKeyStatusUpdated = Collections.newSetFromMap(new WeakHashMap<OnKeyStatusUpdated, Boolean>());
     private final Set<OnJingleRtpConnectionUpdate> onJingleRtpConnectionUpdate = Collections.newSetFromMap(new WeakHashMap<OnJingleRtpConnectionUpdate, Boolean>());
-    private final Set<String> alreadyAttemptedSVGDownload = new HashSet<>();
 
     private final Object LISTENER_LOCK = new Object();
 
@@ -1432,35 +1431,6 @@ public class XmppConnectionService extends Service {
         toggleForegroundService();
         setupPhoneStateListener();
         rescanStickers();
-        com.caverock.androidsvg.SVG.registerExternalFileResolver(new com.caverock.androidsvg.SVGExternalFileResolver() {
-            @Override
-            public Bitmap resolveImage(String filename) {
-                final LruCache<String, Drawable> cache = getDrawableCache();
-                final Drawable d = cache.get(filename);
-                if (d != null) return FileBackend.drawDrawable(d);
-                if (getLongPreference("auto_accept_file_size", R.integer.auto_accept_filesize) < 3000000) {
-                    return null;
-                }
-                synchronized(alreadyAttemptedSVGDownload) {
-                    if (alreadyAttemptedSVGDownload.contains(filename)) return null;
-                    alreadyAttemptedSVGDownload.add(filename);
-                }
-                final HttpConnectionManager httpManager = getHttpConnectionManager();
-                Message dummy = new Message(new Conversation(null, getAccounts().get(0), null, 0), filename, Message.ENCRYPTION_NONE);
-                dummy.setFileParams(new Message.FileParams(filename));
-                httpManager.createNewDownloadConnection(dummy, true, (file) -> {
-                    if (file == null) {
-                        dummy.getTransferable().start();
-                    } else {
-                        try {
-                            int size = (int)(getResources().getDisplayMetrics().density * 288);
-                            getFileBackend().getThumbnail(file, getResources(), size, false, filename);
-                        } catch (final Exception e) { }
-                    }
-                });
-                return null;
-            }
-        });
     }
 
 

src/main/java/eu/siacs/conversations/xmpp/forms/Option.java 🔗

@@ -10,6 +10,7 @@ public class Option {
     protected final String value;
     protected final String label;
     protected final SVG icon;
+    protected final Element iconEl;
 
     public static List<Option> forField(Element field) {
         List<Option> options = new ArrayList<>();
@@ -25,18 +26,20 @@ public class Option {
         this(
             option.findChildContent("value", "jabber:x:data"),
             option.getAttribute("label"),
-            parseSVG(option.findChild("svg", "http://www.w3.org/2000/svg"))
+            parseSVG(option.findChild("svg", "http://www.w3.org/2000/svg")),
+            option.findChild("svg", "http://www.w3.org/2000/svg")
         );
     }
 
     public Option(final String value, final String label) {
-        this(value, label, null);
+        this(value, label, null, null);
     }
 
-    public Option(final String value, final String label, final SVG icon) {
+    public Option(final String value, final String label, final SVG icon, final Element iconEl) {
         this.value = value;
         this.label = label == null ? value : label;
         this.icon = icon;
+        this.iconEl = iconEl;
     }
 
     public boolean equals(Object o) {
@@ -53,6 +56,8 @@ public class Option {
 
     public SVG getIcon() { return icon; }
 
+    public Element getIconEl() { return iconEl; }
+
     private static SVG parseSVG(final Element svg) {
         if (svg == null) return null;
         try {