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