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