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 boolean startsWithQuote = handleTextQuotes(viewHolder.messageBody, body, bubbleColor);
602 if (!message.isPrivateMessage()) {
603 if (hasMeCommand) {
604 body.setSpan(
605 new StyleSpan(Typeface.BOLD_ITALIC),
606 0,
607 nick.length(),
608 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
609 }
610 } else {
611 String privateMarker;
612 if (message.getStatus() <= Message.STATUS_RECEIVED) {
613 privateMarker = activity.getString(R.string.private_message);
614 } else {
615 Jid cp = message.getCounterpart();
616 privateMarker =
617 activity.getString(
618 R.string.private_message_to,
619 Strings.nullToEmpty(cp == null ? null : cp.getResource()));
620 }
621 body.insert(0, privateMarker);
622 int privateMarkerIndex = privateMarker.length();
623 if (startsWithQuote) {
624 body.insert(privateMarkerIndex, "\n\n");
625 body.setSpan(
626 new DividerSpan(false),
627 privateMarkerIndex,
628 privateMarkerIndex + 2,
629 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
630 } else {
631 body.insert(privateMarkerIndex, " ");
632 }
633 body.setSpan(
634 new ForegroundColorSpan(
635 bubbleToOnSurfaceVariant(viewHolder.messageBody, bubbleColor)),
636 0,
637 privateMarkerIndex,
638 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
639 body.setSpan(
640 new StyleSpan(Typeface.BOLD),
641 0,
642 privateMarkerIndex,
643 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
644 if (hasMeCommand) {
645 body.setSpan(
646 new StyleSpan(Typeface.BOLD_ITALIC),
647 privateMarkerIndex + 1,
648 privateMarkerIndex + 1 + nick.length(),
649 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
650 }
651 }
652 if (message.getConversation().getMode() == Conversation.MODE_MULTI
653 && message.getStatus() == Message.STATUS_RECEIVED) {
654 if (message.getConversation() instanceof Conversation conversation) {
655 Pattern pattern =
656 NotificationService.generateNickHighlightPattern(
657 conversation.getMucOptions().getActualNick());
658 Matcher matcher = pattern.matcher(body);
659 while (matcher.find()) {
660 body.setSpan(
661 new StyleSpan(Typeface.BOLD),
662 matcher.start(),
663 matcher.end(),
664 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
665 }
666
667 pattern = NotificationService.generateNickHighlightPattern(conversation.getMucOptions().getActualName());
668 matcher = pattern.matcher(body);
669 while (matcher.find()) {
670 body.setSpan(new StyleSpan(Typeface.BOLD), matcher.start(), matcher.end(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
671 }
672 }
673 }
674 Matcher matcher = Emoticons.getEmojiPattern(body).matcher(body);
675 while (matcher.find()) {
676 if (matcher.start() < matcher.end()) {
677 body.setSpan(
678 new RelativeSizeSpan(1.2f),
679 matcher.start(),
680 matcher.end(),
681 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
682 }
683 }
684
685 StylingHelper.format(body, viewHolder.messageBody.getCurrentTextColor());
686 if (highlightedTerm != null) {
687 StylingHelper.highlight(viewHolder.messageBody, body, highlightedTerm);
688 }
689 MyLinkify.addLinks(body, message.getConversation().getAccount(), message.getConversation().getJid());
690 viewHolder.messageBody.setAutoLinkMask(0);
691 viewHolder.messageBody.setText(body);
692 BetterLinkMovementMethod method = new BetterLinkMovementMethod() {
693 @Override
694 protected void dispatchUrlLongClick(TextView tv, ClickableSpan span) {
695 if (span instanceof URLSpan || mOnInlineImageLongClickedListener == null) {
696 tv.dispatchTouchEvent(MotionEvent.obtain(0, 0, MotionEvent.ACTION_CANCEL, 0f, 0f, 0));
697 super.dispatchUrlLongClick(tv, span);
698 return;
699 }
700
701 Spannable body = (Spannable) tv.getText();
702 ImageSpan[] imageSpans = body.getSpans(body.getSpanStart(span), body.getSpanEnd(span), ImageSpan.class);
703 if (imageSpans.length > 0) {
704 Uri uri = Uri.parse(imageSpans[0].getSource());
705 Cid cid = BobTransfer.cid(uri);
706 if (cid == null) return;
707 if (mOnInlineImageLongClickedListener.onInlineImageLongClicked(cid)) {
708 tv.dispatchTouchEvent(MotionEvent.obtain(0, 0, MotionEvent.ACTION_CANCEL, 0f, 0f, 0));
709 }
710 }
711 }
712 };
713 method.setOnLinkLongClickListener((tv, url) -> {
714 tv.dispatchTouchEvent(MotionEvent.obtain(0, 0, MotionEvent.ACTION_CANCEL, 0f, 0f, 0));
715 ShareUtil.copyLinkToClipboard(activity, url);
716 return true;
717 });
718 viewHolder.messageBody.setMovementMethod(method);
719 } else {
720 viewHolder.messageBody.setText("");
721 viewHolder.messageBody.setTextIsSelectable(false);
722 toggleWhisperInfo(viewHolder, message, bubbleColor);
723 }
724 }
725
726 private void displayDownloadableMessage(
727 ViewHolder viewHolder,
728 final Message message,
729 String text,
730 final BubbleColor bubbleColor, final int type) {
731 displayTextMessage(viewHolder, message, bubbleColor, type);
732 toggleWhisperInfo(viewHolder, message, bubbleColor);
733 viewHolder.image.setVisibility(View.GONE);
734 List<Element> thumbs = message.getFileParams() != null ? message.getFileParams().getThumbnails() : null;
735 if (thumbs != null && !thumbs.isEmpty()) {
736 for (Element thumb : thumbs) {
737 Uri uri = Uri.parse(thumb.getAttribute("uri"));
738 if (uri.getScheme().equals("data")) {
739 String[] parts = uri.getSchemeSpecificPart().split(",", 2);
740 parts = parts[0].split(";");
741 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;
742 } else if (uri.getScheme().equals("cid")) {
743 Cid cid = BobTransfer.cid(uri);
744 if (cid == null) continue;
745 DownloadableFile f = activity.xmppConnectionService.getFileForCid(cid);
746 if (f == null || !f.canRead()) {
747 if (!message.trusted() && !message.getConversation().canInferPresence()) continue;
748
749 try {
750 new BobTransfer(BobTransfer.uri(cid), message.getConversation().getAccount(), message.getCounterpart(), activity.xmppConnectionService).start();
751 } catch (final NoSuchAlgorithmException | URISyntaxException e) { }
752 continue;
753 }
754 } else {
755 continue;
756 }
757
758 int width = message.getFileParams().width;
759 if (width < 1 && thumb.getAttribute("width") != null) width = Integer.parseInt(thumb.getAttribute("width"));
760 if (width < 1) width = 1920;
761
762 int height = message.getFileParams().height;
763 if (height < 1 && thumb.getAttribute("height") != null) height = Integer.parseInt(thumb.getAttribute("height"));
764 if (height < 1) height = 1080;
765
766 viewHolder.image.setVisibility(View.VISIBLE);
767 imagePreviewLayout(width, height, viewHolder.image, message.getBody() != null && message.getBody().length() > 0);
768 activity.loadBitmap(message, viewHolder.image);
769 viewHolder.image.setOnClickListener(v -> ConversationFragment.downloadFile(activity, message));
770
771 break;
772 }
773 }
774 viewHolder.audioPlayer.setVisibility(View.GONE);
775 viewHolder.download_button.setVisibility(View.VISIBLE);
776 viewHolder.download_button.setText(text);
777 final var attachment = Attachment.of(message);
778 final @DrawableRes int imageResource = MediaAdapter.getImageDrawable(attachment);
779 viewHolder.download_button.setIconResource(imageResource);
780 viewHolder.download_button.setOnClickListener(
781 v -> ConversationFragment.downloadFile(activity, message));
782 }
783
784 private void displayWebxdcMessage(ViewHolder viewHolder, final Message message, final BubbleColor bubbleColor, final int type) {
785 Cid webxdcCid = message.getFileParams().getCids().get(0);
786 WebxdcPage webxdc = new WebxdcPage(activity, webxdcCid, message, activity.xmppConnectionService);
787 displayTextMessage(viewHolder, message, bubbleColor, type);
788 viewHolder.image.setVisibility(View.GONE);
789 viewHolder.audioPlayer.setVisibility(View.GONE);
790 viewHolder.download_button.setVisibility(View.VISIBLE);
791 viewHolder.download_button.setText("Open " + webxdc.getName());
792 viewHolder.download_button.setOnClickListener(v -> {
793 Conversation conversation = (Conversation) message.getConversation();
794 if (!conversation.switchToSession("webxdc\0" + message.getUuid())) {
795 conversation.startWebxdc(webxdc);
796 }
797 });
798
799 final WebxdcUpdate lastUpdate;
800 synchronized(lastWebxdcUpdate) { lastUpdate = lastWebxdcUpdate.get(message.getUuid()); }
801 if (lastUpdate == null) {
802 new Thread(() -> {
803 final WebxdcUpdate update = activity.xmppConnectionService.findLastWebxdcUpdate(message);
804 if (update != null) {
805 synchronized(lastWebxdcUpdate) { lastWebxdcUpdate.put(message.getUuid(), update); }
806 activity.xmppConnectionService.updateConversationUi();
807 }
808 }).start();
809 } else {
810 if (lastUpdate != null && (lastUpdate.getSummary() != null || lastUpdate.getDocument() != null)) {
811 viewHolder.messageBody.setVisibility(View.VISIBLE);
812 viewHolder.messageBody.setText(
813 (lastUpdate.getDocument() == null ? "" : lastUpdate.getDocument() + "\n") +
814 (lastUpdate.getSummary() == null ? "" : lastUpdate.getSummary())
815 );
816 }
817 }
818
819 final LruCache<String, Drawable> cache = activity.xmppConnectionService.getDrawableCache();
820 final Drawable d = cache.get("webxdc:icon:" + webxdcCid);
821 if (d == null) {
822 new Thread(() -> {
823 Drawable icon = webxdc.getIcon();
824 if (icon != null) {
825 cache.put("webxdc:icon:" + webxdcCid, icon);
826 activity.xmppConnectionService.updateConversationUi();
827 }
828 }).start();
829 } else {
830 viewHolder.image.setVisibility(View.VISIBLE);
831 viewHolder.image.setImageDrawable(d);
832 }
833 }
834
835 private void displayOpenableMessage(
836 ViewHolder viewHolder, final Message message, final BubbleColor bubbleColor, final int type) {
837 displayTextMessage(viewHolder, message, bubbleColor, type);
838 toggleWhisperInfo(viewHolder, message, bubbleColor);
839 viewHolder.image.setVisibility(View.GONE);
840 viewHolder.audioPlayer.setVisibility(View.GONE);
841 viewHolder.download_button.setVisibility(View.VISIBLE);
842 viewHolder.download_button.setText(
843 activity.getString(
844 R.string.open_x_file,
845 UIHelper.getFileDescriptionString(activity, message)));
846 final var attachment = Attachment.of(message);
847 final @DrawableRes int imageResource = MediaAdapter.getImageDrawable(attachment);
848 viewHolder.download_button.setIconResource(imageResource);
849 viewHolder.download_button.setOnClickListener(v -> openDownloadable(message));
850 }
851
852 private void displayLocationMessage(
853 ViewHolder viewHolder, final Message message, final BubbleColor bubbleColor, final int type) {
854 displayTextMessage(viewHolder, message, bubbleColor, type);
855 toggleWhisperInfo(viewHolder, message, bubbleColor);
856 viewHolder.image.setVisibility(View.GONE);
857 viewHolder.audioPlayer.setVisibility(View.GONE);
858 viewHolder.download_button.setVisibility(View.VISIBLE);
859 viewHolder.download_button.setText(R.string.show_location);
860 final var attachment = Attachment.of(message);
861 final @DrawableRes int imageResource = MediaAdapter.getImageDrawable(attachment);
862 viewHolder.download_button.setIconResource(imageResource);
863 viewHolder.download_button.setOnClickListener(v -> showLocation(message));
864 }
865
866 private void displayAudioMessage(
867 ViewHolder viewHolder, Message message, final BubbleColor bubbleColor, final int type) {
868 displayTextMessage(viewHolder, message, bubbleColor, type);
869 toggleWhisperInfo(viewHolder, message, bubbleColor);
870 viewHolder.image.setVisibility(View.GONE);
871 viewHolder.download_button.setVisibility(View.GONE);
872 final RelativeLayout audioPlayer = viewHolder.audioPlayer;
873 audioPlayer.setVisibility(View.VISIBLE);
874 AudioPlayer.ViewHolder.get(audioPlayer).setBubbleColor(bubbleColor);
875 this.audioPlayer.init(audioPlayer, message);
876 }
877
878 private void displayMediaPreviewMessage(
879 ViewHolder viewHolder, final Message message, final BubbleColor bubbleColor, final int type) {
880 displayTextMessage(viewHolder, message, bubbleColor, type);
881 toggleWhisperInfo(viewHolder, message, bubbleColor);
882 viewHolder.download_button.setVisibility(View.GONE);
883 viewHolder.audioPlayer.setVisibility(View.GONE);
884 viewHolder.image.setVisibility(View.VISIBLE);
885 final FileParams params = message.getFileParams();
886 imagePreviewLayout(params.width, params.height, viewHolder.image, message.getBody() != null && message.getBody().length() > 0);
887 activity.loadBitmap(message, viewHolder.image);
888 viewHolder.image.setOnClickListener(v -> openDownloadable(message));
889 }
890
891 private void imagePreviewLayout(int w, int h, ImageView image, boolean topMargin) {
892 final float target = activity.getResources().getDimension(R.dimen.image_preview_width);
893 final int scaledW;
894 final int scaledH;
895 if (Math.max(h, w) * metrics.density <= target) {
896 scaledW = (int) (w * metrics.density);
897 scaledH = (int) (h * metrics.density);
898 } else if (Math.max(h, w) <= target) {
899 scaledW = w;
900 scaledH = h;
901 } else if (w <= h) {
902 scaledW = (int) (w / ((double) h / target));
903 scaledH = (int) target;
904 } else {
905 scaledW = (int) target;
906 scaledH = (int) (h / ((double) w / target));
907 }
908 final LinearLayout.LayoutParams layoutParams =
909 new LinearLayout.LayoutParams(scaledW, scaledH);
910 layoutParams.setMargins(0, topMargin ? (int) (metrics.density * 4) : 0, 0, (int) (metrics.density * 4));
911 layoutParams.gravity = Gravity.CENTER;
912 image.setLayoutParams(layoutParams);
913 }
914
915 private void toggleWhisperInfo(
916 ViewHolder viewHolder, final Message message, final BubbleColor bubbleColor) {
917 if (message.isPrivateMessage()) {
918 final String privateMarker;
919 if (message.getStatus() <= Message.STATUS_RECEIVED) {
920 privateMarker = activity.getString(R.string.private_message);
921 } else {
922 Jid cp = message.getCounterpart();
923 privateMarker =
924 activity.getString(
925 R.string.private_message_to,
926 Strings.nullToEmpty(cp == null ? null : cp.getResource()));
927 }
928 final SpannableString body = new SpannableString(privateMarker);
929 body.setSpan(
930 new ForegroundColorSpan(
931 bubbleToOnSurfaceVariant(viewHolder.messageBody, bubbleColor)),
932 0,
933 privateMarker.length(),
934 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
935 body.setSpan(
936 new StyleSpan(Typeface.BOLD),
937 0,
938 privateMarker.length(),
939 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
940 viewHolder.messageBody.setText(body);
941 viewHolder.messageBody.setVisibility(View.VISIBLE);
942 } else {
943 viewHolder.messageBody.setVisibility(View.GONE);
944 }
945 }
946
947 private void loadMoreMessages(Conversation conversation) {
948 conversation.setLastClearHistory(0, null);
949 activity.xmppConnectionService.updateConversation(conversation);
950 conversation.setHasMessagesLeftOnServer(true);
951 conversation.setFirstMamReference(null);
952 long timestamp = conversation.getLastMessageTransmitted().getTimestamp();
953 if (timestamp == 0) {
954 timestamp = System.currentTimeMillis();
955 }
956 conversation.messagesLoaded.set(true);
957 MessageArchiveService.Query query =
958 activity.xmppConnectionService
959 .getMessageArchiveService()
960 .query(conversation, new MamReference(0), timestamp, false);
961 if (query != null) {
962 Toast.makeText(activity, R.string.fetching_history_from_server, Toast.LENGTH_LONG)
963 .show();
964 } else {
965 Toast.makeText(
966 activity,
967 R.string.not_fetching_history_retention_period,
968 Toast.LENGTH_SHORT)
969 .show();
970 }
971 }
972
973 @Override
974 public View getView(final int position, View view, final @NonNull ViewGroup parent) {
975 final Message message = getItem(position);
976 final boolean omemoEncryption = message.getEncryption() == Message.ENCRYPTION_AXOLOTL;
977 final boolean isInValidSession =
978 message.isValidInSession() && (!omemoEncryption || message.isTrusted());
979 final Conversational conversation = message.getConversation();
980 final Account account = conversation.getAccount();
981 final List<Element> commands = message.getCommands();
982 final int type = getItemViewType(position);
983 ViewHolder viewHolder;
984 if (view == null) {
985 viewHolder = new ViewHolder();
986 switch (type) {
987 case DATE_SEPARATOR:
988 view =
989 activity.getLayoutInflater()
990 .inflate(R.layout.item_message_date_bubble, parent, false);
991 viewHolder.status_message = view.findViewById(R.id.message_body);
992 viewHolder.message_box = view.findViewById(R.id.message_box);
993 break;
994 case RTP_SESSION:
995 view =
996 activity.getLayoutInflater()
997 .inflate(R.layout.item_message_rtp_session, parent, false);
998 viewHolder.status_message = view.findViewById(R.id.message_body);
999 viewHolder.message_box = view.findViewById(R.id.message_box);
1000 viewHolder.indicatorReceived = view.findViewById(R.id.indicator_received);
1001 break;
1002 case SENT:
1003 view = activity.getLayoutInflater().inflate(R.layout.item_message_sent, parent, false);
1004 viewHolder.status_line = view.findViewById(R.id.status_line);
1005 viewHolder.message_box_inner = view.findViewById(R.id.message_box_inner);
1006 viewHolder.message_box = view.findViewById(R.id.message_box);
1007 viewHolder.contact_picture = view.findViewById(R.id.message_photo);
1008 viewHolder.download_button = view.findViewById(R.id.download_button);
1009 viewHolder.indicator = view.findViewById(R.id.security_indicator);
1010 viewHolder.edit_indicator = view.findViewById(R.id.edit_indicator);
1011 viewHolder.image = view.findViewById(R.id.message_image);
1012 viewHolder.messageBody = view.findViewById(R.id.message_body);
1013 viewHolder.time = view.findViewById(R.id.message_time);
1014 viewHolder.subject = view.findViewById(R.id.message_subject);
1015 viewHolder.indicatorReceived = view.findViewById(R.id.indicator_received);
1016 viewHolder.audioPlayer = view.findViewById(R.id.audio_player);
1017 viewHolder.thread_identicon = view.findViewById(R.id.thread_identicon);
1018 break;
1019 case RECEIVED:
1020 view = activity.getLayoutInflater().inflate(R.layout.item_message_received, parent, false);
1021 viewHolder.status_line = view.findViewById(R.id.status_line);
1022 viewHolder.message_box_inner = view.findViewById(R.id.message_box_inner);
1023 viewHolder.message_box = view.findViewById(R.id.message_box);
1024 viewHolder.contact_picture = view.findViewById(R.id.message_photo);
1025 viewHolder.download_button = view.findViewById(R.id.download_button);
1026 viewHolder.indicator = view.findViewById(R.id.security_indicator);
1027 viewHolder.edit_indicator = view.findViewById(R.id.edit_indicator);
1028 viewHolder.image = view.findViewById(R.id.message_image);
1029 viewHolder.messageBody = view.findViewById(R.id.message_body);
1030 viewHolder.time = view.findViewById(R.id.message_time);
1031 viewHolder.subject = view.findViewById(R.id.message_subject);
1032 viewHolder.indicatorReceived = view.findViewById(R.id.indicator_received);
1033 viewHolder.encryption = view.findViewById(R.id.message_encryption);
1034 viewHolder.audioPlayer = view.findViewById(R.id.audio_player);
1035 viewHolder.commands_list = view.findViewById(R.id.commands_list);
1036 viewHolder.thread_identicon = view.findViewById(R.id.thread_identicon);
1037 break;
1038 case STATUS:
1039 view =
1040 activity.getLayoutInflater()
1041 .inflate(R.layout.item_message_status, parent, false);
1042 viewHolder.contact_picture = view.findViewById(R.id.message_photo);
1043 viewHolder.status_message = view.findViewById(R.id.status_message);
1044 viewHolder.load_more_messages = view.findViewById(R.id.load_more_messages);
1045 break;
1046 default:
1047 throw new AssertionError("Unknown view type");
1048 }
1049 view.setTag(viewHolder);
1050 } else {
1051 viewHolder = (ViewHolder) view.getTag();
1052 if (viewHolder == null) {
1053 return view;
1054 }
1055 }
1056
1057 if (viewHolder.messageBody != null) {
1058 viewHolder.messageBody.setCustomSelectionActionModeCallback(new MessageTextActionModeCallback(this, viewHolder.messageBody));
1059 }
1060
1061 if (viewHolder.thread_identicon != null) {
1062 viewHolder.thread_identicon.setVisibility(View.GONE);
1063 final Element thread = message.getThread();
1064 if (thread != null) {
1065 final String threadId = thread.getContent();
1066 if (threadId != null) {
1067 viewHolder.thread_identicon.setVisibility(View.VISIBLE);
1068 viewHolder.thread_identicon.setColor(UIHelper.getColorForName(threadId));
1069 viewHolder.thread_identicon.setHash(UIHelper.identiconHash(threadId));
1070 }
1071 }
1072 }
1073
1074 final var black = MaterialColors.getColor(view, com.google.android.material.R.attr.colorSecondaryContainer) == view.getContext().getColor(android.R.color.black);
1075 final boolean colorfulBackground = this.bubbleDesign.colorfulChatBubbles;
1076 final BubbleColor bubbleColor;
1077 if (type == RECEIVED) {
1078 if (isInValidSession) {
1079 bubbleColor = colorfulBackground || black ? BubbleColor.SECONDARY : BubbleColor.SURFACE;
1080 } else {
1081 bubbleColor = BubbleColor.WARNING;
1082 }
1083 } else {
1084 if (!colorfulBackground && black) {
1085 bubbleColor = BubbleColor.SECONDARY;
1086 } else {
1087 bubbleColor = colorfulBackground ? BubbleColor.TERTIARY : BubbleColor.SURFACE_HIGH;
1088 }
1089 }
1090
1091 if (type == DATE_SEPARATOR) {
1092 if (UIHelper.today(message.getTimeSent())) {
1093 viewHolder.status_message.setText(R.string.today);
1094 } else if (UIHelper.yesterday(message.getTimeSent())) {
1095 viewHolder.status_message.setText(R.string.yesterday);
1096 } else {
1097 viewHolder.status_message.setText(
1098 DateUtils.formatDateTime(
1099 activity,
1100 message.getTimeSent(),
1101 DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR));
1102 }
1103 if (colorfulBackground) {
1104 setBackgroundTint(viewHolder.message_box, BubbleColor.PRIMARY);
1105 setTextColor(viewHolder.status_message, BubbleColor.PRIMARY);
1106 } else {
1107 setBackgroundTint(viewHolder.message_box, BubbleColor.SURFACE_HIGH);
1108 setTextColor(viewHolder.status_message, BubbleColor.SURFACE_HIGH);
1109 }
1110 return view;
1111 } else if (type == RTP_SESSION) {
1112 final boolean received = message.getStatus() <= Message.STATUS_RECEIVED;
1113 final RtpSessionStatus rtpSessionStatus = RtpSessionStatus.of(message.getBody());
1114 final long duration = rtpSessionStatus.duration;
1115 final String callTime = UIHelper.readableTimeDifferenceFull(activity, message.getTimeSent());
1116 if (received) {
1117 if (duration > 0) {
1118 viewHolder.status_message.setText(
1119 activity.getString(
1120 R.string.incoming_call_duration_timestamp,
1121 TimeFrameUtils.resolve(activity, duration),
1122 UIHelper.readableTimeDifferenceFull(
1123 activity, message.getTimeSent())));
1124 } else if (rtpSessionStatus.successful) {
1125 viewHolder.status_message.setText(activity.getString(R.string.incoming_call_timestamp, callTime));
1126 } else {
1127 viewHolder.status_message.setText(
1128 activity.getString(
1129 R.string.missed_call_timestamp,
1130 UIHelper.readableTimeDifferenceFull(
1131 activity, message.getTimeSent())));
1132 }
1133 } else {
1134 if (duration > 0) {
1135 viewHolder.status_message.setText(
1136 activity.getString(
1137 R.string.outgoing_call_duration_timestamp,
1138 TimeFrameUtils.resolve(activity, duration),
1139 UIHelper.readableTimeDifferenceFull(
1140 activity, message.getTimeSent())));
1141 } else {
1142 viewHolder.status_message.setText(
1143 activity.getString(
1144 R.string.outgoing_call_timestamp,
1145 UIHelper.readableTimeDifferenceFull(
1146 activity, message.getTimeSent())));
1147 }
1148 }
1149 if (colorfulBackground) {
1150 setBackgroundTint(viewHolder.message_box, BubbleColor.SECONDARY);
1151 setTextColor(viewHolder.status_message, BubbleColor.SECONDARY);
1152 setImageTint(viewHolder.indicatorReceived, BubbleColor.SECONDARY);
1153 } else {
1154 setBackgroundTint(viewHolder.message_box, BubbleColor.SURFACE_HIGH);
1155 setTextColor(viewHolder.status_message, BubbleColor.SURFACE_HIGH);
1156 setImageTint(viewHolder.indicatorReceived, BubbleColor.SURFACE_HIGH);
1157 }
1158 viewHolder.indicatorReceived.setImageResource(
1159 RtpSessionStatus.getDrawable(received, rtpSessionStatus.successful));
1160 return view;
1161 } else if (type == STATUS) {
1162 if ("LOAD_MORE".equals(message.getBody())) {
1163 viewHolder.status_message.setVisibility(View.GONE);
1164 viewHolder.contact_picture.setVisibility(View.GONE);
1165 viewHolder.load_more_messages.setVisibility(View.VISIBLE);
1166 viewHolder.load_more_messages.setOnClickListener(
1167 v -> loadMoreMessages((Conversation) message.getConversation()));
1168 } else {
1169 viewHolder.status_message.setVisibility(View.VISIBLE);
1170 viewHolder.load_more_messages.setVisibility(View.GONE);
1171 viewHolder.status_message.setText(message.getBody());
1172 boolean showAvatar;
1173 if (conversation.getMode() == Conversation.MODE_SINGLE) {
1174 showAvatar = true;
1175 AvatarWorkerTask.loadAvatar(
1176 message, viewHolder.contact_picture, R.dimen.avatar_on_status_message);
1177 } else if (message.getCounterpart() != null
1178 || message.getTrueCounterpart() != null
1179 || (message.getCounterparts() != null
1180 && message.getCounterparts().size() > 0)) {
1181 showAvatar = true;
1182 AvatarWorkerTask.loadAvatar(
1183 message, viewHolder.contact_picture, R.dimen.avatar_on_status_message);
1184 } else {
1185 showAvatar = false;
1186 }
1187 if (showAvatar) {
1188 viewHolder.contact_picture.setAlpha(0.5f);
1189 viewHolder.contact_picture.setVisibility(View.VISIBLE);
1190 } else {
1191 viewHolder.contact_picture.setVisibility(View.GONE);
1192 }
1193 }
1194 return view;
1195 } else {
1196 AvatarWorkerTask.loadAvatar(message, viewHolder.contact_picture, R.dimen.avatar);
1197 }
1198
1199 resetClickListener(viewHolder.message_box, viewHolder.messageBody);
1200
1201 viewHolder.message_box.setOnClickListener(v -> {
1202 if (MessageAdapter.this.mOnMessageBoxClickedListener != null) {
1203 MessageAdapter.this.mOnMessageBoxClickedListener
1204 .onContactPictureClicked(message);
1205 }
1206 });
1207 SwipeDetector swipeDetector = new SwipeDetector((action) -> {
1208 if (action == SwipeDetector.Action.LR && MessageAdapter.this.mOnMessageBoxSwipedListener != null) {
1209 MessageAdapter.this.mOnMessageBoxSwipedListener.onContactPictureClicked(message);
1210 }
1211 });
1212 viewHolder.message_box.setOnTouchListener(swipeDetector);
1213 viewHolder.image.setOnTouchListener(swipeDetector);
1214 viewHolder.time.setOnTouchListener(swipeDetector);
1215
1216 // Treat touch-up as click so we don't have to touch twice
1217 // (touch twice is because it's waiting to see if you double-touch for text selection)
1218 viewHolder.messageBody.setOnTouchListener((v, event) -> {
1219 if (event.getAction() == MotionEvent.ACTION_UP) {
1220 if (MessageAdapter.this.mOnMessageBoxClickedListener != null) {
1221 MessageAdapter.this.mOnMessageBoxClickedListener
1222 .onContactPictureClicked(message);
1223 }
1224 }
1225
1226 swipeDetector.onTouch(v, event);
1227
1228 return false;
1229 });
1230 viewHolder.messageBody.setOnClickListener(v -> {
1231 if (MessageAdapter.this.mOnMessageBoxClickedListener != null) {
1232 MessageAdapter.this.mOnMessageBoxClickedListener
1233 .onContactPictureClicked(message);
1234 }
1235 });
1236 viewHolder.contact_picture.setOnClickListener(v -> {
1237 if (MessageAdapter.this.mOnContactPictureClickedListener != null) {
1238 MessageAdapter.this.mOnContactPictureClickedListener
1239 .onContactPictureClicked(message);
1240 }
1241
1242 });
1243 viewHolder.contact_picture.setOnLongClickListener(v -> {
1244 if (MessageAdapter.this.mOnContactPictureLongClickedListener != null) {
1245 MessageAdapter.this.mOnContactPictureLongClickedListener
1246 .onContactPictureLongClicked(v, message);
1247 return true;
1248 } else {
1249 return false;
1250 }
1251 });
1252 viewHolder.messageBody.setAccessibilityDelegate(null);
1253
1254 boolean footerWrap = false;
1255
1256 final Transferable transferable = message.getTransferable();
1257 final boolean unInitiatedButKnownSize = MessageUtils.unInitiatedButKnownSize(message);
1258
1259 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));
1260 if (muted) {
1261 // Muted MUC participant
1262 displayInfoMessage(viewHolder, "Muted", bubbleColor);
1263 } else if (unInitiatedButKnownSize || message.isDeleted() || (transferable != null && transferable.getStatus() != Transferable.STATUS_UPLOADING)) {
1264 if (unInitiatedButKnownSize || (message.isDeleted() && message.getModerated() == null) || transferable != null && transferable.getStatus() == Transferable.STATUS_OFFER) {
1265 displayDownloadableMessage(viewHolder, message, activity.getString(R.string.download_x_file, UIHelper.getFileDescriptionString(activity, message)), bubbleColor, type);
1266 } else if (transferable != null && transferable.getStatus() == Transferable.STATUS_OFFER_CHECK_FILESIZE) {
1267 displayDownloadableMessage(viewHolder, message, activity.getString(R.string.check_x_filesize, UIHelper.getFileDescriptionString(activity, message)), bubbleColor, type);
1268 } else {
1269 displayInfoMessage(viewHolder, UIHelper.getMessagePreview(activity.xmppConnectionService, message).first, bubbleColor);
1270 }
1271 } else if (message.isFileOrImage()
1272 && message.getEncryption() != Message.ENCRYPTION_PGP
1273 && message.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED) {
1274 if (message.getFileParams().width > 0 && message.getFileParams().height > 0) {
1275 displayMediaPreviewMessage(viewHolder, message, bubbleColor, type);
1276 if (!black && viewHolder.image.getLayoutParams().width > metrics.density * 110) {
1277 footerWrap = true;
1278 }
1279 } else if (message.getFileParams().runtime > 0) {
1280 displayAudioMessage(viewHolder, message, bubbleColor, type);
1281 } else if ("application/xdc+zip".equals(message.getFileParams().getMediaType()) && message.getConversation() instanceof Conversation && message.getThread() != null && !message.getFileParams().getCids().isEmpty()) {
1282 displayWebxdcMessage(viewHolder, message, bubbleColor, type);
1283 } else {
1284 displayOpenableMessage(viewHolder, message, bubbleColor, type);
1285 }
1286 } else if (message.getEncryption() == Message.ENCRYPTION_PGP) {
1287 if (account.isPgpDecryptionServiceConnected()) {
1288 if (conversation instanceof Conversation
1289 && !account.hasPendingPgpIntent((Conversation) conversation)) {
1290 displayInfoMessage(
1291 viewHolder,
1292 activity.getString(R.string.message_decrypting),
1293 bubbleColor);
1294 } else {
1295 displayInfoMessage(
1296 viewHolder, activity.getString(R.string.pgp_message), bubbleColor);
1297 }
1298 } else {
1299 displayInfoMessage(
1300 viewHolder, activity.getString(R.string.install_openkeychain), bubbleColor);
1301 viewHolder.message_box.setOnClickListener(this::promptOpenKeychainInstall);
1302 viewHolder.messageBody.setOnClickListener(this::promptOpenKeychainInstall);
1303 }
1304 } else if (message.getEncryption() == Message.ENCRYPTION_DECRYPTION_FAILED) {
1305 displayInfoMessage(
1306 viewHolder, activity.getString(R.string.decryption_failed), bubbleColor);
1307 } else if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE) {
1308 displayInfoMessage(
1309 viewHolder,
1310 activity.getString(R.string.not_encrypted_for_this_device),
1311 bubbleColor);
1312 } else if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL_FAILED) {
1313 displayInfoMessage(
1314 viewHolder, activity.getString(R.string.omemo_decryption_failed), bubbleColor);
1315 } else {
1316 if (message.isGeoUri()) {
1317 displayLocationMessage(viewHolder, message, bubbleColor, type);
1318 } else if (message.treatAsDownloadable()) {
1319 try {
1320 final URI uri = message.getOob();
1321 displayDownloadableMessage(viewHolder,
1322 message,
1323 activity.getString(
1324 R.string.check_x_filesize_on_host,
1325 UIHelper.getFileDescriptionString(activity, message),
1326 uri.getHost()),
1327 bubbleColor, type);
1328 } catch (Exception e) {
1329 displayDownloadableMessage(
1330 viewHolder,
1331 message,
1332 activity.getString(
1333 R.string.check_x_filesize,
1334 UIHelper.getFileDescriptionString(activity, message)),
1335 bubbleColor, type);
1336 }
1337 } else if (message.bodyIsOnlyEmojis() && message.getType() != Message.TYPE_PRIVATE) {
1338 displayEmojiMessage(viewHolder, getSpannableBody(message), bubbleColor);
1339 } else {
1340 displayTextMessage(viewHolder, message, bubbleColor, message.getType());
1341 }
1342 }
1343
1344 viewHolder.message_box_inner.setMinimumWidth(footerWrap ? (int) (110 * metrics.density) : 0);
1345 LinearLayout.LayoutParams statusParams = (LinearLayout.LayoutParams) viewHolder.status_line.getLayoutParams();
1346 statusParams.width = footerWrap ? ViewGroup.LayoutParams.MATCH_PARENT : ViewGroup.LayoutParams.WRAP_CONTENT;
1347 viewHolder.status_line.setLayoutParams(statusParams);
1348
1349 setBackgroundTint(viewHolder.message_box, bubbleColor);
1350 setTextColor(viewHolder.messageBody, bubbleColor);
1351
1352 if (type == RECEIVED) {
1353 if (!muted && commands != null && conversation instanceof Conversation) {
1354 CommandButtonAdapter adapter = new CommandButtonAdapter(activity);
1355 adapter.addAll(commands);
1356 viewHolder.commands_list.setAdapter(adapter);
1357 viewHolder.commands_list.setVisibility(View.VISIBLE);
1358 viewHolder.commands_list.setOnItemClickListener((p, v, pos, id) -> {
1359 final Element command = adapter.getItem(pos);
1360 activity.startCommand(conversation.getAccount(), command.getAttributeAsJid("jid"), command.getAttribute("node"));
1361 });
1362 } else {
1363 // It's unclear if we can set this to null...
1364 ListAdapter adapter = viewHolder.commands_list.getAdapter();
1365 if (adapter instanceof ArrayAdapter) {
1366 ((ArrayAdapter<?>) adapter).clear();
1367 }
1368 viewHolder.commands_list.setVisibility(View.GONE);
1369 viewHolder.commands_list.setOnItemClickListener(null);
1370 }
1371
1372 setTextColor(viewHolder.encryption, bubbleColor);
1373
1374 if (isInValidSession) {
1375 viewHolder.encryption.setVisibility(View.GONE);
1376 } else {
1377 viewHolder.encryption.setVisibility(View.VISIBLE);
1378 if (omemoEncryption && !message.isTrusted()) {
1379 viewHolder.encryption.setText(R.string.not_trusted);
1380 } else {
1381 viewHolder.encryption.setText(
1382 CryptoHelper.encryptionTypeToText(message.getEncryption()));
1383 }
1384 }
1385 }
1386
1387 if (type == RECEIVED || type == SENT) {
1388 String subject = message.getSubject();
1389 if (subject == null && message.getThread() != null) {
1390 subject = ((Conversation) message.getConversation()).getThread(message.getThread().getContent()).getSubject();
1391 }
1392 if (muted || subject == null) {
1393 viewHolder.subject.setVisibility(View.GONE);
1394 } else {
1395 viewHolder.subject.setVisibility(View.VISIBLE);
1396 viewHolder.subject.setText(subject);
1397 }
1398 }
1399
1400 /* TODO
1401 if (darkBackground) {
1402 if (viewHolder.subject != null) viewHolder.subject.setTextAppearance(getContext(), R.style.TextAppearance_Conversations_Caption_OnDark_Bold);
1403 if (viewHolder.encryption != null) viewHolder.encryption.setTextAppearance(getContext(), R.style.TextAppearance_Conversations_Caption_OnDark_Bold);
1404 } else {
1405 if (viewHolder.subject != null) viewHolder.subject.setTextAppearance(getContext(), R.style.TextAppearance_Conversations_Caption_Bold);
1406 if (viewHolder.encryption != null) viewHolder.encryption.setTextAppearance(getContext(), R.style.TextAppearance_Conversations_Caption_Bold);
1407 }
1408 */
1409
1410 displayStatus(viewHolder, message, type, bubbleColor);
1411
1412 viewHolder.messageBody.setAccessibilityDelegate(new View.AccessibilityDelegate() {
1413 @Override
1414 public void sendAccessibilityEvent(View host, int eventType) {
1415 super.sendAccessibilityEvent(host, eventType);
1416 if (eventType == AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED) {
1417 if (viewHolder.messageBody.hasSelection()) {
1418 selectionUuid = message.getUuid();
1419 } else if (message.getUuid() != null && message.getUuid().equals(selectionUuid)) {
1420 selectionUuid = null;
1421 }
1422 }
1423 }
1424 });
1425
1426 return view;
1427 }
1428
1429 private void promptOpenKeychainInstall(View view) {
1430 activity.showInstallPgpDialog();
1431 }
1432
1433 public FileBackend getFileBackend() {
1434 return activity.xmppConnectionService.getFileBackend();
1435 }
1436
1437 public void stopAudioPlayer() {
1438 audioPlayer.stop();
1439 }
1440
1441 public void unregisterListenerInAudioPlayer() {
1442 audioPlayer.unregisterListener();
1443 }
1444
1445 public void startStopPending() {
1446 audioPlayer.startStopPending();
1447 }
1448
1449 public void openDownloadable(Message message) {
1450 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU
1451 && ContextCompat.checkSelfPermission(
1452 activity, Manifest.permission.WRITE_EXTERNAL_STORAGE)
1453 != PackageManager.PERMISSION_GRANTED) {
1454 ConversationFragment.registerPendingMessage(activity, message);
1455 ActivityCompat.requestPermissions(
1456 activity,
1457 new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE},
1458 ConversationsActivity.REQUEST_OPEN_MESSAGE);
1459 return;
1460 }
1461 final DownloadableFile file =
1462 activity.xmppConnectionService.getFileBackend().getFile(message);
1463 ViewUtil.view(activity, file);
1464 }
1465
1466 private void showLocation(Message message) {
1467 for (Intent intent : GeoHelper.createGeoIntentsFromMessage(activity, message)) {
1468 if (intent.resolveActivity(getContext().getPackageManager()) != null) {
1469 getContext().startActivity(intent);
1470 return;
1471 }
1472 }
1473 Toast.makeText(
1474 activity,
1475 R.string.no_application_found_to_display_location,
1476 Toast.LENGTH_SHORT)
1477 .show();
1478 }
1479
1480 public void updatePreferences() {
1481 final AppSettings appSettings = new AppSettings(activity);
1482 this.bubbleDesign =
1483 new BubbleDesign(appSettings.isColorfulChatBubbles(), appSettings.isLargeFont());
1484 }
1485
1486 public void setHighlightedTerm(List<String> terms) {
1487 this.highlightedTerm = terms == null ? null : StylingHelper.filterHighlightedWords(terms);
1488 }
1489
1490 public interface OnContactPictureClicked {
1491 void onContactPictureClicked(Message message);
1492 }
1493
1494 public interface OnContactPictureLongClicked {
1495 void onContactPictureLongClicked(View v, Message message);
1496 }
1497
1498 public interface OnInlineImageLongClicked {
1499 boolean onInlineImageLongClicked(Cid cid);
1500 }
1501
1502 private static void setBackgroundTint(final View view, final BubbleColor bubbleColor) {
1503 view.setBackgroundTintList(bubbleToColorStateList(view, bubbleColor));
1504 }
1505
1506 private static ColorStateList bubbleToColorStateList(
1507 final View view, final BubbleColor bubbleColor) {
1508 final @AttrRes int colorAttributeResId =
1509 switch (bubbleColor) {
1510 case SURFACE -> Activities.isNightMode(view.getContext())
1511 ? com.google.android.material.R.attr.colorSurfaceContainerHigh
1512 : com.google.android.material.R.attr.colorSurfaceContainerLow;
1513 case SURFACE_HIGH -> Activities.isNightMode(view.getContext())
1514 ? com.google.android.material.R.attr.colorSurfaceContainerHighest
1515 : com.google.android.material.R.attr.colorSurfaceContainerHigh;
1516 case PRIMARY -> com.google.android.material.R.attr.colorPrimaryContainer;
1517 case SECONDARY -> com.google.android.material.R.attr.colorSecondaryContainer;
1518 case TERTIARY -> com.google.android.material.R.attr.colorTertiaryContainer;
1519 case WARNING -> com.google.android.material.R.attr.colorErrorContainer;
1520 };
1521 return ColorStateList.valueOf(MaterialColors.getColor(view, colorAttributeResId));
1522 }
1523
1524 public static void setImageTint(final ImageView imageView, final BubbleColor bubbleColor) {
1525 ImageViewCompat.setImageTintList(
1526 imageView, bubbleToOnSurfaceColorStateList(imageView, bubbleColor));
1527 }
1528
1529 public static void setImageTintError(final ImageView imageView) {
1530 ImageViewCompat.setImageTintList(
1531 imageView,
1532 ColorStateList.valueOf(
1533 MaterialColors.getColor(
1534 imageView, com.google.android.material.R.attr.colorError)));
1535 }
1536
1537 public static void setTextColor(final TextView textView, final BubbleColor bubbleColor) {
1538 textView.setTextColor(bubbleToOnSurfaceColor(textView, bubbleColor));
1539 }
1540
1541 private static void setTextSize(final TextView textView, final boolean largeFont) {
1542 if (largeFont) {
1543 textView.setTextAppearance(
1544 com.google.android.material.R.style.TextAppearance_Material3_BodyLarge);
1545 } else {
1546 textView.setTextAppearance(
1547 com.google.android.material.R.style.TextAppearance_Material3_BodyMedium);
1548 }
1549 }
1550
1551 private static @ColorInt int bubbleToOnSurfaceVariant(
1552 final View view, final BubbleColor bubbleColor) {
1553 final @AttrRes int colorAttributeResId;
1554 if (bubbleColor == BubbleColor.SURFACE_HIGH || bubbleColor == BubbleColor.SURFACE) {
1555 colorAttributeResId = com.google.android.material.R.attr.colorOnSurfaceVariant;
1556 } else {
1557 colorAttributeResId = bubbleToOnSurface(bubbleColor);
1558 }
1559 return MaterialColors.getColor(view, colorAttributeResId);
1560 }
1561
1562 private static @ColorInt int bubbleToOnSurfaceColor(
1563 final View view, final BubbleColor bubbleColor) {
1564 return MaterialColors.getColor(view, bubbleToOnSurface(bubbleColor));
1565 }
1566
1567 public static ColorStateList bubbleToOnSurfaceColorStateList(
1568 final View view, final BubbleColor bubbleColor) {
1569 return ColorStateList.valueOf(bubbleToOnSurfaceColor(view, bubbleColor));
1570 }
1571
1572 private static @AttrRes int bubbleToOnSurface(final BubbleColor bubbleColor) {
1573 return switch (bubbleColor) {
1574 case SURFACE, SURFACE_HIGH -> com.google.android.material.R.attr.colorOnSurface;
1575 case PRIMARY -> com.google.android.material.R.attr.colorOnPrimaryContainer;
1576 case SECONDARY -> com.google.android.material.R.attr.colorOnSecondaryContainer;
1577 case TERTIARY -> com.google.android.material.R.attr.colorOnTertiaryContainer;
1578 case WARNING -> com.google.android.material.R.attr.colorOnErrorContainer;
1579 };
1580 }
1581
1582 public enum BubbleColor {
1583 SURFACE,
1584 SURFACE_HIGH,
1585 PRIMARY,
1586 SECONDARY,
1587 TERTIARY,
1588 WARNING
1589 }
1590
1591 private static class BubbleDesign {
1592 public final boolean colorfulChatBubbles;
1593 public final boolean largeFont;
1594
1595 private BubbleDesign(final boolean colorfulChatBubbles, final boolean largeFont) {
1596 this.colorfulChatBubbles = colorfulChatBubbles;
1597 this.largeFont = largeFont;
1598 }
1599 }
1600
1601 private static class ViewHolder {
1602
1603 public MaterialButton load_more_messages;
1604 public ImageView edit_indicator;
1605 public RelativeLayout audioPlayer;
1606 protected View status_line;
1607 protected LinearLayout message_box;
1608 protected View message_box_inner;
1609 protected MaterialButton download_button;
1610 protected ImageView image;
1611 protected ImageView indicator;
1612 protected ImageView indicatorReceived;
1613 protected TextView time;
1614 protected TextView subject;
1615 protected TextView messageBody;
1616 protected ImageView contact_picture;
1617 protected TextView status_message;
1618 protected TextView encryption;
1619 protected ListView commands_list;
1620 protected GithubIdenticonView thread_identicon;
1621 }
1622
1623 class ThumbnailTask extends AsyncTask<DownloadableFile, Void, Drawable[]> {
1624 @Override
1625 protected Drawable[] doInBackground(DownloadableFile... params) {
1626 if (isCancelled()) return null;
1627
1628 Drawable[] d = new Drawable[params.length];
1629 for (int i = 0; i < params.length; i++) {
1630 try {
1631 d[i] = activity.xmppConnectionService.getFileBackend().getThumbnail(params[i], activity.getResources(), (int) (metrics.density * 288), false);
1632 } catch (final IOException e) {
1633 d[i] = null;
1634 }
1635 }
1636
1637 return d;
1638 }
1639
1640 @Override
1641 protected void onPostExecute(final Drawable[] d) {
1642 if (isCancelled()) return;
1643 activity.xmppConnectionService.updateConversationUi();
1644 }
1645 }
1646}