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