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