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