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