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