show conversation media in contact/conference details

Daniel Gultsch created

Change summary

src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java    |  23 
src/main/java/eu/siacs/conversations/persistance/FileBackend.java        |  24 
src/main/java/eu/siacs/conversations/services/XmppConnectionService.java |  12 
src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java   |  26 
src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java      |  34 
src/main/java/eu/siacs/conversations/ui/adapter/MediaAdapter.java        | 225 
src/main/java/eu/siacs/conversations/ui/adapter/MediaPreviewAdapter.java |  38 
src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java      |  23 
src/main/java/eu/siacs/conversations/ui/interfaces/OnMediaLoaded.java    |  10 
src/main/java/eu/siacs/conversations/ui/util/Attachment.java             |  13 
src/main/java/eu/siacs/conversations/ui/util/GridManager.java            |  71 
src/main/java/eu/siacs/conversations/ui/util/ViewUtil.java               |  51 
src/main/java/eu/siacs/conversations/ui/widget/SquareFrameLayout.java    |  25 
src/main/res/layout/activity_contact_details.xml                         |  77 
src/main/res/layout/activity_muc_details.xml                             |  49 
src/main/res/layout/media.xml                                            |  14 
src/main/res/values/dimens.xml                                           |   1 
src/main/res/values/strings.xml                                          |   1 
18 files changed, 633 insertions(+), 84 deletions(-)

Detailed changes

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

@@ -34,6 +34,7 @@ import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.UUID;
 import java.util.concurrent.CopyOnWriteArrayList;
 
 import org.json.JSONException;
@@ -774,6 +775,28 @@ public class DatabaseBackend extends SQLiteOpenHelper {
 		};
 	}
 
