MessageAdapter.java

   1package eu.siacs.conversations.ui.adapter;
   2
   3import android.Manifest;
   4import android.app.Activity;
   5import android.content.Intent;
   6import android.content.SharedPreferences;
   7import android.content.pm.PackageManager;
   8import android.graphics.PorterDuff;
   9import android.graphics.drawable.Drawable;
  10import android.graphics.Typeface;
  11import android.net.Uri;
  12import android.os.AsyncTask;
  13import android.preference.PreferenceManager;
  14import android.text.Spannable;
  15import android.text.SpannableString;
  16import android.text.SpannableStringBuilder;
  17import android.text.format.DateUtils;
  18import android.text.style.ForegroundColorSpan;
  19import android.text.style.RelativeSizeSpan;
  20import android.text.style.StyleSpan;
  21import android.text.style.URLSpan;
  22import android.util.DisplayMetrics;
  23import android.view.View;
  24import android.view.ViewGroup;
  25import android.view.WindowManager;
  26import android.widget.ArrayAdapter;
  27import android.widget.Button;
  28import android.widget.ImageView;
  29import android.widget.LinearLayout;
  30import android.widget.ListView;
  31import android.widget.RelativeLayout;
  32import android.widget.TextView;
  33import android.widget.Toast;
  34
  35import androidx.core.app.ActivityCompat;
  36import androidx.core.content.ContextCompat;
  37import androidx.core.content.res.ResourcesCompat;
  38
  39import com.cheogram.android.BobTransfer;
  40
  41import com.google.common.base.Strings;
  42
  43import java.io.IOException;
  44import java.net.URI;
  45import java.net.URISyntaxException;
  46import java.security.NoSuchAlgorithmException;
  47import java.util.List;
  48import java.util.Locale;
  49import java.util.regex.Matcher;
  50import java.util.regex.Pattern;
  51
  52import io.ipfs.cid.Cid;
  53
  54import eu.siacs.conversations.Config;
  55import eu.siacs.conversations.R;
  56import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
  57import eu.siacs.conversations.entities.Account;
  58import eu.siacs.conversations.entities.Contact;
  59import eu.siacs.conversations.entities.Conversation;
  60import eu.siacs.conversations.entities.Conversational;
  61import eu.siacs.conversations.entities.DownloadableFile;
  62import eu.siacs.conversations.entities.Message.FileParams;
  63import eu.siacs.conversations.entities.Message;
  64import eu.siacs.conversations.entities.Roster;
  65import eu.siacs.conversations.entities.RtpSessionStatus;
  66import eu.siacs.conversations.entities.Transferable;
  67import eu.siacs.conversations.persistance.FileBackend;
  68import eu.siacs.conversations.services.MessageArchiveService;
  69import eu.siacs.conversations.services.NotificationService;
  70import eu.siacs.conversations.ui.ConversationFragment;
  71import eu.siacs.conversations.ui.ConversationsActivity;
  72import eu.siacs.conversations.ui.XmppActivity;
  73import eu.siacs.conversations.ui.service.AudioPlayer;
  74import eu.siacs.conversations.ui.text.DividerSpan;
  75import eu.siacs.conversations.ui.text.QuoteSpan;
  76import eu.siacs.conversations.ui.util.AvatarWorkerTask;
  77import eu.siacs.conversations.ui.util.MyLinkify;
  78import eu.siacs.conversations.ui.util.QuoteHelper;
  79import eu.siacs.conversations.ui.util.StyledAttributes;
  80import eu.siacs.conversations.ui.util.ViewUtil;
  81import eu.siacs.conversations.ui.widget.ClickableMovementMethod;
  82import eu.siacs.conversations.utils.CryptoHelper;
  83import eu.siacs.conversations.utils.Emoticons;
  84import eu.siacs.conversations.utils.GeoHelper;
  85import eu.siacs.conversations.utils.MessageUtils;
  86import eu.siacs.conversations.utils.StylingHelper;
  87import eu.siacs.conversations.utils.TimeFrameUtils;
  88import eu.siacs.conversations.utils.UIHelper;
  89import eu.siacs.conversations.xmpp.Jid;
  90import eu.siacs.conversations.xmpp.mam.MamReference;
  91import eu.siacs.conversations.xml.Element;
  92
  93public class MessageAdapter extends ArrayAdapter<Message> {
  94
  95    public static final String DATE_SEPARATOR_BODY = "DATE_SEPARATOR";
  96    private static final int SENT = 0;
  97    private static final int RECEIVED = 1;
  98    private static final int STATUS = 2;
  99    private static final int DATE_SEPARATOR = 3;
 100    private static final int RTP_SESSION = 4;
 101    private final XmppActivity activity;
 102    private final AudioPlayer audioPlayer;
 103    private List<String> highlightedTerm = null;
 104    private final DisplayMetrics metrics;
 105    private OnContactPictureClicked mOnContactPictureClickedListener;
 106    private OnContactPictureLongClicked mOnContactPictureLongClickedListener;
 107    private boolean mUseGreenBackground = false;
 108    private boolean mForceNames = false;
 109
 110    public MessageAdapter(XmppActivity activity, List<Message> messages) {
 111        super(activity, 0, messages);
 112        this.audioPlayer = new AudioPlayer(this);
 113        this.activity = activity;
 114        metrics = getContext().getResources().getDisplayMetrics();
 115        updatePreferences();
 116    }
 117
 118    public MessageAdapter(XmppActivity activity, List<Message> messages, boolean forceNames) {
 119        this(activity, messages);
 120        mForceNames = forceNames;
 121    }
 122
 123    private static void resetClickListener(View... views) {
 124        for (View view : views) {
 125            if (view != null) view.setOnClickListener(null);
 126        }
 127    }
 128
 129    public void flagScreenOn() {
 130        activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
 131    }
 132
 133    public void flagScreenOff() {
 134        activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
 135    }
 136
 137    public void setVolumeControl(final int stream) {
 138        activity.setVolumeControlStream(stream);
 139    }
 140
 141    public void setOnContactPictureClicked(OnContactPictureClicked listener) {
 142        this.mOnContactPictureClickedListener = listener;
 143    }
 144
 145    public Activity getActivity() {
 146        return activity;
 147    }
 148
 149    public void setOnContactPictureLongClicked(
 150            OnContactPictureLongClicked listener) {
 151        this.mOnContactPictureLongClickedListener = listener;
 152    }
 153
 154    @Override
 155    public int getViewTypeCount() {
 156        return 5;
 157    }
 158
 159    private int getItemViewType(Message message) {
 160        if (message.getType() == Message.TYPE_STATUS) {
 161            if (DATE_SEPARATOR_BODY.equals(message.getBody())) {
 162                return DATE_SEPARATOR;
 163            } else {
 164                return STATUS;
 165            }
 166        } else if (message.getType() == Message.TYPE_RTP_SESSION) {
 167            return RTP_SESSION;
 168        } else if (message.getStatus() <= Message.STATUS_RECEIVED) {
 169            return RECEIVED;
 170        } else {
 171            return SENT;
 172        }
 173    }
 174
 175    @Override
 176    public int getItemViewType(int position) {
 177        return this.getItemViewType(getItem(position));
 178    }
 179
 180    private int getMessageTextColor(boolean onDark, boolean primary) {
 181        if (onDark) {
 182            return ContextCompat.getColor(activity, primary ? R.color.white : R.color.white70);
 183        } else {
 184            return ContextCompat.getColor(activity, primary ? R.color.black87 : R.color.black54);
 185        }
 186    }
 187
 188    private void displayStatus(ViewHolder viewHolder, Message message, int type, boolean darkBackground) {
 189        String filesize = null;
 190        String info = null;
 191        boolean error = false;
 192        if (viewHolder.indicatorReceived != null) {
 193            viewHolder.indicatorReceived.setVisibility(View.GONE);
 194        }
 195
 196        if (viewHolder.edit_indicator != null) {
 197            if (message.edited()) {
 198                viewHolder.edit_indicator.setVisibility(View.VISIBLE);
 199                viewHolder.edit_indicator.setImageResource(darkBackground ? R.drawable.ic_mode_edit_white_18dp : R.drawable.ic_mode_edit_black_18dp);
 200                viewHolder.edit_indicator.setAlpha(darkBackground ? 0.7f : 0.57f);
 201            } else {
 202                viewHolder.edit_indicator.setVisibility(View.GONE);
 203            }
 204        }
 205        final Transferable transferable = message.getTransferable();
 206        boolean multiReceived = message.getConversation().getMode() == Conversation.MODE_MULTI
 207                && message.getMergedStatus() <= Message.STATUS_RECEIVED;
 208        if (message.isFileOrImage() || transferable != null || MessageUtils.unInitiatedButKnownSize(message)) {
 209            FileParams params = message.getFileParams();
 210            filesize = params.size != null ? UIHelper.filesizeToString(params.size) : null;
 211            if (transferable != null && (transferable.getStatus() == Transferable.STATUS_FAILED || transferable.getStatus() == Transferable.STATUS_CANCELLED)) {
 212                error = true;
 213            }
 214        }
 215        switch (message.getMergedStatus()) {
 216            case Message.STATUS_WAITING:
 217                info = getContext().getString(R.string.waiting);
 218                break;
 219            case Message.STATUS_UNSEND:
 220                if (transferable != null) {
 221                    info = getContext().getString(R.string.sending_file, transferable.getProgress());
 222                } else {
 223                    info = getContext().getString(R.string.sending);
 224                }
 225                break;
 226            case Message.STATUS_OFFERED:
 227                info = getContext().getString(R.string.offering);
 228                break;
 229            case Message.STATUS_SEND_RECEIVED:
 230            case Message.STATUS_SEND_DISPLAYED:
 231                viewHolder.indicatorReceived.setImageResource(darkBackground ? R.drawable.ic_done_white_18dp : R.drawable.ic_done_black_18dp);
 232                viewHolder.indicatorReceived.setAlpha(darkBackground ? 0.7f : 0.57f);
 233                viewHolder.indicatorReceived.setVisibility(View.VISIBLE);
 234                break;
 235            case Message.STATUS_SEND_FAILED:
 236                final String errorMessage = message.getErrorMessage();
 237                if (Message.ERROR_MESSAGE_CANCELLED.equals(errorMessage)) {
 238                    info = getContext().getString(R.string.cancelled);
 239                } else if (errorMessage != null) {
 240                    final String[] errorParts = errorMessage.split("\\u001f", 2);
 241                    if (errorParts.length == 2) {
 242                        switch (errorParts[0]) {
 243                            case "file-too-large":
 244                                info = getContext().getString(R.string.file_too_large);
 245                                break;
 246                            default:
 247                                info = getContext().getString(R.string.send_failed);
 248                                break;
 249                        }
 250                    } else {
 251                        info = getContext().getString(R.string.send_failed);
 252                    }
 253                } else {
 254                    info = getContext().getString(R.string.send_failed);
 255                }
 256                error = true;
 257                break;
 258            default:
 259                if (mForceNames || multiReceived) {
 260                    info = UIHelper.getMessageDisplayName(message);
 261                }
 262                break;
 263        }
 264        if (error && type == SENT) {
 265            if (darkBackground) {
 266                viewHolder.time.setTextAppearance(getContext(), R.style.TextAppearance_Conversations_Caption_Warning_OnDark);
 267            } else {
 268                viewHolder.time.setTextAppearance(getContext(), R.style.TextAppearance_Conversations_Caption_Warning);
 269            }
 270        } else {
 271            if (darkBackground) {
 272                viewHolder.time.setTextAppearance(getContext(), R.style.TextAppearance_Conversations_Caption_OnDark);
 273            } else {
 274                viewHolder.time.setTextAppearance(getContext(), R.style.TextAppearance_Conversations_Caption);
 275            }
 276            viewHolder.time.setTextColor(this.getMessageTextColor(darkBackground, false));
 277        }
 278        if (message.getEncryption() == Message.ENCRYPTION_NONE) {
 279            viewHolder.indicator.setVisibility(View.GONE);
 280        } else {
 281            boolean verified = false;
 282            if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) {
 283                final FingerprintStatus status = message.getConversation()
 284                        .getAccount().getAxolotlService().getFingerprintTrust(
 285                                message.getFingerprint());
 286                if (status != null && status.isVerified()) {
 287                    verified = true;
 288                }
 289            }
 290            if (verified) {
 291                viewHolder.indicator.setImageResource(darkBackground ? R.drawable.ic_verified_user_white_18dp : R.drawable.ic_verified_user_black_18dp);
 292            } else {
 293                viewHolder.indicator.setImageResource(darkBackground ? R.drawable.ic_lock_white_18dp : R.drawable.ic_lock_black_18dp);
 294            }
 295            if (darkBackground) {
 296                viewHolder.indicator.setAlpha(0.7f);
 297            } else {
 298                viewHolder.indicator.setAlpha(0.57f);
 299            }
 300            viewHolder.indicator.setVisibility(View.VISIBLE);
 301        }
 302
 303        final String formattedTime = UIHelper.readableTimeDifferenceFull(getContext(), message.getMergedTimeSent());
 304        final String bodyLanguage = message.getBodyLanguage();
 305        final String bodyLanguageInfo = bodyLanguage == null ? "" : String.format(" \u00B7 %s", bodyLanguage.toUpperCase(Locale.US));
 306        if (message.getStatus() <= Message.STATUS_RECEIVED) {
 307            if ((filesize != null) && (info != null)) {
 308                viewHolder.time.setText(formattedTime + " \u00B7 " + filesize + " \u00B7 " + info + bodyLanguageInfo);
 309            } else if ((filesize == null) && (info != null)) {
 310                viewHolder.time.setText(formattedTime + " \u00B7 " + info + bodyLanguageInfo);
 311            } else if ((filesize != null) && (info == null)) {
 312                viewHolder.time.setText(formattedTime + " \u00B7 " + filesize + bodyLanguageInfo);
 313            } else {
 314                viewHolder.time.setText(formattedTime + bodyLanguageInfo);
 315            }
 316        } else {
 317            if ((filesize != null) && (info != null)) {
 318                viewHolder.time.setText(filesize + " \u00B7 " + info + bodyLanguageInfo);
 319            } else if ((filesize == null) && (info != null)) {
 320                if (error) {
 321                    viewHolder.time.setText(info + " \u00B7 " + formattedTime + bodyLanguageInfo);
 322                } else {
 323                    viewHolder.time.setText(info);
 324                }
 325            } else if ((filesize != null) && (info == null)) {
 326                viewHolder.time.setText(filesize + " \u00B7 " + formattedTime + bodyLanguageInfo);
 327            } else {
 328                viewHolder.time.setText(formattedTime + bodyLanguageInfo);
 329            }
 330        }
 331    }
 332
 333    private void displayInfoMessage(ViewHolder viewHolder, CharSequence text, boolean darkBackground) {
 334        viewHolder.download_button.setVisibility(View.GONE);
 335        viewHolder.audioPlayer.setVisibility(View.GONE);
 336        viewHolder.image.setVisibility(View.GONE);
 337        viewHolder.messageBody.setVisibility(View.VISIBLE);
 338        viewHolder.messageBody.setText(text);
 339        if (darkBackground) {
 340            viewHolder.messageBody.setTextAppearance(getContext(), R.style.TextAppearance_Conversations_Body1_Secondary_OnDark);
 341        } else {
 342            viewHolder.messageBody.setTextAppearance(getContext(), R.style.TextAppearance_Conversations_Body1_Secondary);
 343        }
 344        viewHolder.messageBody.setTextIsSelectable(false);
 345    }
 346
 347    private void displayEmojiMessage(final ViewHolder viewHolder, final String body, final boolean darkBackground) {
 348        viewHolder.download_button.setVisibility(View.GONE);
 349        viewHolder.audioPlayer.setVisibility(View.GONE);
 350        viewHolder.image.setVisibility(View.GONE);
 351        viewHolder.messageBody.setVisibility(View.VISIBLE);
 352        if (darkBackground) {
 353            viewHolder.messageBody.setTextAppearance(getContext(), R.style.TextAppearance_Conversations_Body1_Emoji_OnDark);
 354        } else {
 355            viewHolder.messageBody.setTextAppearance(getContext(), R.style.TextAppearance_Conversations_Body1_Emoji);
 356        }
 357        Spannable span = new SpannableString(body);
 358        float size = Emoticons.isEmoji(body) ? 3.0f : 2.0f;
 359        span.setSpan(new RelativeSizeSpan(size), 0, body.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
 360        viewHolder.messageBody.setText(span);
 361    }
 362
 363    private void applyQuoteSpan(SpannableStringBuilder body, int start, int end, boolean darkBackground) {
 364        if (start > 1 && !"\n\n".equals(body.subSequence(start - 2, start).toString())) {
 365            body.insert(start++, "\n");
 366            body.setSpan(new DividerSpan(false), start - 2, start, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
 367            end++;
 368        }
 369        if (end < body.length() - 1 && !"\n\n".equals(body.subSequence(end, end + 2).toString())) {
 370            body.insert(end, "\n");
 371            body.setSpan(new DividerSpan(false), end, end + 2, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
 372        }
 373        int color = darkBackground ? this.getMessageTextColor(darkBackground, false)
 374                : ContextCompat.getColor(activity, R.color.green700_desaturated);
 375        DisplayMetrics metrics = getContext().getResources().getDisplayMetrics();
 376        body.setSpan(new QuoteSpan(color, metrics), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
 377    }
 378
 379    /**
 380     * Applies QuoteSpan to group of lines which starts with > or » characters.
 381     * Appends likebreaks and applies DividerSpan to them to show a padding between quote and text.
 382     */
 383    private boolean handleTextQuotes(SpannableStringBuilder body, boolean darkBackground) {
 384        boolean startsWithQuote = false;
 385        int quoteDepth = 1;
 386        while (QuoteHelper.bodyContainsQuoteStart(body) && quoteDepth <= Config.QUOTE_MAX_DEPTH) {
 387            char previous = '\n';
 388            int lineStart = -1;
 389            int lineTextStart = -1;
 390            int quoteStart = -1;
 391            for (int i = 0; i <= body.length(); i++) {
 392                char current = body.length() > i ? body.charAt(i) : '\n';
 393                if (lineStart == -1) {
 394                    if (previous == '\n') {
 395                        if (i < body.length() && QuoteHelper.isPositionQuoteStart(body, i)) {
 396                            // Line start with quote
 397                            lineStart = i;
 398                            if (quoteStart == -1) quoteStart = i;
 399                            if (i == 0) startsWithQuote = true;
 400                        } else if (quoteStart >= 0) {
 401                            // Line start without quote, apply spans there
 402                            applyQuoteSpan(body, quoteStart, i - 1, darkBackground);
 403                            quoteStart = -1;
 404                        }
 405                    }
 406                } else {
 407                    // Remove extra spaces between > and first character in the line
 408                    // > character will be removed too
 409                    if (current != ' ' && lineTextStart == -1) {
 410                        lineTextStart = i;
 411                    }
 412                    if (current == '\n') {
 413                        body.delete(lineStart, lineTextStart);
 414                        i -= lineTextStart - lineStart;
 415                        if (i == lineStart) {
 416                            // Avoid empty lines because span over empty line can be hidden
 417                            body.insert(i++, " ");
 418                        }
 419                        lineStart = -1;
 420                        lineTextStart = -1;
 421                    }
 422                }
 423                previous = current;
 424            }
 425            if (quoteStart >= 0) {
 426                // Apply spans to finishing open quote
 427                applyQuoteSpan(body, quoteStart, body.length(), darkBackground);
 428            }
 429            quoteDepth++;
 430        }
 431        return startsWithQuote;
 432    }
 433
 434    private void displayTextMessage(final ViewHolder viewHolder, final Message message, boolean darkBackground, int type) {
 435        viewHolder.download_button.setVisibility(View.GONE);
 436        viewHolder.image.setVisibility(View.GONE);
 437        viewHolder.audioPlayer.setVisibility(View.GONE);
 438        viewHolder.messageBody.setVisibility(View.GONE);
 439
 440        if (darkBackground) {
 441            viewHolder.messageBody.setTextAppearance(getContext(), R.style.TextAppearance_Conversations_Body1_OnDark);
 442        } else {
 443            viewHolder.messageBody.setTextAppearance(getContext(), R.style.TextAppearance_Conversations_Body1);
 444        }
 445        viewHolder.messageBody.setHighlightColor(ContextCompat.getColor(activity, darkBackground
 446                ? (type == SENT || !mUseGreenBackground ? R.color.black26 : R.color.grey800) : R.color.grey500));
 447        viewHolder.messageBody.setTypeface(null, Typeface.NORMAL);
 448
 449        if (message.getBody() != null && !message.getBody().equals("")) {
 450            viewHolder.messageBody.setVisibility(View.VISIBLE);
 451            final String nick = UIHelper.getMessageDisplayName(message);
 452            Drawable fallbackImg = ResourcesCompat.getDrawable(activity.getResources(), activity.getThemeResource(R.attr.ic_attach_photo, R.drawable.ic_attach_photo), null);
 453            fallbackImg.setBounds(FileBackend.rectForSize(fallbackImg.getIntrinsicWidth(), fallbackImg.getIntrinsicHeight(), (int) (metrics.density * 32)));
 454            SpannableStringBuilder body = message.getMergedBody((cid) -> {
 455                try {
 456                    DownloadableFile f = activity.xmppConnectionService.getFileForCid(cid);
 457                    if (f == null || !f.canRead()) {
 458                        if (!message.trusted() && !message.getConversation().canInferPresence()) return null;
 459
 460                        try {
 461                            new BobTransfer(BobTransfer.uri(cid), message.getConversation().getAccount(), message.getCounterpart(), activity.xmppConnectionService).start();
 462                        } catch (final NoSuchAlgorithmException | URISyntaxException e) { }
 463                        return null;
 464                    }
 465
 466                    Drawable d = activity.xmppConnectionService.getFileBackend().getThumbnail(f, activity.getResources(), (int) (metrics.density * 288), true);
 467                    if (d == null) {
 468                        new ThumbnailTask().execute(f);
 469                    } else {
 470                        d = d.getConstantState().newDrawable();
 471                        d.setBounds(FileBackend.rectForSize(d.getIntrinsicWidth(), d.getIntrinsicHeight(), (int) (metrics.density * 32)));
 472                    }
 473                    return d;
 474                } catch (final IOException e) {
 475                    return fallbackImg;
 476                }
 477            }, fallbackImg);
 478            boolean hasMeCommand = message.hasMeCommand();
 479            if (hasMeCommand) {
 480                body = body.replace(0, Message.ME_COMMAND.length(), nick + " ");
 481            }
 482            if (body.length() > Config.MAX_DISPLAY_MESSAGE_CHARS) {
 483                body = new SpannableStringBuilder(body, 0, Config.MAX_DISPLAY_MESSAGE_CHARS);
 484                body.append("\u2026");
 485            }
 486            Message.MergeSeparator[] mergeSeparators = body.getSpans(0, body.length(), Message.MergeSeparator.class);
 487            for (Message.MergeSeparator mergeSeparator : mergeSeparators) {
 488                int start = body.getSpanStart(mergeSeparator);
 489                int end = body.getSpanEnd(mergeSeparator);
 490                body.setSpan(new DividerSpan(true), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
 491            }
 492            boolean startsWithQuote = handleTextQuotes(body, darkBackground);
 493            if (!message.isPrivateMessage()) {
 494                if (hasMeCommand) {
 495                    body.setSpan(new StyleSpan(Typeface.BOLD_ITALIC), 0, nick.length(),
 496                            Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
 497                }
 498            } else {
 499                String privateMarker;
 500                if (message.getStatus() <= Message.STATUS_RECEIVED) {
 501                    privateMarker = activity.getString(R.string.private_message);
 502                } else {
 503                    Jid cp = message.getCounterpart();
 504                    privateMarker = activity.getString(R.string.private_message_to, Strings.nullToEmpty(cp == null ? null : cp.getResource()));
 505                }
 506                body.insert(0, privateMarker);
 507                int privateMarkerIndex = privateMarker.length();
 508                if (startsWithQuote) {
 509                    body.insert(privateMarkerIndex, "\n\n");
 510                    body.setSpan(new DividerSpan(false), privateMarkerIndex, privateMarkerIndex + 2,
 511                            Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
 512                } else {
 513                    body.insert(privateMarkerIndex, " ");
 514                }
 515                body.setSpan(new ForegroundColorSpan(getMessageTextColor(darkBackground, false)), 0, privateMarkerIndex, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
 516                body.setSpan(new StyleSpan(Typeface.BOLD), 0, privateMarkerIndex, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
 517                if (hasMeCommand) {
 518                    body.setSpan(new StyleSpan(Typeface.BOLD_ITALIC), privateMarkerIndex + 1,
 519                            privateMarkerIndex + 1 + nick.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
 520                }
 521            }
 522            if (message.getConversation().getMode() == Conversation.MODE_MULTI && message.getStatus() == Message.STATUS_RECEIVED) {
 523                if (message.getConversation() instanceof Conversation) {
 524                    final Conversation conversation = (Conversation) message.getConversation();
 525                    Pattern pattern = NotificationService.generateNickHighlightPattern(conversation.getMucOptions().getActualNick());
 526                    Matcher matcher = pattern.matcher(body);
 527                    while (matcher.find()) {
 528                        body.setSpan(new StyleSpan(Typeface.BOLD), matcher.start(), matcher.end(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
 529                    }
 530                }
 531            }
 532            Matcher matcher = Emoticons.getEmojiPattern(body).matcher(body);
 533            while (matcher.find()) {
 534                if (matcher.start() < matcher.end()) {
 535                    body.setSpan(new RelativeSizeSpan(1.2f), matcher.start(), matcher.end(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
 536                }
 537            }
 538
 539            StylingHelper.format(body, viewHolder.messageBody.getCurrentTextColor());
 540            if (highlightedTerm != null) {
 541                StylingHelper.highlight(activity, body, highlightedTerm, StylingHelper.isDarkText(viewHolder.messageBody));
 542            }
 543            MyLinkify.addLinks(body, message.getConversation().getAccount());
 544            viewHolder.messageBody.setAutoLinkMask(0);
 545            viewHolder.messageBody.setText(body);
 546            viewHolder.messageBody.setMovementMethod(ClickableMovementMethod.getInstance());
 547        } else {
 548            viewHolder.messageBody.setText("");
 549            viewHolder.messageBody.setTextIsSelectable(false);
 550        }
 551    }
 552
 553    private void displayDownloadableMessage(ViewHolder viewHolder, final Message message, String text, final boolean darkBackground, final int type) {
 554        displayTextMessage(viewHolder, message, darkBackground, type);
 555        viewHolder.image.setVisibility(View.GONE);
 556        viewHolder.audioPlayer.setVisibility(View.GONE);
 557        viewHolder.download_button.setVisibility(View.VISIBLE);
 558        viewHolder.download_button.setText(text);
 559        viewHolder.download_button.setOnClickListener(v -> ConversationFragment.downloadFile(activity, message));
 560    }
 561
 562    private void displayOpenableMessage(ViewHolder viewHolder, final Message message, final boolean darkBackground, final int type) {
 563        displayTextMessage(viewHolder, message, darkBackground, type);
 564        viewHolder.image.setVisibility(View.GONE);
 565        viewHolder.audioPlayer.setVisibility(View.GONE);
 566        viewHolder.download_button.setVisibility(View.VISIBLE);
 567        viewHolder.download_button.setText(activity.getString(R.string.open_x_file, UIHelper.getFileDescriptionString(activity, message)));
 568        viewHolder.download_button.setOnClickListener(v -> openDownloadable(message));
 569    }
 570
 571    private void displayLocationMessage(ViewHolder viewHolder, final Message message, final boolean darkBackground, final int type) {
 572        displayTextMessage(viewHolder, message, darkBackground, type);
 573        viewHolder.image.setVisibility(View.GONE);
 574        viewHolder.audioPlayer.setVisibility(View.GONE);
 575        viewHolder.download_button.setVisibility(View.VISIBLE);
 576        viewHolder.download_button.setText(R.string.show_location);
 577        viewHolder.download_button.setOnClickListener(v -> showLocation(message));
 578    }
 579
 580    private void displayAudioMessage(ViewHolder viewHolder, Message message, boolean darkBackground, final int type) {
 581        displayTextMessage(viewHolder, message, darkBackground, type);
 582        viewHolder.image.setVisibility(View.GONE);
 583        viewHolder.download_button.setVisibility(View.GONE);
 584        final RelativeLayout audioPlayer = viewHolder.audioPlayer;
 585        audioPlayer.setVisibility(View.VISIBLE);
 586        AudioPlayer.ViewHolder.get(audioPlayer).setDarkBackground(darkBackground);
 587        this.audioPlayer.init(audioPlayer, message);
 588    }
 589
 590    private void displayMediaPreviewMessage(ViewHolder viewHolder, final Message message, final boolean darkBackground, final int type) {
 591        displayTextMessage(viewHolder, message, darkBackground, type);
 592        viewHolder.download_button.setVisibility(View.GONE);
 593        viewHolder.audioPlayer.setVisibility(View.GONE);
 594        viewHolder.image.setVisibility(View.VISIBLE);
 595        final FileParams params = message.getFileParams();
 596        final float target = activity.getResources().getDimension(R.dimen.image_preview_width);
 597        final int scaledW;
 598        final int scaledH;
 599        if (Math.max(params.height, params.width) * metrics.density <= target) {
 600            scaledW = (int) (params.width * metrics.density);
 601            scaledH = (int) (params.height * metrics.density);
 602        } else if (Math.max(params.height, params.width) <= target) {
 603            scaledW = params.width;
 604            scaledH = params.height;
 605        } else if (params.width <= params.height) {
 606            scaledW = (int) (params.width / ((double) params.height / target));
 607            scaledH = (int) target;
 608        } else {
 609            scaledW = (int) target;
 610            scaledH = (int) (params.height / ((double) params.width / target));
 611        }
 612        final LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(scaledW, scaledH);
 613        layoutParams.setMargins(0, (int) (metrics.density * 4), 0, (int) (metrics.density * 4));
 614        viewHolder.image.setLayoutParams(layoutParams);
 615        activity.loadBitmap(message, viewHolder.image);
 616        viewHolder.image.setOnClickListener(v -> openDownloadable(message));
 617    }
 618
 619    private void toggleWhisperInfo(ViewHolder viewHolder, final Message message, final boolean darkBackground) {
 620        if (message.isPrivateMessage()) {
 621            final String privateMarker;
 622            if (message.getStatus() <= Message.STATUS_RECEIVED) {
 623                privateMarker = activity.getString(R.string.private_message);
 624            } else {
 625                Jid cp = message.getCounterpart();
 626                privateMarker = activity.getString(R.string.private_message_to, Strings.nullToEmpty(cp == null ? null : cp.getResource()));
 627            }
 628            final SpannableString body = new SpannableString(privateMarker);
 629            body.setSpan(new ForegroundColorSpan(getMessageTextColor(darkBackground, false)), 0, privateMarker.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
 630            body.setSpan(new StyleSpan(Typeface.BOLD), 0, privateMarker.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
 631            viewHolder.messageBody.setText(body);
 632            viewHolder.messageBody.setVisibility(View.VISIBLE);
 633        } else {
 634            viewHolder.messageBody.setVisibility(View.GONE);
 635        }
 636    }
 637
 638    private void loadMoreMessages(Conversation conversation) {
 639        conversation.setLastClearHistory(0, null);
 640        activity.xmppConnectionService.updateConversation(conversation);
 641        conversation.setHasMessagesLeftOnServer(true);
 642        conversation.setFirstMamReference(null);
 643        long timestamp = conversation.getLastMessageTransmitted().getTimestamp();
 644        if (timestamp == 0) {
 645            timestamp = System.currentTimeMillis();
 646        }
 647        conversation.messagesLoaded.set(true);
 648        MessageArchiveService.Query query = activity.xmppConnectionService.getMessageArchiveService().query(conversation, new MamReference(0), timestamp, false);
 649        if (query != null) {
 650            Toast.makeText(activity, R.string.fetching_history_from_server, Toast.LENGTH_LONG).show();
 651        } else {
 652            Toast.makeText(activity, R.string.not_fetching_history_retention_period, Toast.LENGTH_SHORT).show();
 653        }
 654    }
 655
 656    @Override
 657    public View getView(int position, View view, ViewGroup parent) {
 658        final Message message = getItem(position);
 659        final boolean omemoEncryption = message.getEncryption() == Message.ENCRYPTION_AXOLOTL;
 660        final boolean isInValidSession = message.isValidInSession() && (!omemoEncryption || message.isTrusted());
 661        final Conversational conversation = message.getConversation();
 662        final Account account = conversation.getAccount();
 663        final List<Element> commands = message.getCommands();
 664        final int type = getItemViewType(position);
 665        ViewHolder viewHolder;
 666        if (view == null) {
 667            viewHolder = new ViewHolder();
 668            switch (type) {
 669                case DATE_SEPARATOR:
 670                    view = activity.getLayoutInflater().inflate(R.layout.message_date_bubble, parent, false);
 671                    viewHolder.status_message = view.findViewById(R.id.message_body);
 672                    viewHolder.message_box = view.findViewById(R.id.message_box);
 673                    viewHolder.indicatorReceived = view.findViewById(R.id.indicator_received);
 674                    break;
 675                case RTP_SESSION:
 676                    view = activity.getLayoutInflater().inflate(R.layout.message_rtp_session, parent, false);
 677                    viewHolder.status_message = view.findViewById(R.id.message_body);
 678                    viewHolder.message_box = view.findViewById(R.id.message_box);
 679                    viewHolder.indicatorReceived = view.findViewById(R.id.indicator_received);
 680                    break;
 681                case SENT:
 682                    view = activity.getLayoutInflater().inflate(R.layout.message_sent, parent, false);
 683                    viewHolder.message_box = view.findViewById(R.id.message_box);
 684                    viewHolder.contact_picture = view.findViewById(R.id.message_photo);
 685                    viewHolder.download_button = view.findViewById(R.id.download_button);
 686                    viewHolder.indicator = view.findViewById(R.id.security_indicator);
 687                    viewHolder.edit_indicator = view.findViewById(R.id.edit_indicator);
 688                    viewHolder.image = view.findViewById(R.id.message_image);
 689                    viewHolder.messageBody = view.findViewById(R.id.message_body);
 690                    viewHolder.time = view.findViewById(R.id.message_time);
 691                    viewHolder.subject = view.findViewById(R.id.message_subject);
 692                    viewHolder.indicatorReceived = view.findViewById(R.id.indicator_received);
 693                    viewHolder.audioPlayer = view.findViewById(R.id.audio_player);
 694                    break;
 695                case RECEIVED:
 696                    view = activity.getLayoutInflater().inflate(R.layout.message_received, parent, false);
 697                    viewHolder.message_box = view.findViewById(R.id.message_box);
 698                    viewHolder.contact_picture = view.findViewById(R.id.message_photo);
 699                    viewHolder.download_button = view.findViewById(R.id.download_button);
 700                    viewHolder.indicator = view.findViewById(R.id.security_indicator);
 701                    viewHolder.edit_indicator = view.findViewById(R.id.edit_indicator);
 702                    viewHolder.image = view.findViewById(R.id.message_image);
 703                    viewHolder.messageBody = view.findViewById(R.id.message_body);
 704                    viewHolder.time = view.findViewById(R.id.message_time);
 705                    viewHolder.subject = view.findViewById(R.id.message_subject);
 706                    viewHolder.indicatorReceived = view.findViewById(R.id.indicator_received);
 707                    viewHolder.encryption = view.findViewById(R.id.message_encryption);
 708                    viewHolder.audioPlayer = view.findViewById(R.id.audio_player);
 709                    viewHolder.commands_list = view.findViewById(R.id.commands_list);
 710                    break;
 711                case STATUS:
 712                    view = activity.getLayoutInflater().inflate(R.layout.message_status, parent, false);
 713                    viewHolder.contact_picture = view.findViewById(R.id.message_photo);
 714                    viewHolder.status_message = view.findViewById(R.id.status_message);
 715                    viewHolder.load_more_messages = view.findViewById(R.id.load_more_messages);
 716                    break;
 717                default:
 718                    throw new AssertionError("Unknown view type");
 719            }
 720            view.setTag(viewHolder);
 721        } else {
 722            viewHolder = (ViewHolder) view.getTag();
 723            if (viewHolder == null) {
 724                return view;
 725            }
 726        }
 727
 728        boolean darkBackground = type == RECEIVED && (!isInValidSession || mUseGreenBackground) || activity.isDarkTheme();
 729
 730        if (type == DATE_SEPARATOR) {
 731            if (UIHelper.today(message.getTimeSent())) {
 732                viewHolder.status_message.setText(R.string.today);
 733            } else if (UIHelper.yesterday(message.getTimeSent())) {
 734                viewHolder.status_message.setText(R.string.yesterday);
 735            } else {
 736                viewHolder.status_message.setText(DateUtils.formatDateTime(activity, message.getTimeSent(), DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR));
 737            }
 738            viewHolder.message_box.setBackgroundResource(activity.isDarkTheme() ? R.drawable.date_bubble_grey : R.drawable.date_bubble_white);
 739            return view;
 740        } else if (type == RTP_SESSION) {
 741            final boolean isDarkTheme = activity.isDarkTheme();
 742            final boolean received = message.getStatus() <= Message.STATUS_RECEIVED;
 743            final RtpSessionStatus rtpSessionStatus = RtpSessionStatus.of(message.getBody());
 744            final long duration = rtpSessionStatus.duration;
 745            final String callTime = UIHelper.readableTimeDifferenceFull(activity, message.getTimeSent());
 746            if (received) {
 747                if (duration > 0) {
 748                    viewHolder.status_message.setText(activity.getString(R.string.incoming_call_duration_timestamp, TimeFrameUtils.resolve(activity, duration), callTime));
 749                } else if (rtpSessionStatus.successful) {
 750                    viewHolder.status_message.setText(activity.getString(R.string.incoming_call_timestamp, callTime));
 751                } else {
 752                    viewHolder.status_message.setText(activity.getString(R.string.missed_call_timestamp, callTime));
 753                }
 754            } else {
 755                if (duration > 0) {
 756                    viewHolder.status_message.setText(activity.getString(R.string.outgoing_call_duration_timestamp, TimeFrameUtils.resolve(activity, duration), callTime));
 757                } else {
 758                    viewHolder.status_message.setText(activity.getString(R.string.outgoing_call_timestamp, callTime));
 759                }
 760            }
 761            viewHolder.indicatorReceived.setImageResource(RtpSessionStatus.getDrawable(received, rtpSessionStatus.successful, isDarkTheme));
 762            viewHolder.indicatorReceived.setAlpha(isDarkTheme ? 0.7f : 0.57f);
 763            viewHolder.message_box.setBackgroundResource(isDarkTheme ? R.drawable.date_bubble_grey : R.drawable.date_bubble_white);
 764            return view;
 765        } else if (type == STATUS) {
 766            if ("LOAD_MORE".equals(message.getBody())) {
 767                viewHolder.status_message.setVisibility(View.GONE);
 768                viewHolder.contact_picture.setVisibility(View.GONE);
 769                viewHolder.load_more_messages.setVisibility(View.VISIBLE);
 770                viewHolder.load_more_messages.setOnClickListener(v -> loadMoreMessages((Conversation) message.getConversation()));
 771            } else {
 772                viewHolder.status_message.setVisibility(View.VISIBLE);
 773                viewHolder.load_more_messages.setVisibility(View.GONE);
 774                viewHolder.status_message.setText(message.getBody());
 775                boolean showAvatar;
 776                if (conversation.getMode() == Conversation.MODE_SINGLE) {
 777                    showAvatar = true;
 778                    AvatarWorkerTask.loadAvatar(message, viewHolder.contact_picture, R.dimen.avatar_on_status_message);
 779                } else if (message.getCounterpart() != null || message.getTrueCounterpart() != null || (message.getCounterparts() != null && message.getCounterparts().size() > 0)) {
 780                    showAvatar = true;
 781                    AvatarWorkerTask.loadAvatar(message, viewHolder.contact_picture, R.dimen.avatar_on_status_message);
 782                } else {
 783                    showAvatar = false;
 784                }
 785                if (showAvatar) {
 786                    viewHolder.contact_picture.setAlpha(0.5f);
 787                    viewHolder.contact_picture.setVisibility(View.VISIBLE);
 788                } else {
 789                    viewHolder.contact_picture.setVisibility(View.GONE);
 790                }
 791            }
 792            return view;
 793        } else {
 794            AvatarWorkerTask.loadAvatar(message, viewHolder.contact_picture, R.dimen.avatar);
 795        }
 796
 797        resetClickListener(viewHolder.message_box, viewHolder.messageBody);
 798
 799        viewHolder.contact_picture.setOnClickListener(v -> {
 800            if (MessageAdapter.this.mOnContactPictureClickedListener != null) {
 801                MessageAdapter.this.mOnContactPictureClickedListener
 802                        .onContactPictureClicked(message);
 803            }
 804
 805        });
 806        viewHolder.contact_picture.setOnLongClickListener(v -> {
 807            if (MessageAdapter.this.mOnContactPictureLongClickedListener != null) {
 808                MessageAdapter.this.mOnContactPictureLongClickedListener
 809                        .onContactPictureLongClicked(v, message);
 810                return true;
 811            } else {
 812                return false;
 813            }
 814        });
 815
 816        final Transferable transferable = message.getTransferable();
 817        final boolean unInitiatedButKnownSize = MessageUtils.unInitiatedButKnownSize(message);
 818        if (unInitiatedButKnownSize || message.isDeleted() || (transferable != null && transferable.getStatus() != Transferable.STATUS_UPLOADING)) {
 819            if (unInitiatedButKnownSize || transferable != null && transferable.getStatus() == Transferable.STATUS_OFFER) {
 820                displayDownloadableMessage(viewHolder, message, activity.getString(R.string.download_x_file, UIHelper.getFileDescriptionString(activity, message)), darkBackground, type);
 821            } else if (transferable != null && transferable.getStatus() == Transferable.STATUS_OFFER_CHECK_FILESIZE) {
 822                displayDownloadableMessage(viewHolder, message, activity.getString(R.string.check_x_filesize, UIHelper.getFileDescriptionString(activity, message)), darkBackground, type);
 823            } else {
 824                displayInfoMessage(viewHolder, UIHelper.getMessagePreview(activity, message).first, darkBackground);
 825            }
 826        } else if (message.isFileOrImage() && message.getEncryption() != Message.ENCRYPTION_PGP && message.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED) {
 827            if (message.getFileParams().width > 0 && message.getFileParams().height > 0) {
 828                displayMediaPreviewMessage(viewHolder, message, darkBackground, type);
 829            } else if (message.getFileParams().runtime > 0) {
 830                displayAudioMessage(viewHolder, message, darkBackground, type);
 831            } else {
 832                displayOpenableMessage(viewHolder, message, darkBackground, type);
 833            }
 834        } else if (message.getEncryption() == Message.ENCRYPTION_PGP) {
 835            if (account.isPgpDecryptionServiceConnected()) {
 836                if (conversation instanceof Conversation && !account.hasPendingPgpIntent((Conversation) conversation)) {
 837                    displayInfoMessage(viewHolder, activity.getString(R.string.message_decrypting), darkBackground);
 838                } else {
 839                    displayInfoMessage(viewHolder, activity.getString(R.string.pgp_message), darkBackground);
 840                }
 841            } else {
 842                displayInfoMessage(viewHolder, activity.getString(R.string.install_openkeychain), darkBackground);
 843                viewHolder.message_box.setOnClickListener(this::promptOpenKeychainInstall);
 844                viewHolder.messageBody.setOnClickListener(this::promptOpenKeychainInstall);
 845            }
 846        } else if (message.getEncryption() == Message.ENCRYPTION_DECRYPTION_FAILED) {
 847            displayInfoMessage(viewHolder, activity.getString(R.string.decryption_failed), darkBackground);
 848        } else if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE) {
 849            displayInfoMessage(viewHolder, activity.getString(R.string.not_encrypted_for_this_device), darkBackground);
 850        } else if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL_FAILED) {
 851            displayInfoMessage(viewHolder, activity.getString(R.string.omemo_decryption_failed), darkBackground);
 852        } else {
 853            if (message.isGeoUri()) {
 854                displayLocationMessage(viewHolder, message, darkBackground, type);
 855            } else if (message.treatAsDownloadable()) {
 856                try {
 857                    final URI uri = message.getOob();
 858                    displayDownloadableMessage(viewHolder,
 859                            message,
 860                            activity.getString(R.string.check_x_filesize_on_host,
 861                                    UIHelper.getFileDescriptionString(activity, message),
 862                                    uri.getHost()),
 863                            darkBackground, type);
 864                } catch (Exception e) {
 865                    displayDownloadableMessage(viewHolder,
 866                            message,
 867                            activity.getString(R.string.check_x_filesize,
 868                                    UIHelper.getFileDescriptionString(activity, message)),
 869                            darkBackground, type);
 870                }
 871            } else if (message.bodyIsOnlyEmojis() && message.getType() != Message.TYPE_PRIVATE) {
 872                displayEmojiMessage(viewHolder, message.getBody().trim(), darkBackground);
 873            } else {
 874                displayTextMessage(viewHolder, message, darkBackground, type);
 875            }
 876        }
 877
 878        if (type == RECEIVED) {
 879            if (commands != null && conversation instanceof Conversation) {
 880                CommandButtonAdapter adapter = new CommandButtonAdapter(activity);
 881                adapter.addAll(commands);
 882                viewHolder.commands_list.setAdapter(adapter);
 883                viewHolder.commands_list.setVisibility(View.VISIBLE);
 884                viewHolder.commands_list.setOnItemClickListener((p, v, pos, id) -> {
 885                    ((Conversation) conversation).startCommand(adapter.getItem(pos), activity.xmppConnectionService);
 886                });
 887            }
 888
 889            if (isInValidSession) {
 890                if (!mUseGreenBackground) {
 891                    viewHolder.message_box.getBackground().setColorFilter(
 892                        StyledAttributes.getColor(activity, mUseGreenBackground ? R.attr.message_bubble_received_bg : R.attr.color_background_primary),
 893                        PorterDuff.Mode.SRC_ATOP
 894                    );
 895                }
 896                viewHolder.encryption.setVisibility(View.GONE);
 897            } else {
 898                viewHolder.encryption.setVisibility(View.VISIBLE);
 899                if (omemoEncryption && !message.isTrusted()) {
 900                    viewHolder.encryption.setText(R.string.not_trusted);
 901                } else {
 902                    viewHolder.encryption.setText(CryptoHelper.encryptionTypeToText(message.getEncryption()));
 903                }
 904            }
 905        }
 906
 907        if (type == RECEIVED || type == SENT) {
 908            if (message.getSubject() == null) {
 909                viewHolder.subject.setVisibility(View.GONE);
 910            } else {
 911                viewHolder.subject.setVisibility(View.VISIBLE);
 912                viewHolder.subject.setText(message.getSubject());
 913            }
 914        }
 915
 916        displayStatus(viewHolder, message, type, darkBackground);
 917
 918        return view;
 919    }
 920
 921    private void promptOpenKeychainInstall(View view) {
 922        activity.showInstallPgpDialog();
 923    }
 924
 925    public FileBackend getFileBackend() {
 926        return activity.xmppConnectionService.getFileBackend();
 927    }
 928
 929    public void stopAudioPlayer() {
 930        audioPlayer.stop();
 931    }
 932
 933    public void unregisterListenerInAudioPlayer() {
 934        audioPlayer.unregisterListener();
 935    }
 936
 937    public void startStopPending() {
 938        audioPlayer.startStopPending();
 939    }
 940
 941    public void openDownloadable(Message message) {
 942        if (ContextCompat.checkSelfPermission(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
 943            ConversationFragment.registerPendingMessage(activity, message);
 944            ActivityCompat.requestPermissions(activity, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, ConversationsActivity.REQUEST_OPEN_MESSAGE);
 945            return;
 946        }
 947        final DownloadableFile file = activity.xmppConnectionService.getFileBackend().getFile(message);
 948        ViewUtil.view(activity, file);
 949    }
 950
 951    private void showLocation(Message message) {
 952        for (Intent intent : GeoHelper.createGeoIntentsFromMessage(activity, message)) {
 953            if (intent.resolveActivity(getContext().getPackageManager()) != null) {
 954                getContext().startActivity(intent);
 955                return;
 956            }
 957        }
 958        Toast.makeText(activity, R.string.no_application_found_to_display_location, Toast.LENGTH_SHORT).show();
 959    }
 960
 961    public void updatePreferences() {
 962        SharedPreferences p = PreferenceManager.getDefaultSharedPreferences(activity);
 963        this.mUseGreenBackground = p.getBoolean("use_green_background", activity.getResources().getBoolean(R.bool.use_green_background));
 964    }
 965
 966
 967    public void setHighlightedTerm(List<String> terms) {
 968        this.highlightedTerm = terms == null ? null : StylingHelper.filterHighlightedWords(terms);
 969    }
 970
 971    public interface OnContactPictureClicked {
 972        void onContactPictureClicked(Message message);
 973    }
 974
 975    public interface OnContactPictureLongClicked {
 976        void onContactPictureLongClicked(View v, Message message);
 977    }
 978
 979    private static class ViewHolder {
 980
 981        public Button load_more_messages;
 982        public ImageView edit_indicator;
 983        public RelativeLayout audioPlayer;
 984        protected LinearLayout message_box;
 985        protected Button download_button;
 986        protected ImageView image;
 987        protected ImageView indicator;
 988        protected ImageView indicatorReceived;
 989        protected TextView time;
 990        protected TextView subject;
 991        protected TextView messageBody;
 992        protected ImageView contact_picture;
 993        protected TextView status_message;
 994        protected TextView encryption;
 995        protected ListView commands_list;
 996    }
 997
 998    class ThumbnailTask extends AsyncTask<DownloadableFile, Void, Drawable[]> {
 999        @Override
1000        protected Drawable[] doInBackground(DownloadableFile... params) {
1001            if (isCancelled()) return null;
1002
1003            Drawable[] d = new Drawable[params.length];
1004            for (int i = 0; i < params.length; i++) {
1005                try {
1006                    d[i] = activity.xmppConnectionService.getFileBackend().getThumbnail(params[i], activity.getResources(), (int) (metrics.density * 288), false);
1007                } catch (final IOException e) {
1008                    d[i] = null;
1009                }
1010            }
1011
1012            return d;
1013        }
1014
1015        @Override
1016        protected void onPostExecute(final Drawable[] d) {
1017            if (isCancelled()) return;
1018            activity.xmppConnectionService.updateConversationUi();
1019        }
1020    }
1021}