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