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.graphics.PorterDuff;
8import android.graphics.drawable.Drawable;
9import android.content.res.ColorStateList;
10import android.graphics.Typeface;
11import android.net.Uri;
12import android.os.AsyncTask;
13import android.os.Build;
14import android.preference.PreferenceManager;
15import android.text.Editable;
16import android.text.Spanned;
17import android.text.Spannable;
18import android.text.SpannableString;
19import android.text.SpannableStringBuilder;
20import android.text.style.ImageSpan;
21import android.text.style.ClickableSpan;
22import android.text.format.DateUtils;
23import android.text.style.ForegroundColorSpan;
24import android.text.style.RelativeSizeSpan;
25import android.text.style.StyleSpan;
26import android.text.style.URLSpan;
27import android.util.DisplayMetrics;
28import android.util.LruCache;
29import android.view.accessibility.AccessibilityEvent;
30import android.view.Gravity;
31import android.view.MotionEvent;
32import android.view.View;
33import android.view.ViewGroup;
34import android.view.WindowManager;
35import android.widget.ArrayAdapter;
36import android.widget.ImageView;
37import android.widget.LinearLayout;
38import android.widget.ListAdapter;
39import android.widget.ListView;
40import android.widget.RelativeLayout;
41import android.widget.TextView;
42import android.widget.Toast;
43
44import androidx.annotation.AttrRes;
45import androidx.annotation.ColorInt;
46import androidx.annotation.DrawableRes;
47import androidx.annotation.NonNull;
48import androidx.annotation.Nullable;
49import androidx.core.app.ActivityCompat;
50import androidx.core.content.ContextCompat;
51import androidx.core.content.res.ResourcesCompat;
52import androidx.core.widget.ImageViewCompat;
53
54import com.google.android.material.imageview.ShapeableImageView;
55import com.google.android.material.shape.CornerFamily;
56import com.google.android.material.shape.ShapeAppearanceModel;
57
58import com.cheogram.android.BobTransfer;
59import com.cheogram.android.MessageTextActionModeCallback;
60import com.cheogram.android.SwipeDetector;
61import com.cheogram.android.WebxdcPage;
62import com.cheogram.android.WebxdcUpdate;
63
64import com.google.android.material.button.MaterialButton;
65import com.google.android.material.color.MaterialColors;
66import com.google.common.base.Joiner;
67import com.google.common.base.Strings;
68import com.google.common.collect.ImmutableList;
69
70import com.lelloman.identicon.view.GithubIdenticonView;
71
72import java.io.IOException;
73import java.net.URI;
74import java.net.URISyntaxException;
75import java.security.NoSuchAlgorithmException;
76import java.util.HashMap;
77import java.util.List;
78import java.util.Map;
79import java.util.Locale;
80import java.util.regex.Matcher;
81import java.util.regex.Pattern;
82
83import io.ipfs.cid.Cid;
84
85import me.saket.bettermovementmethod.BetterLinkMovementMethod;
86
87import eu.siacs.conversations.AppSettings;
88import eu.siacs.conversations.Config;
89import eu.siacs.conversations.R;
90import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
91import eu.siacs.conversations.entities.Account;
92import eu.siacs.conversations.entities.Contact;
93import eu.siacs.conversations.entities.Conversation;
94import eu.siacs.conversations.entities.Conversational;
95import eu.siacs.conversations.entities.DownloadableFile;
96import eu.siacs.conversations.entities.Message.FileParams;
97import eu.siacs.conversations.entities.Message;
98import eu.siacs.conversations.entities.MucOptions;
99import eu.siacs.conversations.entities.Roster;
100import eu.siacs.conversations.entities.RtpSessionStatus;
101import eu.siacs.conversations.entities.Transferable;
102import eu.siacs.conversations.persistance.FileBackend;
103import eu.siacs.conversations.services.MessageArchiveService;
104import eu.siacs.conversations.services.NotificationService;
105import eu.siacs.conversations.ui.Activities;
106import eu.siacs.conversations.ui.ConversationFragment;
107import eu.siacs.conversations.ui.ConversationsActivity;
108import eu.siacs.conversations.ui.XmppActivity;
109import eu.siacs.conversations.ui.service.AudioPlayer;
110import eu.siacs.conversations.ui.text.DividerSpan;
111import eu.siacs.conversations.ui.text.QuoteSpan;
112import eu.siacs.conversations.ui.util.Attachment;
113import eu.siacs.conversations.ui.util.AvatarWorkerTask;
114import eu.siacs.conversations.ui.util.MyLinkify;
115import eu.siacs.conversations.ui.util.QuoteHelper;
116import eu.siacs.conversations.ui.util.ShareUtil;
117import eu.siacs.conversations.ui.util.ViewUtil;
118import eu.siacs.conversations.utils.CryptoHelper;
119import eu.siacs.conversations.utils.Emoticons;
120import eu.siacs.conversations.utils.GeoHelper;
121import eu.siacs.conversations.utils.MessageUtils;
122import eu.siacs.conversations.utils.StylingHelper;
123import eu.siacs.conversations.utils.TimeFrameUtils;
124import eu.siacs.conversations.utils.UIHelper;
125import eu.siacs.conversations.xmpp.Jid;
126import eu.siacs.conversations.xmpp.mam.MamReference;
127import eu.siacs.conversations.xml.Element;
128
129import java.net.URI;
130import java.util.Arrays;
131import java.util.Collection;
132import java.util.List;
133import java.util.Locale;
134import java.util.regex.Matcher;
135import java.util.regex.Pattern;
136
137public class MessageAdapter extends ArrayAdapter<Message> {
138
139 public static final String DATE_SEPARATOR_BODY = "DATE_SEPARATOR";
140 private static final int SENT = 0;
141 private static final int RECEIVED = 1;
142 private static final int STATUS = 2;
143 private static final int DATE_SEPARATOR = 3;
144 private static final int RTP_SESSION = 4;
145 private final XmppActivity activity;
146 private final AudioPlayer audioPlayer;
147 private List<String> highlightedTerm = null;
148 private final DisplayMetrics metrics;
149 private ConversationFragment mConversationFragment = null;
150 private OnContactPictureClicked mOnContactPictureClickedListener;
151 private OnContactPictureClicked mOnMessageBoxClickedListener;
152 private OnContactPictureClicked mOnMessageBoxSwipedListener;
153 private OnContactPictureLongClicked mOnContactPictureLongClickedListener;
154 private OnInlineImageLongClicked mOnInlineImageLongClickedListener;
155 private boolean mUseGreenBackground = false;
156 private BubbleDesign bubbleDesign = new BubbleDesign(false, false);
157 private final boolean mForceNames;
158 private final Map<String, WebxdcUpdate> lastWebxdcUpdate = new HashMap<>();
159 private String selectionUuid = null;
160
161 public MessageAdapter(
162 final XmppActivity activity, final List<Message> messages, final boolean forceNames) {
163 super(activity, 0, messages);
164 this.audioPlayer = new AudioPlayer(this);
165 this.activity = activity;
166 metrics = getContext().getResources().getDisplayMetrics();
167 updatePreferences();
168 this.mForceNames = forceNames;
169 }
170
171 public MessageAdapter(final XmppActivity activity, final List<Message> messages) {
172 this(activity, messages, false);
173 }
174
175 private static void resetClickListener(View... views) {
176 for (View view : views) {
177 if (view != null) view.setOnClickListener(null);
178 }
179 }
180
181 public void flagScreenOn() {
182 activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
183 }
184
185 public void flagScreenOff() {
186 activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
187 }
188
189 public void setVolumeControl(final int stream) {
190 activity.setVolumeControlStream(stream);
191 }
192
193 public void setOnContactPictureClicked(OnContactPictureClicked listener) {
194 this.mOnContactPictureClickedListener = listener;
195 }
196
197 public void setOnMessageBoxClicked(OnContactPictureClicked listener) {
198 this.mOnMessageBoxClickedListener = listener;
199 }
200
201 public void setOnMessageBoxSwiped(OnContactPictureClicked listener) {
202 this.mOnMessageBoxSwipedListener = listener;
203 }
204
205 public void setConversationFragment(ConversationFragment frag) {
206 mConversationFragment = frag;
207 }
208
209 public void quoteText(String text) {
210 if (mConversationFragment != null) mConversationFragment.quoteText(text);
211 }
212
213 public boolean hasSelection() {
214 return selectionUuid != null;
215 }
216
217 public Activity getActivity() {
218 return activity;
219 }
220
221 public void setOnContactPictureLongClicked(OnContactPictureLongClicked listener) {
222 this.mOnContactPictureLongClickedListener = listener;
223 }
224
225 public void setOnInlineImageLongClicked(OnInlineImageLongClicked listener) {
226 this.mOnInlineImageLongClickedListener = listener;
227 }
228
229 @Override
230 public int getViewTypeCount() {
231 return 5;
232 }
233
234 private int getItemViewType(Message message) {
235 if (message.getType() == Message.TYPE_STATUS) {
236 if (DATE_SEPARATOR_BODY.equals(message.getBody())) {
237 return DATE_SEPARATOR;
238 } else {
239 return STATUS;
240 }
241 } else if (message.getType() == Message.TYPE_RTP_SESSION) {
242 return RTP_SESSION;
243 } else if (message.getStatus() <= Message.STATUS_RECEIVED) {
244 return RECEIVED;
245 } else {
246 return SENT;
247 }
248 }
249
250 @Override
251 public int getItemViewType(int position) {
252 return this.getItemViewType(getItem(position));
253 }
254
255 private void displayStatus(
256 final ViewHolder viewHolder,
257 final Message message,
258 final int type,
259 final BubbleColor bubbleColor) {
260 final int mergedStatus = message.getMergedStatus();
261 final boolean error;
262 if (viewHolder.indicatorReceived != null) {
263 viewHolder.indicatorReceived.setVisibility(View.GONE);
264 }
265 final Transferable transferable = message.getTransferable();
266 final boolean multiReceived =
267 message.getConversation().getMode() == Conversation.MODE_MULTI
268 && mergedStatus <= Message.STATUS_RECEIVED;
269 final String fileSize;
270 if (message.isFileOrImage()
271 || transferable != null
272 || MessageUtils.unInitiatedButKnownSize(message)) {
273 final FileParams params = message.getFileParams();
274 fileSize = params.size != null ? UIHelper.filesizeToString(params.size) : null;
275 if (message.getStatus() == Message.STATUS_SEND_FAILED
276 || (transferable != null
277 && (transferable.getStatus() == Transferable.STATUS_FAILED
278 || transferable.getStatus()
279 == Transferable.STATUS_CANCELLED))) {
280 error = true;
281 } else {
282 error = message.getStatus() == Message.STATUS_SEND_FAILED;
283 }
284 } else {
285 fileSize = null;
286 error = message.getStatus() == Message.STATUS_SEND_FAILED;
287 }
288 if (type == SENT) {
289 final @DrawableRes Integer receivedIndicator =
290 getMessageStatusAsDrawable(message, mergedStatus);
291 if (receivedIndicator == null) {
292 viewHolder.indicatorReceived.setVisibility(View.INVISIBLE);
293 } else {
294 viewHolder.indicatorReceived.setImageResource(receivedIndicator);
295 if (mergedStatus == Message.STATUS_SEND_FAILED) {
296 setImageTintError(viewHolder.indicatorReceived);
297 } else {
298 setImageTint(viewHolder.indicatorReceived, bubbleColor);
299 }
300 viewHolder.indicatorReceived.setVisibility(View.VISIBLE);
301 }
302 }
303 final var additionalStatusInfo = getAdditionalStatusInfo(message, mergedStatus);
304
305 if (error && type == SENT) {
306 viewHolder.time.setTextColor(
307 MaterialColors.getColor(
308 viewHolder.time, com.google.android.material.R.attr.colorError));
309 } else {
310 setTextColor(viewHolder.time, bubbleColor);
311 }
312 setTextColor(viewHolder.subject, bubbleColor);
313 if (message.getEncryption() == Message.ENCRYPTION_NONE) {
314 viewHolder.indicator.setVisibility(View.GONE);
315 } else {
316 boolean verified = false;
317 if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) {
318 final FingerprintStatus status =
319 message.getConversation()
320 .getAccount()
321 .getAxolotlService()
322 .getFingerprintTrust(message.getFingerprint());
323 if (status != null && status.isVerified()) {
324 verified = true;
325 }
326 }
327 if (verified) {
328 viewHolder.indicator.setImageResource(R.drawable.ic_verified_user_24dp);
329 } else {
330 viewHolder.indicator.setImageResource(R.drawable.ic_lock_24dp);
331 }
332 if (error && type == SENT) {
333 setImageTintError(viewHolder.indicator);
334 } else {
335 setImageTint(viewHolder.indicator, bubbleColor);
336 }
337 viewHolder.indicator.setVisibility(View.VISIBLE);
338 }
339
340 if (viewHolder.edit_indicator != null) {
341 if (message.edited()) {
342 viewHolder.edit_indicator.setVisibility(View.VISIBLE);
343 if (error && type == SENT) {
344 setImageTintError(viewHolder.edit_indicator);
345 } else {
346 setImageTint(viewHolder.edit_indicator, bubbleColor);
347 }
348 } else {
349 viewHolder.edit_indicator.setVisibility(View.GONE);
350 }
351 }
352
353 final String formattedTime =
354 UIHelper.readableTimeDifferenceFull(getContext(), message.getMergedTimeSent());
355 final String bodyLanguage = message.getBodyLanguage();
356 final ImmutableList.Builder<String> timeInfoBuilder = new ImmutableList.Builder<>();
357 if (message.getStatus() <= Message.STATUS_RECEIVED) {
358 timeInfoBuilder.add(formattedTime);
359 if (fileSize != null) {
360 timeInfoBuilder.add(fileSize);
361 }
362 if (mForceNames || multiReceived || (message.getTrueCounterpart() != null && message.getContact() != null)) {
363 final String displayName = UIHelper.getMessageDisplayName(message);
364 if (displayName != null) {
365 timeInfoBuilder.add(displayName);
366 }
367 }
368 if (bodyLanguage != null) {
369 timeInfoBuilder.add(bodyLanguage.toUpperCase(Locale.US));
370 }
371 } else {
372 if (bodyLanguage != null) {
373 timeInfoBuilder.add(bodyLanguage.toUpperCase(Locale.US));
374 }
375 if (fileSize != null) {
376 timeInfoBuilder.add(fileSize);
377 }
378 // for space reasons we display only 'additional status info' (send progress or concrete
379 // failure reason) or the time
380 if (additionalStatusInfo != null) {
381 timeInfoBuilder.add(additionalStatusInfo);
382 } else {
383 timeInfoBuilder.add(formattedTime);
384 }
385 }
386 final var timeInfo = timeInfoBuilder.build();
387 viewHolder.time.setText(Joiner.on(" \u00B7 ").join(timeInfo));
388 }
389
390 public static @DrawableRes Integer getMessageStatusAsDrawable(
391 final Message message, final int status) {
392 final var transferable = message.getTransferable();
393 return switch (status) {
394 case Message.STATUS_WAITING -> R.drawable.ic_more_horiz_24dp;
395 case Message.STATUS_UNSEND -> transferable == null ? null : R.drawable.ic_upload_24dp;
396 case Message.STATUS_SEND -> R.drawable.ic_done_24dp;
397 case Message.STATUS_SEND_RECEIVED, Message.STATUS_SEND_DISPLAYED -> R.drawable
398 .ic_done_all_24dp;
399 case Message.STATUS_SEND_FAILED -> {
400 final String errorMessage = message.getErrorMessage();
401 if (Message.ERROR_MESSAGE_CANCELLED.equals(errorMessage)) {
402 yield R.drawable.ic_cancel_24dp;
403 } else {
404 yield R.drawable.ic_error_24dp;
405 }
406 }
407 case Message.STATUS_OFFERED -> R.drawable.ic_p2p_24dp;
408 default -> null;
409 };
410 }
411
412 @Nullable
413 private String getAdditionalStatusInfo(final Message message, final int mergedStatus) {
414 final String additionalStatusInfo;
415 if (mergedStatus == Message.STATUS_SEND_FAILED) {
416 final String errorMessage = Strings.nullToEmpty(message.getErrorMessage());
417 final String[] errorParts = errorMessage.split("\\u001f", 2);
418 if (errorParts.length == 2 && errorParts[0].equals("file-too-large")) {
419 additionalStatusInfo = getContext().getString(R.string.file_too_large);
420 } else {
421 additionalStatusInfo = null;
422 }
423 } else if (mergedStatus == Message.STATUS_UNSEND) {
424 final var transferable = message.getTransferable();
425 if (transferable == null) {
426 return null;
427 }
428 return getContext().getString(R.string.sending_file, transferable.getProgress());
429 } else {
430 additionalStatusInfo = null;
431 }
432 return additionalStatusInfo;
433 }
434
435 private void displayInfoMessage(
436 ViewHolder viewHolder, CharSequence text, final BubbleColor bubbleColor) {
437 viewHolder.download_button.setVisibility(View.GONE);
438 viewHolder.audioPlayer.setVisibility(View.GONE);
439 viewHolder.image.setVisibility(View.GONE);
440 viewHolder.messageBody.setVisibility(View.VISIBLE);
441 viewHolder.messageBody.setText(text);
442 viewHolder.messageBody.setTextColor(
443 bubbleToOnSurfaceVariant(viewHolder.messageBody, bubbleColor));
444 viewHolder.messageBody.setTextIsSelectable(false);
445 }
446
447 private void displayEmojiMessage(
448 final ViewHolder viewHolder, final SpannableStringBuilder body, final BubbleColor bubbleColor) {
449 viewHolder.download_button.setVisibility(View.GONE);
450 viewHolder.audioPlayer.setVisibility(View.GONE);
451 viewHolder.image.setVisibility(View.GONE);
452 viewHolder.messageBody.setVisibility(View.VISIBLE);
453 setTextColor(viewHolder.messageBody, bubbleColor);
454 ImageSpan[] imageSpans = body.getSpans(0, body.length(), ImageSpan.class);
455 float size = imageSpans.length == 1 || Emoticons.isEmoji(body.toString()) ? 3.0f : 2.0f;
456 body.setSpan(
457 new RelativeSizeSpan(size), 0, body.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
458 viewHolder.messageBody.setText(body);
459 }
460
461 private void applyQuoteSpan(
462 final TextView textView,
463 Editable body,
464 int start,
465 int end,
466 final BubbleColor bubbleColor,
467 final boolean makeEdits) {
468 if (makeEdits && start > 1 && !"\n\n".equals(body.subSequence(start - 2, start).toString())) {
469 body.insert(start++, "\n");
470 body.setSpan(
471 new DividerSpan(false), start - 2, start, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
472 end++;
473 }
474 if (makeEdits && end < body.length() - 1 && !"\n\n".equals(body.subSequence(end, end + 2).toString())) {
475 body.insert(end, "\n");
476 body.setSpan(
477 new DividerSpan(false),
478 end,
479 end + ("\n".equals(body.subSequence(end + 1, end + 2).toString()) ? 2 : 1),
480 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
481 );
482 }
483 final DisplayMetrics metrics = getContext().getResources().getDisplayMetrics();
484 body.setSpan(
485 new QuoteSpan(bubbleToOnSurfaceVariant(textView, bubbleColor), metrics),
486 start,
487 end,
488 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
489 }
490
491 public boolean handleTextQuotes(final TextView textView, final Editable body) {
492 return handleTextQuotes(textView, body, true);
493 }
494
495 public boolean handleTextQuotes(final TextView textView, final Editable body, final boolean deleteMarkers) {
496 final boolean colorfulBackground = this.bubbleDesign.colorfulChatBubbles;
497 final BubbleColor bubbleColor = colorfulBackground ? BubbleColor.SECONDARY : BubbleColor.SURFACE;
498 return handleTextQuotes(textView, body, bubbleColor, deleteMarkers);
499 }
500
501 /**
502 * Applies QuoteSpan to group of lines which starts with > or » characters. Appends likebreaks
503 * and applies DividerSpan to them to show a padding between quote and text.
504 */
505 public boolean handleTextQuotes(
506 final TextView textView,
507 final Editable body,
508 final BubbleColor bubbleColor,
509 final boolean deleteMarkers) {
510 boolean startsWithQuote = false;
511 int quoteDepth = 1;
512 while (QuoteHelper.bodyContainsQuoteStart(body) && quoteDepth <= Config.QUOTE_MAX_DEPTH) {
513 char previous = '\n';
514 int lineStart = -1;
515 int lineTextStart = -1;
516 int quoteStart = -1;
517 int skipped = 0;
518 for (int i = 0; i <= body.length(); i++) {
519 if (!deleteMarkers && QuoteHelper.isRelativeSizeSpanned(body, i)) {
520 skipped++;
521 continue;
522 }
523 char current = body.length() > i ? body.charAt(i) : '\n';
524 if (lineStart == -1) {
525 if (previous == '\n') {
526 if (i < body.length() && QuoteHelper.isPositionQuoteStart(body, i)) {
527 // Line start with quote
528 lineStart = i;
529 if (quoteStart == -1) quoteStart = i - skipped;
530 if (i == 0) startsWithQuote = true;
531 } else if (quoteStart >= 0) {
532 // Line start without quote, apply spans there
533 applyQuoteSpan(textView, body, quoteStart, i - 1, bubbleColor, deleteMarkers);
534 quoteStart = -1;
535 }
536 }
537 } else {
538 // Remove extra spaces between > and first character in the line
539 // > character will be removed too
540 if (current != ' ' && lineTextStart == -1) {
541 lineTextStart = i;
542 }
543 if (current == '\n') {
544 if (deleteMarkers) {
545 i -= lineTextStart - lineStart;
546 body.delete(lineStart, lineTextStart);
547 if (i == lineStart) {
548 // Avoid empty lines because span over empty line can be hidden
549 body.insert(i++, " ");
550 }
551 } else {
552 body.setSpan(new RelativeSizeSpan(i - (lineTextStart - lineStart) == lineStart ? 1 : 0), lineStart, lineTextStart, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE | StylingHelper.XHTML_REMOVE << Spanned.SPAN_USER_SHIFT);
553 }
554 lineStart = -1;
555 lineTextStart = -1;
556 }
557 }
558 previous = current;
559 skipped = 0;
560 }
561 if (quoteStart >= 0) {
562 // Apply spans to finishing open quote
563 applyQuoteSpan(textView, body, quoteStart, body.length(), bubbleColor, deleteMarkers);
564 }
565 quoteDepth++;
566 }
567 return startsWithQuote;
568 }
569
570 private SpannableStringBuilder getSpannableBody(final Message message) {
571 Drawable fallbackImg = ResourcesCompat.getDrawable(activity.getResources(), R.drawable.ic_photo_24dp, null);
572 return message.getMergedBody((cid) -> {
573 try {
574 DownloadableFile f = activity.xmppConnectionService.getFileForCid(cid);
575 if (f == null || !f.canRead()) {
576 if (!message.trusted() && !message.getConversation().canInferPresence()) return null;
577
578 try {
579 new BobTransfer(BobTransfer.uri(cid), message.getConversation().getAccount(), message.getCounterpart(), activity.xmppConnectionService).start();
580 } catch (final NoSuchAlgorithmException | URISyntaxException e) { }
581 return null;
582 }
583
584 Drawable d = activity.xmppConnectionService.getFileBackend().getThumbnail(f, activity.getResources(), (int) (metrics.density * 288), true);
585 if (d == null) {
586 new ThumbnailTask().execute(f);
587 }
588 return d;
589 } catch (final IOException e) {
590 return null;
591 }
592 }, fallbackImg);
593 }
594
595 private void displayTextMessage(
596 final ViewHolder viewHolder, final Message message, final BubbleColor bubbleColor, final int type) {
597 viewHolder.download_button.setVisibility(View.GONE);
598 viewHolder.image.setVisibility(View.GONE);
599 viewHolder.audioPlayer.setVisibility(View.GONE);
600 viewHolder.messageBody.setVisibility(View.VISIBLE);
601 setTextColor(viewHolder.messageBody, bubbleColor);
602 setTextSize(viewHolder.messageBody, this.bubbleDesign.largeFont);
603
604 final ViewGroup.LayoutParams layoutParams = viewHolder.messageBody.getLayoutParams();
605 layoutParams.width = ViewGroup.LayoutParams.WRAP_CONTENT;
606 viewHolder.messageBody.setLayoutParams(layoutParams);
607
608 viewHolder.messageBody.setTypeface(null, Typeface.NORMAL);
609
610 if (message.getBody() != null && !message.getBody().equals("")) {
611 viewHolder.messageBody.setTextIsSelectable(true);
612 viewHolder.messageBody.setVisibility(View.VISIBLE);
613 final String nick = UIHelper.getMessageDisplayName(message);
614 SpannableStringBuilder body = getSpannableBody(message);
615 final var processMarkup = body.getSpans(0, body.length(), Message.PlainTextSpan.class).length > 0;
616 boolean hasMeCommand = message.hasMeCommand();
617 if (hasMeCommand) {
618 body = body.replace(0, Message.ME_COMMAND.length(), nick + " ");
619 }
620 if (body.length() > Config.MAX_DISPLAY_MESSAGE_CHARS) {
621 body = new SpannableStringBuilder(body, 0, Config.MAX_DISPLAY_MESSAGE_CHARS);
622 body.append("\u2026");
623 }
624 Message.MergeSeparator[] mergeSeparators =
625 body.getSpans(0, body.length(), Message.MergeSeparator.class);
626 for (Message.MergeSeparator mergeSeparator : mergeSeparators) {
627 int start = body.getSpanStart(mergeSeparator);
628 int end = body.getSpanEnd(mergeSeparator);
629 body.setSpan(new DividerSpan(true), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
630 }
631 for (final android.text.style.QuoteSpan quote : body.getSpans(0, body.length(), android.text.style.QuoteSpan.class)) {
632 int start = body.getSpanStart(quote);
633 int end = body.getSpanEnd(quote);
634 body.removeSpan(quote);
635 applyQuoteSpan(viewHolder.messageBody, body, start, end, bubbleColor, true);
636 }
637 boolean startsWithQuote = processMarkup ? handleTextQuotes(viewHolder.messageBody, body, bubbleColor, true) : false;
638 if (!message.isPrivateMessage()) {
639 if (hasMeCommand) {
640 body.setSpan(
641 new StyleSpan(Typeface.BOLD_ITALIC),
642 0,
643 nick.length(),
644 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
645 }
646 } else {
647 String privateMarker;
648 if (message.getStatus() <= Message.STATUS_RECEIVED) {
649 privateMarker = activity.getString(R.string.private_message);
650 } else {
651 Jid cp = message.getCounterpart();
652 privateMarker =
653 activity.getString(
654 R.string.private_message_to,
655 Strings.nullToEmpty(cp == null ? null : cp.getResource()));
656 }
657 body.insert(0, privateMarker);
658 int privateMarkerIndex = privateMarker.length();
659 if (startsWithQuote) {
660 body.insert(privateMarkerIndex, "\n\n");
661 body.setSpan(
662 new DividerSpan(false),
663 privateMarkerIndex,
664 privateMarkerIndex + 2,
665 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
666 } else {
667 body.insert(privateMarkerIndex, " ");
668 }
669 body.setSpan(
670 new ForegroundColorSpan(
671 bubbleToOnSurfaceVariant(viewHolder.messageBody, bubbleColor)),
672 0,
673 privateMarkerIndex,
674 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
675 body.setSpan(
676 new StyleSpan(Typeface.BOLD),
677 0,
678 privateMarkerIndex,
679 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
680 if (hasMeCommand) {
681 body.setSpan(
682 new StyleSpan(Typeface.BOLD_ITALIC),
683 privateMarkerIndex + 1,
684 privateMarkerIndex + 1 + nick.length(),
685 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
686 }
687 }
688 if (message.getConversation().getMode() == Conversation.MODE_MULTI
689 && message.getStatus() == Message.STATUS_RECEIVED) {
690 if (message.getConversation() instanceof Conversation conversation) {
691 Pattern pattern =
692 NotificationService.generateNickHighlightPattern(
693 conversation.getMucOptions().getActualNick());
694 Matcher matcher = pattern.matcher(body);
695 while (matcher.find()) {
696 body.setSpan(
697 new StyleSpan(Typeface.BOLD),
698 matcher.start(),
699 matcher.end(),
700 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
701 }
702
703 pattern = NotificationService.generateNickHighlightPattern(conversation.getMucOptions().getActualName());
704 matcher = pattern.matcher(body);
705 while (matcher.find()) {
706 body.setSpan(new StyleSpan(Typeface.BOLD), matcher.start(), matcher.end(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
707 }
708 }
709 }
710 Matcher matcher = Emoticons.getEmojiPattern(body).matcher(body);
711 while (matcher.find()) {
712 if (matcher.start() < matcher.end()) {
713 body.setSpan(
714 new RelativeSizeSpan(1.2f),
715 matcher.start(),
716 matcher.end(),
717 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
718 }
719 }
720
721 if (processMarkup) StylingHelper.format(body, viewHolder.messageBody.getCurrentTextColor());
722 MyLinkify.addLinks(body, message.getConversation().getAccount(), message.getConversation().getJid());
723 if (highlightedTerm != null) {
724 StylingHelper.highlight(viewHolder.messageBody, body, highlightedTerm);
725 }
726
727 viewHolder.messageBody.setAutoLinkMask(0);
728 viewHolder.messageBody.setText(body);
729 BetterLinkMovementMethod method = new BetterLinkMovementMethod() {
730 @Override
731 protected void dispatchUrlLongClick(TextView tv, ClickableSpan span) {
732 if (span instanceof URLSpan || mOnInlineImageLongClickedListener == null) {
733 tv.dispatchTouchEvent(MotionEvent.obtain(0, 0, MotionEvent.ACTION_CANCEL, 0f, 0f, 0));
734 super.dispatchUrlLongClick(tv, span);
735 return;
736 }
737
738 Spannable body = (Spannable) tv.getText();
739 ImageSpan[] imageSpans = body.getSpans(body.getSpanStart(span), body.getSpanEnd(span), ImageSpan.class);
740 if (imageSpans.length > 0) {
741 Uri uri = Uri.parse(imageSpans[0].getSource());
742 Cid cid = BobTransfer.cid(uri);
743 if (cid == null) return;
744 if (mOnInlineImageLongClickedListener.onInlineImageLongClicked(cid)) {
745 tv.dispatchTouchEvent(MotionEvent.obtain(0, 0, MotionEvent.ACTION_CANCEL, 0f, 0f, 0));
746 }
747 }
748 }
749 };
750 method.setOnLinkLongClickListener((tv, url) -> {
751 tv.dispatchTouchEvent(MotionEvent.obtain(0, 0, MotionEvent.ACTION_CANCEL, 0f, 0f, 0));
752 ShareUtil.copyLinkToClipboard(activity, url);
753 return true;
754 });
755 viewHolder.messageBody.setMovementMethod(method);
756 } else {
757 viewHolder.messageBody.setText("");
758 viewHolder.messageBody.setTextIsSelectable(false);
759 toggleWhisperInfo(viewHolder, message, bubbleColor);
760 }
761 }
762
763 private void displayDownloadableMessage(
764 ViewHolder viewHolder,
765 final Message message,
766 String text,
767 final BubbleColor bubbleColor, final int type) {
768 displayTextMessage(viewHolder, message, bubbleColor, type);
769 viewHolder.image.setVisibility(View.GONE);
770 List<Element> thumbs = message.getFileParams() != null ? message.getFileParams().getThumbnails() : null;
771 if (thumbs != null && !thumbs.isEmpty()) {
772 for (Element thumb : thumbs) {
773 Uri uri = Uri.parse(thumb.getAttribute("uri"));
774 if (uri.getScheme().equals("data")) {
775 String[] parts = uri.getSchemeSpecificPart().split(",", 2);
776 parts = parts[0].split(";");
777 if (!parts[0].equals("image/blurhash") && !parts[0].equals("image/thumbhash") && !parts[0].equals("image/jpeg") && !parts[0].equals("image/png") && !parts[0].equals("image/webp") && !parts[0].equals("image/gif")) continue;
778 } else if (uri.getScheme().equals("cid")) {
779 Cid cid = BobTransfer.cid(uri);
780 if (cid == null) continue;
781 DownloadableFile f = activity.xmppConnectionService.getFileForCid(cid);
782 if (f == null || !f.canRead()) {
783 if (!message.trusted() && !message.getConversation().canInferPresence()) continue;
784
785 try {
786 new BobTransfer(BobTransfer.uri(cid), message.getConversation().getAccount(), message.getCounterpart(), activity.xmppConnectionService).start();
787 } catch (final NoSuchAlgorithmException | URISyntaxException e) { }
788 continue;
789 }
790 } else {
791 continue;
792 }
793
794 int width = message.getFileParams().width;
795 if (width < 1 && thumb.getAttribute("width") != null) width = Integer.parseInt(thumb.getAttribute("width"));
796 if (width < 1) width = 1920;
797
798 int height = message.getFileParams().height;
799 if (height < 1 && thumb.getAttribute("height") != null) height = Integer.parseInt(thumb.getAttribute("height"));
800 if (height < 1) height = 1080;
801
802 viewHolder.image.setVisibility(View.VISIBLE);
803 imagePreviewLayout(width, height, viewHolder.image, true, type, viewHolder);
804 activity.loadBitmap(message, viewHolder.image);
805 viewHolder.image.setOnClickListener(v -> ConversationFragment.downloadFile(activity, message));
806
807 break;
808 }
809 }
810 viewHolder.audioPlayer.setVisibility(View.GONE);
811 viewHolder.download_button.setVisibility(View.VISIBLE);
812 viewHolder.download_button.setText(text);
813 final var attachment = Attachment.of(message);
814 final @DrawableRes int imageResource = MediaAdapter.getImageDrawable(attachment);
815 viewHolder.download_button.setIconResource(imageResource);
816 viewHolder.download_button.setOnClickListener(
817 v -> ConversationFragment.downloadFile(activity, message));
818 }
819
820 private void displayWebxdcMessage(ViewHolder viewHolder, final Message message, final BubbleColor bubbleColor, final int type) {
821 Cid webxdcCid = message.getFileParams().getCids().get(0);
822 WebxdcPage webxdc = new WebxdcPage(activity, webxdcCid, message, activity.xmppConnectionService);
823 displayTextMessage(viewHolder, message, bubbleColor, type);
824 viewHolder.image.setVisibility(View.GONE);
825 viewHolder.audioPlayer.setVisibility(View.GONE);
826 viewHolder.download_button.setVisibility(View.VISIBLE);
827 viewHolder.download_button.setText("Open " + webxdc.getName());
828 viewHolder.download_button.setOnClickListener(v -> {
829 Conversation conversation = (Conversation) message.getConversation();
830 if (!conversation.switchToSession("webxdc\0" + message.getUuid())) {
831 conversation.startWebxdc(webxdc);
832 }
833 });
834
835 final WebxdcUpdate lastUpdate;
836 synchronized(lastWebxdcUpdate) { lastUpdate = lastWebxdcUpdate.get(message.getUuid()); }
837 if (lastUpdate == null) {
838 new Thread(() -> {
839 final WebxdcUpdate update = activity.xmppConnectionService.findLastWebxdcUpdate(message);
840 if (update != null) {
841 synchronized(lastWebxdcUpdate) { lastWebxdcUpdate.put(message.getUuid(), update); }
842 activity.xmppConnectionService.updateConversationUi();
843 }
844 }).start();
845 } else {
846 if (lastUpdate != null && (lastUpdate.getSummary() != null || lastUpdate.getDocument() != null)) {
847 viewHolder.messageBody.setVisibility(View.VISIBLE);
848 viewHolder.messageBody.setText(
849 (lastUpdate.getDocument() == null ? "" : lastUpdate.getDocument() + "\n") +
850 (lastUpdate.getSummary() == null ? "" : lastUpdate.getSummary())
851 );
852 }
853 }
854
855 final LruCache<String, Drawable> cache = activity.xmppConnectionService.getDrawableCache();
856 final Drawable d = cache.get("webxdc:icon:" + webxdcCid);
857 if (d == null) {
858 new Thread(() -> {
859 Drawable icon = webxdc.getIcon();
860 if (icon != null) {
861 cache.put("webxdc:icon:" + webxdcCid, icon);
862 activity.xmppConnectionService.updateConversationUi();
863 }
864 }).start();
865 } else {
866 viewHolder.image.setVisibility(View.VISIBLE);
867 viewHolder.image.setImageDrawable(d);
868 }
869 }
870
871 private void displayOpenableMessage(
872 ViewHolder viewHolder, final Message message, final BubbleColor bubbleColor, final int type) {
873 displayTextMessage(viewHolder, message, bubbleColor, type);
874 viewHolder.image.setVisibility(View.GONE);
875 viewHolder.audioPlayer.setVisibility(View.GONE);
876 viewHolder.download_button.setVisibility(View.VISIBLE);
877 viewHolder.download_button.setText(
878 activity.getString(
879 R.string.open_x_file,
880 UIHelper.getFileDescriptionString(activity, message)));
881 final var attachment = Attachment.of(message);
882 final @DrawableRes int imageResource = MediaAdapter.getImageDrawable(attachment);
883 viewHolder.download_button.setIconResource(imageResource);
884 viewHolder.download_button.setOnClickListener(v -> openDownloadable(message));
885 }
886
887 private void displayLocationMessage(
888 ViewHolder viewHolder, final Message message, final BubbleColor bubbleColor, final int type) {
889 displayTextMessage(viewHolder, message, bubbleColor, type);
890 viewHolder.image.setVisibility(View.GONE);
891 viewHolder.audioPlayer.setVisibility(View.GONE);
892 viewHolder.download_button.setVisibility(View.VISIBLE);
893 viewHolder.download_button.setText(R.string.show_location);
894 final var attachment = Attachment.of(message);
895 final @DrawableRes int imageResource = MediaAdapter.getImageDrawable(attachment);
896 viewHolder.download_button.setIconResource(imageResource);
897 viewHolder.download_button.setOnClickListener(v -> showLocation(message));
898 }
899
900 private void displayAudioMessage(
901 ViewHolder viewHolder, Message message, final BubbleColor bubbleColor, final int type) {
902 displayTextMessage(viewHolder, message, bubbleColor, type);
903 viewHolder.image.setVisibility(View.GONE);
904 viewHolder.download_button.setVisibility(View.GONE);
905 final RelativeLayout audioPlayer = viewHolder.audioPlayer;
906 audioPlayer.setVisibility(View.VISIBLE);
907 AudioPlayer.ViewHolder.get(audioPlayer).setBubbleColor(bubbleColor);
908 this.audioPlayer.init(audioPlayer, message);
909 }
910
911 private void displayMediaPreviewMessage(
912 ViewHolder viewHolder, final Message message, final BubbleColor bubbleColor, final int type) {
913 displayTextMessage(viewHolder, message, bubbleColor, type);
914 viewHolder.download_button.setVisibility(View.GONE);
915 viewHolder.audioPlayer.setVisibility(View.GONE);
916 viewHolder.image.setVisibility(View.VISIBLE);
917 final FileParams params = message.getFileParams();
918 imagePreviewLayout(params.width, params.height, viewHolder.image, viewHolder.messageBody.getVisibility() != View.GONE, type, viewHolder);
919 activity.loadBitmap(message, viewHolder.image);
920 viewHolder.image.setOnClickListener(v -> openDownloadable(message));
921 }
922
923 private void imagePreviewLayout(int w, int h, ShapeableImageView image, boolean withOther, int type, ViewHolder viewHolder) {
924 final float target = activity.getResources().getDimension(R.dimen.image_preview_width);
925 final int scaledW;
926 final int scaledH;
927 if (Math.max(h, w) * metrics.density <= target) {
928 scaledW = (int) (w * metrics.density);
929 scaledH = (int) (h * metrics.density);
930 } else if (Math.max(h, w) <= target) {
931 scaledW = w;
932 scaledH = h;
933 } else if (w <= h) {
934 scaledW = (int) (w / ((double) h / target));
935 scaledH = (int) target;
936 } else {
937 scaledW = (int) target;
938 scaledH = (int) (h / ((double) w / target));
939 }
940 final var small = withOther ? scaledW < target : scaledW < 110 * metrics.density;
941 final LinearLayout.LayoutParams layoutParams =
942 new LinearLayout.LayoutParams(scaledW, scaledH);
943 image.setLayoutParams(layoutParams);
944
945 final var bubbleRadius = activity.getResources().getDimension(R.dimen.bubble_radius);
946 var shape = new ShapeAppearanceModel.Builder().setTopRightCorner(CornerFamily.ROUNDED, bubbleRadius);
947 if (type == SENT) {
948 shape = shape.setTopLeftCorner(CornerFamily.ROUNDED, bubbleRadius);
949 }
950 if (small) {
951 final var imageRadius = activity.getResources().getDimension(R.dimen.image_radius);
952 shape = shape.setAllCorners(CornerFamily.ROUNDED, imageRadius);
953 image.setPadding(0, (int)(8 * metrics.density), 0, 0);
954 } else {
955 image.setPadding(0, 0, 0, 0);
956 }
957 image.setShapeAppearanceModel(shape.build());
958
959 if (!small) {
960 final ViewGroup.LayoutParams blayoutParams = viewHolder.messageBody.getLayoutParams();
961 blayoutParams.width = (int) (target - (22 * metrics.density));
962 viewHolder.messageBody.setLayoutParams(blayoutParams);
963 }
964 }
965
966 private void toggleWhisperInfo(
967 ViewHolder viewHolder, final Message message, final BubbleColor bubbleColor) {
968 if (message.isPrivateMessage()) {
969 final String privateMarker;
970 if (message.getStatus() <= Message.STATUS_RECEIVED) {
971 privateMarker = activity.getString(R.string.private_message);
972 } else {
973 Jid cp = message.getCounterpart();
974 privateMarker =
975 activity.getString(
976 R.string.private_message_to,
977 Strings.nullToEmpty(cp == null ? null : cp.getResource()));
978 }
979 final SpannableString body = new SpannableString(privateMarker);
980 body.setSpan(
981 new ForegroundColorSpan(
982 bubbleToOnSurfaceVariant(viewHolder.messageBody, bubbleColor)),
983 0,
984 privateMarker.length(),
985 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
986 body.setSpan(
987 new StyleSpan(Typeface.BOLD),
988 0,
989 privateMarker.length(),
990 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
991 viewHolder.messageBody.setText(body);
992 viewHolder.messageBody.setVisibility(View.VISIBLE);
993 } else {
994 viewHolder.messageBody.setVisibility(View.GONE);
995 }
996 }
997
998 private void loadMoreMessages(Conversation conversation) {
999 conversation.setLastClearHistory(0, null);
1000 activity.xmppConnectionService.updateConversation(conversation);
1001 conversation.setHasMessagesLeftOnServer(true);
1002 conversation.setFirstMamReference(null);
1003 long timestamp = conversation.getLastMessageTransmitted().getTimestamp();
1004 if (timestamp == 0) {
1005 timestamp = System.currentTimeMillis();
1006 }
1007 conversation.messagesLoaded.set(true);
1008 MessageArchiveService.Query query =
1009 activity.xmppConnectionService
1010 .getMessageArchiveService()
1011 .query(conversation, new MamReference(0), timestamp, false);
1012 if (query != null) {
1013 Toast.makeText(activity, R.string.fetching_history_from_server, Toast.LENGTH_LONG)
1014 .show();
1015 } else {
1016 Toast.makeText(
1017 activity,
1018 R.string.not_fetching_history_retention_period,
1019 Toast.LENGTH_SHORT)
1020 .show();
1021 }
1022 }
1023
1024 @Override
1025 public View getView(final int position, View view, final @NonNull ViewGroup parent) {
1026 final Message message = getItem(position);
1027 final boolean omemoEncryption = message.getEncryption() == Message.ENCRYPTION_AXOLOTL;
1028 final boolean isInValidSession =
1029 message.isValidInSession() && (!omemoEncryption || message.isTrusted());
1030 final Conversational conversation = message.getConversation();
1031 final Account account = conversation.getAccount();
1032 final List<Element> commands = message.getCommands();
1033 final int type = getItemViewType(position);
1034 ViewHolder viewHolder;
1035 if (view == null) {
1036 viewHolder = new ViewHolder();
1037 switch (type) {
1038 case DATE_SEPARATOR:
1039 view =
1040 activity.getLayoutInflater()
1041 .inflate(R.layout.item_message_date_bubble, parent, false);
1042 viewHolder.status_message = view.findViewById(R.id.message_body);
1043 viewHolder.message_box = view.findViewById(R.id.message_box);
1044 break;
1045 case RTP_SESSION:
1046 view =
1047 activity.getLayoutInflater()
1048 .inflate(R.layout.item_message_rtp_session, parent, false);
1049 viewHolder.status_message = view.findViewById(R.id.message_body);
1050 viewHolder.message_box = view.findViewById(R.id.message_box);
1051 viewHolder.indicatorReceived = view.findViewById(R.id.indicator_received);
1052 break;
1053 case SENT:
1054 view = activity.getLayoutInflater().inflate(R.layout.item_message_sent, parent, false);
1055 viewHolder.status_line = view.findViewById(R.id.status_line);
1056 viewHolder.message_box_inner = view.findViewById(R.id.message_box_inner);
1057 viewHolder.message_box = view.findViewById(R.id.message_box);
1058 viewHolder.contact_picture = view.findViewById(R.id.message_photo);
1059 viewHolder.download_button = view.findViewById(R.id.download_button);
1060 viewHolder.indicator = view.findViewById(R.id.security_indicator);
1061 viewHolder.edit_indicator = view.findViewById(R.id.edit_indicator);
1062 viewHolder.image = view.findViewById(R.id.message_image);
1063 viewHolder.messageBody = view.findViewById(R.id.message_body);
1064 viewHolder.time = view.findViewById(R.id.message_time);
1065 viewHolder.subject = view.findViewById(R.id.message_subject);
1066 viewHolder.indicatorReceived = view.findViewById(R.id.indicator_received);
1067 viewHolder.audioPlayer = view.findViewById(R.id.audio_player);
1068 viewHolder.thread_identicon = view.findViewById(R.id.thread_identicon);
1069 break;
1070 case RECEIVED:
1071 view = activity.getLayoutInflater().inflate(R.layout.item_message_received, parent, false);
1072 viewHolder.status_line = view.findViewById(R.id.status_line);
1073 viewHolder.message_box_inner = view.findViewById(R.id.message_box_inner);
1074 viewHolder.message_box = view.findViewById(R.id.message_box);
1075 viewHolder.contact_picture = view.findViewById(R.id.message_photo);
1076 viewHolder.download_button = view.findViewById(R.id.download_button);
1077 viewHolder.indicator = view.findViewById(R.id.security_indicator);
1078 viewHolder.edit_indicator = view.findViewById(R.id.edit_indicator);
1079 viewHolder.image = view.findViewById(R.id.message_image);
1080 viewHolder.messageBody = view.findViewById(R.id.message_body);
1081 viewHolder.time = view.findViewById(R.id.message_time);
1082 viewHolder.subject = view.findViewById(R.id.message_subject);
1083 viewHolder.indicatorReceived = view.findViewById(R.id.indicator_received);
1084 viewHolder.encryption = view.findViewById(R.id.message_encryption);
1085 viewHolder.audioPlayer = view.findViewById(R.id.audio_player);
1086 viewHolder.commands_list = view.findViewById(R.id.commands_list);
1087 viewHolder.thread_identicon = view.findViewById(R.id.thread_identicon);
1088 break;
1089 case STATUS:
1090 view =
1091 activity.getLayoutInflater()
1092 .inflate(R.layout.item_message_status, parent, false);
1093 viewHolder.contact_picture = view.findViewById(R.id.message_photo);
1094 viewHolder.status_message = view.findViewById(R.id.status_message);
1095 viewHolder.load_more_messages = view.findViewById(R.id.load_more_messages);
1096 break;
1097 default:
1098 throw new AssertionError("Unknown view type");
1099 }
1100 view.setTag(viewHolder);
1101 } else {
1102 viewHolder = (ViewHolder) view.getTag();
1103 if (viewHolder == null) {
1104 return view;
1105 }
1106 }
1107
1108 if (viewHolder.messageBody != null) {
1109 viewHolder.messageBody.setCustomSelectionActionModeCallback(new MessageTextActionModeCallback(this, viewHolder.messageBody));
1110 }
1111
1112 if (viewHolder.thread_identicon != null) {
1113 viewHolder.thread_identicon.setVisibility(View.GONE);
1114 final Element thread = message.getThread();
1115 if (thread != null) {
1116 final String threadId = thread.getContent();
1117 if (threadId != null) {
1118 viewHolder.thread_identicon.setVisibility(View.VISIBLE);
1119 viewHolder.thread_identicon.setColor(UIHelper.getColorForName(threadId));
1120 viewHolder.thread_identicon.setHash(UIHelper.identiconHash(threadId));
1121 }
1122 }
1123 }
1124
1125 final var black = MaterialColors.getColor(view, com.google.android.material.R.attr.colorSecondaryContainer) == view.getContext().getColor(android.R.color.black);
1126 final boolean colorfulBackground = this.bubbleDesign.colorfulChatBubbles;
1127 final BubbleColor bubbleColor;
1128 if (type == RECEIVED) {
1129 if (isInValidSession) {
1130 bubbleColor = colorfulBackground || black ? BubbleColor.SECONDARY : BubbleColor.SURFACE;
1131 } else {
1132 bubbleColor = BubbleColor.WARNING;
1133 }
1134 } else {
1135 if (!colorfulBackground && black) {
1136 bubbleColor = BubbleColor.SECONDARY;
1137 } else {
1138 bubbleColor = colorfulBackground ? BubbleColor.TERTIARY : BubbleColor.SURFACE_HIGH;
1139 }
1140 }
1141
1142 if (type == DATE_SEPARATOR) {
1143 if (UIHelper.today(message.getTimeSent())) {
1144 viewHolder.status_message.setText(R.string.today);
1145 } else if (UIHelper.yesterday(message.getTimeSent())) {
1146 viewHolder.status_message.setText(R.string.yesterday);
1147 } else {
1148 viewHolder.status_message.setText(
1149 DateUtils.formatDateTime(
1150 activity,
1151 message.getTimeSent(),
1152 DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR));
1153 }
1154 if (colorfulBackground) {
1155 setBackgroundTint(viewHolder.message_box, BubbleColor.PRIMARY);
1156 setTextColor(viewHolder.status_message, BubbleColor.PRIMARY);
1157 } else {
1158 setBackgroundTint(viewHolder.message_box, BubbleColor.SURFACE_HIGH);
1159 setTextColor(viewHolder.status_message, BubbleColor.SURFACE_HIGH);
1160 }
1161 return view;
1162 } else if (type == RTP_SESSION) {
1163 final boolean received = message.getStatus() <= Message.STATUS_RECEIVED;
1164 final RtpSessionStatus rtpSessionStatus = RtpSessionStatus.of(message.getBody());
1165 final long duration = rtpSessionStatus.duration;
1166 final String callTime = UIHelper.readableTimeDifferenceFull(activity, message.getTimeSent());
1167 if (received) {
1168 if (duration > 0) {
1169 viewHolder.status_message.setText(
1170 activity.getString(
1171 R.string.incoming_call_duration_timestamp,
1172 TimeFrameUtils.resolve(activity, duration),
1173 UIHelper.readableTimeDifferenceFull(
1174 activity, message.getTimeSent())));
1175 } else if (rtpSessionStatus.successful) {
1176 viewHolder.status_message.setText(activity.getString(R.string.incoming_call_timestamp, callTime));
1177 } else {
1178 viewHolder.status_message.setText(
1179 activity.getString(
1180 R.string.missed_call_timestamp,
1181 UIHelper.readableTimeDifferenceFull(
1182 activity, message.getTimeSent())));
1183 }
1184 } else {
1185 if (duration > 0) {
1186 viewHolder.status_message.setText(
1187 activity.getString(
1188 R.string.outgoing_call_duration_timestamp,
1189 TimeFrameUtils.resolve(activity, duration),
1190 UIHelper.readableTimeDifferenceFull(
1191 activity, message.getTimeSent())));
1192 } else {
1193 viewHolder.status_message.setText(
1194 activity.getString(
1195 R.string.outgoing_call_timestamp,
1196 UIHelper.readableTimeDifferenceFull(
1197 activity, message.getTimeSent())));
1198 }
1199 }
1200 if (colorfulBackground) {
1201 setBackgroundTint(viewHolder.message_box, BubbleColor.SECONDARY);
1202 setTextColor(viewHolder.status_message, BubbleColor.SECONDARY);
1203 setImageTint(viewHolder.indicatorReceived, BubbleColor.SECONDARY);
1204 } else {
1205 setBackgroundTint(viewHolder.message_box, BubbleColor.SURFACE_HIGH);
1206 setTextColor(viewHolder.status_message, BubbleColor.SURFACE_HIGH);
1207 setImageTint(viewHolder.indicatorReceived, BubbleColor.SURFACE_HIGH);
1208 }
1209 viewHolder.indicatorReceived.setImageResource(
1210 RtpSessionStatus.getDrawable(received, rtpSessionStatus.successful));
1211 return view;
1212 } else if (type == STATUS) {
1213 if ("LOAD_MORE".equals(message.getBody())) {
1214 viewHolder.status_message.setVisibility(View.GONE);
1215 viewHolder.contact_picture.setVisibility(View.GONE);
1216 viewHolder.load_more_messages.setVisibility(View.VISIBLE);
1217 viewHolder.load_more_messages.setOnClickListener(
1218 v -> loadMoreMessages((Conversation) message.getConversation()));
1219 } else {
1220 viewHolder.status_message.setVisibility(View.VISIBLE);
1221 viewHolder.load_more_messages.setVisibility(View.GONE);
1222 viewHolder.status_message.setText(message.getBody());
1223 boolean showAvatar;
1224 if (conversation.getMode() == Conversation.MODE_SINGLE) {
1225 showAvatar = true;
1226 AvatarWorkerTask.loadAvatar(
1227 message, viewHolder.contact_picture, R.dimen.avatar_on_status_message);
1228 } else if (message.getCounterpart() != null
1229 || message.getTrueCounterpart() != null
1230 || (message.getCounterparts() != null
1231 && message.getCounterparts().size() > 0)) {
1232 showAvatar = true;
1233 AvatarWorkerTask.loadAvatar(
1234 message, viewHolder.contact_picture, R.dimen.avatar_on_status_message);
1235 } else {
1236 showAvatar = false;
1237 }
1238 if (showAvatar) {
1239 viewHolder.contact_picture.setAlpha(0.5f);
1240 viewHolder.contact_picture.setVisibility(View.VISIBLE);
1241 } else {
1242 viewHolder.contact_picture.setVisibility(View.GONE);
1243 }
1244 }
1245 return view;
1246 } else {
1247 // viewHolder.message_box.setClipToOutline(true); This eats the bubble tails on A14 for some reason
1248 AvatarWorkerTask.loadAvatar(message, viewHolder.contact_picture, R.dimen.avatar);
1249 }
1250
1251 resetClickListener(viewHolder.message_box, viewHolder.messageBody);
1252
1253 viewHolder.message_box.setOnClickListener(v -> {
1254 if (MessageAdapter.this.mOnMessageBoxClickedListener != null) {
1255 MessageAdapter.this.mOnMessageBoxClickedListener
1256 .onContactPictureClicked(message);
1257 }
1258 });
1259 SwipeDetector swipeDetector = new SwipeDetector((action) -> {
1260 if (action == SwipeDetector.Action.LR && MessageAdapter.this.mOnMessageBoxSwipedListener != null) {
1261 MessageAdapter.this.mOnMessageBoxSwipedListener.onContactPictureClicked(message);
1262 }
1263 });
1264 viewHolder.message_box.setOnTouchListener(swipeDetector);
1265 viewHolder.image.setOnTouchListener(swipeDetector);
1266 viewHolder.time.setOnTouchListener(swipeDetector);
1267
1268 // Treat touch-up as click so we don't have to touch twice
1269 // (touch twice is because it's waiting to see if you double-touch for text selection)
1270 viewHolder.messageBody.setOnTouchListener((v, event) -> {
1271 if (event.getAction() == MotionEvent.ACTION_UP) {
1272 if (MessageAdapter.this.mOnMessageBoxClickedListener != null) {
1273 MessageAdapter.this.mOnMessageBoxClickedListener
1274 .onContactPictureClicked(message);
1275 }
1276 }
1277
1278 swipeDetector.onTouch(v, event);
1279
1280 return false;
1281 });
1282 viewHolder.messageBody.setOnClickListener(v -> {
1283 if (MessageAdapter.this.mOnMessageBoxClickedListener != null) {
1284 MessageAdapter.this.mOnMessageBoxClickedListener
1285 .onContactPictureClicked(message);
1286 }
1287 });
1288 viewHolder.contact_picture.setOnClickListener(v -> {
1289 if (MessageAdapter.this.mOnContactPictureClickedListener != null) {
1290 MessageAdapter.this.mOnContactPictureClickedListener
1291 .onContactPictureClicked(message);
1292 }
1293
1294 });
1295 viewHolder.contact_picture.setOnLongClickListener(v -> {
1296 if (MessageAdapter.this.mOnContactPictureLongClickedListener != null) {
1297 MessageAdapter.this.mOnContactPictureLongClickedListener
1298 .onContactPictureLongClicked(v, message);
1299 return true;
1300 } else {
1301 return false;
1302 }
1303 });
1304 viewHolder.messageBody.setAccessibilityDelegate(null);
1305
1306 boolean footerWrap = false;
1307
1308 final Transferable transferable = message.getTransferable();
1309 final boolean unInitiatedButKnownSize = MessageUtils.unInitiatedButKnownSize(message);
1310
1311 final boolean muted = message.getStatus() == Message.STATUS_RECEIVED && conversation.getMode() == Conversation.MODE_MULTI && activity.xmppConnectionService.isMucUserMuted(new MucOptions.User(null, conversation.getJid(), message.getOccupantId(), null, null));
1312 if (muted) {
1313 // Muted MUC participant
1314 displayInfoMessage(viewHolder, "Muted", bubbleColor);
1315 } else if (unInitiatedButKnownSize || message.isDeleted() || (transferable != null && transferable.getStatus() != Transferable.STATUS_UPLOADING)) {
1316 if (unInitiatedButKnownSize || (message.isDeleted() && message.getModerated() == null) || transferable != null && transferable.getStatus() == Transferable.STATUS_OFFER) {
1317 displayDownloadableMessage(viewHolder, message, activity.getString(R.string.download_x_file, UIHelper.getFileDescriptionString(activity, message)), bubbleColor, type);
1318 } else if (transferable != null && transferable.getStatus() == Transferable.STATUS_OFFER_CHECK_FILESIZE) {
1319 displayDownloadableMessage(viewHolder, message, activity.getString(R.string.check_x_filesize, UIHelper.getFileDescriptionString(activity, message)), bubbleColor, type);
1320 } else {
1321 displayInfoMessage(viewHolder, UIHelper.getMessagePreview(activity.xmppConnectionService, message).first, bubbleColor);
1322 }
1323 } else if (message.isFileOrImage()
1324 && message.getEncryption() != Message.ENCRYPTION_PGP
1325 && message.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED) {
1326 if (message.getFileParams().width > 0 && message.getFileParams().height > 0) {
1327 displayMediaPreviewMessage(viewHolder, message, bubbleColor, type);
1328 if (!black && viewHolder.image.getLayoutParams().width > metrics.density * 110) {
1329 footerWrap = true;
1330 }
1331 } else if (message.getFileParams().runtime > 0) {
1332 displayAudioMessage(viewHolder, message, bubbleColor, type);
1333 } else if ("application/xdc+zip".equals(message.getFileParams().getMediaType()) && message.getConversation() instanceof Conversation && message.getThread() != null && !message.getFileParams().getCids().isEmpty()) {
1334 displayWebxdcMessage(viewHolder, message, bubbleColor, type);
1335 } else {
1336 displayOpenableMessage(viewHolder, message, bubbleColor, type);
1337 }
1338 } else if (message.getEncryption() == Message.ENCRYPTION_PGP) {
1339 if (account.isPgpDecryptionServiceConnected()) {
1340 if (conversation instanceof Conversation
1341 && !account.hasPendingPgpIntent((Conversation) conversation)) {
1342 displayInfoMessage(
1343 viewHolder,
1344 activity.getString(R.string.message_decrypting),
1345 bubbleColor);
1346 } else {
1347 displayInfoMessage(
1348 viewHolder, activity.getString(R.string.pgp_message), bubbleColor);
1349 }
1350 } else {
1351 displayInfoMessage(
1352 viewHolder, activity.getString(R.string.install_openkeychain), bubbleColor);
1353 viewHolder.message_box.setOnClickListener(this::promptOpenKeychainInstall);
1354 viewHolder.messageBody.setOnClickListener(this::promptOpenKeychainInstall);
1355 }
1356 } else if (message.getEncryption() == Message.ENCRYPTION_DECRYPTION_FAILED) {
1357 displayInfoMessage(
1358 viewHolder, activity.getString(R.string.decryption_failed), bubbleColor);
1359 } else if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE) {
1360 displayInfoMessage(
1361 viewHolder,
1362 activity.getString(R.string.not_encrypted_for_this_device),
1363 bubbleColor);
1364 } else if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL_FAILED) {
1365 displayInfoMessage(
1366 viewHolder, activity.getString(R.string.omemo_decryption_failed), bubbleColor);
1367 } else {
1368 if (message.isGeoUri()) {
1369 displayLocationMessage(viewHolder, message, bubbleColor, type);
1370 } else if (message.treatAsDownloadable()) {
1371 try {
1372 final URI uri = message.getOob();
1373 displayDownloadableMessage(viewHolder,
1374 message,
1375 activity.getString(
1376 R.string.check_x_filesize_on_host,
1377 UIHelper.getFileDescriptionString(activity, message),
1378 uri.getHost()),
1379 bubbleColor, type);
1380 } catch (Exception e) {
1381 displayDownloadableMessage(
1382 viewHolder,
1383 message,
1384 activity.getString(
1385 R.string.check_x_filesize,
1386 UIHelper.getFileDescriptionString(activity, message)),
1387 bubbleColor, type);
1388 }
1389 } else if (message.bodyIsOnlyEmojis() && message.getType() != Message.TYPE_PRIVATE) {
1390 displayEmojiMessage(viewHolder, getSpannableBody(message), bubbleColor);
1391 } else {
1392 displayTextMessage(viewHolder, message, bubbleColor, message.getType());
1393 }
1394 }
1395
1396 viewHolder.message_box_inner.setMinimumWidth(footerWrap ? (int) (110 * metrics.density) : 0);
1397 LinearLayout.LayoutParams statusParams = (LinearLayout.LayoutParams) viewHolder.status_line.getLayoutParams();
1398 statusParams.width = footerWrap ? ViewGroup.LayoutParams.MATCH_PARENT : ViewGroup.LayoutParams.WRAP_CONTENT;
1399 viewHolder.status_line.setLayoutParams(statusParams);
1400
1401 setBackgroundTint(viewHolder.message_box, bubbleColor);
1402 setTextColor(viewHolder.messageBody, bubbleColor);
1403 viewHolder.messageBody.setLinkTextColor(bubbleToOnSurfaceColor(viewHolder.messageBody, bubbleColor));
1404
1405 if (type == RECEIVED) {
1406 if (!muted && commands != null && conversation instanceof Conversation) {
1407 CommandButtonAdapter adapter = new CommandButtonAdapter(activity);
1408 adapter.addAll(commands);
1409 viewHolder.commands_list.setAdapter(adapter);
1410 viewHolder.commands_list.setVisibility(View.VISIBLE);
1411 viewHolder.commands_list.setOnItemClickListener((p, v, pos, id) -> {
1412 final Element command = adapter.getItem(pos);
1413 activity.startCommand(conversation.getAccount(), command.getAttributeAsJid("jid"), command.getAttribute("node"));
1414 });
1415 } else {
1416 // It's unclear if we can set this to null...
1417 ListAdapter adapter = viewHolder.commands_list.getAdapter();
1418 if (adapter instanceof ArrayAdapter) {
1419 ((ArrayAdapter<?>) adapter).clear();
1420 }
1421 viewHolder.commands_list.setVisibility(View.GONE);
1422 viewHolder.commands_list.setOnItemClickListener(null);
1423 }
1424
1425 setTextColor(viewHolder.encryption, bubbleColor);
1426
1427 if (isInValidSession) {
1428 viewHolder.encryption.setVisibility(View.GONE);
1429 } else {
1430 viewHolder.encryption.setVisibility(View.VISIBLE);
1431 if (omemoEncryption && !message.isTrusted()) {
1432 viewHolder.encryption.setText(R.string.not_trusted);
1433 } else {
1434 viewHolder.encryption.setText(
1435 CryptoHelper.encryptionTypeToText(message.getEncryption()));
1436 }
1437 }
1438 }
1439
1440 if (type == RECEIVED || type == SENT) {
1441 String subject = message.getSubject();
1442 if (subject == null && message.getThread() != null) {
1443 final var thread = ((Conversation) message.getConversation()).getThread(message.getThread().getContent());
1444 if (thread != null) subject = thread.getSubject();
1445 }
1446 if (muted || subject == null) {
1447 viewHolder.subject.setVisibility(View.GONE);
1448 } else {
1449 viewHolder.subject.setVisibility(View.VISIBLE);
1450 viewHolder.subject.setText(subject);
1451 }
1452 }
1453
1454 displayStatus(viewHolder, message, type, bubbleColor);
1455
1456 viewHolder.messageBody.setAccessibilityDelegate(new View.AccessibilityDelegate() {
1457 @Override
1458 public void sendAccessibilityEvent(View host, int eventType) {
1459 super.sendAccessibilityEvent(host, eventType);
1460 if (eventType == AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED) {
1461 if (viewHolder.messageBody.hasSelection()) {
1462 selectionUuid = message.getUuid();
1463 } else if (message.getUuid() != null && message.getUuid().equals(selectionUuid)) {
1464 selectionUuid = null;
1465 }
1466 }
1467 }
1468 });
1469
1470 return view;
1471 }
1472
1473 private void promptOpenKeychainInstall(View view) {
1474 activity.showInstallPgpDialog();
1475 }
1476
1477 public FileBackend getFileBackend() {
1478 return activity.xmppConnectionService.getFileBackend();
1479 }
1480
1481 public void stopAudioPlayer() {
1482 audioPlayer.stop();
1483 }
1484
1485 public void unregisterListenerInAudioPlayer() {
1486 audioPlayer.unregisterListener();
1487 }
1488
1489 public void startStopPending() {
1490 audioPlayer.startStopPending();
1491 }
1492
1493 public void openDownloadable(Message message) {
1494 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU
1495 && ContextCompat.checkSelfPermission(
1496 activity, Manifest.permission.WRITE_EXTERNAL_STORAGE)
1497 != PackageManager.PERMISSION_GRANTED) {
1498 ConversationFragment.registerPendingMessage(activity, message);
1499 ActivityCompat.requestPermissions(
1500 activity,
1501 new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE},
1502 ConversationsActivity.REQUEST_OPEN_MESSAGE);
1503 return;
1504 }
1505 final DownloadableFile file =
1506 activity.xmppConnectionService.getFileBackend().getFile(message);
1507 ViewUtil.view(activity, file);
1508 }
1509
1510 private void showLocation(Message message) {
1511 for (Intent intent : GeoHelper.createGeoIntentsFromMessage(activity, message)) {
1512 if (intent.resolveActivity(getContext().getPackageManager()) != null) {
1513 getContext().startActivity(intent);
1514 return;
1515 }
1516 }
1517 Toast.makeText(
1518 activity,
1519 R.string.no_application_found_to_display_location,
1520 Toast.LENGTH_SHORT)
1521 .show();
1522 }
1523
1524 public void updatePreferences() {
1525 final AppSettings appSettings = new AppSettings(activity);
1526 this.bubbleDesign =
1527 new BubbleDesign(appSettings.isColorfulChatBubbles(), appSettings.isLargeFont());
1528 }
1529
1530 public void setHighlightedTerm(List<String> terms) {
1531 this.highlightedTerm = terms == null ? null : StylingHelper.filterHighlightedWords(terms);
1532 }
1533
1534 public interface OnContactPictureClicked {
1535 void onContactPictureClicked(Message message);
1536 }
1537
1538 public interface OnContactPictureLongClicked {
1539 void onContactPictureLongClicked(View v, Message message);
1540 }
1541
1542 public interface OnInlineImageLongClicked {
1543 boolean onInlineImageLongClicked(Cid cid);
1544 }
1545
1546 private static void setBackgroundTint(final View view, final BubbleColor bubbleColor) {
1547 view.setBackgroundTintList(bubbleToColorStateList(view, bubbleColor));
1548 }
1549
1550 private static ColorStateList bubbleToColorStateList(
1551 final View view, final BubbleColor bubbleColor) {
1552 final @AttrRes int colorAttributeResId =
1553 switch (bubbleColor) {
1554 case SURFACE -> Activities.isNightMode(view.getContext())
1555 ? com.google.android.material.R.attr.colorSurfaceContainerHigh
1556 : com.google.android.material.R.attr.colorSurfaceContainerLow;
1557 case SURFACE_HIGH -> Activities.isNightMode(view.getContext())
1558 ? com.google.android.material.R.attr.colorSurfaceContainerHighest
1559 : com.google.android.material.R.attr.colorSurfaceContainerHigh;
1560 case PRIMARY -> com.google.android.material.R.attr.colorPrimaryContainer;
1561 case SECONDARY -> com.google.android.material.R.attr.colorSecondaryContainer;
1562 case TERTIARY -> com.google.android.material.R.attr.colorTertiaryContainer;
1563 case WARNING -> com.google.android.material.R.attr.colorErrorContainer;
1564 };
1565 return ColorStateList.valueOf(MaterialColors.getColor(view, colorAttributeResId));
1566 }
1567
1568 public static void setImageTint(final ImageView imageView, final BubbleColor bubbleColor) {
1569 ImageViewCompat.setImageTintList(
1570 imageView, bubbleToOnSurfaceColorStateList(imageView, bubbleColor));
1571 }
1572
1573 public static void setImageTintError(final ImageView imageView) {
1574 ImageViewCompat.setImageTintList(
1575 imageView,
1576 ColorStateList.valueOf(
1577 MaterialColors.getColor(
1578 imageView, com.google.android.material.R.attr.colorError)));
1579 }
1580
1581 public static void setTextColor(final TextView textView, final BubbleColor bubbleColor) {
1582 final var color = bubbleToOnSurfaceColor(textView, bubbleColor);
1583 textView.setTextColor(color);
1584 if (BubbleColor.SURFACES.contains(bubbleColor)) {
1585 textView.setLinkTextColor(
1586 MaterialColors.getColor(
1587 textView, com.google.android.material.R.attr.colorPrimary));
1588 } else {
1589 textView.setLinkTextColor(color);
1590 }
1591 }
1592
1593 private static void setTextSize(final TextView textView, final boolean largeFont) {
1594 if (largeFont) {
1595 textView.setTextAppearance(
1596 com.google.android.material.R.style.TextAppearance_Material3_BodyLarge);
1597 textView.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 22);
1598 } else {
1599 textView.setTextAppearance(
1600 com.google.android.material.R.style.TextAppearance_Material3_BodyMedium);
1601 }
1602 }
1603
1604 private static @ColorInt int bubbleToOnSurfaceVariant(
1605 final View view, final BubbleColor bubbleColor) {
1606 final @AttrRes int colorAttributeResId;
1607 if (BubbleColor.SURFACES.contains(bubbleColor)) {
1608 colorAttributeResId = com.google.android.material.R.attr.colorOnSurfaceVariant;
1609 } else {
1610 colorAttributeResId = bubbleToOnSurface(bubbleColor);
1611 }
1612 return MaterialColors.getColor(view, colorAttributeResId);
1613 }
1614
1615 private static @ColorInt int bubbleToOnSurfaceColor(
1616 final View view, final BubbleColor bubbleColor) {
1617 return MaterialColors.getColor(view, bubbleToOnSurface(bubbleColor));
1618 }
1619
1620 public static ColorStateList bubbleToOnSurfaceColorStateList(
1621 final View view, final BubbleColor bubbleColor) {
1622 return ColorStateList.valueOf(bubbleToOnSurfaceColor(view, bubbleColor));
1623 }
1624
1625 private static @AttrRes int bubbleToOnSurface(final BubbleColor bubbleColor) {
1626 return switch (bubbleColor) {
1627 case SURFACE, SURFACE_HIGH -> com.google.android.material.R.attr.colorOnSurface;
1628 case PRIMARY -> com.google.android.material.R.attr.colorOnPrimaryContainer;
1629 case SECONDARY -> com.google.android.material.R.attr.colorOnSecondaryContainer;
1630 case TERTIARY -> com.google.android.material.R.attr.colorOnTertiaryContainer;
1631 case WARNING -> com.google.android.material.R.attr.colorOnErrorContainer;
1632 };
1633 }
1634
1635 public enum BubbleColor {
1636 SURFACE,
1637 SURFACE_HIGH,
1638 PRIMARY,
1639 SECONDARY,
1640 TERTIARY,
1641 WARNING;
1642
1643 private static final Collection<BubbleColor> SURFACES =
1644 Arrays.asList(BubbleColor.SURFACE, BubbleColor.SURFACE_HIGH);
1645 }
1646
1647 private static class BubbleDesign {
1648 public final boolean colorfulChatBubbles;
1649 public final boolean largeFont;
1650
1651 private BubbleDesign(final boolean colorfulChatBubbles, final boolean largeFont) {
1652 this.colorfulChatBubbles = colorfulChatBubbles;
1653 this.largeFont = largeFont;
1654 }
1655 }
1656
1657 private static class ViewHolder {
1658
1659 public MaterialButton load_more_messages;
1660 public ImageView edit_indicator;
1661 public RelativeLayout audioPlayer;
1662 protected View status_line;
1663 protected LinearLayout message_box;
1664 protected View message_box_inner;
1665 protected MaterialButton download_button;
1666 protected ShapeableImageView image;
1667 protected ImageView indicator;
1668 protected ImageView indicatorReceived;
1669 protected TextView time;
1670 protected TextView subject;
1671 protected TextView messageBody;
1672 protected ImageView contact_picture;
1673 protected TextView status_message;
1674 protected TextView encryption;
1675 protected ListView commands_list;
1676 protected GithubIdenticonView thread_identicon;
1677 }
1678
1679 class ThumbnailTask extends AsyncTask<DownloadableFile, Void, Drawable[]> {
1680 @Override
1681 protected Drawable[] doInBackground(DownloadableFile... params) {
1682 if (isCancelled()) return null;
1683
1684 Drawable[] d = new Drawable[params.length];
1685 for (int i = 0; i < params.length; i++) {
1686 try {
1687 d[i] = activity.xmppConnectionService.getFileBackend().getThumbnail(params[i], activity.getResources(), (int) (metrics.density * 288), false);
1688 } catch (final IOException e) {
1689 d[i] = null;
1690 }
1691 }
1692
1693 return d;
1694 }
1695
1696 @Override
1697 protected void onPostExecute(final Drawable[] d) {
1698 if (isCancelled()) return;
1699 activity.xmppConnectionService.updateConversationUi();
1700 }
1701 }
1702}