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