MessageAdapter.java

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