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