+	public List<FilePath> getRelativeFilePaths(Account account, Jid jid, int limit) {
+		SQLiteDatabase db = this.getReadableDatabase();
+		final String SQL = "select uuid,relativeFilePath from messages where type in (1,2) and conversationUuid=(select uuid from conversations where accountUuid=? and (contactJid=? or contactJid like ?)) order by timeSent desc";
+		final String[] args = {account.getUuid(), jid.asBareJid().toEscapedString(), jid.asBareJid().toEscapedString()+"/%"};
+		Cursor cursor = db.rawQuery(SQL+(limit > 0 ? " limit "+String.valueOf(limit) : ""), args);
+		List<FilePath> filesPaths = new ArrayList<>();
+		while(cursor.moveToNext()) {
+			filesPaths.add(new FilePath(cursor.getString(0),cursor.getString(1)));
+		}
+		cursor.close();
+		return filesPaths;
+	}
+
+	public static class FilePath {
+		public final UUID uuid;
+		public final String path;
+		private FilePath(String uuid, String path) {
+			this.uuid = UUID.fromString(uuid);
+			this.path = path;
+		}
+	}
+
 	public Conversation findConversation(final Account account, final Jid contactJid) {
 		SQLiteDatabase db = this.getReadableDatabase();
 		String[] selectionArgs = {account.getUuid(),

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

@@ -43,6 +43,7 @@ import java.security.DigestOutputStream;
 import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
 import java.text.SimpleDateFormat;
+import java.util.ArrayList;
 import java.util.Date;
 import java.util.List;
 import java.util.Locale;
@@ -240,12 +241,12 @@ public class FileBackend {
     }
 
     public Bitmap getPreviewForUri(Attachment attachment, int size, boolean cacheOnly) {
+        final String key = "attachment_"+attachment.getUuid().toString()+"_"+String.valueOf(size);
         final LruCache<String, Bitmap> cache = mXmppConnectionService.getBitmapCache();
-        Bitmap bitmap = cache.get(attachment.getUuid().toString());
+        Bitmap bitmap = cache.get(key);
         if (bitmap != null || cacheOnly) {
             return bitmap;
         }
-        Log.d(Config.LOGTAG,"attachment mime="+attachment.getMime());
         if (attachment.getMime() != null && attachment.getMime().startsWith("video/")) {
             bitmap = cropCenterSquareVideo(attachment.getUri(), size);
             drawOverlay(bitmap, paintOverlayBlack(bitmap) ? R.drawable.play_video_black : R.drawable.play_video_white, 0.75f);
@@ -258,7 +259,7 @@ public class FileBackend {
                 bitmap = withGifOverlay;
             }
         }
-        cache.put(attachment.getUuid().toString(), bitmap);
+        cache.put(key, bitmap);
         return bitmap;
     }
 
@@ -452,7 +453,22 @@ public class FileBackend {
         }
     }
 
-    public String getConversationsDirectory(final String type) {
+    public List<Attachment> convertToAttachments(List<DatabaseBackend.FilePath> relativeFilePaths) {
+        List<Attachment> attachments = new ArrayList<>();
+        for(DatabaseBackend.FilePath relativeFilePath : relativeFilePaths) {
+            final String mime = MimeUtils.guessMimeTypeFromExtension(MimeUtils.extractRelevantExtension(relativeFilePath.path));
+            Log.d(Config.LOGTAG,"mime="+mime);
+            File file = getFileForPath(relativeFilePath.path, mime);
+            if (file.exists()) {
+                attachments.add(Attachment.of(relativeFilePath.uuid, file,mime));
+            } else {
+                Log.d(Config.LOGTAG,"file "+file.getAbsolutePath()+" doesnt exist");
+            }
+        }
+        return attachments;
+    }
+
+    private String getConversationsDirectory(final String type) {
         return getConversationsDirectory(mXmppConnectionService, type);
     }
 

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

@@ -106,7 +106,9 @@ import eu.siacs.conversations.persistance.FileBackend;
 import eu.siacs.conversations.ui.SettingsActivity;
 import eu.siacs.conversations.ui.UiCallback;
 import eu.siacs.conversations.ui.interfaces.OnAvatarPublication;
+import eu.siacs.conversations.ui.interfaces.OnMediaLoaded;
 import eu.siacs.conversations.ui.interfaces.OnSearchResultsAvailable;
+import eu.siacs.conversations.ui.util.Attachment;
 import eu.siacs.conversations.utils.Compatibility;
 import eu.siacs.conversations.utils.ConversationsFileObserver;
 import eu.siacs.conversations.utils.CryptoHelper;
@@ -2418,6 +2420,16 @@ public class XmppConnectionService extends Service {
 		return false;
 	}
 
+
+	public void getAttachments(final Conversation conversation, int limit, final OnMediaLoaded onMediaLoaded) {
+        getAttachments(conversation.getAccount(), conversation.getJid().asBareJid(), limit, onMediaLoaded);
+    }
+
+
+	public void getAttachments(final Account account, final Jid jid, final int limit, final OnMediaLoaded onMediaLoaded) {
+        new Thread(() -> onMediaLoaded.onMediaLoaded(fileBackend.convertToAttachments(databaseBackend.getRelativeFilePaths(account, jid, limit)))).start();
+    }
+
 	public void persistSelfNick(MucOptions.User self) {
 		final Conversation conversation = self.getConversation();
 		Jid full = self.getFullJid();

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

@@ -11,13 +11,12 @@ import android.graphics.drawable.BitmapDrawable;
 import android.graphics.drawable.Drawable;
 import android.os.AsyncTask;
 import android.os.Bundle;
-import android.preference.PreferenceManager;
+import android.support.v4.content.ContextCompat;
 import android.support.v7.app.AlertDialog;
 import android.support.v7.widget.Toolbar;
 import android.text.Editable;
 import android.text.SpannableStringBuilder;
 import android.text.TextWatcher;
-import android.util.Log;
 import android.view.ContextMenu;
 import android.view.LayoutInflater;
 import android.view.Menu;
@@ -32,6 +31,7 @@ import org.openintents.openpgp.util.OpenPgpUtils;
 import java.lang.ref.WeakReference;
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.List;
 import java.util.concurrent.RejectedExecutionException;
 import java.util.concurrent.atomic.AtomicInteger;
 
@@ -49,6 +49,10 @@ import eu.siacs.conversations.entities.MucOptions.User;
 import eu.siacs.conversations.services.XmppConnectionService;
 import eu.siacs.conversations.services.XmppConnectionService.OnConversationUpdate;
 import eu.siacs.conversations.services.XmppConnectionService.OnMucRosterUpdate;
+import eu.siacs.conversations.ui.adapter.MediaAdapter;
+import eu.siacs.conversations.ui.interfaces.OnMediaLoaded;
+import eu.siacs.conversations.ui.util.Attachment;
+import eu.siacs.conversations.ui.util.GridManager;
 import eu.siacs.conversations.ui.util.MenuDoubleTabUtil;
 import eu.siacs.conversations.ui.util.MucDetailsContextMenuHelper;
 import eu.siacs.conversations.ui.util.MyLinkify;
@@ -63,7 +67,7 @@ import rocks.xmpp.addr.Jid;
 import static eu.siacs.conversations.entities.Bookmark.printableValue;
 import static eu.siacs.conversations.utils.StringUtils.changed;
 
-public class ConferenceDetailsActivity extends XmppActivity implements OnConversationUpdate, OnMucRosterUpdate, XmppConnectionService.OnAffiliationChanged, XmppConnectionService.OnRoleChanged, XmppConnectionService.OnConfigurationPushed, TextWatcher {
+public class ConferenceDetailsActivity extends XmppActivity implements OnConversationUpdate, OnMucRosterUpdate, XmppConnectionService.OnAffiliationChanged, XmppConnectionService.OnRoleChanged, XmppConnectionService.OnConfigurationPushed, TextWatcher, OnMediaLoaded {
     public static final String ACTION_VIEW_MUC = "view_muc";
 
     private static final float INACTIVE_ALPHA = 0.4684f; //compromise between dark and light theme
@@ -77,6 +81,7 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
         }
     };
     private ActivityMucDetailsBinding binding;
+    private MediaAdapter mMediaAdapter;
     private String uuid = null;
     private User mSelectedUser = null;
 
@@ -273,6 +278,9 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
         this.binding.mucEditTitle.addTextChangedListener(this);
         this.binding.mucEditSubject.addTextChangedListener(this);
         this.binding.mucEditSubject.addTextChangedListener(new StylingHelper.MessageEditorStyler(this.binding.mucEditSubject));
+        mMediaAdapter = new MediaAdapter(this,R.dimen.media_size);
+        this.binding.media.setAdapter(mMediaAdapter);
+        GridManager.setupLayoutManager(this, this.binding.media, R.dimen.media_size);
     }
 
     @Override
@@ -442,6 +450,16 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
         return true;
     }
 
+    @Override
+    public void onMediaLoaded(List<Attachment> attachments) {
+        runOnUiThread(() -> {
+            int limit = GridManager.getCurrentColumnCount(binding.media);
+            mMediaAdapter.setAttachments(attachments.subList(0, Math.min(limit,attachments.size())));
+            binding.mediaWrapper.setVisibility(attachments.size() > 0 ? View.VISIBLE : View.GONE);
+        });
+
+    }
+
 
     protected void saveAsBookmark() {
         xmppConnectionService.saveConversationAsBookmark(mConversation, mConversation.getMucOptions().getName());
@@ -468,6 +486,8 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
         if (uuid != null) {
             this.mConversation = xmppConnectionService.findConversationByUuid(uuid);
             if (this.mConversation != null) {
+                final int limit = GridManager.getCurrentColumnCount(this.binding.media);
+                xmppConnectionService.getAttachments(this.mConversation, limit, this);
                 updateView();
             }
         }

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

@@ -1,6 +1,7 @@
 package eu.siacs.conversations.ui;
 
 import android.content.ActivityNotFoundException;
+import android.content.Context;
 import android.content.DialogInterface;
 import android.content.Intent;
 import android.content.SharedPreferences;
@@ -11,13 +12,18 @@ import android.preference.PreferenceManager;
 import android.provider.ContactsContract.CommonDataKinds;
 import android.provider.ContactsContract.Contacts;
 import android.provider.ContactsContract.Intents;
+import android.support.v4.content.ContextCompat;
 import android.support.v7.app.AlertDialog;
+import android.support.v7.widget.GridLayoutManager;
+import android.support.v7.widget.RecyclerView;
 import android.support.v7.widget.Toolbar;
+import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.Menu;
 import android.view.MenuItem;
 import android.view.View;
 import android.view.View.OnClickListener;
+import android.view.ViewTreeObserver;
 import android.widget.CompoundButton;
 import android.widget.CompoundButton.OnCheckedChangeListener;
 import android.widget.TextView;
@@ -25,6 +31,7 @@ import android.widget.Toast;
 
 import org.openintents.openpgp.util.OpenPgpUtils;
 
+import java.util.Collections;
 import java.util.List;
 
 import eu.siacs.conversations.Config;
@@ -38,6 +45,10 @@ import eu.siacs.conversations.entities.Contact;
 import eu.siacs.conversations.entities.ListItem;
 import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate;
 import eu.siacs.conversations.services.XmppConnectionService.OnRosterUpdate;
+import eu.siacs.conversations.ui.adapter.MediaAdapter;
+import eu.siacs.conversations.ui.interfaces.OnMediaLoaded;
+import eu.siacs.conversations.ui.util.Attachment;
+import eu.siacs.conversations.ui.util.GridManager;
 import eu.siacs.conversations.ui.util.MenuDoubleTabUtil;
 import eu.siacs.conversations.utils.IrregularUnicodeDetector;
 import eu.siacs.conversations.utils.UIHelper;
@@ -48,9 +59,12 @@ import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
 import eu.siacs.conversations.xmpp.XmppConnection;
 import rocks.xmpp.addr.Jid;
 
-public class ContactDetailsActivity extends OmemoActivity implements OnAccountUpdate, OnRosterUpdate, OnUpdateBlocklist, OnKeyStatusUpdated {
+public class ContactDetailsActivity extends OmemoActivity implements OnAccountUpdate, OnRosterUpdate, OnUpdateBlocklist, OnKeyStatusUpdated, OnMediaLoaded {
     public static final String ACTION_VIEW_CONTACT = "view_contact";
     ActivityContactDetailsBinding binding;
+
+    private MediaAdapter mMediaAdapter;
+
     private Contact contact;
     private DialogInterface.OnClickListener removeFromRoster = new DialogInterface.OnClickListener() {
 
@@ -185,6 +199,10 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
             populateView();
         });
         binding.addContactButton.setOnClickListener(v -> showAddToRosterDialog(contact));
+
+        mMediaAdapter = new MediaAdapter(this,R.dimen.media_size);
+        this.binding.media.setAdapter(mMediaAdapter);
+        GridManager.setupLayoutManager(this, this.binding.media, R.dimen.media_size);
     }
 
     @Override
@@ -204,6 +222,7 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
             this.showDynamicTags = preferences.getBoolean(SettingsActivity.SHOW_DYNAMIC_TAGS, false);
             this.showLastSeen = preferences.getBoolean("last_activity", false);
         }
+        mMediaAdapter.setAttachments(Collections.emptyList());
     }
 
     @Override
