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.content.res.ColorStateList;
8import android.graphics.Typeface;
9import android.os.Build;
10import android.text.Spannable;
11import android.text.SpannableString;
12import android.text.SpannableStringBuilder;
13import android.text.format.DateUtils;
14import android.text.style.ForegroundColorSpan;
15import android.text.style.RelativeSizeSpan;
16import android.text.style.StyleSpan;
17import android.util.DisplayMetrics;
18import android.util.Log;
19import android.view.View;
20import android.view.ViewGroup;
21import android.view.WindowManager;
22import android.widget.ArrayAdapter;
23import android.widget.Button;
24import android.widget.ImageView;
25import android.widget.LinearLayout;
26import android.widget.RelativeLayout;
27import android.widget.TextView;
28import android.widget.Toast;
29
30import androidx.annotation.AttrRes;
31import androidx.annotation.ColorInt;
32import androidx.annotation.DrawableRes;
33import androidx.annotation.NonNull;
34import androidx.annotation.Nullable;
35import androidx.core.app.ActivityCompat;
36import androidx.core.content.ContextCompat;
37import androidx.core.widget.ImageViewCompat;
38import androidx.databinding.DataBindingUtil;
39import androidx.emoji2.emojipicker.EmojiViewItem;
40import androidx.emoji2.emojipicker.RecentEmojiProvider;
41
42import com.google.android.material.button.MaterialButton;
43import com.google.android.material.chip.ChipGroup;
44import com.google.android.material.color.MaterialColors;
45import com.google.android.material.dialog.MaterialAlertDialogBuilder;
46import com.google.common.base.Joiner;
47import com.google.common.base.Strings;
48import com.google.common.collect.ImmutableList;
49import com.google.common.collect.ImmutableSet;
50
51import eu.siacs.conversations.AppSettings;
52import eu.siacs.conversations.Config;
53import eu.siacs.conversations.R;
54import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
55import eu.siacs.conversations.databinding.DialogAddReactionBinding;
56import eu.siacs.conversations.entities.Account;
57import eu.siacs.conversations.entities.Conversation;
58import eu.siacs.conversations.entities.Conversational;
59import eu.siacs.conversations.entities.DownloadableFile;
60import eu.siacs.conversations.entities.Message;
61import eu.siacs.conversations.entities.Message.FileParams;
62import eu.siacs.conversations.entities.RtpSessionStatus;
63import eu.siacs.conversations.entities.Transferable;
64import eu.siacs.conversations.persistance.FileBackend;
65import eu.siacs.conversations.services.MessageArchiveService;
66import eu.siacs.conversations.services.NotificationService;
67import eu.siacs.conversations.ui.Activities;
68import eu.siacs.conversations.ui.BindingAdapters;
69import eu.siacs.conversations.ui.ConversationFragment;
70import eu.siacs.conversations.ui.ConversationsActivity;
71import eu.siacs.conversations.ui.XmppActivity;
72import eu.siacs.conversations.ui.service.AudioPlayer;
73import eu.siacs.conversations.ui.text.DividerSpan;
74import eu.siacs.conversations.ui.text.QuoteSpan;
75import eu.siacs.conversations.ui.util.Attachment;
76import eu.siacs.conversations.ui.util.AvatarWorkerTask;
77import eu.siacs.conversations.ui.util.MyLinkify;
78import eu.siacs.conversations.ui.util.QuoteHelper;
79import eu.siacs.conversations.ui.util.ViewUtil;
80import eu.siacs.conversations.ui.widget.ClickableMovementMethod;
81import eu.siacs.conversations.utils.CryptoHelper;
82import eu.siacs.conversations.utils.Emoticons;
83import eu.siacs.conversations.utils.GeoHelper;
84import eu.siacs.conversations.utils.MessageUtils;
85import eu.siacs.conversations.utils.StylingHelper;
86import eu.siacs.conversations.utils.TimeFrameUtils;
87import eu.siacs.conversations.utils.UIHelper;
88import eu.siacs.conversations.xmpp.Jid;
89import eu.siacs.conversations.xmpp.mam.MamReference;
90import kotlin.coroutines.Continuation;
91
92import java.net.URI;
93import java.util.Arrays;
94import java.util.Collection;
95import java.util.List;
96import java.util.Locale;
97import java.util.function.Consumer;
98import java.util.regex.Matcher;
99import java.util.regex.Pattern;
100
101public class MessageAdapter extends ArrayAdapter<Message> {
102
103 public static final String DATE_SEPARATOR_BODY = "DATE_SEPARATOR";
104 private static final int SENT = 0;
105 private static final int RECEIVED = 1;
106 private static final int STATUS = 2;
107 private static final int DATE_SEPARATOR = 3;
108 private static final int RTP_SESSION = 4;
109 private final XmppActivity activity;
110 private final AudioPlayer audioPlayer;
111 private List<String> highlightedTerm = null;
112 private final DisplayMetrics metrics;
113 private OnContactPictureClicked mOnContactPictureClickedListener;
114 private OnContactPictureLongClicked mOnContactPictureLongClickedListener;
115 private BubbleDesign bubbleDesign = new BubbleDesign(false, false);
116 private final boolean mForceNames;
117
118 public MessageAdapter(
119 final XmppActivity activity, final List<Message> messages, final boolean forceNames) {
120 super(activity, 0, messages);
121 this.audioPlayer = new AudioPlayer(this);
122 this.activity = activity;
123 metrics = getContext().getResources().getDisplayMetrics();
124 updatePreferences();
125 this.mForceNames = forceNames;
126 }
127
128 public MessageAdapter(final XmppActivity activity, final List<Message> messages) {
129 this(activity, messages, false);
130 }
131
132 private static void resetClickListener(View... views) {
133 for (View view : views) {
134 view.setOnClickListener(null);
135 }
136 }
137
138 public void flagScreenOn() {
139 activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
140 }
141
142 public void flagScreenOff() {
143 activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
144 }
145
146 public void setVolumeControl(final int stream) {
147 activity.setVolumeControlStream(stream);
148 }
149
150 public void setOnContactPictureClicked(OnContactPictureClicked listener) {
151 this.mOnContactPictureClickedListener = listener;
152 }
153
154 public Activity getActivity() {
155 return activity;
156 }
157
158 public void setOnContactPictureLongClicked(OnContactPictureLongClicked listener) {
159 this.mOnContactPictureLongClickedListener = listener;
160 }
161
162 @Override
163 public int getViewTypeCount() {
164 return 5;
165 }
166
167 private int getItemViewType(Message message) {
168 if (message.getType() == Message.TYPE_STATUS) {
169 if (DATE_SEPARATOR_BODY.equals(message.getBody())) {
170 return DATE_SEPARATOR;
171 } else {
172 return STATUS;
173 }
174 } else if (message.getType() == Message.TYPE_RTP_SESSION) {
175 return RTP_SESSION;
176 } else if (message.getStatus() <= Message.STATUS_RECEIVED) {
177 return RECEIVED;
178 } else {
179 return SENT;
180 }
181 }
182
183 @Override
184 public int getItemViewType(int position) {
185 return this.getItemViewType(getItem(position));
186 }
187
188 private void displayStatus(
189 final ViewHolder viewHolder,
190 final Message message,
191 final int type,
192 final BubbleColor bubbleColor) {
193 final int mergedStatus = message.getMergedStatus();
194 final boolean error;
195 if (viewHolder.indicatorReceived != null) {
196 viewHolder.indicatorReceived.setVisibility(View.GONE);
197 }
198 final Transferable transferable = message.getTransferable();
199 final boolean multiReceived =
200 message.getConversation().getMode() == Conversation.MODE_MULTI
201 && mergedStatus <= Message.STATUS_RECEIVED;
202 final String fileSize;
203 if (message.isFileOrImage()
204 || transferable != null
205 || MessageUtils.unInitiatedButKnownSize(message)) {
206 final FileParams params = message.getFileParams();
207 fileSize = params.size != null ? UIHelper.filesizeToString(params.size) : null;
208 if (message.getStatus() == Message.STATUS_SEND_FAILED
209 || (transferable != null
210 && (transferable.getStatus() == Transferable.STATUS_FAILED
211 || transferable.getStatus()
212 == Transferable.STATUS_CANCELLED))) {
213 error = true;
214 } else {
215 error = message.getStatus() == Message.STATUS_SEND_FAILED;
216 }
217 } else {
218 fileSize = null;
219 error = message.getStatus() == Message.STATUS_SEND_FAILED;
220 }
221 if (type == SENT) {
222 final @DrawableRes Integer receivedIndicator =
223 getMessageStatusAsDrawable(message, mergedStatus);
224 if (receivedIndicator == null) {
225 viewHolder.indicatorReceived.setVisibility(View.INVISIBLE);
226 } else {
227 viewHolder.indicatorReceived.setImageResource(receivedIndicator);
228 if (mergedStatus == Message.STATUS_SEND_FAILED) {
229 setImageTintError(viewHolder.indicatorReceived);
230 } else {
231 setImageTint(viewHolder.indicatorReceived, bubbleColor);
232 }
233 viewHolder.indicatorReceived.setVisibility(View.VISIBLE);
234 }
235 }
236 final var additionalStatusInfo = getAdditionalStatusInfo(message, mergedStatus);
237
238 if (error && type == SENT) {
239 viewHolder.time.setTextColor(
240 MaterialColors.getColor(
241 viewHolder.time, com.google.android.material.R.attr.colorError));
242 } else {
243 setTextColor(viewHolder.time, bubbleColor);
244 }
245 if (message.getEncryption() == Message.ENCRYPTION_NONE) {
246 viewHolder.indicator.setVisibility(View.GONE);
247 } else {
248 boolean verified = false;
249 if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) {
250 final FingerprintStatus status =
251 message.getConversation()
252 .getAccount()
253 .getAxolotlService()
254 .getFingerprintTrust(message.getFingerprint());
255 if (status != null && status.isVerified()) {
256 verified = true;
257 }
258 }
259 if (verified) {
260 viewHolder.indicator.setImageResource(R.drawable.ic_verified_user_24dp);
261 } else {
262 viewHolder.indicator.setImageResource(R.drawable.ic_lock_24dp);
263 }
264 if (error && type == SENT) {
265 setImageTintError(viewHolder.indicator);
266 } else {
267 setImageTint(viewHolder.indicator, bubbleColor);
268 }
269 viewHolder.indicator.setVisibility(View.VISIBLE);
270 }
271
272 if (viewHolder.edit_indicator != null) {
273 if (message.edited()) {
274 viewHolder.edit_indicator.setVisibility(View.VISIBLE);
275 if (error && type == SENT) {
276 setImageTintError(viewHolder.edit_indicator);
277 } else {
278 setImageTint(viewHolder.edit_indicator, bubbleColor);
279 }
280 } else {
281 viewHolder.edit_indicator.setVisibility(View.GONE);
282 }
283 }
284
285 final String formattedTime =
286 UIHelper.readableTimeDifferenceFull(getContext(), message.getMergedTimeSent());
287 final String bodyLanguage = message.getBodyLanguage();
288 final ImmutableList.Builder<String> timeInfoBuilder = new ImmutableList.Builder<>();
289 if (message.getStatus() <= Message.STATUS_RECEIVED) {
290 timeInfoBuilder.add(formattedTime);
291 if (fileSize != null) {
292 timeInfoBuilder.add(fileSize);
293 }
294 if (mForceNames || multiReceived) {
295 final String displayName = UIHelper.getMessageDisplayName(message);
296 if (displayName != null) {
297 timeInfoBuilder.add(displayName);
298 }
299 }
300 if (bodyLanguage != null) {
301 timeInfoBuilder.add(bodyLanguage.toUpperCase(Locale.US));
302 }
303 } else {
304 if (bodyLanguage != null) {
305 timeInfoBuilder.add(bodyLanguage.toUpperCase(Locale.US));
306 }
307 if (fileSize != null) {
308 timeInfoBuilder.add(fileSize);
309 }
310 // for space reasons we display only 'additional status info' (send progress or concrete
311 // failure reason) or the time
312 if (additionalStatusInfo != null) {
313 timeInfoBuilder.add(additionalStatusInfo);
314 } else {
315 timeInfoBuilder.add(formattedTime);
316 }
317 }
318 final var timeInfo = timeInfoBuilder.build();
319 viewHolder.time.setText(Joiner.on(" \u00B7 ").join(timeInfo));
320 }
321
322 public static @DrawableRes Integer getMessageStatusAsDrawable(
323 final Message message, final int status) {
324 final var transferable = message.getTransferable();
325 return switch (status) {
326 case Message.STATUS_WAITING -> R.drawable.ic_more_horiz_24dp;
327 case Message.STATUS_UNSEND -> transferable == null ? null : R.drawable.ic_upload_24dp;
328 case Message.STATUS_SEND -> R.drawable.ic_done_24dp;
329 case Message.STATUS_SEND_RECEIVED, Message.STATUS_SEND_DISPLAYED -> R.drawable
330 .ic_done_all_24dp;
331 case Message.STATUS_SEND_FAILED -> {
332 final String errorMessage = message.getErrorMessage();
333 if (Message.ERROR_MESSAGE_CANCELLED.equals(errorMessage)) {
334 yield R.drawable.ic_cancel_24dp;
335 } else {
336 yield R.drawable.ic_error_24dp;
337 }
338 }
339 case Message.STATUS_OFFERED -> R.drawable.ic_p2p_24dp;
340 default -> null;
341 };
342 }
343
344 @Nullable
345 private String getAdditionalStatusInfo(final Message message, final int mergedStatus) {
346 final String additionalStatusInfo;
347 if (mergedStatus == Message.STATUS_SEND_FAILED) {
348 final String errorMessage = Strings.nullToEmpty(message.getErrorMessage());
349 final String[] errorParts = errorMessage.split("\\u001f", 2);
350 if (errorParts.length == 2 && errorParts[0].equals("file-too-large")) {
351 additionalStatusInfo = getContext().getString(R.string.file_too_large);
352 } else {
353 additionalStatusInfo = null;
354 }
355 } else if (mergedStatus == Message.STATUS_UNSEND) {
356 final var transferable = message.getTransferable();
357 if (transferable == null) {
358 return null;
359 }
360 return getContext().getString(R.string.sending_file, transferable.getProgress());
361 } else {
362 additionalStatusInfo = null;
363 }
364 return additionalStatusInfo;
365 }
366
367 private void displayInfoMessage(
368 ViewHolder viewHolder, CharSequence text, final BubbleColor bubbleColor) {
369 viewHolder.download_button.setVisibility(View.GONE);
370 viewHolder.audioPlayer.setVisibility(View.GONE);
371 viewHolder.image.setVisibility(View.GONE);
372 viewHolder.messageBody.setVisibility(View.VISIBLE);
373 viewHolder.messageBody.setText(text);
374 viewHolder.messageBody.setTextColor(
375 bubbleToOnSurfaceVariant(viewHolder.messageBody, bubbleColor));
376 viewHolder.messageBody.setTextIsSelectable(false);
377 }
378
379 private void displayEmojiMessage(
380 final ViewHolder viewHolder, final String body, final BubbleColor bubbleColor) {
381 viewHolder.download_button.setVisibility(View.GONE);
382 viewHolder.audioPlayer.setVisibility(View.GONE);
383 viewHolder.image.setVisibility(View.GONE);
384 viewHolder.messageBody.setVisibility(View.VISIBLE);
385 setTextColor(viewHolder.messageBody, bubbleColor);
386 final Spannable span = new SpannableString(body);
387 float size = Emoticons.isEmoji(body) ? 3.0f : 2.0f;
388 span.setSpan(
389 new RelativeSizeSpan(size), 0, body.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
390 viewHolder.messageBody.setText(span);
391 }
392
393 private void applyQuoteSpan(
394 final TextView textView,
395 SpannableStringBuilder body,
396 int start,
397 int end,
398 final BubbleColor bubbleColor) {
399 if (start > 1 && !"\n\n".equals(body.subSequence(start - 2, start).toString())) {
400 body.insert(start++, "\n");
401 body.setSpan(
402 new DividerSpan(false), start - 2, start, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
403 end++;
404 }
405 if (end < body.length() - 1 && !"\n\n".equals(body.subSequence(end, end + 2).toString())) {
406 body.insert(end, "\n");
407 body.setSpan(new DividerSpan(false), end, end + 2, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
408 }
409 final DisplayMetrics metrics = getContext().getResources().getDisplayMetrics();
410 body.setSpan(
411 new QuoteSpan(bubbleToOnSurfaceVariant(textView, bubbleColor), metrics),
412 start,
413 end,
414 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
415 }
416
417 /**
418 * Applies QuoteSpan to group of lines which starts with > or » characters. Appends likebreaks
419 * and applies DividerSpan to them to show a padding between quote and text.
420 */
421 private boolean handleTextQuotes(
422 final TextView textView,
423 final SpannableStringBuilder body,
424 final BubbleColor bubbleColor) {
425 boolean startsWithQuote = false;
426 int quoteDepth = 1;
427 while (QuoteHelper.bodyContainsQuoteStart(body) && quoteDepth <= Config.QUOTE_MAX_DEPTH) {
428 char previous = '\n';
429 int lineStart = -1;
430 int lineTextStart = -1;
431 int quoteStart = -1;
432 for (int i = 0; i <= body.length(); i++) {
433 char current = body.length() > i ? body.charAt(i) : '\n';
434 if (lineStart == -1) {
435 if (previous == '\n') {
436 if (i < body.length() && QuoteHelper.isPositionQuoteStart(body, i)) {
437 // Line start with quote
438 lineStart = i;
439 if (quoteStart == -1) quoteStart = i;
440 if (i == 0) startsWithQuote = true;
441 } else if (quoteStart >= 0) {
442 // Line start without quote, apply spans there
443 applyQuoteSpan(textView, body, quoteStart, i - 1, bubbleColor);
444 quoteStart = -1;
445 }
446 }
447 } else {
448 // Remove extra spaces between > and first character in the line
449 // > character will be removed too
450 if (current != ' ' && lineTextStart == -1) {
451 lineTextStart = i;
452 }
453 if (current == '\n') {
454 body.delete(lineStart, lineTextStart);
455 i -= lineTextStart - lineStart;
456 if (i == lineStart) {
457 // Avoid empty lines because span over empty line can be hidden
458 body.insert(i++, " ");
459 }
460 lineStart = -1;
461 lineTextStart = -1;
462 }
463 }
464 previous = current;
465 }
466 if (quoteStart >= 0) {
467 // Apply spans to finishing open quote
468 applyQuoteSpan(textView, body, quoteStart, body.length(), bubbleColor);
469 }
470 quoteDepth++;
471 }
472 return startsWithQuote;
473 }
474
475 private void displayTextMessage(
476 final ViewHolder viewHolder, final Message message, final BubbleColor bubbleColor) {
477 viewHolder.download_button.setVisibility(View.GONE);
478 viewHolder.image.setVisibility(View.GONE);
479 viewHolder.audioPlayer.setVisibility(View.GONE);
480 viewHolder.messageBody.setVisibility(View.VISIBLE);
481 setTextColor(viewHolder.messageBody, bubbleColor);
482 setTextSize(viewHolder.messageBody, this.bubbleDesign.largeFont);
483 viewHolder.messageBody.setTypeface(null, Typeface.NORMAL);
484
485 if (message.getBody() != null) {
486 final String nick = UIHelper.getMessageDisplayName(message);
487 SpannableStringBuilder body = message.getMergedBody();
488 boolean hasMeCommand = message.hasMeCommand();
489 if (hasMeCommand) {
490 body = body.replace(0, Message.ME_COMMAND.length(), nick + " ");
491 }
492 if (body.length() > Config.MAX_DISPLAY_MESSAGE_CHARS) {
493 body = new SpannableStringBuilder(body, 0, Config.MAX_DISPLAY_MESSAGE_CHARS);
494 body.append("\u2026");
495 }
496 Message.MergeSeparator[] mergeSeparators =
497 body.getSpans(0, body.length(), Message.MergeSeparator.class);
498 for (Message.MergeSeparator mergeSeparator : mergeSeparators) {
499 int start = body.getSpanStart(mergeSeparator);
500 int end = body.getSpanEnd(mergeSeparator);
501 body.setSpan(new DividerSpan(true), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
502 }
503 boolean startsWithQuote = handleTextQuotes(viewHolder.messageBody, body, bubbleColor);
504 if (!message.isPrivateMessage()) {
505 if (hasMeCommand) {
506 body.setSpan(
507 new StyleSpan(Typeface.BOLD_ITALIC),
508 0,
509 nick.length(),
510 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
511 }
512 } else {
513 String privateMarker;
514 if (message.getStatus() <= Message.STATUS_RECEIVED) {
515 privateMarker = activity.getString(R.string.private_message);
516 } else {
517 Jid cp = message.getCounterpart();
518 privateMarker =
519 activity.getString(
520 R.string.private_message_to,
521 Strings.nullToEmpty(cp == null ? null : cp.getResource()));
522 }
523 body.insert(0, privateMarker);
524 int privateMarkerIndex = privateMarker.length();
525 if (startsWithQuote) {
526 body.insert(privateMarkerIndex, "\n\n");
527 body.setSpan(
528 new DividerSpan(false),
529 privateMarkerIndex,
530 privateMarkerIndex + 2,
531 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
532 } else {
533 body.insert(privateMarkerIndex, " ");
534 }
535 body.setSpan(
536 new ForegroundColorSpan(
537 bubbleToOnSurfaceVariant(viewHolder.messageBody, bubbleColor)),
538 0,
539 privateMarkerIndex,
540 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
541 body.setSpan(
542 new StyleSpan(Typeface.BOLD),
543 0,
544 privateMarkerIndex,
545 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
546 if (hasMeCommand) {
547 body.setSpan(
548 new StyleSpan(Typeface.BOLD_ITALIC),
549 privateMarkerIndex + 1,
550 privateMarkerIndex + 1 + nick.length(),
551 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
552 }
553 }
554 if (message.getConversation().getMode() == Conversation.MODE_MULTI
555 && message.getStatus() == Message.STATUS_RECEIVED) {
556 if (message.getConversation() instanceof Conversation conversation) {
557 Pattern pattern =
558 NotificationService.generateNickHighlightPattern(
559 conversation.getMucOptions().getActualNick());
560 Matcher matcher = pattern.matcher(body);
561 while (matcher.find()) {
562 body.setSpan(
563 new StyleSpan(Typeface.BOLD),
564 matcher.start(),
565 matcher.end(),
566 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
567 }
568 }
569 }
570 Matcher matcher = Emoticons.getEmojiPattern(body).matcher(body);
571 while (matcher.find()) {
572 if (matcher.start() < matcher.end()) {
573 body.setSpan(
574 new RelativeSizeSpan(1.2f),
575 matcher.start(),
576 matcher.end(),
577 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
578 }
579 }
580
581 StylingHelper.format(body, viewHolder.messageBody.getCurrentTextColor());
582 MyLinkify.addLinks(body, true);
583 if (highlightedTerm != null) {
584 StylingHelper.highlight(viewHolder.messageBody, body, highlightedTerm);
585 }
586 viewHolder.messageBody.setAutoLinkMask(0);
587 viewHolder.messageBody.setText(body);
588 viewHolder.messageBody.setMovementMethod(ClickableMovementMethod.getInstance());
589 } else {
590 viewHolder.messageBody.setText("");
591 viewHolder.messageBody.setTextIsSelectable(false);
592 }
593 }
594
595 private void displayDownloadableMessage(
596 ViewHolder viewHolder,
597 final Message message,
598 String text,
599 final BubbleColor bubbleColor) {
600 toggleWhisperInfo(viewHolder, message, bubbleColor);
601 viewHolder.image.setVisibility(View.GONE);
602 viewHolder.audioPlayer.setVisibility(View.GONE);
603 viewHolder.download_button.setVisibility(View.VISIBLE);
604 viewHolder.download_button.setText(text);
605 final var attachment = Attachment.of(message);
606 final @DrawableRes int imageResource = MediaAdapter.getImageDrawable(attachment);
607 viewHolder.download_button.setIconResource(imageResource);
608 viewHolder.download_button.setOnClickListener(
609 v -> ConversationFragment.downloadFile(activity, message));
610 }
611
612 private void displayOpenableMessage(
613 ViewHolder viewHolder, final Message message, final BubbleColor bubbleColor) {
614 toggleWhisperInfo(viewHolder, message, bubbleColor);
615 viewHolder.image.setVisibility(View.GONE);
616 viewHolder.audioPlayer.setVisibility(View.GONE);
617 viewHolder.download_button.setVisibility(View.VISIBLE);
618 viewHolder.download_button.setText(
619 activity.getString(
620 R.string.open_x_file,
621 UIHelper.getFileDescriptionString(activity, message)));
622 final var attachment = Attachment.of(message);
623 final @DrawableRes int imageResource = MediaAdapter.getImageDrawable(attachment);
624 viewHolder.download_button.setIconResource(imageResource);
625 viewHolder.download_button.setOnClickListener(v -> openDownloadable(message));
626 }
627
628 private void displayLocationMessage(
629 ViewHolder viewHolder, final Message message, final BubbleColor bubbleColor) {
630 toggleWhisperInfo(viewHolder, message, bubbleColor);
631 viewHolder.image.setVisibility(View.GONE);
632 viewHolder.audioPlayer.setVisibility(View.GONE);
633 viewHolder.download_button.setVisibility(View.VISIBLE);
634 viewHolder.download_button.setText(R.string.show_location);
635 final var attachment = Attachment.of(message);
636 final @DrawableRes int imageResource = MediaAdapter.getImageDrawable(attachment);
637 viewHolder.download_button.setIconResource(imageResource);
638 viewHolder.download_button.setOnClickListener(v -> showLocation(message));
639 }
640
641 private void displayAudioMessage(
642 ViewHolder viewHolder, Message message, final BubbleColor bubbleColor) {
643 toggleWhisperInfo(viewHolder, message, bubbleColor);
644 viewHolder.image.setVisibility(View.GONE);
645 viewHolder.download_button.setVisibility(View.GONE);
646 final RelativeLayout audioPlayer = viewHolder.audioPlayer;
647 audioPlayer.setVisibility(View.VISIBLE);
648 AudioPlayer.ViewHolder.get(audioPlayer).setBubbleColor(bubbleColor);
649 this.audioPlayer.init(audioPlayer, message);
650 }
651
652 private void displayMediaPreviewMessage(
653 ViewHolder viewHolder, final Message message, final BubbleColor bubbleColor) {
654 toggleWhisperInfo(viewHolder, message, bubbleColor);
655 viewHolder.download_button.setVisibility(View.GONE);
656 viewHolder.audioPlayer.setVisibility(View.GONE);
657 viewHolder.image.setVisibility(View.VISIBLE);
658 final FileParams params = message.getFileParams();
659 final float target = activity.getResources().getDimension(R.dimen.image_preview_width);
660 final int scaledW;
661 final int scaledH;
662 if (Math.max(params.height, params.width) * metrics.density <= target) {
663 scaledW = (int) (params.width * metrics.density);
664 scaledH = (int) (params.height * metrics.density);
665 } else if (Math.max(params.height, params.width) <= target) {
666 scaledW = params.width;
667 scaledH = params.height;
668 } else if (params.width <= params.height) {
669 scaledW = (int) (params.width / ((double) params.height / target));
670 scaledH = (int) target;
671 } else {
672 scaledW = (int) target;
673 scaledH = (int) (params.height / ((double) params.width / target));
674 }
675 final LinearLayout.LayoutParams layoutParams =
676 new LinearLayout.LayoutParams(scaledW, scaledH);
677 viewHolder.image.setLayoutParams(layoutParams);
678 activity.loadBitmap(message, viewHolder.image);
679 viewHolder.image.setOnClickListener(v -> openDownloadable(message));
680 }
681
682 private void toggleWhisperInfo(
683 ViewHolder viewHolder, final Message message, final BubbleColor bubbleColor) {
684 if (message.isPrivateMessage()) {
685 final String privateMarker;
686 if (message.getStatus() <= Message.STATUS_RECEIVED) {
687 privateMarker = activity.getString(R.string.private_message);
688 } else {
689 Jid cp = message.getCounterpart();
690 privateMarker =
691 activity.getString(
692 R.string.private_message_to,
693 Strings.nullToEmpty(cp == null ? null : cp.getResource()));
694 }
695 final SpannableString body = new SpannableString(privateMarker);
696 body.setSpan(
697 new ForegroundColorSpan(
698 bubbleToOnSurfaceVariant(viewHolder.messageBody, bubbleColor)),
699 0,
700 privateMarker.length(),
701 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
702 body.setSpan(
703 new StyleSpan(Typeface.BOLD),
704 0,
705 privateMarker.length(),
706 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
707 viewHolder.messageBody.setText(body);
708 viewHolder.messageBody.setVisibility(View.VISIBLE);
709 } else {
710 viewHolder.messageBody.setVisibility(View.GONE);
711 }
712 }
713
714 private void loadMoreMessages(Conversation conversation) {
715 conversation.setLastClearHistory(0, null);
716 activity.xmppConnectionService.updateConversation(conversation);
717 conversation.setHasMessagesLeftOnServer(true);
718 conversation.setFirstMamReference(null);
719 long timestamp = conversation.getLastMessageTransmitted().getTimestamp();
720 if (timestamp == 0) {
721 timestamp = System.currentTimeMillis();
722 }
723 conversation.messagesLoaded.set(true);
724 MessageArchiveService.Query query =
725 activity.xmppConnectionService
726 .getMessageArchiveService()
727 .query(conversation, new MamReference(0), timestamp, false);
728 if (query != null) {
729 Toast.makeText(activity, R.string.fetching_history_from_server, Toast.LENGTH_LONG)
730 .show();
731 } else {
732 Toast.makeText(
733 activity,
734 R.string.not_fetching_history_retention_period,
735 Toast.LENGTH_SHORT)
736 .show();
737 }
738 }
739
740 @Override
741 public View getView(final int position, View view, final @NonNull ViewGroup parent) {
742 final Message message = getItem(position);
743 final boolean omemoEncryption = message.getEncryption() == Message.ENCRYPTION_AXOLOTL;
744 final boolean isInValidSession =
745 message.isValidInSession() && (!omemoEncryption || message.isTrusted());
746 final Conversational conversation = message.getConversation();
747 final Account account = conversation.getAccount();
748 final int type = getItemViewType(position);
749 ViewHolder viewHolder;
750 if (view == null) {
751 viewHolder = new ViewHolder();
752 switch (type) {
753 case DATE_SEPARATOR:
754 view =
755 activity.getLayoutInflater()
756 .inflate(R.layout.item_message_date_bubble, parent, false);
757 viewHolder.status_message = view.findViewById(R.id.message_body);
758 viewHolder.message_box = view.findViewById(R.id.message_box);
759 break;
760 case RTP_SESSION:
761 view =
762 activity.getLayoutInflater()
763 .inflate(R.layout.item_message_rtp_session, parent, false);
764 viewHolder.status_message = view.findViewById(R.id.message_body);
765 viewHolder.message_box = view.findViewById(R.id.message_box);
766 viewHolder.indicatorReceived = view.findViewById(R.id.indicator_received);
767 break;
768 case SENT:
769 view =
770 activity.getLayoutInflater()
771 .inflate(R.layout.item_message_sent, parent, false);
772 viewHolder.message_box = view.findViewById(R.id.message_box);
773 viewHolder.contact_picture = view.findViewById(R.id.message_photo);
774 viewHolder.download_button = view.findViewById(R.id.download_button);
775 viewHolder.indicator = view.findViewById(R.id.security_indicator);
776 viewHolder.edit_indicator = view.findViewById(R.id.edit_indicator);
777 viewHolder.image = view.findViewById(R.id.message_image);
778 viewHolder.messageBody = view.findViewById(R.id.message_body);
779 viewHolder.time = view.findViewById(R.id.message_time);
780 viewHolder.indicatorReceived = view.findViewById(R.id.indicator_received);
781 viewHolder.audioPlayer = view.findViewById(R.id.audio_player);
782 viewHolder.reactions = view.findViewById(R.id.reactions);
783 break;
784 case RECEIVED:
785 view =
786 activity.getLayoutInflater()
787 .inflate(R.layout.item_message_received, parent, false);
788 viewHolder.message_box = view.findViewById(R.id.message_box);
789 viewHolder.contact_picture = view.findViewById(R.id.message_photo);
790 viewHolder.download_button = view.findViewById(R.id.download_button);
791 viewHolder.indicator = view.findViewById(R.id.security_indicator);
792 viewHolder.edit_indicator = view.findViewById(R.id.edit_indicator);
793 viewHolder.image = view.findViewById(R.id.message_image);
794 viewHolder.messageBody = view.findViewById(R.id.message_body);
795 viewHolder.time = view.findViewById(R.id.message_time);
796 viewHolder.indicatorReceived = view.findViewById(R.id.indicator_received);
797 viewHolder.encryption = view.findViewById(R.id.message_encryption);
798 viewHolder.audioPlayer = view.findViewById(R.id.audio_player);
799 viewHolder.reactions = view.findViewById(R.id.reactions);
800 break;
801 case STATUS:
802 view =
803 activity.getLayoutInflater()
804 .inflate(R.layout.item_message_status, parent, false);
805 viewHolder.contact_picture = view.findViewById(R.id.message_photo);
806 viewHolder.status_message = view.findViewById(R.id.status_message);
807 viewHolder.load_more_messages = view.findViewById(R.id.load_more_messages);
808 break;
809 default:
810 throw new AssertionError("Unknown view type");
811 }
812 view.setTag(viewHolder);
813 } else {
814 viewHolder = (ViewHolder) view.getTag();
815 if (viewHolder == null) {
816 return view;
817 }
818 }
819
820 final boolean colorfulBackground = this.bubbleDesign.colorfulChatBubbles;
821 final BubbleColor bubbleColor;
822 if (type == RECEIVED) {
823 if (isInValidSession) {
824 bubbleColor = colorfulBackground ? BubbleColor.SECONDARY : BubbleColor.SURFACE;
825 } else {
826 bubbleColor = BubbleColor.WARNING;
827 }
828 } else {
829 bubbleColor = colorfulBackground ? BubbleColor.TERTIARY : BubbleColor.SURFACE_HIGH;
830 }
831
832 if (type == DATE_SEPARATOR) {
833 if (UIHelper.today(message.getTimeSent())) {
834 viewHolder.status_message.setText(R.string.today);
835 } else if (UIHelper.yesterday(message.getTimeSent())) {
836 viewHolder.status_message.setText(R.string.yesterday);
837 } else {
838 viewHolder.status_message.setText(
839 DateUtils.formatDateTime(
840 activity,
841 message.getTimeSent(),
842 DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR));
843 }
844 if (colorfulBackground) {
845 setBackgroundTint(viewHolder.message_box, BubbleColor.PRIMARY);
846 setTextColor(viewHolder.status_message, BubbleColor.PRIMARY);
847 } else {
848 setBackgroundTint(viewHolder.message_box, BubbleColor.SURFACE_HIGH);
849 setTextColor(viewHolder.status_message, BubbleColor.SURFACE_HIGH);
850 }
851 return view;
852 } else if (type == RTP_SESSION) {
853 final boolean received = message.getStatus() <= Message.STATUS_RECEIVED;
854 final RtpSessionStatus rtpSessionStatus = RtpSessionStatus.of(message.getBody());
855 final long duration = rtpSessionStatus.duration;
856 if (received) {
857 if (duration > 0) {
858 viewHolder.status_message.setText(
859 activity.getString(
860 R.string.incoming_call_duration_timestamp,
861 TimeFrameUtils.resolve(activity, duration),
862 UIHelper.readableTimeDifferenceFull(
863 activity, message.getTimeSent())));
864 } else if (rtpSessionStatus.successful) {
865 viewHolder.status_message.setText(R.string.incoming_call);
866 } else {
867 viewHolder.status_message.setText(
868 activity.getString(
869 R.string.missed_call_timestamp,
870 UIHelper.readableTimeDifferenceFull(
871 activity, message.getTimeSent())));
872 }
873 } else {
874 if (duration > 0) {
875 viewHolder.status_message.setText(
876 activity.getString(
877 R.string.outgoing_call_duration_timestamp,
878 TimeFrameUtils.resolve(activity, duration),
879 UIHelper.readableTimeDifferenceFull(
880 activity, message.getTimeSent())));
881 } else {
882 viewHolder.status_message.setText(
883 activity.getString(
884 R.string.outgoing_call_timestamp,
885 UIHelper.readableTimeDifferenceFull(
886 activity, message.getTimeSent())));
887 }
888 }
889 if (colorfulBackground) {
890 setBackgroundTint(viewHolder.message_box, BubbleColor.SECONDARY);
891 setTextColor(viewHolder.status_message, BubbleColor.SECONDARY);
892 setImageTint(viewHolder.indicatorReceived, BubbleColor.SECONDARY);
893 } else {
894 setBackgroundTint(viewHolder.message_box, BubbleColor.SURFACE_HIGH);
895 setTextColor(viewHolder.status_message, BubbleColor.SURFACE_HIGH);
896 setImageTint(viewHolder.indicatorReceived, BubbleColor.SURFACE_HIGH);
897 }
898 viewHolder.indicatorReceived.setImageResource(
899 RtpSessionStatus.getDrawable(received, rtpSessionStatus.successful));
900 return view;
901 } else if (type == STATUS) {
902 if ("LOAD_MORE".equals(message.getBody())) {
903 viewHolder.status_message.setVisibility(View.GONE);
904 viewHolder.contact_picture.setVisibility(View.GONE);
905 viewHolder.load_more_messages.setVisibility(View.VISIBLE);
906 viewHolder.load_more_messages.setOnClickListener(
907 v -> loadMoreMessages((Conversation) message.getConversation()));
908 } else {
909 viewHolder.status_message.setVisibility(View.VISIBLE);
910 viewHolder.load_more_messages.setVisibility(View.GONE);
911 viewHolder.status_message.setText(message.getBody());
912 boolean showAvatar;
913 if (conversation.getMode() == Conversation.MODE_SINGLE) {
914 showAvatar = true;
915 AvatarWorkerTask.loadAvatar(
916 message, viewHolder.contact_picture, R.dimen.avatar_on_status_message);
917 } else if (message.getCounterpart() != null
918 || message.getTrueCounterpart() != null
919 || (message.getCounterparts() != null
920 && message.getCounterparts().size() > 0)) {
921 showAvatar = true;
922 AvatarWorkerTask.loadAvatar(
923 message, viewHolder.contact_picture, R.dimen.avatar_on_status_message);
924 } else {
925 showAvatar = false;
926 }
927 if (showAvatar) {
928 viewHolder.contact_picture.setAlpha(0.5f);
929 viewHolder.contact_picture.setVisibility(View.VISIBLE);
930 } else {
931 viewHolder.contact_picture.setVisibility(View.GONE);
932 }
933 }
934 return view;
935 } else {
936 viewHolder.message_box.setClipToOutline(true);
937 AvatarWorkerTask.loadAvatar(message, viewHolder.contact_picture, R.dimen.avatar);
938 }
939
940 resetClickListener(viewHolder.message_box, viewHolder.messageBody);
941
942 viewHolder.contact_picture.setOnClickListener(
943 v -> {
944 if (MessageAdapter.this.mOnContactPictureClickedListener != null) {
945 MessageAdapter.this.mOnContactPictureClickedListener
946 .onContactPictureClicked(message);
947 }
948 });
949 viewHolder.contact_picture.setOnLongClickListener(
950 v -> {
951 if (MessageAdapter.this.mOnContactPictureLongClickedListener != null) {
952 MessageAdapter.this.mOnContactPictureLongClickedListener
953 .onContactPictureLongClicked(v, message);
954 return true;
955 } else {
956 return false;
957 }
958 });
959
960 final Transferable transferable = message.getTransferable();
961 final boolean unInitiatedButKnownSize = MessageUtils.unInitiatedButKnownSize(message);
962 if (unInitiatedButKnownSize
963 || message.isDeleted()
964 || (transferable != null
965 && transferable.getStatus() != Transferable.STATUS_UPLOADING)) {
966 if (unInitiatedButKnownSize
967 || transferable != null
968 && transferable.getStatus() == Transferable.STATUS_OFFER) {
969 displayDownloadableMessage(
970 viewHolder,
971 message,
972 activity.getString(
973 R.string.download_x_file,
974 UIHelper.getFileDescriptionString(activity, message)),
975 bubbleColor);
976 } else if (transferable != null
977 && transferable.getStatus() == Transferable.STATUS_OFFER_CHECK_FILESIZE) {
978 displayDownloadableMessage(
979 viewHolder,
980 message,
981 activity.getString(
982 R.string.check_x_filesize,
983 UIHelper.getFileDescriptionString(activity, message)),
984 bubbleColor);
985 } else {
986 displayInfoMessage(
987 viewHolder,
988 UIHelper.getMessagePreview(activity, message).first,
989 bubbleColor);
990 }
991 } else if (message.isFileOrImage()
992 && message.getEncryption() != Message.ENCRYPTION_PGP
993 && message.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED) {
994 if (message.getFileParams().width > 0 && message.getFileParams().height > 0) {
995 displayMediaPreviewMessage(viewHolder, message, bubbleColor);
996 } else if (message.getFileParams().runtime > 0) {
997 displayAudioMessage(viewHolder, message, bubbleColor);
998 } else {
999 displayOpenableMessage(viewHolder, message, bubbleColor);
1000 }
1001 } else if (message.getEncryption() == Message.ENCRYPTION_PGP) {
1002 if (account.isPgpDecryptionServiceConnected()) {
1003 if (conversation instanceof Conversation
1004 && !account.hasPendingPgpIntent((Conversation) conversation)) {
1005 displayInfoMessage(
1006 viewHolder,
1007 activity.getString(R.string.message_decrypting),
1008 bubbleColor);
1009 } else {
1010 displayInfoMessage(
1011 viewHolder, activity.getString(R.string.pgp_message), bubbleColor);
1012 }
1013 } else {
1014 displayInfoMessage(
1015 viewHolder, activity.getString(R.string.install_openkeychain), bubbleColor);
1016 viewHolder.message_box.setOnClickListener(this::promptOpenKeychainInstall);
1017 viewHolder.messageBody.setOnClickListener(this::promptOpenKeychainInstall);
1018 }
1019 } else if (message.getEncryption() == Message.ENCRYPTION_DECRYPTION_FAILED) {
1020 displayInfoMessage(
1021 viewHolder, activity.getString(R.string.decryption_failed), bubbleColor);
1022 } else if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE) {
1023 displayInfoMessage(
1024 viewHolder,
1025 activity.getString(R.string.not_encrypted_for_this_device),
1026 bubbleColor);
1027 } else if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL_FAILED) {
1028 displayInfoMessage(
1029 viewHolder, activity.getString(R.string.omemo_decryption_failed), bubbleColor);
1030 } else {
1031 if (message.isGeoUri()) {
1032 displayLocationMessage(viewHolder, message, bubbleColor);
1033 } else if (message.bodyIsOnlyEmojis() && message.getType() != Message.TYPE_PRIVATE) {
1034 displayEmojiMessage(viewHolder, message.getBody().trim(), bubbleColor);
1035 } else if (message.treatAsDownloadable()) {
1036 try {
1037 final URI uri = new URI(message.getBody());
1038 displayDownloadableMessage(
1039 viewHolder,
1040 message,
1041 activity.getString(
1042 R.string.check_x_filesize_on_host,
1043 UIHelper.getFileDescriptionString(activity, message),
1044 uri.getHost()),
1045 bubbleColor);
1046 } catch (Exception e) {
1047 displayDownloadableMessage(
1048 viewHolder,
1049 message,
1050 activity.getString(
1051 R.string.check_x_filesize,
1052 UIHelper.getFileDescriptionString(activity, message)),
1053 bubbleColor);
1054 }
1055 } else {
1056 displayTextMessage(viewHolder, message, bubbleColor);
1057 }
1058 }
1059
1060 setBackgroundTint(viewHolder.message_box, bubbleColor);
1061 setTextColor(viewHolder.messageBody, bubbleColor);
1062
1063 if (type == RECEIVED) {
1064 setTextColor(viewHolder.encryption, bubbleColor);
1065 if (isInValidSession) {
1066 viewHolder.encryption.setVisibility(View.GONE);
1067 } else {
1068 viewHolder.encryption.setVisibility(View.VISIBLE);
1069 if (omemoEncryption && !message.isTrusted()) {
1070 viewHolder.encryption.setText(R.string.not_trusted);
1071 } else {
1072 viewHolder.encryption.setText(
1073 CryptoHelper.encryptionTypeToText(message.getEncryption()));
1074 }
1075 }
1076 BindingAdapters.setReactionsOnReceived(
1077 viewHolder.reactions,
1078 message.getAggregatedReactions(),
1079 reactions -> sendReactions(message, reactions),
1080 () -> addReaction(message));
1081 } else if (type == SENT) {
1082 BindingAdapters.setReactionsOnSent(
1083 viewHolder.reactions,
1084 message.getAggregatedReactions(),
1085 reactions -> sendReactions(message, reactions));
1086 }
1087
1088 displayStatus(viewHolder, message, type, bubbleColor);
1089 return view;
1090 }
1091
1092 private void sendReactions(final Message message, final Collection<String> reactions) {
1093 if (activity.xmppConnectionService.sendReactions(message, reactions)) {
1094 return;
1095 }
1096 Toast.makeText(activity, R.string.could_not_add_reaction, Toast.LENGTH_LONG).show();
1097 }
1098
1099 private void addReaction(final Message message) {
1100 activity.addReaction(message, reactions -> activity.xmppConnectionService.sendReactions(message,reactions));
1101 }
1102
1103 private void promptOpenKeychainInstall(View view) {
1104 activity.showInstallPgpDialog();
1105 }
1106
1107 public FileBackend getFileBackend() {
1108 return activity.xmppConnectionService.getFileBackend();
1109 }
1110
1111 public void stopAudioPlayer() {
1112 audioPlayer.stop();
1113 }
1114
1115 public void unregisterListenerInAudioPlayer() {
1116 audioPlayer.unregisterListener();
1117 }
1118
1119 public void startStopPending() {
1120 audioPlayer.startStopPending();
1121 }
1122
1123 public void openDownloadable(Message message) {
1124 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU
1125 && ContextCompat.checkSelfPermission(
1126 activity, Manifest.permission.WRITE_EXTERNAL_STORAGE)
1127 != PackageManager.PERMISSION_GRANTED) {
1128 ConversationFragment.registerPendingMessage(activity, message);
1129 ActivityCompat.requestPermissions(
1130 activity,
1131 new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE},
1132 ConversationsActivity.REQUEST_OPEN_MESSAGE);
1133 return;
1134 }
1135 final DownloadableFile file =
1136 activity.xmppConnectionService.getFileBackend().getFile(message);
1137 ViewUtil.view(activity, file);
1138 }
1139
1140 private void showLocation(Message message) {
1141 for (Intent intent : GeoHelper.createGeoIntentsFromMessage(activity, message)) {
1142 if (intent.resolveActivity(getContext().getPackageManager()) != null) {
1143 getContext().startActivity(intent);
1144 return;
1145 }
1146 }
1147 Toast.makeText(
1148 activity,
1149 R.string.no_application_found_to_display_location,
1150 Toast.LENGTH_SHORT)
1151 .show();
1152 }
1153
1154 public void updatePreferences() {
1155 final AppSettings appSettings = new AppSettings(activity);
1156 this.bubbleDesign =
1157 new BubbleDesign(appSettings.isColorfulChatBubbles(), appSettings.isLargeFont());
1158 }
1159
1160 public void setHighlightedTerm(List<String> terms) {
1161 this.highlightedTerm = terms == null ? null : StylingHelper.filterHighlightedWords(terms);
1162 }
1163
1164 public interface OnContactPictureClicked {
1165 void onContactPictureClicked(Message message);
1166 }
1167
1168 public interface OnContactPictureLongClicked {
1169 void onContactPictureLongClicked(View v, Message message);
1170 }
1171
1172 private static void setBackgroundTint(final View view, final BubbleColor bubbleColor) {
1173 view.setBackgroundTintList(bubbleToColorStateList(view, bubbleColor));
1174 }
1175
1176 private static ColorStateList bubbleToColorStateList(
1177 final View view, final BubbleColor bubbleColor) {
1178 final @AttrRes int colorAttributeResId =
1179 switch (bubbleColor) {
1180 case SURFACE -> Activities.isNightMode(view.getContext())
1181 ? com.google.android.material.R.attr.colorSurfaceContainerHigh
1182 : com.google.android.material.R.attr.colorSurfaceContainerLow;
1183 case SURFACE_HIGH -> Activities.isNightMode(view.getContext())
1184 ? com.google.android.material.R.attr.colorSurfaceContainerHighest
1185 : com.google.android.material.R.attr.colorSurfaceContainerHigh;
1186 case PRIMARY -> com.google.android.material.R.attr.colorPrimaryContainer;
1187 case SECONDARY -> com.google.android.material.R.attr.colorSecondaryContainer;
1188 case TERTIARY -> com.google.android.material.R.attr.colorTertiaryContainer;
1189 case WARNING -> com.google.android.material.R.attr.colorErrorContainer;
1190 };
1191 return ColorStateList.valueOf(MaterialColors.getColor(view, colorAttributeResId));
1192 }
1193
1194 public static void setImageTint(final ImageView imageView, final BubbleColor bubbleColor) {
1195 ImageViewCompat.setImageTintList(
1196 imageView, bubbleToOnSurfaceColorStateList(imageView, bubbleColor));
1197 }
1198
1199 public static void setImageTintError(final ImageView imageView) {
1200 ImageViewCompat.setImageTintList(
1201 imageView,
1202 ColorStateList.valueOf(
1203 MaterialColors.getColor(
1204 imageView, com.google.android.material.R.attr.colorError)));
1205 }
1206
1207 public static void setTextColor(final TextView textView, final BubbleColor bubbleColor) {
1208 final var color = bubbleToOnSurfaceColor(textView, bubbleColor);
1209 textView.setTextColor(color);
1210 if (BubbleColor.SURFACES.contains(bubbleColor)) {
1211 textView.setLinkTextColor(
1212 MaterialColors.getColor(
1213 textView, com.google.android.material.R.attr.colorPrimary));
1214 } else {
1215 textView.setLinkTextColor(color);
1216 }
1217 }
1218
1219 private static void setTextSize(final TextView textView, final boolean largeFont) {
1220 if (largeFont) {
1221 textView.setTextAppearance(
1222 com.google.android.material.R.style.TextAppearance_Material3_BodyLarge);
1223 } else {
1224 textView.setTextAppearance(
1225 com.google.android.material.R.style.TextAppearance_Material3_BodyMedium);
1226 }
1227 }
1228
1229 private static @ColorInt int bubbleToOnSurfaceVariant(
1230 final View view, final BubbleColor bubbleColor) {
1231 final @AttrRes int colorAttributeResId;
1232 if (BubbleColor.SURFACES.contains(bubbleColor)) {
1233 colorAttributeResId = com.google.android.material.R.attr.colorOnSurfaceVariant;
1234 } else {
1235 colorAttributeResId = bubbleToOnSurface(bubbleColor);
1236 }
1237 return MaterialColors.getColor(view, colorAttributeResId);
1238 }
1239
1240 private static @ColorInt int bubbleToOnSurfaceColor(
1241 final View view, final BubbleColor bubbleColor) {
1242 return MaterialColors.getColor(view, bubbleToOnSurface(bubbleColor));
1243 }
1244
1245 public static ColorStateList bubbleToOnSurfaceColorStateList(
1246 final View view, final BubbleColor bubbleColor) {
1247 return ColorStateList.valueOf(bubbleToOnSurfaceColor(view, bubbleColor));
1248 }
1249
1250 private static @AttrRes int bubbleToOnSurface(final BubbleColor bubbleColor) {
1251 return switch (bubbleColor) {
1252 case SURFACE, SURFACE_HIGH -> com.google.android.material.R.attr.colorOnSurface;
1253 case PRIMARY -> com.google.android.material.R.attr.colorOnPrimaryContainer;
1254 case SECONDARY -> com.google.android.material.R.attr.colorOnSecondaryContainer;
1255 case TERTIARY -> com.google.android.material.R.attr.colorOnTertiaryContainer;
1256 case WARNING -> com.google.android.material.R.attr.colorOnErrorContainer;
1257 };
1258 }
1259
1260 public enum BubbleColor {
1261 SURFACE,
1262 SURFACE_HIGH,
1263 PRIMARY,
1264 SECONDARY,
1265 TERTIARY,
1266 WARNING;
1267
1268 private static final Collection<BubbleColor> SURFACES =
1269 Arrays.asList(BubbleColor.SURFACE, BubbleColor.SURFACE_HIGH);
1270 }
1271
1272 private static class BubbleDesign {
1273 public final boolean colorfulChatBubbles;
1274 public final boolean largeFont;
1275
1276 private BubbleDesign(final boolean colorfulChatBubbles, final boolean largeFont) {
1277 this.colorfulChatBubbles = colorfulChatBubbles;
1278 this.largeFont = largeFont;
1279 }
1280 }
1281
1282 private static class ViewHolder {
1283
1284 public MaterialButton load_more_messages;
1285 public ImageView edit_indicator;
1286 public RelativeLayout audioPlayer;
1287 protected LinearLayout message_box;
1288 protected MaterialButton download_button;
1289 protected ImageView image;
1290 protected ImageView indicator;
1291 protected ImageView indicatorReceived;
1292 protected TextView time;
1293 protected TextView messageBody;
1294 protected ImageView contact_picture;
1295 protected TextView status_message;
1296 protected TextView encryption;
1297 protected ChipGroup reactions;
1298 }
1299}