1package eu.siacs.conversations.ui.adapter;
2
3import android.Manifest;
4import android.app.Activity;
5import android.content.Intent;
6import android.content.pm.PackageManager;
7import android.graphics.PorterDuff;
8import android.graphics.drawable.Drawable;
9import android.content.res.ColorStateList;
10import android.graphics.Typeface;
11import android.net.Uri;
12import android.os.AsyncTask;
13import android.os.Build;
14import android.preference.PreferenceManager;
15import android.text.Spannable;
16import android.text.SpannableString;
17import android.text.SpannableStringBuilder;
18import android.text.style.ImageSpan;
19import android.text.style.ClickableSpan;
20import android.text.format.DateUtils;
21import android.text.style.ForegroundColorSpan;
22import android.text.style.RelativeSizeSpan;
23import android.text.style.StyleSpan;
24import android.text.style.URLSpan;
25import android.util.DisplayMetrics;
26import android.util.LruCache;
27import android.view.accessibility.AccessibilityEvent;
28import android.view.Gravity;
29import android.view.MotionEvent;
30import android.view.View;
31import android.view.ViewGroup;
32import android.view.WindowManager;
33import android.widget.ArrayAdapter;
34import android.widget.ImageView;
35import android.widget.LinearLayout;
36import android.widget.ListAdapter;
37import android.widget.ListView;
38import android.widget.RelativeLayout;
39import android.widget.TextView;
40import android.widget.Toast;
41
42import androidx.annotation.AttrRes;
43import androidx.annotation.ColorInt;
44import androidx.annotation.DrawableRes;
45import androidx.annotation.NonNull;
46import androidx.annotation.Nullable;
47import androidx.core.app.ActivityCompat;
48import androidx.core.content.ContextCompat;
49import androidx.core.content.res.ResourcesCompat;
50
51import com.cheogram.android.BobTransfer;
52import com.cheogram.android.MessageTextActionModeCallback;
53import com.cheogram.android.SwipeDetector;
54import com.cheogram.android.WebxdcPage;
55import com.cheogram.android.WebxdcUpdate;
56import androidx.core.widget.ImageViewCompat;
57
58import com.google.android.material.button.MaterialButton;
59import com.google.android.material.color.MaterialColors;
60import com.google.common.base.Joiner;
61import com.google.common.base.Strings;
62import com.google.common.collect.ImmutableList;
63
64import com.lelloman.identicon.view.GithubIdenticonView;
65
66import java.io.IOException;
67import java.net.URI;
68import java.net.URISyntaxException;
69import java.security.NoSuchAlgorithmException;
70import java.util.HashMap;
71import java.util.List;
72import java.util.Map;
73import java.util.Locale;
74import java.util.regex.Matcher;
75import java.util.regex.Pattern;
76
77import io.ipfs.cid.Cid;
78
79import me.saket.bettermovementmethod.BetterLinkMovementMethod;
80
81import eu.siacs.conversations.AppSettings;
82import eu.siacs.conversations.Config;
83import eu.siacs.conversations.R;
84import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
85import eu.siacs.conversations.entities.Account;
86import eu.siacs.conversations.entities.Contact;
87import eu.siacs.conversations.entities.Conversation;
88import eu.siacs.conversations.entities.Conversational;
89import eu.siacs.conversations.entities.DownloadableFile;
90import eu.siacs.conversations.entities.Message.FileParams;
91import eu.siacs.conversations.entities.Message;
92import eu.siacs.conversations.entities.MucOptions;
93import eu.siacs.conversations.entities.Roster;
94import eu.siacs.conversations.entities.RtpSessionStatus;
95import eu.siacs.conversations.entities.Transferable;
96import eu.siacs.conversations.persistance.FileBackend;
97import eu.siacs.conversations.services.MessageArchiveService;
98import eu.siacs.conversations.services.NotificationService;
99import eu.siacs.conversations.ui.Activities;
100import eu.siacs.conversations.ui.ConversationFragment;
101import eu.siacs.conversations.ui.ConversationsActivity;
102import eu.siacs.conversations.ui.XmppActivity;
103import eu.siacs.conversations.ui.service.AudioPlayer;
104import eu.siacs.conversations.ui.text.DividerSpan;
105import eu.siacs.conversations.ui.text.QuoteSpan;
106import eu.siacs.conversations.ui.util.Attachment;
107import eu.siacs.conversations.ui.util.AvatarWorkerTask;
108import eu.siacs.conversations.ui.util.MyLinkify;
109import eu.siacs.conversations.ui.util.QuoteHelper;
110import eu.siacs.conversations.ui.util.ShareUtil;
111import eu.siacs.conversations.ui.util.ViewUtil;
112import eu.siacs.conversations.utils.CryptoHelper;
113import eu.siacs.conversations.utils.Emoticons;
114import eu.siacs.conversations.utils.GeoHelper;
115import eu.siacs.conversations.utils.MessageUtils;
116import eu.siacs.conversations.utils.StylingHelper;
117import eu.siacs.conversations.utils.TimeFrameUtils;
118import eu.siacs.conversations.utils.UIHelper;
119import eu.siacs.conversations.xmpp.Jid;
120import eu.siacs.conversations.xmpp.mam.MamReference;
121import eu.siacs.conversations.xml.Element;
122
123import java.net.URI;
124import java.util.List;
125import java.util.Locale;
126import java.util.regex.Matcher;
127import java.util.regex.Pattern;
128
129public class MessageAdapter extends ArrayAdapter<Message> {
130
131 public static final String DATE_SEPARATOR_BODY = "DATE_SEPARATOR";
132 private static final int SENT = 0;
133 private static final int RECEIVED = 1;
134 private static final int STATUS = 2;
135 private static final int DATE_SEPARATOR = 3;
136 private static final int RTP_SESSION = 4;
137 private final XmppActivity activity;
138 private final AudioPlayer audioPlayer;
139 private List<String> highlightedTerm = null;
140 private final DisplayMetrics metrics;
141 private ConversationFragment mConversationFragment = null;
142 private OnContactPictureClicked mOnContactPictureClickedListener;
143 private OnContactPictureClicked mOnMessageBoxClickedListener;
144 private OnContactPictureClicked mOnMessageBoxSwipedListener;
145 private OnContactPictureLongClicked mOnContactPictureLongClickedListener;
146 private OnInlineImageLongClicked mOnInlineImageLongClickedListener;
147 private boolean mUseGreenBackground = false;
148 private BubbleDesign bubbleDesign = new BubbleDesign(false, false);
149 private final boolean mForceNames;
150 private final Map<String, WebxdcUpdate> lastWebxdcUpdate = new HashMap<>();
151 private String selectionUuid = null;
152
153 public MessageAdapter(
154 final XmppActivity activity, final List<Message> messages, final boolean forceNames) {
155 super(activity, 0, messages);
156 this.audioPlayer = new AudioPlayer(this);
157 this.activity = activity;
158 metrics = getContext().getResources().getDisplayMetrics();
159 updatePreferences();
160 this.mForceNames = forceNames;
161 }
162
163 public MessageAdapter(final XmppActivity activity, final List<Message> messages) {
164 this(activity, messages, false);
165 }
166
167 private static void resetClickListener(View... views) {
168 for (View view : views) {
169 if (view != null) view.setOnClickListener(null);
170 }
171 }
172
173 public void flagScreenOn() {
174 activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
175 }
176
177 public void flagScreenOff() {
178 activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
179 }
180
181 public void setVolumeControl(final int stream) {
182 activity.setVolumeControlStream(stream);
183 }
184
185 public void setOnContactPictureClicked(OnContactPictureClicked listener) {
186 this.mOnContactPictureClickedListener = listener;
187 }
188
189 public void setOnMessageBoxClicked(OnContactPictureClicked listener) {
190 this.mOnMessageBoxClickedListener = listener;
191 }
192
193 public void setOnMessageBoxSwiped(OnContactPictureClicked listener) {
194 this.mOnMessageBoxSwipedListener = listener;
195 }
196
197 public void setConversationFragment(ConversationFragment frag) {
198 mConversationFragment = frag;
199 }
200
201 public void quoteText(String text) {
202 if (mConversationFragment != null) mConversationFragment.quoteText(text);
203 }
204
205 public boolean hasSelection() {
206 return selectionUuid != null;
207 }
208
209 public Activity getActivity() {
210 return activity;
211 }
212
213 public void setOnContactPictureLongClicked(OnContactPictureLongClicked listener) {
214 this.mOnContactPictureLongClickedListener = listener;
215 }
216
217 public void setOnInlineImageLongClicked(OnInlineImageLongClicked listener) {
218 this.mOnInlineImageLongClickedListener = listener;
219 }
220
221 @Override
222 public int getViewTypeCount() {
223 return 5;
224 }
225
226 private int getItemViewType(Message message) {
227 if (message.getType() == Message.TYPE_STATUS) {
228 if (DATE_SEPARATOR_BODY.equals(message.getBody())) {
229 return DATE_SEPARATOR;
230 } else {
231 return STATUS;
232 }
233 } else if (message.getType() == Message.TYPE_RTP_SESSION) {
234 return RTP_SESSION;
235 } else if (message.getStatus() <= Message.STATUS_RECEIVED) {
236 return RECEIVED;
237 } else {
238 return SENT;
239 }
240 }
241
242 @Override
243 public int getItemViewType(int position) {
244 return this.getItemViewType(getItem(position));
245 }
246
247 private void displayStatus(
248 final ViewHolder viewHolder,
249 final Message message,
250 final int type,
251 final BubbleColor bubbleColor) {
252 final int mergedStatus = message.getMergedStatus();
253 final boolean error;
254 if (viewHolder.indicatorReceived != null) {
255 viewHolder.indicatorReceived.setVisibility(View.GONE);
256 }
257 final Transferable transferable = message.getTransferable();
258 final boolean multiReceived =
259 message.getConversation().getMode() == Conversation.MODE_MULTI
260 && mergedStatus <= Message.STATUS_RECEIVED;
261 final String fileSize;
262 if (message.isFileOrImage()
263 || transferable != null
264 || MessageUtils.unInitiatedButKnownSize(message)) {
265 final FileParams params = message.getFileParams();
266 fileSize = params.size != null ? UIHelper.filesizeToString(params.size) : null;
267 if (message.getStatus() == Message.STATUS_SEND_FAILED
268 || (transferable != null
269 && (transferable.getStatus() == Transferable.STATUS_FAILED
270 || transferable.getStatus()
271 == Transferable.STATUS_CANCELLED))) {
272 error = true;
273 } else {
274 error = message.getStatus() == Message.STATUS_SEND_FAILED;
275 }
276 } else {
277 fileSize = null;
278 error = message.getStatus() == Message.STATUS_SEND_FAILED;
279 }
280 if (type == SENT) {
281 final @DrawableRes Integer receivedIndicator =
282 getMessageStatusAsDrawable(message, mergedStatus);
283 if (receivedIndicator == null) {
284 viewHolder.indicatorReceived.setVisibility(View.INVISIBLE);
285 } else {
286 viewHolder.indicatorReceived.setImageResource(receivedIndicator);
287 if (mergedStatus == Message.STATUS_SEND_FAILED) {
288 setImageTintError(viewHolder.indicatorReceived);
289 } else {
290 setImageTint(viewHolder.indicatorReceived, bubbleColor);
291 }
292 viewHolder.indicatorReceived.setVisibility(View.VISIBLE);
293 }
294 }
295 final var additionalStatusInfo = getAdditionalStatusInfo(message, mergedStatus);
296
297 if (error && type == SENT) {
298 viewHolder.time.setTextColor(
299 MaterialColors.getColor(
300 viewHolder.time, com.google.android.material.R.attr.colorError));
301 } else {
302 setTextColor(viewHolder.time, bubbleColor);
303 }
304 if (message.getEncryption() == Message.ENCRYPTION_NONE) {
305 viewHolder.indicator.setVisibility(View.GONE);
306 } else {
307 boolean verified = false;
308 if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) {
309 final FingerprintStatus status =
310 message.getConversation()
311 .getAccount()
312 .getAxolotlService()
313 .getFingerprintTrust(message.getFingerprint());
314 if (status != null && status.isVerified()) {
315 verified = true;
316 }
317 }
318 if (verified) {
319 viewHolder.indicator.setImageResource(R.drawable.ic_verified_user_24dp);
320 } else {
321 viewHolder.indicator.setImageResource(R.drawable.ic_lock_24dp);
322 }
323 if (error && type == SENT) {
324 setImageTintError(viewHolder.indicator);
325 } else {
326 setImageTint(viewHolder.indicator, bubbleColor);
327 }
328 viewHolder.indicator.setVisibility(View.VISIBLE);
329 }
330
331 if (viewHolder.edit_indicator != null) {
332 if (message.edited()) {
333 viewHolder.edit_indicator.setVisibility(View.VISIBLE);
334 if (error && type == SENT) {
335 setImageTintError(viewHolder.edit_indicator);
336 } else {
337 setImageTint(viewHolder.edit_indicator, bubbleColor);
338 }
339 } else {
340 viewHolder.edit_indicator.setVisibility(View.GONE);
341 }
342 }
343
344 final String formattedTime =
345 UIHelper.readableTimeDifferenceFull(getContext(), message.getMergedTimeSent());
346 final String bodyLanguage = message.getBodyLanguage();
347 final ImmutableList.Builder<String> timeInfoBuilder = new ImmutableList.Builder<>();
348 if (message.getStatus() <= Message.STATUS_RECEIVED) {
349 timeInfoBuilder.add(formattedTime);
350 if (fileSize != null) {
351 timeInfoBuilder.add(fileSize);
352 }
353 if (mForceNames || multiReceived || (message.getTrueCounterpart() != null && message.getContact() != null)) {
354 final String displayName = UIHelper.getMessageDisplayName(message);
355 if (displayName != null) {
356 timeInfoBuilder.add(displayName);
357 }
358 }
359 if (bodyLanguage != null) {
360 timeInfoBuilder.add(bodyLanguage.toUpperCase(Locale.US));
361 }
362 } else {
363 if (bodyLanguage != null) {
364 timeInfoBuilder.add(bodyLanguage.toUpperCase(Locale.US));
365 }
366 if (fileSize != null) {
367 timeInfoBuilder.add(fileSize);
368 }
369 // for space reasons we display only 'additional status info' (send progress or concrete
370 // failure reason) or the time
371 if (additionalStatusInfo != null) {
372 timeInfoBuilder.add(additionalStatusInfo);
373 } else {
374 timeInfoBuilder.add(formattedTime);
375 }
376 }
377 final var timeInfo = timeInfoBuilder.build();
378 viewHolder.time.setText(Joiner.on(" \u00B7 ").join(timeInfo));
379 }
380
381 public static @DrawableRes Integer getMessageStatusAsDrawable(
382 final Message message, final int status) {
383 final var transferable = message.getTransferable();
384 return switch (status) {
385 case Message.STATUS_WAITING -> R.drawable.ic_more_horiz_24dp;
386 case Message.STATUS_UNSEND -> transferable == null ? null : R.drawable.ic_upload_24dp;
387 case Message.STATUS_SEND -> R.drawable.ic_done_24dp;
388 case Message.STATUS_SEND_RECEIVED, Message.STATUS_SEND_DISPLAYED -> R.drawable
389 .ic_done_all_24dp;
390 case Message.STATUS_SEND_FAILED -> {
391 final String errorMessage = message.getErrorMessage();
392 if (Message.ERROR_MESSAGE_CANCELLED.equals(errorMessage)) {
393 yield R.drawable.ic_cancel_24dp;
394 } else {
395 yield R.drawable.ic_error_24dp;
396 }
397 }
398 case Message.STATUS_OFFERED -> R.drawable.ic_p2p_24dp;
399 default -> null;
400 };
401 }
402
403 @Nullable
404 private String getAdditionalStatusInfo(final Message message, final int mergedStatus) {
405 final String additionalStatusInfo;
406 if (mergedStatus == Message.STATUS_SEND_FAILED) {
407 final String errorMessage = Strings.nullToEmpty(message.getErrorMessage());
408 final String[] errorParts = errorMessage.split("\\u001f", 2);
409 if (errorParts.length == 2 && errorParts[0].equals("file-too-large")) {
410 additionalStatusInfo = getContext().getString(R.string.file_too_large);
411 } else {
412 additionalStatusInfo = null;
413 }
414 } else if (mergedStatus == Message.STATUS_UNSEND) {
415 final var transferable = message.getTransferable();
416 if (transferable == null) {
417 return null;
418 }
419 return getContext().getString(R.string.sending_file, transferable.getProgress());
420 } else {
421 additionalStatusInfo = null;
422 }
423 return additionalStatusInfo;
424 }
425
426 private void displayInfoMessage(
427 ViewHolder viewHolder, CharSequence text, final BubbleColor bubbleColor) {
428 viewHolder.download_button.setVisibility(View.GONE);
429 viewHolder.audioPlayer.setVisibility(View.GONE);
430 viewHolder.image.setVisibility(View.GONE);
431 viewHolder.messageBody.setVisibility(View.VISIBLE);
432 viewHolder.messageBody.setText(text);
433 viewHolder.messageBody.setTextColor(
434 bubbleToOnSurfaceVariant(viewHolder.messageBody, bubbleColor));
435 viewHolder.messageBody.setTextIsSelectable(false);
436 }
437
438 private void displayEmojiMessage(
439 final ViewHolder viewHolder, final SpannableStringBuilder body, final BubbleColor bubbleColor) {
440 viewHolder.download_button.setVisibility(View.GONE);
441 viewHolder.audioPlayer.setVisibility(View.GONE);
442 viewHolder.image.setVisibility(View.GONE);
443 viewHolder.messageBody.setVisibility(View.VISIBLE);
444 setTextColor(viewHolder.messageBody, bubbleColor);
445 ImageSpan[] imageSpans = body.getSpans(0, body.length(), ImageSpan.class);
446 float size = imageSpans.length == 1 || Emoticons.isEmoji(body.toString()) ? 3.0f : 2.0f;
447 body.setSpan(
448 new RelativeSizeSpan(size), 0, body.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
449 viewHolder.messageBody.setText(body);
450 }
451
452 private void applyQuoteSpan(
453 final TextView textView,
454 SpannableStringBuilder body,
455 int start,
456 int end,
457 final BubbleColor bubbleColor) {
458 if (start > 1 && !"\n\n".equals(body.subSequence(start - 2, start).toString())) {
459 body.insert(start++, "\n");
460 body.setSpan(
461 new DividerSpan(false), start - 2, start, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
462 end++;
463 }
464 if (end < body.length() - 1 && !"\n\n".equals(body.subSequence(end, end + 2).toString())) {
465 body.insert(end, "\n");
466 body.setSpan(
467 new DividerSpan(false),
468 end,
469 end + ("\n".equals(body.subSequence(end + 1, end + 2).toString()) ? 2 : 1),
470 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
471 );
472 }
473 final DisplayMetrics metrics = getContext().getResources().getDisplayMetrics();
474 body.setSpan(
475 new QuoteSpan(bubbleToOnSurfaceVariant(textView, bubbleColor), metrics),
476 start,
477 end,
478 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
479 }
480
481 public boolean handleTextQuotes(final TextView textView, final SpannableStringBuilder body) {
482 final boolean colorfulBackground = this.bubbleDesign.colorfulChatBubbles;
483 final BubbleColor bubbleColor = colorfulBackground ? BubbleColor.SECONDARY : BubbleColor.SURFACE;
484 return handleTextQuotes(textView, body, bubbleColor);
485 }
486
487 /**
488 * Applies QuoteSpan to group of lines which starts with > or » characters. Appends likebreaks
489 * and applies DividerSpan to them to show a padding between quote and text.
490 */
491 public boolean handleTextQuotes(
492 final TextView textView,
493 final SpannableStringBuilder body,
494 final BubbleColor bubbleColor) {
495 boolean startsWithQuote = false;
496 int quoteDepth = 1;
497 while (QuoteHelper.bodyContainsQuoteStart(body) && quoteDepth <= Config.QUOTE_MAX_DEPTH) {
498 char previous = '\n';
499 int lineStart = -1;
500 int lineTextStart = -1;
501 int quoteStart = -1;
502 for (int i = 0; i <= body.length(); i++) {
503 char current = body.length() > i ? body.charAt(i) : '\n';
504 if (lineStart == -1) {
505 if (previous == '\n') {
506 if (i < body.length() && QuoteHelper.isPositionQuoteStart(body, i)) {
507 // Line start with quote
508 lineStart = i;
509 if (quoteStart == -1) quoteStart = i;
510 if (i == 0) startsWithQuote = true;
511 } else if (quoteStart >= 0) {
512 // Line start without quote, apply spans there
513 applyQuoteSpan(textView, body, quoteStart, i - 1, bubbleColor);
514 quoteStart = -1;
515 }
516 }
517 } else {
518 // Remove extra spaces between > and first character in the line
519 // > character will be removed too
520 if (current != ' ' && lineTextStart == -1) {
521 lineTextStart = i;
522 }
523 if (current == '\n') {
524 body.delete(lineStart, lineTextStart);
525 i -= lineTextStart - lineStart;
526 if (i == lineStart) {
527 // Avoid empty lines because span over empty line can be hidden
528 body.insert(i++, " ");
529 }
530 lineStart = -1;
531 lineTextStart = -1;
532 }
533 }
534 previous = current;
535 }
536 if (quoteStart >= 0) {
537 // Apply spans to finishing open quote
538 applyQuoteSpan(textView, body, quoteStart, body.length(), bubbleColor);
539 }
540 quoteDepth++;
541 }
542 return startsWithQuote;
543 }
544
545 private SpannableStringBuilder getSpannableBody(final Message message) {
546 Drawable fallbackImg = ResourcesCompat.getDrawable(activity.getResources(), R.drawable.ic_photo_24dp, null);
547 return message.getMergedBody((cid) -> {
548 try {
549 DownloadableFile f = activity.xmppConnectionService.getFileForCid(cid);
550 if (f == null || !f.canRead()) {
551 if (!message.trusted() && !message.getConversation().canInferPresence()) return null;
552
553 try {
554 new BobTransfer(BobTransfer.uri(cid), message.getConversation().getAccount(), message.getCounterpart(), activity.xmppConnectionService).start();
555 } catch (final NoSuchAlgorithmException | URISyntaxException e) { }
556 return null;
557 }
558
559 Drawable d = activity.xmppConnectionService.getFileBackend().getThumbnail(f, activity.getResources(), (int) (metrics.density * 288), true);
560 if (d == null) {
561 new ThumbnailTask().execute(f);
562 }
563 return d;
564 } catch (final IOException e) {
565 return null;
566 }
567 }, fallbackImg);
568 }
569
570 private void displayTextMessage(
571 final ViewHolder viewHolder, final Message message, final BubbleColor bubbleColor, final int type) {
572 viewHolder.download_button.setVisibility(View.GONE);
573 viewHolder.image.setVisibility(View.GONE);
574 viewHolder.audioPlayer.setVisibility(View.GONE);
575 viewHolder.messageBody.setVisibility(View.VISIBLE);
576 setTextColor(viewHolder.messageBody, bubbleColor);
577 setTextSize(viewHolder.messageBody, this.bubbleDesign.largeFont);
578
579 viewHolder.messageBody.setTypeface(null, Typeface.NORMAL);
580
581 if (message.getBody() != null && !message.getBody().equals("")) {
582 viewHolder.messageBody.setTextIsSelectable(true);
583 viewHolder.messageBody.setVisibility(View.VISIBLE);
584 final String nick = UIHelper.getMessageDisplayName(message);
585 SpannableStringBuilder body = getSpannableBody(message);
586 boolean hasMeCommand = message.hasMeCommand();
587 if (hasMeCommand) {
588 body = body.replace(0, Message.ME_COMMAND.length(), nick + " ");
589 }
590 if (body.length() > Config.MAX_DISPLAY_MESSAGE_CHARS) {
591 body = new SpannableStringBuilder(body, 0, Config.MAX_DISPLAY_MESSAGE_CHARS);
592 body.append("\u2026");
593 }
594 Message.MergeSeparator[] mergeSeparators =
595 body.getSpans(0, body.length(), Message.MergeSeparator.class);
596 for (Message.MergeSeparator mergeSeparator : mergeSeparators) {
597 int start = body.getSpanStart(mergeSeparator);
598 int end = body.getSpanEnd(mergeSeparator);
599 body.setSpan(new DividerSpan(true), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
600 }
601 for (final android.text.style.QuoteSpan quote : body.getSpans(0, body.length(), android.text.style.QuoteSpan.class)) {
602 int start = body.getSpanStart(quote);
603 int end = body.getSpanEnd(quote);
604 body.removeSpan(quote);
605 applyQuoteSpan(viewHolder.messageBody, body, start, end, bubbleColor);
606 }
607 boolean startsWithQuote = handleTextQuotes(viewHolder.messageBody, body, bubbleColor);
608 if (!message.isPrivateMessage()) {
609 if (hasMeCommand) {
610 body.setSpan(
611 new StyleSpan(Typeface.BOLD_ITALIC),
612 0,
613 nick.length(),
614 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
615 }
616 } else {
617 String privateMarker;
618 if (message.getStatus() <= Message.STATUS_RECEIVED) {
619 privateMarker = activity.getString(R.string.private_message);
620 } else {
621 Jid cp = message.getCounterpart();
622 privateMarker =
623 activity.getString(
624 R.string.private_message_to,
625 Strings.nullToEmpty(cp == null ? null : cp.getResource()));
626 }
627 body.insert(0, privateMarker);
628 int privateMarkerIndex = privateMarker.length();
629 if (startsWithQuote) {
630 body.insert(privateMarkerIndex, "\n\n");
631 body.setSpan(
632 new DividerSpan(false),
633 privateMarkerIndex,
634 privateMarkerIndex + 2,
635 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
636 } else {
637 body.insert(privateMarkerIndex, " ");
638 }
639 body.setSpan(
640 new ForegroundColorSpan(
641 bubbleToOnSurfaceVariant(viewHolder.messageBody, bubbleColor)),
642 0,
643 privateMarkerIndex,
644 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
645 body.setSpan(
646 new StyleSpan(Typeface.BOLD),
647 0,
648 privateMarkerIndex,
649 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
650 if (hasMeCommand) {
651 body.setSpan(
652 new StyleSpan(Typeface.BOLD_ITALIC),
653 privateMarkerIndex + 1,
654 privateMarkerIndex + 1 + nick.length(),
655 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
656 }
657 }
658 if (message.getConversation().getMode() == Conversation.MODE_MULTI
659 && message.getStatus() == Message.STATUS_RECEIVED) {
660 if (message.getConversation() instanceof Conversation conversation) {
661 Pattern pattern =
662 NotificationService.generateNickHighlightPattern(
663 conversation.getMucOptions().getActualNick());
664 Matcher matcher = pattern.matcher(body);
665 while (matcher.find()) {
666 body.setSpan(
667 new StyleSpan(Typeface.BOLD),
668 matcher.start(),
669 matcher.end(),
670 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
671 }
672
673 pattern = NotificationService.generateNickHighlightPattern(conversation.getMucOptions().getActualName());
674 matcher = pattern.matcher(body);
675 while (matcher.find()) {
676 body.setSpan(new StyleSpan(Typeface.BOLD), matcher.start(), matcher.end(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
677 }
678 }
679 }
680 Matcher matcher = Emoticons.getEmojiPattern(body).matcher(body);
681 while (matcher.find()) {
682 if (matcher.start() < matcher.end()) {
683 body.setSpan(
684 new RelativeSizeSpan(1.2f),
685 matcher.start(),
686 matcher.end(),
687 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
688 }
689 }
690
691 StylingHelper.format(body, viewHolder.messageBody.getCurrentTextColor());
692 if (highlightedTerm != null) {
693 StylingHelper.highlight(viewHolder.messageBody, body, highlightedTerm);
694 }
695 MyLinkify.addLinks(body, message.getConversation().getAccount(), message.getConversation().getJid());
696 viewHolder.messageBody.setAutoLinkMask(0);
697 viewHolder.messageBody.setText(body);
698 BetterLinkMovementMethod method = new BetterLinkMovementMethod() {
699 @Override
700 protected void dispatchUrlLongClick(TextView tv, ClickableSpan span) {
701 if (span instanceof URLSpan || mOnInlineImageLongClickedListener == null) {
702 tv.dispatchTouchEvent(MotionEvent.obtain(0, 0, MotionEvent.ACTION_CANCEL, 0f, 0f, 0));
703 super.dispatchUrlLongClick(tv, span);
704 return;
705 }
706
707 Spannable body = (Spannable) tv.getText();
708 ImageSpan[] imageSpans = body.getSpans(body.getSpanStart(span), body.getSpanEnd(span), ImageSpan.class);
709 if (imageSpans.length > 0) {
710 Uri uri = Uri.parse(imageSpans[0].getSource());
711 Cid cid = BobTransfer.cid(uri);
712 if (cid == null) return;
713 if (mOnInlineImageLongClickedListener.onInlineImageLongClicked(cid)) {
714 tv.dispatchTouchEvent(MotionEvent.obtain(0, 0, MotionEvent.ACTION_CANCEL, 0f, 0f, 0));
715 }
716 }
717 }
718 };
719 method.setOnLinkLongClickListener((tv, url) -> {
720 tv.dispatchTouchEvent(MotionEvent.obtain(0, 0, MotionEvent.ACTION_CANCEL, 0f, 0f, 0));
721 ShareUtil.copyLinkToClipboard(activity, url);
722 return true;
723 });
724 viewHolder.messageBody.setMovementMethod(method);
725 } else {
726 viewHolder.messageBody.setText("");
727 viewHolder.messageBody.setTextIsSelectable(false);
728 toggleWhisperInfo(viewHolder, message, bubbleColor);
729 }
730 }
731
732 private void displayDownloadableMessage(
733 ViewHolder viewHolder,
734 final Message message,
735 String text,
736 final BubbleColor bubbleColor, final int type) {
737 displayTextMessage(viewHolder, message, bubbleColor, type);
738 toggleWhisperInfo(viewHolder, message, bubbleColor);
739 viewHolder.image.setVisibility(View.GONE);
740 List<Element> thumbs = message.getFileParams() != null ? message.getFileParams().getThumbnails() : null;
741 if (thumbs != null && !thumbs.isEmpty()) {
742 for (Element thumb : thumbs) {
743 Uri uri = Uri.parse(thumb.getAttribute("uri"));
744 if (uri.getScheme().equals("data")) {
745 String[] parts = uri.getSchemeSpecificPart().split(",", 2);
746 parts = parts[0].split(";");
747 if (!parts[0].equals("image/blurhash") && !parts[0].equals("image/thumbhash") && !parts[0].equals("image/jpeg") && !parts[0].equals("image/png") && !parts[0].equals("image/webp") && !parts[0].equals("image/gif")) continue;
748 } else if (uri.getScheme().equals("cid")) {
749 Cid cid = BobTransfer.cid(uri);
750 if (cid == null) continue;
751 DownloadableFile f = activity.xmppConnectionService.getFileForCid(cid);
752 if (f == null || !f.canRead()) {
753 if (!message.trusted() && !message.getConversation().canInferPresence()) continue;
754
755 try {
756 new BobTransfer(BobTransfer.uri(cid), message.getConversation().getAccount(), message.getCounterpart(), activity.xmppConnectionService).start();
757 } catch (final NoSuchAlgorithmException | URISyntaxException e) { }
758 continue;
759 }
760 } else {
761 continue;
762 }
763
764 int width = message.getFileParams().width;
765 if (width < 1 && thumb.getAttribute("width") != null) width = Integer.parseInt(thumb.getAttribute("width"));
766 if (width < 1) width = 1920;
767
768 int height = message.getFileParams().height;
769 if (height < 1 && thumb.getAttribute("height") != null) height = Integer.parseInt(thumb.getAttribute("height"));
770 if (height < 1) height = 1080;
771
772 viewHolder.image.setVisibility(View.VISIBLE);
773 imagePreviewLayout(width, height, viewHolder.image, message.getBody() != null && message.getBody().length() > 0);
774 activity.loadBitmap(message, viewHolder.image);
775 viewHolder.image.setOnClickListener(v -> ConversationFragment.downloadFile(activity, message));
776
777 break;
778 }
779 }
780 viewHolder.audioPlayer.setVisibility(View.GONE);
781 viewHolder.download_button.setVisibility(View.VISIBLE);
782 viewHolder.download_button.setText(text);
783 final var attachment = Attachment.of(message);
784 final @DrawableRes int imageResource = MediaAdapter.getImageDrawable(attachment);
785 viewHolder.download_button.setIconResource(imageResource);
786 viewHolder.download_button.setOnClickListener(
787 v -> ConversationFragment.downloadFile(activity, message));
788 }
789
790 private void displayWebxdcMessage(ViewHolder viewHolder, final Message message, final BubbleColor bubbleColor, final int type) {
791 Cid webxdcCid = message.getFileParams().getCids().get(0);
792 WebxdcPage webxdc = new WebxdcPage(activity, webxdcCid, message, activity.xmppConnectionService);
793 displayTextMessage(viewHolder, message, bubbleColor, type);
794 viewHolder.image.setVisibility(View.GONE);
795 viewHolder.audioPlayer.setVisibility(View.GONE);
796 viewHolder.download_button.setVisibility(View.VISIBLE);
797 viewHolder.download_button.setText("Open " + webxdc.getName());
798 viewHolder.download_button.setOnClickListener(v -> {
799 Conversation conversation = (Conversation) message.getConversation();
800 if (!conversation.switchToSession("webxdc\0" + message.getUuid())) {
801 conversation.startWebxdc(webxdc);
802 }
803 });
804
805 final WebxdcUpdate lastUpdate;
806 synchronized(lastWebxdcUpdate) { lastUpdate = lastWebxdcUpdate.get(message.getUuid()); }
807 if (lastUpdate == null) {
808 new Thread(() -> {
809 final WebxdcUpdate update = activity.xmppConnectionService.findLastWebxdcUpdate(message);
810 if (update != null) {
811 synchronized(lastWebxdcUpdate) { lastWebxdcUpdate.put(message.getUuid(), update); }
812 activity.xmppConnectionService.updateConversationUi();
813 }
814 }).start();
815 } else {
816 if (lastUpdate != null && (lastUpdate.getSummary() != null || lastUpdate.getDocument() != null)) {
817 viewHolder.messageBody.setVisibility(View.VISIBLE);
818 viewHolder.messageBody.setText(
819 (lastUpdate.getDocument() == null ? "" : lastUpdate.getDocument() + "\n") +
820 (lastUpdate.getSummary() == null ? "" : lastUpdate.getSummary())
821 );
822 }
823 }
824
825 final LruCache<String, Drawable> cache = activity.xmppConnectionService.getDrawableCache();
826 final Drawable d = cache.get("webxdc:icon:" + webxdcCid);
827 if (d == null) {
828 new Thread(() -> {
829 Drawable icon = webxdc.getIcon();
830 if (icon != null) {
831 cache.put("webxdc:icon:" + webxdcCid, icon);
832 activity.xmppConnectionService.updateConversationUi();
833 }
834 }).start();
835 } else {
836 viewHolder.image.setVisibility(View.VISIBLE);
837 viewHolder.image.setImageDrawable(d);
838 }
839 }
840
841 private void displayOpenableMessage(
842 ViewHolder viewHolder, final Message message, final BubbleColor bubbleColor, final int type) {
843 displayTextMessage(viewHolder, message, bubbleColor, type);
844 toggleWhisperInfo(viewHolder, message, bubbleColor);
845 viewHolder.image.setVisibility(View.GONE);
846 viewHolder.audioPlayer.setVisibility(View.GONE);
847 viewHolder.download_button.setVisibility(View.VISIBLE);
848 viewHolder.download_button.setText(
849 activity.getString(
850 R.string.open_x_file,
851 UIHelper.getFileDescriptionString(activity, message)));
852 final var attachment = Attachment.of(message);
853 final @DrawableRes int imageResource = MediaAdapter.getImageDrawable(attachment);
854 viewHolder.download_button.setIconResource(imageResource);
855 viewHolder.download_button.setOnClickListener(v -> openDownloadable(message));
856 }
857
858 private void displayLocationMessage(
859 ViewHolder viewHolder, final Message message, final BubbleColor bubbleColor, final int type) {
860 displayTextMessage(viewHolder, message, bubbleColor, type);
861 toggleWhisperInfo(viewHolder, message, bubbleColor);
862 viewHolder.image.setVisibility(View.GONE);
863 viewHolder.audioPlayer.setVisibility(View.GONE);
864 viewHolder.download_button.setVisibility(View.VISIBLE);
865 viewHolder.download_button.setText(R.string.show_location);
866 final var attachment = Attachment.of(message);
867 final @DrawableRes int imageResource = MediaAdapter.getImageDrawable(attachment);
868 viewHolder.download_button.setIconResource(imageResource);
869 viewHolder.download_button.setOnClickListener(v -> showLocation(message));
870 }
871
872 private void displayAudioMessage(
873 ViewHolder viewHolder, Message message, final BubbleColor bubbleColor, final int type) {
874 displayTextMessage(viewHolder, message, bubbleColor, type);
875 toggleWhisperInfo(viewHolder, message, bubbleColor);
876 viewHolder.image.setVisibility(View.GONE);
877 viewHolder.download_button.setVisibility(View.GONE);
878 final RelativeLayout audioPlayer = viewHolder.audioPlayer;
879 audioPlayer.setVisibility(View.VISIBLE);
880 AudioPlayer.ViewHolder.get(audioPlayer).setBubbleColor(bubbleColor);
881 this.audioPlayer.init(audioPlayer, message);
882 }
883
884 private void displayMediaPreviewMessage(
885 ViewHolder viewHolder, final Message message, final BubbleColor bubbleColor, final int type) {
886 displayTextMessage(viewHolder, message, bubbleColor, type);
887 toggleWhisperInfo(viewHolder, message, bubbleColor);
888 viewHolder.download_button.setVisibility(View.GONE);
889 viewHolder.audioPlayer.setVisibility(View.GONE);
890 viewHolder.image.setVisibility(View.VISIBLE);
891 final FileParams params = message.getFileParams();
892 imagePreviewLayout(params.width, params.height, viewHolder.image, message.getBody() != null && message.getBody().length() > 0);
893 activity.loadBitmap(message, viewHolder.image);
894 viewHolder.image.setOnClickListener(v -> openDownloadable(message));
895 }
896
897 private void imagePreviewLayout(int w, int h, ImageView image, boolean topMargin) {
898 final float target = activity.getResources().getDimension(R.dimen.image_preview_width);
899 final int scaledW;
900 final int scaledH;
901 if (Math.max(h, w) * metrics.density <= target) {
902 scaledW = (int) (w * metrics.density);
903 scaledH = (int) (h * metrics.density);
904 } else if (Math.max(h, w) <= target) {
905 scaledW = w;
906 scaledH = h;
907 } else if (w <= h) {
908 scaledW = (int) (w / ((double) h / target));
909 scaledH = (int) target;
910 } else {
911 scaledW = (int) target;
912 scaledH = (int) (h / ((double) w / target));
913 }
914 final LinearLayout.LayoutParams layoutParams =
915 new LinearLayout.LayoutParams(scaledW, scaledH);
916 layoutParams.setMargins(0, topMargin ? (int) (metrics.density * 4) : 0, 0, (int) (metrics.density * 4));
917 layoutParams.gravity = Gravity.CENTER;
918 image.setLayoutParams(layoutParams);
919 }
920
921 private void toggleWhisperInfo(
922 ViewHolder viewHolder, final Message message, final BubbleColor bubbleColor) {
923 if (message.isPrivateMessage()) {
924 final String privateMarker;
925 if (message.getStatus() <= Message.STATUS_RECEIVED) {
926 privateMarker = activity.getString(R.string.private_message);
927 } else {
928 Jid cp = message.getCounterpart();
929 privateMarker =
930 activity.getString(
931 R.string.private_message_to,
932 Strings.nullToEmpty(cp == null ? null : cp.getResource()));
933 }
934 final SpannableString body = new SpannableString(privateMarker);
935 body.setSpan(
936 new ForegroundColorSpan(
937 bubbleToOnSurfaceVariant(viewHolder.messageBody, bubbleColor)),
938 0,
939 privateMarker.length(),
940 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
941 body.setSpan(
942 new StyleSpan(Typeface.BOLD),
943 0,
944 privateMarker.length(),
945 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
946 viewHolder.messageBody.setText(body);
947 viewHolder.messageBody.setVisibility(View.VISIBLE);
948 } else {
949 viewHolder.messageBody.setVisibility(View.GONE);
950 }
951 }
952
953 private void loadMoreMessages(Conversation conversation) {
954 conversation.setLastClearHistory(0, null);
955 activity.xmppConnectionService.updateConversation(conversation);
956 conversation.setHasMessagesLeftOnServer(true);
957 conversation.setFirstMamReference(null);
958 long timestamp = conversation.getLastMessageTransmitted().getTimestamp();
959 if (timestamp == 0) {
960 timestamp = System.currentTimeMillis();
961 }
962 conversation.messagesLoaded.set(true);
963 MessageArchiveService.Query query =
964 activity.xmppConnectionService
965 .getMessageArchiveService()
966 .query(conversation, new MamReference(0), timestamp, false);
967 if (query != null) {
968 Toast.makeText(activity, R.string.fetching_history_from_server, Toast.LENGTH_LONG)
969 .show();
970 } else {
971 Toast.makeText(
972 activity,
973 R.string.not_fetching_history_retention_period,
974 Toast.LENGTH_SHORT)
975 .show();
976 }
977 }
978
979 @Override
980 public View getView(final int position, View view, final @NonNull ViewGroup parent) {
981 final Message message = getItem(position);
982 final boolean omemoEncryption = message.getEncryption() == Message.ENCRYPTION_AXOLOTL;
983 final boolean isInValidSession =
984 message.isValidInSession() && (!omemoEncryption || message.isTrusted());
985 final Conversational conversation = message.getConversation();
986 final Account account = conversation.getAccount();
987 final List<Element> commands = message.getCommands();
988 final int type = getItemViewType(position);
989 ViewHolder viewHolder;
990 if (view == null) {
991 viewHolder = new ViewHolder();
992 switch (type) {
993 case DATE_SEPARATOR:
994 view =
995 activity.getLayoutInflater()
996 .inflate(R.layout.item_message_date_bubble, parent, false);
997 viewHolder.status_message = view.findViewById(R.id.message_body);
998 viewHolder.message_box = view.findViewById(R.id.message_box);
999 break;
1000 case RTP_SESSION:
1001 view =
1002 activity.getLayoutInflater()
1003 .inflate(R.layout.item_message_rtp_session, parent, false);
1004 viewHolder.status_message = view.findViewById(R.id.message_body);
1005 viewHolder.message_box = view.findViewById(R.id.message_box);
1006 viewHolder.indicatorReceived = view.findViewById(R.id.indicator_received);
1007 break;
1008 case SENT:
1009 view = activity.getLayoutInflater().inflate(R.layout.item_message_sent, parent, false);
1010 viewHolder.status_line = view.findViewById(R.id.status_line);
1011 viewHolder.message_box_inner = view.findViewById(R.id.message_box_inner);
1012 viewHolder.message_box = view.findViewById(R.id.message_box);
1013 viewHolder.contact_picture = view.findViewById(R.id.message_photo);
1014 viewHolder.download_button = view.findViewById(R.id.download_button);
1015 viewHolder.indicator = view.findViewById(R.id.security_indicator);
1016 viewHolder.edit_indicator = view.findViewById(R.id.edit_indicator);
1017 viewHolder.image = view.findViewById(R.id.message_image);
1018 viewHolder.messageBody = view.findViewById(R.id.message_body);
1019 viewHolder.time = view.findViewById(R.id.message_time);
1020 viewHolder.subject = view.findViewById(R.id.message_subject);
1021 viewHolder.indicatorReceived = view.findViewById(R.id.indicator_received);
1022 viewHolder.audioPlayer = view.findViewById(R.id.audio_player);
1023 viewHolder.thread_identicon = view.findViewById(R.id.thread_identicon);
1024 break;
1025 case RECEIVED:
1026 view = activity.getLayoutInflater().inflate(R.layout.item_message_received, parent, false);
1027 viewHolder.status_line = view.findViewById(R.id.status_line);
1028 viewHolder.message_box_inner = view.findViewById(R.id.message_box_inner);
1029 viewHolder.message_box = view.findViewById(R.id.message_box);
1030 viewHolder.contact_picture = view.findViewById(R.id.message_photo);
1031 viewHolder.download_button = view.findViewById(R.id.download_button);
1032 viewHolder.indicator = view.findViewById(R.id.security_indicator);
1033 viewHolder.edit_indicator = view.findViewById(R.id.edit_indicator);
1034 viewHolder.image = view.findViewById(R.id.message_image);
1035 viewHolder.messageBody = view.findViewById(R.id.message_body);
1036 viewHolder.time = view.findViewById(R.id.message_time);
1037 viewHolder.subject = view.findViewById(R.id.message_subject);
1038 viewHolder.indicatorReceived = view.findViewById(R.id.indicator_received);
1039 viewHolder.encryption = view.findViewById(R.id.message_encryption);
1040 viewHolder.audioPlayer = view.findViewById(R.id.audio_player);
1041 viewHolder.commands_list = view.findViewById(R.id.commands_list);
1042 viewHolder.thread_identicon = view.findViewById(R.id.thread_identicon);
1043 break;
1044 case STATUS:
1045 view =
1046 activity.getLayoutInflater()
1047 .inflate(R.layout.item_message_status, parent, false);
1048 viewHolder.contact_picture = view.findViewById(R.id.message_photo);
1049 viewHolder.status_message = view.findViewById(R.id.status_message);
1050 viewHolder.load_more_messages = view.findViewById(R.id.load_more_messages);
1051 break;
1052 default:
1053 throw new AssertionError("Unknown view type");
1054 }
1055 view.setTag(viewHolder);
1056 } else {
1057 viewHolder = (ViewHolder) view.getTag();
1058 if (viewHolder == null) {
1059 return view;
1060 }
1061 }
1062
1063 if (viewHolder.messageBody != null) {
1064 viewHolder.messageBody.setCustomSelectionActionModeCallback(new MessageTextActionModeCallback(this, viewHolder.messageBody));
1065 }
1066
1067 if (viewHolder.thread_identicon != null) {
1068 viewHolder.thread_identicon.setVisibility(View.GONE);
1069 final Element thread = message.getThread();
1070 if (thread != null) {
1071 final String threadId = thread.getContent();
1072 if (threadId != null) {
1073 viewHolder.thread_identicon.setVisibility(View.VISIBLE);
1074 viewHolder.thread_identicon.setColor(UIHelper.getColorForName(threadId));
1075 viewHolder.thread_identicon.setHash(UIHelper.identiconHash(threadId));
1076 }
1077 }
1078 }
1079
1080 final var black = MaterialColors.getColor(view, com.google.android.material.R.attr.colorSecondaryContainer) == view.getContext().getColor(android.R.color.black);
1081 final boolean colorfulBackground = this.bubbleDesign.colorfulChatBubbles;
1082 final BubbleColor bubbleColor;
1083 if (type == RECEIVED) {
1084 if (isInValidSession) {
1085 bubbleColor = colorfulBackground || black ? BubbleColor.SECONDARY : BubbleColor.SURFACE;
1086 } else {
1087 bubbleColor = BubbleColor.WARNING;
1088 }
1089 } else {
1090 if (!colorfulBackground && black) {
1091 bubbleColor = BubbleColor.SECONDARY;
1092 } else {
1093 bubbleColor = colorfulBackground ? BubbleColor.TERTIARY : BubbleColor.SURFACE_HIGH;
1094 }
1095 }
1096
1097 if (type == DATE_SEPARATOR) {
1098 if (UIHelper.today(message.getTimeSent())) {
1099 viewHolder.status_message.setText(R.string.today);
1100 } else if (UIHelper.yesterday(message.getTimeSent())) {
1101 viewHolder.status_message.setText(R.string.yesterday);
1102 } else {
1103 viewHolder.status_message.setText(
1104 DateUtils.formatDateTime(
1105 activity,
1106 message.getTimeSent(),
1107 DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR));
1108 }
1109 if (colorfulBackground) {
1110 setBackgroundTint(viewHolder.message_box, BubbleColor.PRIMARY);
1111 setTextColor(viewHolder.status_message, BubbleColor.PRIMARY);
1112 } else {
1113 setBackgroundTint(viewHolder.message_box, BubbleColor.SURFACE_HIGH);
1114 setTextColor(viewHolder.status_message, BubbleColor.SURFACE_HIGH);
1115 }
1116 return view;
1117 } else if (type == RTP_SESSION) {
1118 final boolean received = message.getStatus() <= Message.STATUS_RECEIVED;
1119 final RtpSessionStatus rtpSessionStatus = RtpSessionStatus.of(message.getBody());
1120 final long duration = rtpSessionStatus.duration;
1121 final String callTime = UIHelper.readableTimeDifferenceFull(activity, message.getTimeSent());
1122 if (received) {
1123 if (duration > 0) {
1124 viewHolder.status_message.setText(
1125 activity.getString(
1126 R.string.incoming_call_duration_timestamp,
1127 TimeFrameUtils.resolve(activity, duration),
1128 UIHelper.readableTimeDifferenceFull(
1129 activity, message.getTimeSent())));
1130 } else if (rtpSessionStatus.successful) {
1131 viewHolder.status_message.setText(activity.getString(R.string.incoming_call_timestamp, callTime));
1132 } else {
1133 viewHolder.status_message.setText(
1134 activity.getString(
1135 R.string.missed_call_timestamp,
1136 UIHelper.readableTimeDifferenceFull(
1137 activity, message.getTimeSent())));
1138 }
1139 } else {
1140 if (duration > 0) {
1141 viewHolder.status_message.setText(
1142 activity.getString(
1143 R.string.outgoing_call_duration_timestamp,
1144 TimeFrameUtils.resolve(activity, duration),
1145 UIHelper.readableTimeDifferenceFull(
1146 activity, message.getTimeSent())));
1147 } else {
1148 viewHolder.status_message.setText(
1149 activity.getString(
1150 R.string.outgoing_call_timestamp,
1151 UIHelper.readableTimeDifferenceFull(
1152 activity, message.getTimeSent())));
1153 }
1154 }
1155 if (colorfulBackground) {
1156 setBackgroundTint(viewHolder.message_box, BubbleColor.SECONDARY);
1157 setTextColor(viewHolder.status_message, BubbleColor.SECONDARY);
1158 setImageTint(viewHolder.indicatorReceived, BubbleColor.SECONDARY);
1159 } else {
1160 setBackgroundTint(viewHolder.message_box, BubbleColor.SURFACE_HIGH);
1161 setTextColor(viewHolder.status_message, BubbleColor.SURFACE_HIGH);
1162 setImageTint(viewHolder.indicatorReceived, BubbleColor.SURFACE_HIGH);
1163 }
1164 viewHolder.indicatorReceived.setImageResource(
1165 RtpSessionStatus.getDrawable(received, rtpSessionStatus.successful));
1166 return view;
1167 } else if (type == STATUS) {
1168 if ("LOAD_MORE".equals(message.getBody())) {
1169 viewHolder.status_message.setVisibility(View.GONE);
1170 viewHolder.contact_picture.setVisibility(View.GONE);
1171 viewHolder.load_more_messages.setVisibility(View.VISIBLE);
1172 viewHolder.load_more_messages.setOnClickListener(
1173 v -> loadMoreMessages((Conversation) message.getConversation()));
1174 } else {
1175 viewHolder.status_message.setVisibility(View.VISIBLE);
1176 viewHolder.load_more_messages.setVisibility(View.GONE);
1177 viewHolder.status_message.setText(message.getBody());
1178 boolean showAvatar;
1179 if (conversation.getMode() == Conversation.MODE_SINGLE) {
1180 showAvatar = true;
1181 AvatarWorkerTask.loadAvatar(
1182 message, viewHolder.contact_picture, R.dimen.avatar_on_status_message);
1183 } else if (message.getCounterpart() != null
1184 || message.getTrueCounterpart() != null
1185 || (message.getCounterparts() != null
1186 && message.getCounterparts().size() > 0)) {
1187 showAvatar = true;
1188 AvatarWorkerTask.loadAvatar(
1189 message, viewHolder.contact_picture, R.dimen.avatar_on_status_message);
1190 } else {
1191 showAvatar = false;
1192 }
1193 if (showAvatar) {
1194 viewHolder.contact_picture.setAlpha(0.5f);
1195 viewHolder.contact_picture.setVisibility(View.VISIBLE);
1196 } else {
1197 viewHolder.contact_picture.setVisibility(View.GONE);
1198 }
1199 }
1200 return view;
1201 } else {
1202 AvatarWorkerTask.loadAvatar(message, viewHolder.contact_picture, R.dimen.avatar);
1203 }
1204
1205 resetClickListener(viewHolder.message_box, viewHolder.messageBody);
1206
1207 viewHolder.message_box.setOnClickListener(v -> {
1208 if (MessageAdapter.this.mOnMessageBoxClickedListener != null) {
1209 MessageAdapter.this.mOnMessageBoxClickedListener
1210 .onContactPictureClicked(message);
1211 }
1212 });
1213 SwipeDetector swipeDetector = new SwipeDetector((action) -> {
1214 if (action == SwipeDetector.Action.LR && MessageAdapter.this.mOnMessageBoxSwipedListener != null) {
1215 MessageAdapter.this.mOnMessageBoxSwipedListener.onContactPictureClicked(message);
1216 }
1217 });
1218 viewHolder.message_box.setOnTouchListener(swipeDetector);
1219 viewHolder.image.setOnTouchListener(swipeDetector);
1220 viewHolder.time.setOnTouchListener(swipeDetector);
1221
1222 // Treat touch-up as click so we don't have to touch twice
1223 // (touch twice is because it's waiting to see if you double-touch for text selection)
1224 viewHolder.messageBody.setOnTouchListener((v, event) -> {
1225 if (event.getAction() == MotionEvent.ACTION_UP) {
1226 if (MessageAdapter.this.mOnMessageBoxClickedListener != null) {
1227 MessageAdapter.this.mOnMessageBoxClickedListener
1228 .onContactPictureClicked(message);
1229 }
1230 }
1231
1232 swipeDetector.onTouch(v, event);
1233
1234 return false;
1235 });
1236 viewHolder.messageBody.setOnClickListener(v -> {
1237 if (MessageAdapter.this.mOnMessageBoxClickedListener != null) {
1238 MessageAdapter.this.mOnMessageBoxClickedListener
1239 .onContactPictureClicked(message);
1240 }
1241 });
1242 viewHolder.contact_picture.setOnClickListener(v -> {
1243 if (MessageAdapter.this.mOnContactPictureClickedListener != null) {
1244 MessageAdapter.this.mOnContactPictureClickedListener
1245 .onContactPictureClicked(message);
1246 }
1247
1248 });
1249 viewHolder.contact_picture.setOnLongClickListener(v -> {
1250 if (MessageAdapter.this.mOnContactPictureLongClickedListener != null) {
1251 MessageAdapter.this.mOnContactPictureLongClickedListener
1252 .onContactPictureLongClicked(v, message);
1253 return true;
1254 } else {
1255 return false;
1256 }
1257 });
1258 viewHolder.messageBody.setAccessibilityDelegate(null);
1259
1260 boolean footerWrap = false;
1261
1262 final Transferable transferable = message.getTransferable();
1263 final boolean unInitiatedButKnownSize = MessageUtils.unInitiatedButKnownSize(message);
1264
1265 final boolean muted = message.getStatus() == Message.STATUS_RECEIVED && conversation.getMode() == Conversation.MODE_MULTI && activity.xmppConnectionService.isMucUserMuted(new MucOptions.User(null, conversation.getJid(), message.getOccupantId(), null, null));
1266 if (muted) {
1267 // Muted MUC participant
1268 displayInfoMessage(viewHolder, "Muted", bubbleColor);
1269 } else if (unInitiatedButKnownSize || message.isDeleted() || (transferable != null && transferable.getStatus() != Transferable.STATUS_UPLOADING)) {
1270 if (unInitiatedButKnownSize || (message.isDeleted() && message.getModerated() == null) || transferable != null && transferable.getStatus() == Transferable.STATUS_OFFER) {
1271 displayDownloadableMessage(viewHolder, message, activity.getString(R.string.download_x_file, UIHelper.getFileDescriptionString(activity, message)), bubbleColor, type);
1272 } else if (transferable != null && transferable.getStatus() == Transferable.STATUS_OFFER_CHECK_FILESIZE) {
1273 displayDownloadableMessage(viewHolder, message, activity.getString(R.string.check_x_filesize, UIHelper.getFileDescriptionString(activity, message)), bubbleColor, type);
1274 } else {
1275 displayInfoMessage(viewHolder, UIHelper.getMessagePreview(activity.xmppConnectionService, message).first, bubbleColor);
1276 }
1277 } else if (message.isFileOrImage()
1278 && message.getEncryption() != Message.ENCRYPTION_PGP
1279 && message.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED) {
1280 if (message.getFileParams().width > 0 && message.getFileParams().height > 0) {
1281 displayMediaPreviewMessage(viewHolder, message, bubbleColor, type);
1282 if (!black && viewHolder.image.getLayoutParams().width > metrics.density * 110) {
1283 footerWrap = true;
1284 }
1285 } else if (message.getFileParams().runtime > 0) {
1286 displayAudioMessage(viewHolder, message, bubbleColor, type);
1287 } else if ("application/xdc+zip".equals(message.getFileParams().getMediaType()) && message.getConversation() instanceof Conversation && message.getThread() != null && !message.getFileParams().getCids().isEmpty()) {
1288 displayWebxdcMessage(viewHolder, message, bubbleColor, type);
1289 } else {
1290 displayOpenableMessage(viewHolder, message, bubbleColor, type);
1291 }
1292 } else if (message.getEncryption() == Message.ENCRYPTION_PGP) {
1293 if (account.isPgpDecryptionServiceConnected()) {
1294 if (conversation instanceof Conversation
1295 && !account.hasPendingPgpIntent((Conversation) conversation)) {
1296 displayInfoMessage(
1297 viewHolder,
1298 activity.getString(R.string.message_decrypting),
1299 bubbleColor);
1300 } else {
1301 displayInfoMessage(
1302 viewHolder, activity.getString(R.string.pgp_message), bubbleColor);
1303 }
1304 } else {
1305 displayInfoMessage(
1306 viewHolder, activity.getString(R.string.install_openkeychain), bubbleColor);
1307 viewHolder.message_box.setOnClickListener(this::promptOpenKeychainInstall);
1308 viewHolder.messageBody.setOnClickListener(this::promptOpenKeychainInstall);
1309 }
1310 } else if (message.getEncryption() == Message.ENCRYPTION_DECRYPTION_FAILED) {
1311 displayInfoMessage(
1312 viewHolder, activity.getString(R.string.decryption_failed), bubbleColor);
1313 } else if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE) {
1314 displayInfoMessage(
1315 viewHolder,
1316 activity.getString(R.string.not_encrypted_for_this_device),
1317 bubbleColor);
1318 } else if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL_FAILED) {
1319 displayInfoMessage(
1320 viewHolder, activity.getString(R.string.omemo_decryption_failed), bubbleColor);
1321 } else {
1322 if (message.isGeoUri()) {
1323 displayLocationMessage(viewHolder, message, bubbleColor, type);
1324 } else if (message.treatAsDownloadable()) {
1325 try {
1326 final URI uri = message.getOob();
1327 displayDownloadableMessage(viewHolder,
1328 message,
1329 activity.getString(
1330 R.string.check_x_filesize_on_host,
1331 UIHelper.getFileDescriptionString(activity, message),
1332 uri.getHost()),
1333 bubbleColor, type);
1334 } catch (Exception e) {
1335 displayDownloadableMessage(
1336 viewHolder,
1337 message,
1338 activity.getString(
1339 R.string.check_x_filesize,
1340 UIHelper.getFileDescriptionString(activity, message)),
1341 bubbleColor, type);
1342 }
1343 } else if (message.bodyIsOnlyEmojis() && message.getType() != Message.TYPE_PRIVATE) {
1344 displayEmojiMessage(viewHolder, getSpannableBody(message), bubbleColor);
1345 } else {
1346 displayTextMessage(viewHolder, message, bubbleColor, message.getType());
1347 }
1348 }
1349
1350 viewHolder.message_box_inner.setMinimumWidth(footerWrap ? (int) (110 * metrics.density) : 0);
1351 LinearLayout.LayoutParams statusParams = (LinearLayout.LayoutParams) viewHolder.status_line.getLayoutParams();
1352 statusParams.width = footerWrap ? ViewGroup.LayoutParams.MATCH_PARENT : ViewGroup.LayoutParams.WRAP_CONTENT;
1353 viewHolder.status_line.setLayoutParams(statusParams);
1354
1355 setBackgroundTint(viewHolder.message_box, bubbleColor);
1356 setTextColor(viewHolder.messageBody, bubbleColor);
1357
1358 if (type == RECEIVED) {
1359 if (!muted && commands != null && conversation instanceof Conversation) {
1360 CommandButtonAdapter adapter = new CommandButtonAdapter(activity);
1361 adapter.addAll(commands);
1362 viewHolder.commands_list.setAdapter(adapter);
1363 viewHolder.commands_list.setVisibility(View.VISIBLE);
1364 viewHolder.commands_list.setOnItemClickListener((p, v, pos, id) -> {
1365 final Element command = adapter.getItem(pos);
1366 activity.startCommand(conversation.getAccount(), command.getAttributeAsJid("jid"), command.getAttribute("node"));
1367 });
1368 } else {
1369 // It's unclear if we can set this to null...
1370 ListAdapter adapter = viewHolder.commands_list.getAdapter();
1371 if (adapter instanceof ArrayAdapter) {
1372 ((ArrayAdapter<?>) adapter).clear();
1373 }
1374 viewHolder.commands_list.setVisibility(View.GONE);
1375 viewHolder.commands_list.setOnItemClickListener(null);
1376 }
1377
1378 setTextColor(viewHolder.encryption, bubbleColor);
1379
1380 if (isInValidSession) {
1381 viewHolder.encryption.setVisibility(View.GONE);
1382 } else {
1383 viewHolder.encryption.setVisibility(View.VISIBLE);
1384 if (omemoEncryption && !message.isTrusted()) {
1385 viewHolder.encryption.setText(R.string.not_trusted);
1386 } else {
1387 viewHolder.encryption.setText(
1388 CryptoHelper.encryptionTypeToText(message.getEncryption()));
1389 }
1390 }
1391 }
1392
1393 if (type == RECEIVED || type == SENT) {
1394 String subject = message.getSubject();
1395 if (subject == null && message.getThread() != null) {
1396 subject = ((Conversation) message.getConversation()).getThread(message.getThread().getContent()).getSubject();
1397 }
1398 if (muted || subject == null) {
1399 viewHolder.subject.setVisibility(View.GONE);
1400 } else {
1401 viewHolder.subject.setVisibility(View.VISIBLE);
1402 viewHolder.subject.setText(subject);
1403 }
1404 }
1405
1406 /* TODO
1407 if (darkBackground) {
1408 if (viewHolder.subject != null) viewHolder.subject.setTextAppearance(getContext(), R.style.TextAppearance_Conversations_Caption_OnDark_Bold);
1409 if (viewHolder.encryption != null) viewHolder.encryption.setTextAppearance(getContext(), R.style.TextAppearance_Conversations_Caption_OnDark_Bold);
1410 } else {
1411 if (viewHolder.subject != null) viewHolder.subject.setTextAppearance(getContext(), R.style.TextAppearance_Conversations_Caption_Bold);
1412 if (viewHolder.encryption != null) viewHolder.encryption.setTextAppearance(getContext(), R.style.TextAppearance_Conversations_Caption_Bold);
1413 }
1414 */
1415
1416 displayStatus(viewHolder, message, type, bubbleColor);
1417
1418 viewHolder.messageBody.setAccessibilityDelegate(new View.AccessibilityDelegate() {
1419 @Override
1420 public void sendAccessibilityEvent(View host, int eventType) {
1421 super.sendAccessibilityEvent(host, eventType);
1422 if (eventType == AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED) {
1423 if (viewHolder.messageBody.hasSelection()) {
1424 selectionUuid = message.getUuid();
1425 } else if (message.getUuid() != null && message.getUuid().equals(selectionUuid)) {
1426 selectionUuid = null;
1427 }
1428 }
1429 }
1430 });
1431
1432 return view;
1433 }
1434
1435 private void promptOpenKeychainInstall(View view) {
1436 activity.showInstallPgpDialog();
1437 }
1438
1439 public FileBackend getFileBackend() {
1440 return activity.xmppConnectionService.getFileBackend();
1441 }
1442
1443 public void stopAudioPlayer() {
1444 audioPlayer.stop();
1445 }
1446
1447 public void unregisterListenerInAudioPlayer() {
1448 audioPlayer.unregisterListener();
1449 }
1450
1451 public void startStopPending() {
1452 audioPlayer.startStopPending();
1453 }
1454
1455 public void openDownloadable(Message message) {
1456 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU
1457 && ContextCompat.checkSelfPermission(
1458 activity, Manifest.permission.WRITE_EXTERNAL_STORAGE)
1459 != PackageManager.PERMISSION_GRANTED) {
1460 ConversationFragment.registerPendingMessage(activity, message);
1461 ActivityCompat.requestPermissions(
1462 activity,
1463 new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE},
1464 ConversationsActivity.REQUEST_OPEN_MESSAGE);
1465 return;
1466 }
1467 final DownloadableFile file =
1468 activity.xmppConnectionService.getFileBackend().getFile(message);
1469 ViewUtil.view(activity, file);
1470 }
1471
1472 private void showLocation(Message message) {
1473 for (Intent intent : GeoHelper.createGeoIntentsFromMessage(activity, message)) {
1474 if (intent.resolveActivity(getContext().getPackageManager()) != null) {
1475 getContext().startActivity(intent);
1476 return;
1477 }
1478 }
1479 Toast.makeText(
1480 activity,
1481 R.string.no_application_found_to_display_location,
1482 Toast.LENGTH_SHORT)
1483 .show();
1484 }
1485
1486 public void updatePreferences() {
1487 final AppSettings appSettings = new AppSettings(activity);
1488 this.bubbleDesign =
1489 new BubbleDesign(appSettings.isColorfulChatBubbles(), appSettings.isLargeFont());
1490 }
1491
1492 public void setHighlightedTerm(List<String> terms) {
1493 this.highlightedTerm = terms == null ? null : StylingHelper.filterHighlightedWords(terms);
1494 }
1495
1496 public interface OnContactPictureClicked {
1497 void onContactPictureClicked(Message message);
1498 }
1499
1500 public interface OnContactPictureLongClicked {
1501 void onContactPictureLongClicked(View v, Message message);
1502 }
1503
1504 public interface OnInlineImageLongClicked {
1505 boolean onInlineImageLongClicked(Cid cid);
1506 }
1507
1508 private static void setBackgroundTint(final View view, final BubbleColor bubbleColor) {
1509 view.setBackgroundTintList(bubbleToColorStateList(view, bubbleColor));
1510 }
1511
1512 private static ColorStateList bubbleToColorStateList(
1513 final View view, final BubbleColor bubbleColor) {
1514 final @AttrRes int colorAttributeResId =
1515 switch (bubbleColor) {
1516 case SURFACE -> Activities.isNightMode(view.getContext())
1517 ? com.google.android.material.R.attr.colorSurfaceContainerHigh
1518 : com.google.android.material.R.attr.colorSurfaceContainerLow;
1519 case SURFACE_HIGH -> Activities.isNightMode(view.getContext())
1520 ? com.google.android.material.R.attr.colorSurfaceContainerHighest
1521 : com.google.android.material.R.attr.colorSurfaceContainerHigh;
1522 case PRIMARY -> com.google.android.material.R.attr.colorPrimaryContainer;
1523 case SECONDARY -> com.google.android.material.R.attr.colorSecondaryContainer;
1524 case TERTIARY -> com.google.android.material.R.attr.colorTertiaryContainer;
1525 case WARNING -> com.google.android.material.R.attr.colorErrorContainer;
1526 };
1527 return ColorStateList.valueOf(MaterialColors.getColor(view, colorAttributeResId));
1528 }
1529
1530 public static void setImageTint(final ImageView imageView, final BubbleColor bubbleColor) {
1531 ImageViewCompat.setImageTintList(
1532 imageView, bubbleToOnSurfaceColorStateList(imageView, bubbleColor));
1533 }
1534
1535 public static void setImageTintError(final ImageView imageView) {
1536 ImageViewCompat.setImageTintList(
1537 imageView,
1538 ColorStateList.valueOf(
1539 MaterialColors.getColor(
1540 imageView, com.google.android.material.R.attr.colorError)));
1541 }
1542
1543 public static void setTextColor(final TextView textView, final BubbleColor bubbleColor) {
1544 textView.setTextColor(bubbleToOnSurfaceColor(textView, bubbleColor));
1545 }
1546
1547 private static void setTextSize(final TextView textView, final boolean largeFont) {
1548 if (largeFont) {
1549 textView.setTextAppearance(
1550 com.google.android.material.R.style.TextAppearance_Material3_BodyLarge);
1551 } else {
1552 textView.setTextAppearance(
1553 com.google.android.material.R.style.TextAppearance_Material3_BodyMedium);
1554 }
1555 }
1556
1557 private static @ColorInt int bubbleToOnSurfaceVariant(
1558 final View view, final BubbleColor bubbleColor) {
1559 final @AttrRes int colorAttributeResId;
1560 if (bubbleColor == BubbleColor.SURFACE_HIGH || bubbleColor == BubbleColor.SURFACE) {
1561 colorAttributeResId = com.google.android.material.R.attr.colorOnSurfaceVariant;
1562 } else {
1563 colorAttributeResId = bubbleToOnSurface(bubbleColor);
1564 }
1565 return MaterialColors.getColor(view, colorAttributeResId);
1566 }
1567
1568 private static @ColorInt int bubbleToOnSurfaceColor(
1569 final View view, final BubbleColor bubbleColor) {
1570 return MaterialColors.getColor(view, bubbleToOnSurface(bubbleColor));
1571 }
1572
1573 public static ColorStateList bubbleToOnSurfaceColorStateList(
1574 final View view, final BubbleColor bubbleColor) {
1575 return ColorStateList.valueOf(bubbleToOnSurfaceColor(view, bubbleColor));
1576 }
1577
1578 private static @AttrRes int bubbleToOnSurface(final BubbleColor bubbleColor) {
1579 return switch (bubbleColor) {
1580 case SURFACE, SURFACE_HIGH -> com.google.android.material.R.attr.colorOnSurface;
1581 case PRIMARY -> com.google.android.material.R.attr.colorOnPrimaryContainer;
1582 case SECONDARY -> com.google.android.material.R.attr.colorOnSecondaryContainer;
1583 case TERTIARY -> com.google.android.material.R.attr.colorOnTertiaryContainer;
1584 case WARNING -> com.google.android.material.R.attr.colorOnErrorContainer;
1585 };
1586 }
1587
1588 public enum BubbleColor {
1589 SURFACE,
1590 SURFACE_HIGH,
1591 PRIMARY,
1592 SECONDARY,
1593 TERTIARY,
1594 WARNING
1595 }
1596
1597 private static class BubbleDesign {
1598 public final boolean colorfulChatBubbles;
1599 public final boolean largeFont;
1600
1601 private BubbleDesign(final boolean colorfulChatBubbles, final boolean largeFont) {
1602 this.colorfulChatBubbles = colorfulChatBubbles;
1603 this.largeFont = largeFont;
1604 }
1605 }
1606
1607 private static class ViewHolder {
1608
1609 public MaterialButton load_more_messages;
1610 public ImageView edit_indicator;
1611 public RelativeLayout audioPlayer;
1612 protected View status_line;
1613 protected LinearLayout message_box;
1614 protected View message_box_inner;
1615 protected MaterialButton download_button;
1616 protected ImageView image;
1617 protected ImageView indicator;
1618 protected ImageView indicatorReceived;
1619 protected TextView time;
1620 protected TextView subject;
1621 protected TextView messageBody;
1622 protected ImageView contact_picture;
1623 protected TextView status_message;
1624 protected TextView encryption;
1625 protected ListView commands_list;
1626 protected GithubIdenticonView thread_identicon;
1627 }
1628
1629 class ThumbnailTask extends AsyncTask<DownloadableFile, Void, Drawable[]> {
1630 @Override
1631 protected Drawable[] doInBackground(DownloadableFile... params) {
1632 if (isCancelled()) return null;
1633
1634 Drawable[] d = new Drawable[params.length];
1635 for (int i = 0; i < params.length; i++) {
1636 try {
1637 d[i] = activity.xmppConnectionService.getFileBackend().getThumbnail(params[i], activity.getResources(), (int) (metrics.density * 288), false);
1638 } catch (final IOException e) {
1639 d[i] = null;
1640 }
1641 }
1642
1643 return d;
1644 }
1645
1646 @Override
1647 protected void onPostExecute(final Drawable[] d) {
1648 if (isCancelled()) return;
1649 activity.xmppConnectionService.updateConversationUi();
1650 }
1651 }
1652}