@@ -466,6 +485,9 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
                 processFingerprintVerification(mPendingFingerprintVerificationUri);
                 mPendingFingerprintVerificationUri = null;
             }
+
+            final int limit = GridManager.getCurrentColumnCount(this.binding.media);
+            xmppConnectionService.getAttachments(account, contact.getJid().asBareJid(), limit, this);
             populateView();
         }
     }
@@ -485,4 +507,14 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
             Toast.makeText(this, R.string.invalid_barcode, Toast.LENGTH_SHORT).show();
         }
     }
+
+    @Override
+    public void onMediaLoaded(List<Attachment> attachments) {
+        runOnUiThread(() -> {
+            int limit = GridManager.getCurrentColumnCount(binding.media);
+            mMediaAdapter.setAttachments(attachments.subList(0, Math.min(limit,attachments.size())));
+            binding.mediaWrapper.setVisibility(attachments.size() > 0 ? View.VISIBLE : View.GONE);
+        });
+
+    }
 }

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

@@ -0,0 +1,225 @@
+package eu.siacs.conversations.ui.adapter;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.databinding.DataBindingUtil;
+import android.graphics.Bitmap;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.os.AsyncTask;
+import android.support.annotation.AttrRes;
+import android.support.annotation.DimenRes;
+import android.support.annotation.NonNull;
+import android.support.v7.widget.RecyclerView;
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.RejectedExecutionException;
+
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.databinding.MediaBinding;
+import eu.siacs.conversations.ui.XmppActivity;
+import eu.siacs.conversations.ui.util.Attachment;
+import eu.siacs.conversations.ui.util.StyledAttributes;
+import eu.siacs.conversations.ui.util.ViewUtil;
+
+public class MediaAdapter extends RecyclerView.Adapter<MediaAdapter.MediaViewHolder> {
+
+    private static final List<String> DOCUMENT_MIMES = Arrays.asList(
+            "application/pdf",
+            "application/vnd.oasis.opendocument.text",
+            "application/msword",
+            "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
+            "text/x-tex",
+            "text/plain"
+    );
+
+    private final ArrayList<Attachment> attachments = new ArrayList<>();
+
+    private final XmppActivity activity;
+
+    private int mediaSize = 0;
+
+    public MediaAdapter(XmppActivity activity, @DimenRes int mediaSize) {
+        this.activity = activity;
+        this.mediaSize = Math.round(activity.getResources().getDimension(mediaSize));
+    }
+
+    public static void setMediaSize(RecyclerView recyclerView, int mediaSize) {
+        RecyclerView.Adapter adapter = recyclerView.getAdapter();
+        if (adapter instanceof MediaAdapter) {
+            ((MediaAdapter) adapter).setMediaSize(mediaSize);
+        }
+    }
+
+    private static @AttrRes int getImageAttr(Attachment attachment) {
+        final @AttrRes int attr;
+        if (attachment.getType() == Attachment.Type.LOCATION) {
+            attr = R.attr.media_preview_location;
+        } else if (attachment.getType() == Attachment.Type.RECORDING) {
+            attr = R.attr.media_preview_recording;
+        } else {
+            final String mime = attachment.getMime();
+            if (mime == null) {
+                attr = R.attr.media_preview_unknown;
+            } else if (mime.startsWith("audio/")) {
+                attr = R.attr.media_preview_audio;
+            } else if (mime.equals("text/calendar") || (mime.equals("text/x-vcalendar"))) {
+                attr = R.attr.media_preview_calendar;
+            } else if (mime.equals("text/x-vcard")) {
+                attr = R.attr.media_preview_contact;
+            } else if (mime.equals("application/vnd.android.package-archive")) {
+                attr = R.attr.media_preview_app;
+            } else if (mime.equals("application/zip") || mime.equals("application/rar")) {
+                attr = R.attr.media_preview_archive;
+            } else if (DOCUMENT_MIMES.contains(mime)) {
+                attr = R.attr.media_preview_document;
+            } else {
+                attr = R.attr.media_preview_unknown;
+            }
+        }
+        return attr;
+    }
+
+    public static void renderPreview(Context context, Attachment attachment, ImageView imageView) {
+        imageView.setBackgroundColor(StyledAttributes.getColor(context, R.attr.color_background_tertiary));
+        imageView.setImageAlpha(Math.round(StyledAttributes.getFloat(context, R.attr.icon_alpha) * 255));
+        imageView.setImageDrawable(StyledAttributes.getDrawable(context, getImageAttr(attachment)));
+    }
+
+    private static boolean cancelPotentialWork(Attachment attachment, ImageView imageView) {
+        final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
+
+        if (bitmapWorkerTask != null) {
+            final Attachment oldAttachment = bitmapWorkerTask.attachment;
+            if (oldAttachment == null || !oldAttachment.equals(attachment)) {
+                bitmapWorkerTask.cancel(true);
+            } else {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) {
+        if (imageView != null) {
+            final Drawable drawable = imageView.getDrawable();
+            if (drawable instanceof AsyncDrawable) {
+                final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
+                return asyncDrawable.getBitmapWorkerTask();
+            }
+        }
+        return null;
+    }
+
+    @NonNull
+    @Override
+    public MediaViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+        LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
+        MediaBinding binding = DataBindingUtil.inflate(layoutInflater, R.layout.media, parent, false);
+        return new MediaViewHolder(binding);
+    }
+
+    @Override
+    public void onBindViewHolder(@NonNull MediaViewHolder holder, int position) {
+        final Attachment attachment = attachments.get(position);
+        if (attachment.renderThumbnail()) {
+            holder.binding.media.setImageAlpha(255);
+            loadPreview(attachment, holder.binding.media);
+        } else {
+            cancelPotentialWork(attachment, holder.binding.media);
+            renderPreview(activity, attachment, holder.binding.media);
+        }
+        holder.binding.media.setOnClickListener(v -> ViewUtil.view(activity, attachment));
+    }
+
+    public void setAttachments(List<Attachment> attachments) {
+        this.attachments.clear();
+        this.attachments.addAll(attachments);
+        notifyDataSetChanged();
+    }
+
+    private void setMediaSize(int mediaSize) {
+        this.mediaSize = mediaSize;
+    }
+
+    private void loadPreview(Attachment attachment, ImageView imageView) {
+        if (cancelPotentialWork(attachment, imageView)) {
+            final Bitmap bm = activity.xmppConnectionService.getFileBackend().getPreviewForUri(attachment,mediaSize,true);
+            if (bm != null) {
+                cancelPotentialWork(attachment, imageView);
+                imageView.setImageBitmap(bm);
+                imageView.setBackgroundColor(0x00000000);
+            } else {
+                imageView.setBackgroundColor(0xff333333);
+                imageView.setImageDrawable(null);
+                final BitmapWorkerTask task = new BitmapWorkerTask(imageView);
+                final AsyncDrawable asyncDrawable = new AsyncDrawable(activity.getResources(), null, task);
+                imageView.setImageDrawable(asyncDrawable);
+                try {
+                    task.execute(attachment);
+                } catch (final RejectedExecutionException ignored) {
+                }
+            }
+        }
+    }
+
+    @Override
+    public int getItemCount() {
+        return attachments.size();
+    }
+
+    static class AsyncDrawable extends BitmapDrawable {
+        private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference;
+
+        AsyncDrawable(Resources res, Bitmap bitmap, BitmapWorkerTask bitmapWorkerTask) {
+            super(res, bitmap);
+            bitmapWorkerTaskReference = new WeakReference<>(bitmapWorkerTask);
+        }
+
+        BitmapWorkerTask getBitmapWorkerTask() {
+            return bitmapWorkerTaskReference.get();
+        }
+    }
+
+    class MediaViewHolder extends RecyclerView.ViewHolder {
+
+        private final MediaBinding binding;
+
+        MediaViewHolder(MediaBinding binding) {
+            super(binding.getRoot());
+            this.binding = binding;
+        }
+    }
+
+    class BitmapWorkerTask extends AsyncTask<Attachment, Void, Bitmap> {
+        private final WeakReference<ImageView> imageViewReference;
+        private Attachment attachment = null;
+
+        BitmapWorkerTask(ImageView imageView) {
+            imageViewReference = new WeakReference<>(imageView);
+        }
+
+        @Override
+        protected Bitmap doInBackground(Attachment... params) {
+            this.attachment = params[0];
+            return activity.xmppConnectionService.getFileBackend().getPreviewForUri(this.attachment, mediaSize, false);
+        }
+
+        @Override
+        protected void onPostExecute(Bitmap bitmap) {
+            if (bitmap != null && !isCancelled()) {
+                final ImageView imageView = imageViewReference.get();
+                if (imageView != null) {
+                    imageView.setImageBitmap(bitmap);
+                    imageView.setBackgroundColor(0x00000000);
+                }
+            }
+        }
+    }
+}

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

@@ -30,15 +30,6 @@ import eu.siacs.conversations.ui.util.StyledAttributes;
 
 public class MediaPreviewAdapter extends RecyclerView.Adapter<MediaPreviewAdapter.MediaPreviewViewHolder> {
 
-    private static final List<String> DOCUMENT_MIMES = Arrays.asList(
-            "application/pdf",
-            "application/vnd.oasis.opendocument.text",
-            "application/msword",
-            "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
-            "text/x-tex",
-            "text/plain"
-    );
-
     private final ArrayList<Attachment> mediaPreviews = new ArrayList<>();
 
     private final ConversationFragment conversationFragment;
@@ -64,34 +55,7 @@ public class MediaPreviewAdapter extends RecyclerView.Adapter<MediaPreviewAdapte
             loadPreview(attachment, holder.binding.mediaPreview);
         } else {
             cancelPotentialWork(attachment, holder.binding.mediaPreview);
-            holder.binding.mediaPreview.setBackgroundColor(StyledAttributes.getColor(context, R.attr.color_background_tertiary));
-            holder.binding.mediaPreview.setImageAlpha(Math.round(StyledAttributes.getFloat(context, R.attr.icon_alpha) * 255));
-            final @AttrRes int attr;
-            if (attachment.getType() == Attachment.Type.LOCATION) {
-                attr = R.attr.media_preview_location;
-            } else if (attachment.getType() == Attachment.Type.RECORDING) {
-                attr = R.attr.media_preview_recording;
-            } else {
-                final String mime = attachment.getMime();
-                if (mime == null) {
-                    attr = R.attr.media_preview_unknown;
-                } else if (mime.startsWith("audio/")) {
-                    attr = R.attr.media_preview_audio;
-                } else if (mime.equals("text/calendar") || (mime.equals("text/x-vcalendar"))) {
-                    attr = R.attr.media_preview_calendar;
-                } else if (mime.equals("text/x-vcard")) {
-                    attr = R.attr.media_preview_contact;
-                } else if (mime.equals("application/vnd.android.package-archive")) {
-                    attr = R.attr.media_preview_app;
-                } else if (mime.equals("application/zip") || mime.equals("application/rar")) {
-                    attr = R.attr.media_preview_archive;
-                } else if (DOCUMENT_MIMES.contains(mime)) {
-                    attr = R.attr.media_preview_document;
-                } else {
-                    attr = R.attr.media_preview_unknown;
-                }
-            }
-            holder.binding.mediaPreview.setImageDrawable(StyledAttributes.getDrawable(context, attr));
+            MediaAdapter.renderPreview(context, attachment, holder.binding.mediaPreview);
         }
         holder.binding.deleteButton.setOnClickListener(v -> {
             int pos = mediaPreviews.indexOf(attachment);

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

@@ -71,6 +71,7 @@ import eu.siacs.conversations.ui.service.AudioPlayer;
 import eu.siacs.conversations.ui.text.DividerSpan;
 import eu.siacs.conversations.ui.text.QuoteSpan;
 import eu.siacs.conversations.ui.util.MyLinkify;
+import eu.siacs.conversations.ui.util.ViewUtil;
 import eu.siacs.conversations.ui.widget.ClickableMovementMethod;
 import eu.siacs.conversations.ui.widget.CopyTextView;
 import eu.siacs.conversations.ui.widget.ListSelectionManager;
@@ -896,31 +897,11 @@ public class MessageAdapter extends ArrayAdapter<Message> implements CopyTextVie
 			Toast.makeText(activity, R.string.file_deleted, Toast.LENGTH_SHORT).show();
 			return;
 		}
-		Intent openIntent = new Intent(Intent.ACTION_VIEW);
 		String mime = file.getMimeType();
 		if (mime == null) {
 			mime = "*/*";
 		}
-		Uri uri;
-		try {
-			uri = FileBackend.getUriForFile(activity, file);
-		} catch (SecurityException e) {
-			Log.d(Config.LOGTAG, "No permission to access " + file.getAbsolutePath(), e);
-			Toast.makeText(activity, activity.getString(R.string.no_permission_to_access_x, file.getAbsolutePath()), Toast.LENGTH_SHORT).show();
-			return;
-		}
-		openIntent.setDataAndType(uri, mime);
-		openIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
-		PackageManager manager = activity.getPackageManager();
-		List<ResolveInfo> info = manager.queryIntentActivities(openIntent, 0);
-		if (info.size() == 0) {
-			openIntent.setDataAndType(uri, "*/*");
-		}
-		try {
-			getContext().startActivity(openIntent);
-		} catch (ActivityNotFoundException e) {
-			Toast.makeText(activity, R.string.no_application_found_to_open_file, Toast.LENGTH_SHORT).show();
-		}
+		ViewUtil.view(activity, file, mime);
 	}
 
 	public void showLocation(Message message) {

src/main/java/eu/siacs/conversations/ui/util/Attachment.java 🔗

@@ -37,6 +37,7 @@ import android.os.Parcel;
 import android.os.Parcelable;
 import android.util.Log;
 
+import java.io.File;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
@@ -44,7 +45,6 @@ import java.util.UUID;
 
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.utils.MimeUtils;
-import eu.siacs.conversations.xmpp.stanzas.IqPacket;
 
 public class Attachment implements Parcelable {
 
@@ -97,6 +97,13 @@ public class Attachment implements Parcelable {
     private final UUID uuid;
     private final String mime;
 
+    private Attachment(UUID uuid, Uri uri, Type type, String mime) {
+        this.uri = uri;
+        this.type = type;
+        this.mime = mime;
+        this.uuid = uuid;
+    }
+
     private Attachment(Uri uri, Type type, String mime) {
         this.uri = uri;
         this.type = type;
@@ -118,6 +125,10 @@ public class Attachment implements Parcelable {
         return attachments;
     }
 
+    public static Attachment of(UUID uuid, final File file, String mime) {
+        return new Attachment(uuid, Uri.fromFile(file),mime != null && (mime.startsWith("image/") || mime.startsWith("video/")) ? Type.IMAGE : Type.FILE, mime);
+    }
+
     public static List<Attachment> extractAttachments(final Context context, final Intent intent, Type type) {
         List<Attachment> uris = new ArrayList<>();
         if (intent == null) {

src/main/java/eu/siacs/conversations/ui/util/GridManager.java 🔗

@@ -0,0 +1,71 @@
+package eu.siacs.conversations.ui.util;
+
+import android.content.Context;
+import android.support.annotation.DimenRes;
+import android.support.v7.widget.GridLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.util.Log;
+import android.view.ViewTreeObserver;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.ui.adapter.MediaAdapter;
+
+public class GridManager {
+
+    public static void setupLayoutManager(final Context context, RecyclerView recyclerView, @DimenRes int desiredSize) {
+        int maxWidth = context.getResources().getDisplayMetrics().widthPixels;
+        ColumnInfo columnInfo = calculateColumnCount(context, maxWidth, desiredSize);
+        Log.d(Config.LOGTAG, "preliminary count=" + columnInfo.count);
+        MediaAdapter.setMediaSize(recyclerView, columnInfo.width);
+        recyclerView.setLayoutManager(new GridLayoutManager(context, columnInfo.count));
+        recyclerView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
+            @Override
+            public void onGlobalLayout() {
+                recyclerView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
+                final ColumnInfo columnInfo = calculateColumnCount(context, recyclerView.getMeasuredWidth(), desiredSize);
+                Log.d(Config.LOGTAG, "final count " + columnInfo.count);
+                if (recyclerView.getAdapter().getItemCount() != 0) {
+                    Log.e(Config.LOGTAG, "adapter already has items; just go with it now");
+                    return;
+                }
+                setupLayoutManagerInternal(recyclerView, columnInfo);
+                MediaAdapter.setMediaSize(recyclerView, columnInfo.width);
+            }
+        });
+    }
+
+    private static void setupLayoutManagerInternal(RecyclerView recyclerView, final ColumnInfo columnInfo) {
+        RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
+        if (layoutManager instanceof GridLayoutManager) {
+            ((GridLayoutManager) layoutManager).setSpanCount(columnInfo.count);
+        }
+    }
+
+    private static ColumnInfo calculateColumnCount(Context context, int availableWidth, @DimenRes int desiredSize) {
+        final float desiredWidth = context.getResources().getDimension(desiredSize);
+        final int columns = Math.round(availableWidth / desiredWidth);
+        final int realWidth = availableWidth / columns;
+        Log.d(Config.LOGTAG, "desired=" + desiredWidth + " real=" + realWidth);
+        return new ColumnInfo(columns, realWidth);
+    }
+
+    public static int getCurrentColumnCount(RecyclerView recyclerView) {
+        RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
+        if (layoutManager instanceof GridLayoutManager) {
+            return ((GridLayoutManager) layoutManager).getSpanCount();
+        } else {
+            return 0;
+        }
+    }
+
+    public static class ColumnInfo {
+        private final int count;
+        private final int width;
+
+        private ColumnInfo(int count, int width) {
+            this.count = count;
+            this.width = width;
+        }
+    }
+
+}

src/main/java/eu/siacs/conversations/ui/util/ViewUtil.java 🔗

@@ -0,0 +1,51 @@
+package eu.siacs.conversations.ui.util;
+
+import android.content.ActivityNotFoundException;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.net.Uri;
+import android.util.Log;
+import android.widget.Toast;
+
+import java.io.File;
+import java.util.List;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.persistance.FileBackend;
+
+public class ViewUtil {
+
+    public static void view(Context context, Attachment attachment) {
+        File file = new File(attachment.getUri().getPath());
+        final String mime = attachment.getMime() == null ? "*/*" : attachment.getMime();
+        view(context, file, mime);
+    }
+
+    public static void view(Context context, File file, String mime) {
+        Intent openIntent = new Intent(Intent.ACTION_VIEW);
+        Uri uri;
+        try {
+            uri = FileBackend.getUriForFile(context, file);
+        } catch (SecurityException e) {
+            Log.d(Config.LOGTAG, "No permission to access " + file.getAbsolutePath(), e);
+            Toast.makeText(context, context.getString(R.string.no_permission_to_access_x, file.getAbsolutePath()), Toast.LENGTH_SHORT).show();
+            return;
+        }
+        openIntent.setDataAndType(uri, mime);
+        openIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+        PackageManager manager = context.getPackageManager();
+        List<ResolveInfo> info = manager.queryIntentActivities(openIntent, 0);
+        if (info.size() == 0) {
+            openIntent.setDataAndType(uri, "*/*");
+        }
+        try {
+            context.startActivity(openIntent);
+        } catch (ActivityNotFoundException e) {
+            Toast.makeText(context, R.string.no_application_found_to_open_file, Toast.LENGTH_SHORT).show();
+        }
+    }
+
+}

src/main/java/eu/siacs/conversations/ui/widget/SquareFrameLayout.java 🔗

@@ -0,0 +1,25 @@
+package eu.siacs.conversations.ui.widget;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.FrameLayout;
+
+public class SquareFrameLayout extends FrameLayout {
+    public SquareFrameLayout(Context context) {
+        super(context);
+    }
+
+    public SquareFrameLayout(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public SquareFrameLayout(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+    }
+
+    @Override
+    public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        //noinspection SuspiciousNameCombination
+        super.onMeasure(widthMeasureSpec, widthMeasureSpec);
+    }
+}

src/main/res/layout/activity_contact_details.xml 🔗

@@ -1,6 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 <layout xmlns:android="http://schemas.android.com/apk/res/android"
-        xmlns:app="http://schemas.android.com/apk/res-auto">
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools">
 
     <LinearLayout
         android:layout_width="match_parent"
@@ -10,7 +11,7 @@
 
         <include
             android:id="@+id/toolbar"
-            layout="@layout/toolbar"/>
+            layout="@layout/toolbar" />
 
         <ScrollView
             android:layout_width="fill_parent"
@@ -40,7 +41,7 @@
                             android:layout_width="@dimen/avatar_on_details_screen_size"
                             android:layout_height="@dimen/avatar_on_details_screen_size"
                             android:layout_alignParentTop="true"
-                            android:scaleType="centerCrop"/>
+                            android:scaleType="centerCrop" />
 
                         <LinearLayout
                             android:id="@+id/details_jidbox"
@@ -55,7 +56,7 @@
                                 android:layout_width="wrap_content"
                                 android:layout_height="wrap_content"
                                 android:text="@string/account_settings_example_jabber_id"
-                                android:textAppearance="@style/TextAppearance.Conversations.Title"/>
+                                android:textAppearance="@style/TextAppearance.Conversations.Title" />
 
                             <com.wefika.flowlayout.FlowLayout
                                 android:id="@+id/tags"
@@ -64,29 +65,28 @@
                                 android:layout_marginBottom="4dp"
                                 android:layout_marginLeft="-2dp"
                                 android:layout_marginTop="4dp"
-                                android:orientation="horizontal">
-                            </com.wefika.flowlayout.FlowLayout>
+                                android:orientation="horizontal"></com.wefika.flowlayout.FlowLayout>
 
                             <TextView
                                 android:id="@+id/details_lastseen"
                                 android:layout_width="wrap_content"
                                 android:layout_height="wrap_content"
                                 android:layout_marginTop="4dp"
-                                android:textAppearance="@style/TextAppearance.Conversations.Subhead"/>
+                                android:textAppearance="@style/TextAppearance.Conversations.Subhead" />
 
                             <TextView
                                 android:id="@+id/status_message"
                                 android:layout_width="wrap_content"
                                 android:layout_height="wrap_content"
                                 android:layout_marginTop="8dp"
-                                android:textAppearance="@style/TextAppearance.Conversations.Body1"/>
+                                android:textAppearance="@style/TextAppearance.Conversations.Body1" />
 
                             <Button
                                 android:id="@+id/add_contact_button"
                                 android:layout_width="wrap_content"
                                 android:layout_height="wrap_content"
                                 android:layout_marginTop="8dp"
-                                android:text="@string/add_contact"/>
+                                android:text="@string/add_contact" />
 
                             <CheckBox
                                 android:id="@+id/details_send_presence"
@@ -95,7 +95,7 @@
                                 android:layout_height="wrap_content"
                                 android:layout_marginTop="8dp"
                                 android:text="@string/send_presence_updates"
-                                android:textAppearance="@style/TextAppearance.Conversations.Body1"/>
+                                android:textAppearance="@style/TextAppearance.Conversations.Body1" />
 
                             <CheckBox
                                 android:id="@+id/details_receive_presence"
@@ -103,7 +103,7 @@
                                 android:layout_width="wrap_content"
                                 android:layout_height="wrap_content"
                                 android:text="@string/receive_presence_updates"
-                                android:textAppearance="@style/TextAppearance.Conversations.Body1"/>
+                                android:textAppearance="@style/TextAppearance.Conversations.Body1" />
                         </LinearLayout>
 
                         <TextView
@@ -114,10 +114,56 @@
                             android:layout_below="@+id/details_jidbox"
                             android:layout_marginTop="32dp"
                             android:text="@string/using_account"
-                            android:textAppearance="@style/TextAppearance.Conversations.Caption"/>
+                            android:textAppearance="@style/TextAppearance.Conversations.Caption" />
                     </RelativeLayout>
                 </android.support.v7.widget.CardView>
 
+                <android.support.v7.widget.CardView
+                    android:id="@+id/media_wrapper"
+                    android:layout_width="fill_parent"
+                    android:layout_height="wrap_content"
+                    android:layout_marginBottom="@dimen/activity_vertical_margin"
+                    android:layout_marginLeft="@dimen/activity_horizontal_margin"
+                    android:layout_marginRight="@dimen/activity_horizontal_margin"
+                    android:layout_marginTop="@dimen/activity_vertical_margin">
+
+                    <LinearLayout
+                        android:layout_width="match_parent"
+                        android:layout_height="wrap_content"
+                        android:orientation="vertical">
+
+                        <android.support.v7.widget.RecyclerView
+                            android:id="@+id/media"
+                            android:layout_width="match_parent"
+                            android:layout_height="wrap_content"
+                            android:orientation="horizontal"
+                            android:paddingEnd="@dimen/card_padding_regular"
+                            android:paddingStart="@dimen/card_padding_regular"
+                            android:paddingTop="@dimen/card_padding_regular"
+                            android:paddingBottom="@dimen/card_padding_list"
+                            android:layout_marginStart="-2dp"
+                            android:layout_marginEnd="-2dp"/>
+
+                        <LinearLayout
+                            android:layout_width="wrap_content"
+                            android:layout_height="match_parent"
+                            android:orientation="horizontal"
+                            android:layout_gravity="end">
+
+                            <Button
+                                android:id="@+id/show_media"
+                                style="@style/Widget.Conversations.Button.Borderless"
+                                android:layout_width="wrap_content"
+                                android:layout_height="wrap_content"
+                                android:minWidth="0dp"
+                                android:paddingLeft="16dp"
+                                android:paddingRight="16dp"
+                                android:text="@string/show_media"
+                                android:textColor="?attr/colorAccent" />
+                        </LinearLayout>
+                    </LinearLayout>
+                </android.support.v7.widget.CardView>
+
                 <android.support.v7.widget.CardView
                     android:id="@+id/keys_wrapper"
                     android:layout_width="fill_parent"
@@ -137,8 +183,7 @@
                             android:layout_width="match_parent"
                             android:layout_height="wrap_content"
                             android:orientation="vertical"
-                            android:padding="@dimen/card_padding_list">
-                        </LinearLayout>
+                            android:padding="@dimen/card_padding_list"/>
 
                         <LinearLayout
                             android:layout_width="wrap_content"
@@ -156,7 +201,7 @@
                                 android:paddingLeft="16dp"
                                 android:paddingRight="16dp"
                                 android:text="@string/scan_qr_code"
-                                android:textColor="?attr/colorAccent"/>
+                                android:textColor="?attr/colorAccent" />
 
                             <Button
                                 android:id="@+id/show_inactive_devices"
@@ -167,7 +212,7 @@
                                 android:paddingLeft="16dp"
                                 android:paddingRight="16dp"
                                 android:text="@string/show_inactive_devices"
-                                android:textColor="?attr/colorAccent"/>
+                                android:textColor="?attr/colorAccent" />
                         </LinearLayout>
                     </LinearLayout>
                 </android.support.v7.widget.CardView>

src/main/res/layout/activity_muc_details.xml 🔗

@@ -1,6 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 <layout xmlns:android="http://schemas.android.com/apk/res/android"
-        xmlns:app="http://schemas.android.com/apk/res-auto">
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools">
 
     <LinearLayout
         android:layout_width="match_parent"
@@ -207,6 +208,52 @@
                     </LinearLayout>
                 </android.support.v7.widget.CardView>
 
+                <android.support.v7.widget.CardView
+                    android:id="@+id/media_wrapper"
+                    android:layout_width="fill_parent"
+                    android:layout_height="wrap_content"
+                    android:layout_marginBottom="@dimen/activity_vertical_margin"
+                    android:layout_marginLeft="@dimen/activity_horizontal_margin"
+                    android:layout_marginRight="@dimen/activity_horizontal_margin"
+                    android:layout_marginTop="@dimen/activity_vertical_margin">
+
+                    <LinearLayout
+                        android:layout_width="match_parent"
+                        android:layout_height="wrap_content"
+                        android:orientation="vertical">
+
+                        <android.support.v7.widget.RecyclerView
+                            android:id="@+id/media"
+                            android:layout_width="match_parent"
+                            android:layout_height="wrap_content"
+                            android:orientation="horizontal"
+                            android:paddingEnd="@dimen/card_padding_regular"
+                            android:paddingStart="@dimen/card_padding_regular"
+                            android:paddingTop="@dimen/card_padding_regular"
+                            android:paddingBottom="@dimen/card_padding_list"
+                            android:layout_marginStart="-2dp"
+                            android:layout_marginEnd="-2dp"/>
+
+                        <LinearLayout
+                            android:layout_width="wrap_content"
+                            android:layout_height="match_parent"
+                            android:orientation="horizontal"
+                            android:layout_gravity="end">
+
+                            <Button
+                                android:id="@+id/show_media"
+                                style="@style/Widget.Conversations.Button.Borderless"
+                                android:layout_width="wrap_content"
+                                android:layout_height="wrap_content"
+                                android:minWidth="0dp"
+                                android:paddingLeft="16dp"
+                                android:paddingRight="16dp"
+                                android:text="@string/show_media"
+                                android:textColor="?attr/colorAccent" />
+                        </LinearLayout>
+                    </LinearLayout>
+                </android.support.v7.widget.CardView>
+
                 <android.support.v7.widget.CardView
                     android:layout_width="match_parent"
                     android:layout_height="wrap_content"

src/main/res/layout/media.xml 🔗

@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layout xmlns:android="http://schemas.android.com/apk/res/android">
+    <eu.siacs.conversations.ui.widget.SquareFrameLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:padding="2dp">
+        <ImageView
+            android:id="@+id/media"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:background="@color/black54"
+            android:scaleType="centerInside"/>
+    </eu.siacs.conversations.ui.widget.SquareFrameLayout>
+</layout>

src/main/res/values/dimens.xml 🔗

@@ -12,6 +12,7 @@
 	<dimen name="avatar_item_distance">16dp</dimen>
 
 	<dimen name="media_preview_size">80dp</dimen>
+	<dimen name="media_size">64dp</dimen>
 	<dimen name="toolbar_elevation">4dp</dimen>
 
 	<dimen name="publish_avatar_top_margin">8dp</dimen>

src/main/res/values/strings.xml 🔗

@@ -734,4 +734,5 @@
     <string name="pref_more_notification_settings">Notification Settings</string>
     <string name="pref_more_notification_settings_summary">Importance, Sound, Vibrate</string>
     <string name="video_compression_channel_name">Video compression</string>
+    <string name="show_media">Show media</string>
 </resources>