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