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