MediaAdapter.java

  1package eu.siacs.conversations.ui.adapter;
  2
  3import android.content.res.ColorStateList;
  4import android.content.res.Resources;
  5import android.graphics.Bitmap;
  6import android.graphics.Color;
  7import android.graphics.drawable.BitmapDrawable;
  8import android.graphics.drawable.Drawable;
  9import android.os.AsyncTask;
 10import android.view.LayoutInflater;
 11import android.view.ViewGroup;
 12import android.widget.ImageView;
 13
 14import androidx.annotation.DimenRes;
 15import androidx.annotation.DrawableRes;
 16import androidx.annotation.NonNull;
 17import androidx.core.widget.ImageViewCompat;
 18import androidx.databinding.DataBindingUtil;
 19import androidx.recyclerview.widget.RecyclerView;
 20
 21import com.google.android.material.color.MaterialColors;
 22import com.google.common.base.Strings;
 23
 24import eu.siacs.conversations.R;
 25import eu.siacs.conversations.databinding.ItemMediaBinding;
 26import eu.siacs.conversations.ui.XmppActivity;
 27import eu.siacs.conversations.ui.util.Attachment;
 28import eu.siacs.conversations.ui.util.ViewUtil;
 29import eu.siacs.conversations.worker.ExportBackupWorker;
 30
 31import java.lang.ref.WeakReference;
 32import java.util.ArrayList;
 33import java.util.Arrays;
 34import java.util.List;
 35import java.util.concurrent.RejectedExecutionException;
 36
 37public class MediaAdapter extends RecyclerView.Adapter<MediaAdapter.MediaViewHolder> {
 38
 39    public static final List<String> DOCUMENT_MIMES =
 40            Arrays.asList(
 41                    "application/pdf",
 42                    "application/vnd.oasis.opendocument.text",
 43                    "application/msword",
 44                    "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
 45                    "text/x-tex",
 46                    "text/plain");
 47    public static final List<String> CODE_MIMES = Arrays.asList("text/html", "text/xml");
 48
 49    private final ArrayList<Attachment> attachments = new ArrayList<>();
 50
 51    private final XmppActivity activity;
 52
 53    private int mediaSize = 0;
 54
 55    public MediaAdapter(XmppActivity activity, @DimenRes int mediaSize) {
 56        this.activity = activity;
 57        this.mediaSize = Math.round(activity.getResources().getDimension(mediaSize));
 58    }
 59
 60    @SuppressWarnings("rawtypes")
 61    public static void setMediaSize(final RecyclerView recyclerView, int mediaSize) {
 62        final RecyclerView.Adapter adapter = recyclerView.getAdapter();
 63        if (adapter instanceof MediaAdapter mediaAdapter) {
 64            mediaAdapter.setMediaSize(mediaSize);
 65        }
 66    }
 67
 68    public static @DrawableRes int getImageDrawable(final Attachment attachment) {
 69        if (attachment.getType() == Attachment.Type.LOCATION) {
 70            return R.drawable.ic_location_pin_48dp;
 71        } else if (attachment.getType() == Attachment.Type.RECORDING) {
 72            return R.drawable.ic_mic_48dp;
 73        } else {
 74            return getImageDrawable(attachment.getMime());
 75        }
 76    }
 77
 78    private static @DrawableRes int getImageDrawable(final String mime) {
 79
 80        // TODO ideas for more mime types: XML, HTML documents, GPG/PGP files, eml files,
 81        // spreadsheets (table symbol)
 82
 83        // add bz2 and tar.gz to archive detection
 84
 85        if (Strings.isNullOrEmpty(mime)) {
 86            return R.drawable.ic_help_center_48dp;
 87        } else if (mime.equals("audio/x-m4b")) {
 88            return R.drawable.ic_play_lesson_48dp;
 89        } else if (mime.startsWith("audio/")) {
 90            return R.drawable.ic_headphones_48dp;
 91        } else if (mime.equals("text/calendar") || (mime.equals("text/x-vcalendar"))) {
 92            return R.drawable.ic_event_48dp;
 93        } else if (mime.equals("text/x-vcard")) {
 94            return R.drawable.ic_person_48dp;
 95        } else if (mime.equals("application/vnd.android.package-archive")) {
 96            return R.drawable.ic_adb_48dp;
 97        } else if (mime.equals("application/zip") || mime.equals("application/rar")) {
 98            return R.drawable.ic_archive_48dp;
 99        } else if (mime.equals("application/epub+zip")
100                || mime.equals("application/vnd.amazon.mobi8-ebook")) {
101            return R.drawable.ic_book_48dp;
102        } else if (mime.equals(ExportBackupWorker.MIME_TYPE)) {
103            return R.drawable.ic_backup_48dp;
104        } else if (DOCUMENT_MIMES.contains(mime)) {
105            return R.drawable.ic_description_48dp;
106        } else if (mime.equals("application/gpx+xml")) {
107            return R.drawable.ic_tour_48dp;
108        } else if (mime.startsWith("image/")) {
109            return R.drawable.ic_image_48dp;
110        } else if (mime.startsWith("video/")) {
111            return R.drawable.ic_movie_48dp;
112        } else if (CODE_MIMES.contains(mime)) {
113            return R.drawable.ic_code_48dp;
114        } else if (mime.equals("message/rfc822")) {
115            return R.drawable.ic_email_48dp;
116        } else {
117            return R.drawable.ic_help_center_48dp;
118        }
119    }
120
121    static void renderPreview(final Attachment attachment, final ImageView imageView) {
122        ImageViewCompat.setImageTintList(
123                imageView,
124                ColorStateList.valueOf(
125                        MaterialColors.getColor(
126                                imageView, com.google.android.material.R.attr.colorOnSurface)));
127        imageView.setImageResource(getImageDrawable(attachment));
128        imageView.setBackgroundColor(
129                MaterialColors.getColor(
130                        imageView,
131                        com.google.android.material.R.attr.colorSurfaceContainerHighest));
132    }
133
134    private static boolean cancelPotentialWork(Attachment attachment, ImageView imageView) {
135        final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
136
137        if (bitmapWorkerTask != null) {
138            final Attachment oldAttachment = bitmapWorkerTask.attachment;
139            if (oldAttachment == null || !oldAttachment.equals(attachment)) {
140                bitmapWorkerTask.cancel(true);
141            } else {
142                return false;
143            }
144        }
145        return true;
146    }
147
148    private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) {
149        if (imageView != null) {
150            final Drawable drawable = imageView.getDrawable();
151            if (drawable instanceof AsyncDrawable asyncDrawable) {
152                return asyncDrawable.getBitmapWorkerTask();
153            }
154        }
155        return null;
156    }
157
158    @NonNull
159    @Override
160    public MediaViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
161        final LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
162        ItemMediaBinding binding =
163                DataBindingUtil.inflate(layoutInflater, R.layout.item_media, parent, false);
164        return new MediaViewHolder(binding);
165    }
166
167    @Override
168    public void onBindViewHolder(@NonNull MediaViewHolder holder, int position) {
169        final Attachment attachment = attachments.get(position);
170        if (attachment.renderThumbnail()) {
171            loadPreview(attachment, holder.binding.media);
172        } else {
173            cancelPotentialWork(attachment, holder.binding.media);
174            renderPreview(attachment, holder.binding.media);
175        }
176        holder.binding.getRoot().setOnClickListener(v -> ViewUtil.view(activity, attachment));
177    }
178
179    public void setAttachments(final List<Attachment> attachments) {
180        this.attachments.clear();
181        this.attachments.addAll(attachments);
182        notifyDataSetChanged();
183    }
184
185    private void setMediaSize(int mediaSize) {
186        this.mediaSize = mediaSize;
187    }
188
189    private void loadPreview(Attachment attachment, ImageView imageView) {
190        if (cancelPotentialWork(attachment, imageView)) {
191            final Bitmap bm =
192                    activity.xmppConnectionService
193                            .getFileBackend()
194                            .getPreviewForUri(attachment, mediaSize, true);
195            if (bm != null) {
196                cancelPotentialWork(attachment, imageView);
197                imageView.setImageBitmap(bm);
198                imageView.setBackgroundColor(Color.TRANSPARENT);
199            } else {
200                // TODO consider if this is still a good, general purpose loading color
201                imageView.setBackgroundColor(0xff333333);
202                imageView.setImageDrawable(null);
203                final BitmapWorkerTask task = new BitmapWorkerTask(mediaSize, imageView);
204                final AsyncDrawable asyncDrawable =
205                        new AsyncDrawable(activity.getResources(), null, task);
206                imageView.setImageDrawable(asyncDrawable);
207                try {
208                    task.execute(attachment);
209                } catch (final RejectedExecutionException ignored) {
210                }
211            }
212        }
213    }
214
215    @Override
216    public int getItemCount() {
217        return attachments.size();
218    }
219
220    static class AsyncDrawable extends BitmapDrawable {
221        private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference;
222
223        AsyncDrawable(Resources res, Bitmap bitmap, BitmapWorkerTask bitmapWorkerTask) {
224            super(res, bitmap);
225            bitmapWorkerTaskReference = new WeakReference<>(bitmapWorkerTask);
226        }
227
228        BitmapWorkerTask getBitmapWorkerTask() {
229            return bitmapWorkerTaskReference.get();
230        }
231    }
232
233    static class MediaViewHolder extends RecyclerView.ViewHolder {
234
235        private final ItemMediaBinding binding;
236
237        MediaViewHolder(ItemMediaBinding binding) {
238            super(binding.getRoot());
239            this.binding = binding;
240        }
241    }
242
243    private static class BitmapWorkerTask extends AsyncTask<Attachment, Void, Bitmap> {
244        private final WeakReference<ImageView> imageViewReference;
245        private Attachment attachment = null;
246        private final int mediaSize;
247
248        BitmapWorkerTask(int mediaSize, ImageView imageView) {
249            this.mediaSize = mediaSize;
250            imageViewReference = new WeakReference<>(imageView);
251        }
252
253        @Override
254        protected Bitmap doInBackground(final Attachment... params) {
255            this.attachment = params[0];
256            final XmppActivity activity = XmppActivity.find(imageViewReference);
257            if (activity == null) {
258                return null;
259            }
260            return activity.xmppConnectionService
261                    .getFileBackend()
262                    .getPreviewForUri(this.attachment, mediaSize, false);
263        }
264
265        @Override
266        protected void onPostExecute(Bitmap bitmap) {
267            if (bitmap != null && !isCancelled()) {
268                final ImageView imageView = imageViewReference.get();
269                if (imageView != null) {
270                    imageView.setImageBitmap(bitmap);
271                    imageView.setBackgroundColor(0x00000000);
272                }
273            }
274        }
275    }
276}