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}