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.os.Build;
14import android.preference.PreferenceManager;
15import android.text.Spannable;
16import android.text.SpannableString;
17import android.text.SpannableStringBuilder;
18import android.text.style.ImageSpan;
19import android.text.style.ClickableSpan;
20import android.text.format.DateUtils;
21import android.text.style.ForegroundColorSpan;
22import android.text.style.RelativeSizeSpan;
23import android.text.style.StyleSpan;
24import android.text.style.URLSpan;
25import android.util.DisplayMetrics;
26import android.util.LruCache;
27import android.view.accessibility.AccessibilityEvent;
28import android.view.Gravity;
29import android.view.MotionEvent;
30import android.view.View;
31import android.view.ViewGroup;
32import android.view.WindowManager;
33import android.widget.ArrayAdapter;
34import android.widget.Button;
35import android.widget.ImageView;
36import android.widget.LinearLayout;
37import android.widget.ListAdapter;
38import android.widget.ListView;
39import android.widget.RelativeLayout;
40import android.widget.TextView;
41import android.widget.Toast;
42
43import androidx.core.app.ActivityCompat;
44import androidx.core.content.ContextCompat;
45import androidx.core.content.res.ResourcesCompat;
46
47import com.cheogram.android.BobTransfer;
48import com.cheogram.android.MessageTextActionModeCallback;
49import com.cheogram.android.SwipeDetector;
50import com.cheogram.android.WebxdcPage;
51import com.cheogram.android.WebxdcUpdate;
52
53import com.google.common.base.Strings;
54
55import com.lelloman.identicon.view.GithubIdenticonView;
56
57import java.io.IOException;
58import java.net.URI;
59import java.net.URISyntaxException;
60import java.security.NoSuchAlgorithmException;
61import java.util.HashMap;
62import java.util.List;
63import java.util.Map;
64import java.util.Locale;
65import java.util.regex.Matcher;
66import java.util.regex.Pattern;
67
68import io.ipfs.cid.Cid;
69
70import me.saket.bettermovementmethod.BetterLinkMovementMethod;
71
72import eu.siacs.conversations.Config;
73import eu.siacs.conversations.R;
74import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
75import eu.siacs.conversations.entities.Account;
76import eu.siacs.conversations.entities.Contact;
77import eu.siacs.conversations.entities.Conversation;
78import eu.siacs.conversations.entities.Conversational;
79import eu.siacs.conversations.entities.DownloadableFile;
80import eu.siacs.conversations.entities.Message.FileParams;
81import eu.siacs.conversations.entities.Message;
82import eu.siacs.conversations.entities.MucOptions;
83import eu.siacs.conversations.entities.Roster;
84import eu.siacs.conversations.entities.RtpSessionStatus;
85import eu.siacs.conversations.entities.Transferable;
86import eu.siacs.conversations.persistance.FileBackend;
87import eu.siacs.conversations.services.MessageArchiveService;
88import eu.siacs.conversations.services.NotificationService;
89import eu.siacs.conversations.ui.ConversationFragment;
90import eu.siacs.conversations.ui.ConversationsActivity;
91import eu.siacs.conversations.ui.SettingsActivity;
92import eu.siacs.conversations.ui.XmppActivity;
93import eu.siacs.conversations.ui.service.AudioPlayer;
94import eu.siacs.conversations.ui.text.DividerSpan;
95import eu.siacs.conversations.ui.text.QuoteSpan;
96import eu.siacs.conversations.ui.util.AvatarWorkerTask;
97import eu.siacs.conversations.ui.util.MyLinkify;
98import eu.siacs.conversations.ui.util.QuoteHelper;
99import eu.siacs.conversations.ui.util.ShareUtil;
100import eu.siacs.conversations.ui.util.StyledAttributes;
101import eu.siacs.conversations.ui.util.ViewUtil;
102import eu.siacs.conversations.utils.CryptoHelper;
103import eu.siacs.conversations.utils.Emoticons;
104import eu.siacs.conversations.utils.GeoHelper;
105import eu.siacs.conversations.utils.MessageUtils;
106import eu.siacs.conversations.utils.StylingHelper;
107import eu.siacs.conversations.utils.TimeFrameUtils;
108import eu.siacs.conversations.utils.UIHelper;
109import eu.siacs.conversations.xmpp.Jid;
110import eu.siacs.conversations.xmpp.mam.MamReference;
111import eu.siacs.conversations.xml.Element;
112
113public class MessageAdapter extends ArrayAdapter<Message> {
114
115 public static final String DATE_SEPARATOR_BODY = "DATE_SEPARATOR";
116 private static final int SENT = 0;
117 private static final int RECEIVED = 1;
118 private static final int STATUS = 2;
119 private static final int DATE_SEPARATOR = 3;
120 private static final int RTP_SESSION = 4;
121 private final XmppActivity activity;
122 private final AudioPlayer audioPlayer;
123 private List<String> highlightedTerm = null;
124 private final DisplayMetrics metrics;
125 private ConversationFragment mConversationFragment = null;
126 private OnContactPictureClicked mOnContactPictureClickedListener;
127 private OnContactPictureClicked mOnMessageBoxClickedListener;
128 private OnContactPictureClicked mOnMessageBoxSwipedListener;
129 private OnContactPictureLongClicked mOnContactPictureLongClickedListener;
130 private OnInlineImageLongClicked mOnInlineImageLongClickedListener;
131 private boolean mUseGreenBackground = false;
132 private final boolean mForceNames;
133 private final Map<String, WebxdcUpdate> lastWebxdcUpdate = new HashMap<>();
134 private String selectionUuid = null;
135
136 public MessageAdapter(final XmppActivity activity, final List<Message> messages, final boolean forceNames) {
137 super(activity, 0, messages);
138 this.audioPlayer = new AudioPlayer(this);
139 this.activity = activity;
140 metrics = getContext().getResources().getDisplayMetrics();
141 updatePreferences();
142 this.mForceNames = forceNames;
143 }
144
145 public MessageAdapter(final XmppActivity activity, final List<Message> messages) {
146 this(activity, messages, false);
147 }
148
149 private static void resetClickListener(View... views) {
150 for (View view : views) {
151 if (view != null) view.setOnClickListener(null);
152 }
153 }
154
155 public void flagScreenOn() {
156 activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
157 }
158
159 public void flagScreenOff() {
160 activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
161 }
162
163 public void setVolumeControl(final int stream) {
164 activity.setVolumeControlStream(stream);
165 }
166
167 public void setOnContactPictureClicked(OnContactPictureClicked listener) {
168 this.mOnContactPictureClickedListener = listener;
169 }
170
171 public void setOnMessageBoxClicked(OnContactPictureClicked listener) {
172 this.mOnMessageBoxClickedListener = listener;
173 }
174
175 public void setOnMessageBoxSwiped(OnContactPictureClicked listener) {
176 this.mOnMessageBoxSwipedListener = listener;
177 }
178
179 public void setConversationFragment(ConversationFragment frag) {
180 mConversationFragment = frag;
181 }
182
183 public void quoteText(String text) {
184 if (mConversationFragment != null) mConversationFragment.quoteText(text);
185 }
186
187 public boolean hasSelection() {
188 return selectionUuid != null;
189 }
190
191 public Activity getActivity() {
192 return activity;
193 }
194
195 public void setOnContactPictureLongClicked(
196 OnContactPictureLongClicked listener) {
197 this.mOnContactPictureLongClickedListener = listener;
198 }
199
200 public void setOnInlineImageLongClicked(OnInlineImageLongClicked listener) {
201 this.mOnInlineImageLongClickedListener = listener;
202 }
203
204 @Override
205 public int getViewTypeCount() {
206 return 5;
207 }
208
209 private int getItemViewType(Message message) {
210 if (message.getType() == Message.TYPE_STATUS) {
211 if (DATE_SEPARATOR_BODY.equals(message.getBody())) {
212 return DATE_SEPARATOR;
213 } else {
214 return STATUS;
215 }
216 } else if (message.getType() == Message.TYPE_RTP_SESSION) {
217 return RTP_SESSION;
218 } else if (message.getStatus() <= Message.STATUS_RECEIVED) {
219 return RECEIVED;
220 } else {
221 return SENT;
222 }
223 }
224
225 @Override
226 public int getItemViewType(int position) {
227 return this.getItemViewType(getItem(position));
228 }
229
230 private int getMessageTextColor(boolean onDark, boolean primary) {
231 if (onDark) {
232 return ContextCompat.getColor(activity, primary ? R.color.white : R.color.white70);
233 } else {
234 return ContextCompat.getColor(activity, primary ? R.color.black87 : R.color.black54);
235 }
236 }
237
238 private void displayStatus(ViewHolder viewHolder, Message message, int type, boolean darkBackground) {
239 String filesize = null;
240 String info = null;
241 boolean error = false;
242 if (viewHolder.indicatorReceived != null) {
243 viewHolder.indicatorReceived.setVisibility(View.GONE);
244 }
245
246 if (viewHolder.edit_indicator != null) {
247 if (message.edited() && message.getModerated() == null) {
248 viewHolder.edit_indicator.setVisibility(View.VISIBLE);
249 viewHolder.edit_indicator.setImageResource(darkBackground ? R.drawable.ic_mode_edit_white_18dp : R.drawable.ic_mode_edit_black_18dp);
250 viewHolder.edit_indicator.setAlpha(darkBackground ? 0.7f : 0.57f);
251 } else {
252 viewHolder.edit_indicator.setVisibility(View.GONE);
253 }
254 }
255 final Transferable transferable = message.getTransferable();
256 boolean multiReceived = message.getConversation().getMode() == Conversation.MODE_MULTI
257 && message.getMergedStatus() <= Message.STATUS_RECEIVED;
258 if (message.isFileOrImage() || transferable != null || MessageUtils.unInitiatedButKnownSize(message)) {
259 FileParams params = message.getFileParams();
260 filesize = params.size != null ? UIHelper.filesizeToString(params.size) : null;
261 if (transferable != null && (transferable.getStatus() == Transferable.STATUS_FAILED || transferable.getStatus() == Transferable.STATUS_CANCELLED)) {
262 error = true;
263 }
264 }
265 switch (message.getMergedStatus()) {
266 case Message.STATUS_WAITING:
267 info = getContext().getString(R.string.waiting);
268 break;
269 case Message.STATUS_UNSEND:
270 if (transferable != null) {
271 info = getContext().getString(R.string.sending_file, transferable.getProgress());
272 } else {
273 info = getContext().getString(R.string.sending);
274 }
275 break;
276 case Message.STATUS_OFFERED:
277 info = getContext().getString(R.string.offering);
278 break;
279 case Message.STATUS_SEND_RECEIVED:
280 case Message.STATUS_SEND_DISPLAYED:
281 if (viewHolder.indicatorReceived != null) {
282 viewHolder.indicatorReceived.setImageResource(darkBackground ? R.drawable.ic_done_white_18dp : R.drawable.ic_done_black_18dp);
283 viewHolder.indicatorReceived.setAlpha(darkBackground ? 0.7f : 0.57f);
284 viewHolder.indicatorReceived.setVisibility(View.VISIBLE);
285 }
286 break;
287 case Message.STATUS_SEND_FAILED:
288 final String errorMessage = message.getErrorMessage();
289 if (Message.ERROR_MESSAGE_CANCELLED.equals(errorMessage)) {
290 info = getContext().getString(R.string.cancelled);
291 } else if (errorMessage != null) {
292 final String[] errorParts = errorMessage.split("\\u001f", 2);
293 if (errorParts.length == 2) {
294 switch (errorParts[0]) {
295 case "file-too-large":
296 info = getContext().getString(R.string.file_too_large);
297 break;
298 default:
299 info = getContext().getString(R.string.send_failed);
300 break;
301 }
302 } else {
303 info = getContext().getString(R.string.send_failed);
304 }
305 } else {
306 info = getContext().getString(R.string.send_failed);
307 }
308 error = true;
309 break;
310 default:
311 if (mForceNames || multiReceived || (message.getTrueCounterpart() != null && message.getContact() != null)) {
312 info = UIHelper.getMessageDisplayName(message).replace(' ', '\u00A0');
313 }
314 break;
315 }
316 if (error && type == SENT) {
317 if (darkBackground) {
318 viewHolder.time.setTextAppearance(getContext(), R.style.TextAppearance_Conversations_Caption_Warning_OnDark);
319 } else {
320 viewHolder.time.setTextAppearance(getContext(), R.style.TextAppearance_Conversations_Caption_Warning);
321 }
322 } else {
323 if (darkBackground) {
324 viewHolder.time.setTextAppearance(getContext(), R.style.TextAppearance_Conversations_Caption_OnDark);
325 } else {
326 viewHolder.time.setTextAppearance(getContext(), R.style.TextAppearance_Conversations_Caption);
327 }
328 viewHolder.time.setTextColor(this.getMessageTextColor(darkBackground, false));
329 }
330 if (message.getEncryption() == Message.ENCRYPTION_NONE) {
331 viewHolder.indicator.setVisibility(View.GONE);
332 } else {
333 boolean verified = false;
334 if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) {
335 final FingerprintStatus status = message.getConversation()
336 .getAccount().getAxolotlService().getFingerprintTrust(
337 message.getFingerprint());
338 if (status != null && status.isVerified()) {
339 verified = true;
340 }
341 }
342 if (verified) {
343 viewHolder.indicator.setImageResource(darkBackground ? R.drawable.ic_verified_user_white_18dp : R.drawable.ic_verified_user_black_18dp);
344 } else {
345 viewHolder.indicator.setImageResource(darkBackground ? R.drawable.ic_lock_white_18dp : R.drawable.ic_lock_black_18dp);
346 }
347 if (darkBackground) {
348 viewHolder.indicator.setAlpha(0.7f);
349 } else {
350 viewHolder.indicator.setAlpha(0.57f);
351 }
352 viewHolder.indicator.setVisibility(View.VISIBLE);
353 }
354
355 final String formattedTime = UIHelper.readableTimeDifferenceFull(getContext(), message.getMergedTimeSent());
356 final String bodyLanguage = message.getBodyLanguage();
357 final String bodyLanguageInfo = bodyLanguage == null ? "" : String.format("\u00A0\u00B7 %s", bodyLanguage.toUpperCase(Locale.US));
358 if (message.getStatus() <= Message.STATUS_RECEIVED) {
359 if ((filesize != null) && (info != null)) {
360 viewHolder.time.setText(formattedTime + "\u00A0\u00B7" + filesize + "\u00A0\u00B7 " + info + bodyLanguageInfo);
361 } else if ((filesize == null) && (info != null)) {
362 viewHolder.time.setText(formattedTime + "\u00A0\u00B7 " + info + bodyLanguageInfo);
363 } else if ((filesize != null) && (info == null)) {
364 viewHolder.time.setText(formattedTime + "\u00A0\u00B7 " + filesize + bodyLanguageInfo);
365 } else {
366 viewHolder.time.setText(formattedTime + bodyLanguageInfo);
367 }
368 } else {
369 if ((filesize != null) && (info != null)) {
370 viewHolder.time.setText(filesize + "\u00A0\u00B7 " + info + bodyLanguageInfo);
371 } else if ((filesize == null) && (info != null)) {
372 if (error) {
373 viewHolder.time.setText(info + "\u00A0\u00B7 " + formattedTime + bodyLanguageInfo);
374 } else {
375 viewHolder.time.setText(info);
376 }
377 } else if ((filesize != null) && (info == null)) {
378 viewHolder.time.setText(filesize + "\u00A0\u00B7 " + formattedTime + bodyLanguageInfo);
379 } else {
380 viewHolder.time.setText(formattedTime + bodyLanguageInfo);
381 }
382 }
383 }
384
385 private void displayInfoMessage(ViewHolder viewHolder, CharSequence text, boolean darkBackground, final Message message, int type) {
386 displayDownloadableMessage(viewHolder, message, "", darkBackground, type);
387 int imageVisibility = viewHolder.image.getVisibility();
388 displayInfoMessage(viewHolder, text, darkBackground);
389 viewHolder.image.setVisibility(imageVisibility);
390 }
391
392 private void displayInfoMessage(ViewHolder viewHolder, CharSequence text, boolean darkBackground) {
393 viewHolder.download_button.setVisibility(View.GONE);
394 viewHolder.audioPlayer.setVisibility(View.GONE);
395 viewHolder.image.setVisibility(View.GONE);
396 viewHolder.messageBody.setVisibility(View.VISIBLE);
397 viewHolder.messageBody.setText(text);
398 if (darkBackground) {
399 viewHolder.messageBody.setTextAppearance(getContext(), R.style.TextAppearance_Conversations_Body1_Secondary_OnDark);
400 } else {
401 viewHolder.messageBody.setTextAppearance(getContext(), R.style.TextAppearance_Conversations_Body1_Secondary);
402 }
403 viewHolder.messageBody.setTextIsSelectable(false);
404 }
405
406 private void displayEmojiMessage(final ViewHolder viewHolder, final SpannableStringBuilder body, final boolean darkBackground) {
407 viewHolder.download_button.setVisibility(View.GONE);
408 viewHolder.audioPlayer.setVisibility(View.GONE);
409 viewHolder.image.setVisibility(View.GONE);
410 viewHolder.messageBody.setVisibility(View.VISIBLE);
411 if (darkBackground) {
412 viewHolder.messageBody.setTextAppearance(getContext(), R.style.TextAppearance_Conversations_Body1_Emoji_OnDark);
413 } else {
414 viewHolder.messageBody.setTextAppearance(getContext(), R.style.TextAppearance_Conversations_Body1_Emoji);
415 }
416 ImageSpan[] imageSpans = body.getSpans(0, body.length(), ImageSpan.class);
417 float size = imageSpans.length == 1 || Emoticons.isEmoji(body.toString()) ? 3.0f : 2.0f;
418 body.setSpan(new RelativeSizeSpan(size), 0, body.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
419 viewHolder.messageBody.setText(body);
420 }
421
422 private void applyQuoteSpan(SpannableStringBuilder body, int start, int end, boolean darkBackground) {
423 if (start > 1 && !"\n\n".equals(body.subSequence(start - 2, start).toString())) {
424 body.insert(start++, "\n");
425 body.setSpan(
426 new DividerSpan(false),
427 start - ("\n".equals(body.subSequence(start - 2, start - 1).toString()) ? 2 : 1),
428 start,
429 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
430 );
431 end++;
432 }
433 if (end < body.length() - 1 && !"\n\n".equals(body.subSequence(end, end + 2).toString())) {
434 body.insert(end, "\n");
435 body.setSpan(
436 new DividerSpan(false),
437 end,
438 end + ("\n".equals(body.subSequence(end + 1, end + 2).toString()) ? 2 : 1),
439 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
440 );
441 }
442 int color = darkBackground ? this.getMessageTextColor(darkBackground, false)
443 : ContextCompat.getColor(activity, R.color.green700_desaturated);
444 DisplayMetrics metrics = getContext().getResources().getDisplayMetrics();
445 body.setSpan(new QuoteSpan(color, metrics), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
446 }
447
448 /**
449 * Applies QuoteSpan to group of lines which starts with > or » characters.
450 * Appends likebreaks and applies DividerSpan to them to show a padding between quote and text.
451 */
452 public boolean handleTextQuotes(SpannableStringBuilder body, boolean darkBackground) {
453 boolean startsWithQuote = false;
454 int quoteDepth = 1;
455 while (QuoteHelper.bodyContainsQuoteStart(body) && quoteDepth <= Config.QUOTE_MAX_DEPTH) {
456 char previous = '\n';
457 int lineStart = -1;
458 int lineTextStart = -1;
459 int quoteStart = -1;
460 for (int i = 0; i <= body.length(); i++) {
461 char current = body.length() > i ? body.charAt(i) : '\n';
462 if (lineStart == -1) {
463 if (previous == '\n') {
464 if (i < body.length() && QuoteHelper.isPositionQuoteStart(body, i)) {
465 // Line start with quote
466 lineStart = i;
467 if (quoteStart == -1) quoteStart = i;
468 if (i == 0) startsWithQuote = true;
469 } else if (quoteStart >= 0) {
470 // Line start without quote, apply spans there
471 applyQuoteSpan(body, quoteStart, i - 1, darkBackground);
472 quoteStart = -1;
473 }
474 }
475 } else {
476 // Remove extra spaces between > and first character in the line
477 // > character will be removed too
478 if (current != ' ' && lineTextStart == -1) {
479 lineTextStart = i;
480 }
481 if (current == '\n') {
482 body.delete(lineStart, lineTextStart);
483 i -= lineTextStart - lineStart;
484 if (i == lineStart) {
485 // Avoid empty lines because span over empty line can be hidden
486 body.insert(i++, " ");
487 }
488 lineStart = -1;
489 lineTextStart = -1;
490 }
491 }
492 previous = current;
493 }
494 if (quoteStart >= 0) {
495 // Apply spans to finishing open quote
496 applyQuoteSpan(body, quoteStart, body.length(), darkBackground);
497 }
498 quoteDepth++;
499 }
500 return startsWithQuote;
501 }
502
503 private SpannableStringBuilder getSpannableBody(final Message message) {
504 Drawable fallbackImg = ResourcesCompat.getDrawable(activity.getResources(), activity.getThemeResource(R.attr.ic_attach_photo, R.drawable.ic_attach_photo), null);
505 return message.getMergedBody((cid) -> {
506 try {
507 DownloadableFile f = activity.xmppConnectionService.getFileForCid(cid);
508 if (f == null || !f.canRead()) {
509 if (!message.trusted() && !message.getConversation().canInferPresence()) return null;
510
511 try {
512 new BobTransfer(BobTransfer.uri(cid), message.getConversation().getAccount(), message.getCounterpart(), activity.xmppConnectionService).start();
513 } catch (final NoSuchAlgorithmException | URISyntaxException e) { }
514 return null;
515 }
516
517 Drawable d = activity.xmppConnectionService.getFileBackend().getThumbnail(f, activity.getResources(), (int) (metrics.density * 288), true);
518 if (d == null) {
519 new ThumbnailTask().execute(f);
520 }
521 return d;
522 } catch (final IOException e) {
523 return null;
524 }
525 }, fallbackImg);
526 }
527
528 private void displayTextMessage(final ViewHolder viewHolder, final Message message, boolean darkBackground, int type) {
529 viewHolder.download_button.setVisibility(View.GONE);
530 viewHolder.image.setVisibility(View.GONE);
531 viewHolder.audioPlayer.setVisibility(View.GONE);
532 viewHolder.messageBody.setVisibility(View.GONE);
533
534 if (darkBackground) {
535 viewHolder.messageBody.setTextAppearance(getContext(), R.style.TextAppearance_Conversations_Body1_OnDark);
536 } else {
537 viewHolder.messageBody.setTextAppearance(getContext(), R.style.TextAppearance_Conversations_Body1);
538 }
539 viewHolder.messageBody.setHighlightColor(ContextCompat.getColor(activity, darkBackground
540 ? (type == SENT || !mUseGreenBackground ? R.color.black26 : R.color.grey800) : R.color.grey500));
541 viewHolder.messageBody.setTypeface(null, Typeface.NORMAL);
542
543 if (message.getBody() != null && !message.getBody().equals("")) {
544 viewHolder.messageBody.setTextIsSelectable(true);
545 viewHolder.messageBody.setVisibility(View.VISIBLE);
546 final String nick = UIHelper.getMessageDisplayName(message);
547 SpannableStringBuilder body = getSpannableBody(message);
548 boolean hasMeCommand = message.hasMeCommand();
549 if (hasMeCommand) {
550 body = body.replace(0, Message.ME_COMMAND.length(), nick + " ");
551 }
552 if (body.length() > Config.MAX_DISPLAY_MESSAGE_CHARS) {
553 body = new SpannableStringBuilder(body, 0, Config.MAX_DISPLAY_MESSAGE_CHARS);
554 body.append("\u2026");
555 }
556 Message.MergeSeparator[] mergeSeparators = body.getSpans(0, body.length(), Message.MergeSeparator.class);
557 for (Message.MergeSeparator mergeSeparator : mergeSeparators) {
558 int start = body.getSpanStart(mergeSeparator);
559 int end = body.getSpanEnd(mergeSeparator);
560 body.setSpan(new DividerSpan(true), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
561 }
562 for (final android.text.style.QuoteSpan quote : body.getSpans(0, body.length(), android.text.style.QuoteSpan.class)) {
563 int start = body.getSpanStart(quote);
564 int end = body.getSpanEnd(quote);
565 body.removeSpan(quote);
566 applyQuoteSpan(body, start, end, darkBackground);
567 }
568 boolean startsWithQuote = handleTextQuotes(body, darkBackground);
569 if (!message.isPrivateMessage()) {
570 if (hasMeCommand) {
571 body.setSpan(new StyleSpan(Typeface.BOLD_ITALIC), 0, nick.length(),
572 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
573 }
574 } else {
575 String privateMarker;
576 if (message.getStatus() <= Message.STATUS_RECEIVED) {
577 privateMarker = activity.getString(R.string.private_message);
578 } else {
579 Jid cp = message.getCounterpart();
580 privateMarker = activity.getString(R.string.private_message_to, Strings.nullToEmpty(cp == null ? null : cp.getResource()));
581 }
582 body.insert(0, privateMarker);
583 int privateMarkerIndex = privateMarker.length();
584 if (startsWithQuote) {
585 body.insert(privateMarkerIndex, "\n\n");
586 body.setSpan(new DividerSpan(false), privateMarkerIndex, privateMarkerIndex + 2,
587 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
588 } else {
589 body.insert(privateMarkerIndex, " ");
590 }
591 body.setSpan(new ForegroundColorSpan(getMessageTextColor(darkBackground, false)), 0, privateMarkerIndex, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
592 body.setSpan(new StyleSpan(Typeface.BOLD), 0, privateMarkerIndex, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
593 if (hasMeCommand) {
594 body.setSpan(new StyleSpan(Typeface.BOLD_ITALIC), privateMarkerIndex + 1,
595 privateMarkerIndex + 1 + nick.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
596 }
597 }
598 if (message.getConversation().getMode() == Conversation.MODE_MULTI && message.getStatus() == Message.STATUS_RECEIVED) {
599 if (message.getConversation() instanceof Conversation) {
600 final Conversation conversation = (Conversation) message.getConversation();
601 Pattern pattern = NotificationService.generateNickHighlightPattern(conversation.getMucOptions().getActualNick());
602 Matcher matcher = pattern.matcher(body);
603 while (matcher.find()) {
604 body.setSpan(new StyleSpan(Typeface.BOLD), matcher.start(), matcher.end(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
605 }
606
607 pattern = NotificationService.generateNickHighlightPattern(conversation.getMucOptions().getActualName());
608 matcher = pattern.matcher(body);
609 while (matcher.find()) {
610 body.setSpan(new StyleSpan(Typeface.BOLD), matcher.start(), matcher.end(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
611 }
612 }
613 }
614 Matcher matcher = Emoticons.getEmojiPattern(body).matcher(body);
615 while (matcher.find()) {
616 if (matcher.start() < matcher.end()) {
617 body.setSpan(new RelativeSizeSpan(1.2f), matcher.start(), matcher.end(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
618 }
619 }
620
621 StylingHelper.format(body, viewHolder.messageBody.getCurrentTextColor());
622 if (highlightedTerm != null) {
623 StylingHelper.highlight(activity, body, highlightedTerm, StylingHelper.isDarkText(viewHolder.messageBody));
624 }
625 MyLinkify.addLinks(body, message.getConversation().getAccount(), message.getConversation().getJid());
626 viewHolder.messageBody.setAutoLinkMask(0);
627 viewHolder.messageBody.setText(body);
628 BetterLinkMovementMethod method = new BetterLinkMovementMethod() {
629 @Override
630 protected void dispatchUrlLongClick(TextView tv, ClickableSpan span) {
631 if (span instanceof URLSpan || mOnInlineImageLongClickedListener == null) {
632 tv.dispatchTouchEvent(MotionEvent.obtain(0, 0, MotionEvent.ACTION_CANCEL, 0f, 0f, 0));
633 super.dispatchUrlLongClick(tv, span);
634 return;
635 }
636
637 Spannable body = (Spannable) tv.getText();
638 ImageSpan[] imageSpans = body.getSpans(body.getSpanStart(span), body.getSpanEnd(span), ImageSpan.class);
639 if (imageSpans.length > 0) {
640 Uri uri = Uri.parse(imageSpans[0].getSource());
641 Cid cid = BobTransfer.cid(uri);
642 if (cid == null) return;
643 if (mOnInlineImageLongClickedListener.onInlineImageLongClicked(cid)) {
644 tv.dispatchTouchEvent(MotionEvent.obtain(0, 0, MotionEvent.ACTION_CANCEL, 0f, 0f, 0));
645 }
646 }
647 }
648 };
649 method.setOnLinkLongClickListener((tv, url) -> {
650 tv.dispatchTouchEvent(MotionEvent.obtain(0, 0, MotionEvent.ACTION_CANCEL, 0f, 0f, 0));
651 ShareUtil.copyLinkToClipboard(activity, url);
652 return true;
653 });
654 viewHolder.messageBody.setMovementMethod(method);
655 } else {
656 viewHolder.messageBody.setText("");
657 viewHolder.messageBody.setTextIsSelectable(false);
658 toggleWhisperInfo(viewHolder, message, darkBackground);
659 }
660 }
661
662 private void displayDownloadableMessage(ViewHolder viewHolder, final Message message, String text, final boolean darkBackground, final int type) {
663 displayTextMessage(viewHolder, message, darkBackground, type);
664 viewHolder.image.setVisibility(View.GONE);
665 List<Element> thumbs = message.getFileParams() != null ? message.getFileParams().getThumbnails() : null;
666 if (thumbs != null && !thumbs.isEmpty()) {
667 for (Element thumb : thumbs) {
668 Uri uri = Uri.parse(thumb.getAttribute("uri"));
669 if (uri.getScheme().equals("data")) {
670 String[] parts = uri.getSchemeSpecificPart().split(",", 2);
671 parts = parts[0].split(";");
672 if (!parts[0].equals("image/blurhash") && !parts[0].equals("image/thumbhash") && !parts[0].equals("image/jpeg") && !parts[0].equals("image/png") && !parts[0].equals("image/webp") && !parts[0].equals("image/gif")) continue;
673 } else if (uri.getScheme().equals("cid")) {
674 Cid cid = BobTransfer.cid(uri);
675 if (cid == null) continue;
676 DownloadableFile f = activity.xmppConnectionService.getFileForCid(cid);
677 if (f == null || !f.canRead()) {
678 if (!message.trusted() && !message.getConversation().canInferPresence()) continue;
679
680 try {
681 new BobTransfer(BobTransfer.uri(cid), message.getConversation().getAccount(), message.getCounterpart(), activity.xmppConnectionService).start();
682 } catch (final NoSuchAlgorithmException | URISyntaxException e) { }
683 continue;
684 }
685 } else {
686 continue;
687 }
688
689 int width = message.getFileParams().width;
690 if (width < 1 && thumb.getAttribute("width") != null) width = Integer.parseInt(thumb.getAttribute("width"));
691 if (width < 1) width = 1920;
692
693 int height = message.getFileParams().height;
694 if (height < 1 && thumb.getAttribute("height") != null) height = Integer.parseInt(thumb.getAttribute("height"));
695 if (height < 1) height = 1080;
696
697 viewHolder.image.setVisibility(View.VISIBLE);
698 imagePreviewLayout(width, height, viewHolder.image, message.getBody() != null && message.getBody().length() > 0);
699 activity.loadBitmap(message, viewHolder.image);
700 viewHolder.image.setOnClickListener(v -> ConversationFragment.downloadFile(activity, message));
701
702 break;
703 }
704 }
705 viewHolder.audioPlayer.setVisibility(View.GONE);
706 viewHolder.download_button.setVisibility(View.VISIBLE);
707 viewHolder.download_button.setText(text);
708 viewHolder.download_button.setOnClickListener(v -> ConversationFragment.downloadFile(activity, message));
709 }
710
711 private void displayWebxdcMessage(ViewHolder viewHolder, final Message message, final boolean darkBackground, final int type) {
712 Cid webxdcCid = message.getFileParams().getCids().get(0);
713 WebxdcPage webxdc = new WebxdcPage(activity, webxdcCid, message, activity.xmppConnectionService);
714 displayTextMessage(viewHolder, message, darkBackground, type);
715 viewHolder.image.setVisibility(View.GONE);
716 viewHolder.audioPlayer.setVisibility(View.GONE);
717 viewHolder.download_button.setVisibility(View.VISIBLE);
718 viewHolder.download_button.setText("Open " + webxdc.getName());
719 viewHolder.download_button.setOnClickListener(v -> {
720 Conversation conversation = (Conversation) message.getConversation();
721 if (!conversation.switchToSession("webxdc\0" + message.getUuid())) {
722 conversation.startWebxdc(webxdc);
723 }
724 });
725
726 final WebxdcUpdate lastUpdate;
727 synchronized(lastWebxdcUpdate) { lastUpdate = lastWebxdcUpdate.get(message.getUuid()); }
728 if (lastUpdate == null) {
729 new Thread(() -> {
730 final WebxdcUpdate update = activity.xmppConnectionService.findLastWebxdcUpdate(message);
731 if (update != null) {
732 synchronized(lastWebxdcUpdate) { lastWebxdcUpdate.put(message.getUuid(), update); }
733 activity.xmppConnectionService.updateConversationUi();
734 }
735 }).start();
736 } else {
737 if (lastUpdate != null && (lastUpdate.getSummary() != null || lastUpdate.getDocument() != null)) {
738 viewHolder.messageBody.setVisibility(View.VISIBLE);
739 viewHolder.messageBody.setText(
740 (lastUpdate.getDocument() == null ? "" : lastUpdate.getDocument() + "\n") +
741 (lastUpdate.getSummary() == null ? "" : lastUpdate.getSummary())
742 );
743 }
744 }
745
746 final LruCache<String, Drawable> cache = activity.xmppConnectionService.getDrawableCache();
747 final Drawable d = cache.get("webxdc:icon:" + webxdcCid);
748 if (d == null) {
749 new Thread(() -> {
750 Drawable icon = webxdc.getIcon();
751 if (icon != null) {
752 cache.put("webxdc:icon:" + webxdcCid, icon);
753 activity.xmppConnectionService.updateConversationUi();
754 }
755 }).start();
756 } else {
757 viewHolder.image.setVisibility(View.VISIBLE);
758 viewHolder.image.setImageDrawable(d);
759 }
760 }
761
762 private void displayOpenableMessage(ViewHolder viewHolder, final Message message, final boolean darkBackground, final int type) {
763 displayTextMessage(viewHolder, message, darkBackground, type);
764 viewHolder.image.setVisibility(View.GONE);
765 viewHolder.audioPlayer.setVisibility(View.GONE);
766 viewHolder.download_button.setVisibility(View.VISIBLE);
767 viewHolder.download_button.setText(activity.getString(R.string.open_x_file, UIHelper.getFileDescriptionString(activity, message)));
768 viewHolder.download_button.setOnClickListener(v -> openDownloadable(message));
769 }
770
771 private void displayLocationMessage(ViewHolder viewHolder, final Message message, final boolean darkBackground, final int type) {
772 displayTextMessage(viewHolder, message, darkBackground, type);
773 viewHolder.image.setVisibility(View.GONE);
774 viewHolder.audioPlayer.setVisibility(View.GONE);
775 viewHolder.download_button.setVisibility(View.VISIBLE);
776 viewHolder.download_button.setText(R.string.show_location);
777 viewHolder.download_button.setOnClickListener(v -> showLocation(message));
778 }
779
780 private void displayAudioMessage(ViewHolder viewHolder, Message message, boolean darkBackground, final int type) {
781 displayTextMessage(viewHolder, message, darkBackground, type);
782 viewHolder.image.setVisibility(View.GONE);
783 viewHolder.download_button.setVisibility(View.GONE);
784 final RelativeLayout audioPlayer = viewHolder.audioPlayer;
785 audioPlayer.setVisibility(View.VISIBLE);
786 AudioPlayer.ViewHolder.get(audioPlayer).setDarkBackground(darkBackground);
787 this.audioPlayer.init(audioPlayer, message);
788 }
789
790 private void displayMediaPreviewMessage(ViewHolder viewHolder, final Message message, final boolean darkBackground, final int type) {
791 displayTextMessage(viewHolder, message, darkBackground, type);
792 viewHolder.download_button.setVisibility(View.GONE);
793 viewHolder.audioPlayer.setVisibility(View.GONE);
794 viewHolder.image.setVisibility(View.VISIBLE);
795 final FileParams params = message.getFileParams();
796 imagePreviewLayout(params.width, params.height, viewHolder.image, message.getBody() != null && message.getBody().length() > 0);
797 activity.loadBitmap(message, viewHolder.image);
798 viewHolder.image.setOnClickListener(v -> openDownloadable(message));
799 }
800
801 private void imagePreviewLayout(int w, int h, ImageView image, boolean topMargin) {
802 final float target = activity.getResources().getDimension(R.dimen.image_preview_width);
803 final int scaledW;
804 final int scaledH;
805 if (Math.max(h, w) * metrics.density <= target) {
806 scaledW = (int) (w * metrics.density);
807 scaledH = (int) (h * metrics.density);
808 } else if (Math.max(h, w) <= target) {
809 scaledW = w;
810 scaledH = h;
811 } else if (w <= h) {
812 scaledW = (int) (w / ((double) h / target));
813 scaledH = (int) target;
814 } else {
815 scaledW = (int) target;
816 scaledH = (int) (h / ((double) w / target));
817 }
818 final LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(scaledW, scaledH);
819 layoutParams.setMargins(0, topMargin ? (int) (metrics.density * 4) : 0, 0, (int) (metrics.density * 4));
820 layoutParams.gravity = Gravity.CENTER;
821 image.setLayoutParams(layoutParams);
822 }
823
824 private void toggleWhisperInfo(ViewHolder viewHolder, final Message message, final boolean darkBackground) {
825 if (message.isPrivateMessage()) {
826 final String privateMarker;
827 if (message.getStatus() <= Message.STATUS_RECEIVED) {
828 privateMarker = activity.getString(R.string.private_message);
829 } else {
830 Jid cp = message.getCounterpart();
831 privateMarker = activity.getString(R.string.private_message_to, Strings.nullToEmpty(cp == null ? null : cp.getResource()));
832 }
833 final SpannableString body = new SpannableString(privateMarker);
834 body.setSpan(new ForegroundColorSpan(getMessageTextColor(darkBackground, false)), 0, privateMarker.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
835 body.setSpan(new StyleSpan(Typeface.BOLD), 0, privateMarker.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
836 viewHolder.messageBody.setText(body);
837 viewHolder.messageBody.setVisibility(View.VISIBLE);
838 } else {
839 viewHolder.messageBody.setVisibility(View.GONE);
840 }
841 }
842
843 private void loadMoreMessages(Conversation conversation) {
844 conversation.setLastClearHistory(0, null);
845 activity.xmppConnectionService.updateConversation(conversation);
846 conversation.setHasMessagesLeftOnServer(true);
847 conversation.setFirstMamReference(null);
848 long timestamp = conversation.getLastMessageTransmitted().getTimestamp();
849 if (timestamp == 0) {
850 timestamp = System.currentTimeMillis();
851 }
852 conversation.messagesLoaded.set(true);
853 MessageArchiveService.Query query = activity.xmppConnectionService.getMessageArchiveService().query(conversation, new MamReference(0), timestamp, false);
854 if (query != null) {
855 Toast.makeText(activity, R.string.fetching_history_from_server, Toast.LENGTH_LONG).show();
856 } else {
857 Toast.makeText(activity, R.string.not_fetching_history_retention_period, Toast.LENGTH_SHORT).show();
858 }
859 }
860
861 @Override
862 public View getView(int position, View view, ViewGroup parent) {
863 final Message message = getItem(position);
864 final boolean omemoEncryption = message.getEncryption() == Message.ENCRYPTION_AXOLOTL;
865 final boolean isInValidSession = message.isValidInSession() && (!omemoEncryption || message.isTrusted());
866 final Conversational conversation = message.getConversation();
867 final Account account = conversation.getAccount();
868 final List<Element> commands = message.getCommands();
869 final int type = getItemViewType(position);
870 ViewHolder viewHolder;
871 if (view == null) {
872 viewHolder = new ViewHolder();
873 switch (type) {
874 case DATE_SEPARATOR:
875 view = activity.getLayoutInflater().inflate(R.layout.message_date_bubble, parent, false);
876 viewHolder.status_message = view.findViewById(R.id.message_body);
877 viewHolder.message_box = view.findViewById(R.id.message_box);
878 viewHolder.indicatorReceived = view.findViewById(R.id.indicator_received);
879 break;
880 case RTP_SESSION:
881 view = activity.getLayoutInflater().inflate(R.layout.message_rtp_session, parent, false);
882 viewHolder.status_message = view.findViewById(R.id.message_body);
883 viewHolder.message_box = view.findViewById(R.id.message_box);
884 viewHolder.indicatorReceived = view.findViewById(R.id.indicator_received);
885 break;
886 case SENT:
887 view = activity.getLayoutInflater().inflate(R.layout.message_sent, parent, false);
888 viewHolder.status_line = view.findViewById(R.id.status_line);
889 viewHolder.message_box_inner = view.findViewById(R.id.message_box_inner);
890 viewHolder.message_box = view.findViewById(R.id.message_box);
891 viewHolder.contact_picture = view.findViewById(R.id.message_photo);
892 viewHolder.download_button = view.findViewById(R.id.download_button);
893 viewHolder.indicator = view.findViewById(R.id.security_indicator);
894 viewHolder.edit_indicator = view.findViewById(R.id.edit_indicator);
895 viewHolder.image = view.findViewById(R.id.message_image);
896 viewHolder.messageBody = view.findViewById(R.id.message_body);
897 viewHolder.time = view.findViewById(R.id.message_time);
898 viewHolder.subject = view.findViewById(R.id.message_subject);
899 viewHolder.indicatorReceived = view.findViewById(R.id.indicator_received);
900 viewHolder.audioPlayer = view.findViewById(R.id.audio_player);
901 viewHolder.thread_identicon = view.findViewById(R.id.thread_identicon);
902 break;
903 case RECEIVED:
904 view = activity.getLayoutInflater().inflate(R.layout.message_received, parent, false);
905 viewHolder.status_line = view.findViewById(R.id.status_line);
906 viewHolder.message_box_inner = view.findViewById(R.id.message_box_inner);
907 viewHolder.message_box = view.findViewById(R.id.message_box);
908 viewHolder.contact_picture = view.findViewById(R.id.message_photo);
909 viewHolder.download_button = view.findViewById(R.id.download_button);
910 viewHolder.indicator = view.findViewById(R.id.security_indicator);
911 viewHolder.edit_indicator = view.findViewById(R.id.edit_indicator);
912 viewHolder.image = view.findViewById(R.id.message_image);
913 viewHolder.messageBody = view.findViewById(R.id.message_body);
914 viewHolder.time = view.findViewById(R.id.message_time);
915 viewHolder.subject = view.findViewById(R.id.message_subject);
916 viewHolder.indicatorReceived = view.findViewById(R.id.indicator_received);
917 viewHolder.encryption = view.findViewById(R.id.message_encryption);
918 viewHolder.audioPlayer = view.findViewById(R.id.audio_player);
919 viewHolder.commands_list = view.findViewById(R.id.commands_list);
920 viewHolder.thread_identicon = view.findViewById(R.id.thread_identicon);
921 break;
922 case STATUS:
923 view = activity.getLayoutInflater().inflate(R.layout.message_status, parent, false);
924 viewHolder.contact_picture = view.findViewById(R.id.message_photo);
925 viewHolder.status_message = view.findViewById(R.id.status_message);
926 viewHolder.load_more_messages = view.findViewById(R.id.load_more_messages);
927 break;
928 default:
929 throw new AssertionError("Unknown view type");
930 }
931 view.setTag(viewHolder);
932 } else {
933 viewHolder = (ViewHolder) view.getTag();
934 if (viewHolder == null) {
935 return view;
936 }
937 }
938
939 if (viewHolder.messageBody != null) {
940 viewHolder.messageBody.setCustomSelectionActionModeCallback(new MessageTextActionModeCallback(this, viewHolder.messageBody));
941 }
942
943 if (viewHolder.thread_identicon != null) {
944 viewHolder.thread_identicon.setVisibility(View.GONE);
945 final Element thread = message.getThread();
946 if (thread != null) {
947 final String threadId = thread.getContent();
948 if (threadId != null) {
949 viewHolder.thread_identicon.setVisibility(View.VISIBLE);
950 viewHolder.thread_identicon.setColor(UIHelper.getColorForName(threadId));
951 viewHolder.thread_identicon.setHash(UIHelper.identiconHash(threadId));
952 }
953 }
954 }
955
956 boolean darkBackground = (type == RECEIVED && mUseGreenBackground) || activity.isDarkTheme();
957
958 if (type == DATE_SEPARATOR) {
959 if (UIHelper.today(message.getTimeSent())) {
960 viewHolder.status_message.setText(R.string.today);
961 } else if (UIHelper.yesterday(message.getTimeSent())) {
962 viewHolder.status_message.setText(R.string.yesterday);
963 } else {
964 viewHolder.status_message.setText(DateUtils.formatDateTime(activity, message.getTimeSent(), DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR));
965 }
966 viewHolder.message_box.setBackgroundResource(activity.isDarkTheme() ? R.drawable.date_bubble_grey : R.drawable.date_bubble_white);
967 return view;
968 } else if (type == RTP_SESSION) {
969 final boolean isDarkTheme = activity.isDarkTheme();
970 final boolean received = message.getStatus() <= Message.STATUS_RECEIVED;
971 final RtpSessionStatus rtpSessionStatus = RtpSessionStatus.of(message.getBody());
972 final long duration = rtpSessionStatus.duration;
973 final String callTime = UIHelper.readableTimeDifferenceFull(activity, message.getTimeSent());
974 if (received) {
975 if (duration > 0) {
976 viewHolder.status_message.setText(activity.getString(R.string.incoming_call_duration_timestamp, TimeFrameUtils.resolve(activity, duration), callTime));
977 } else if (rtpSessionStatus.successful) {
978 viewHolder.status_message.setText(activity.getString(R.string.incoming_call_timestamp, callTime));
979 } else {
980 viewHolder.status_message.setText(activity.getString(R.string.missed_call_timestamp, callTime));
981 }
982 } else {
983 if (duration > 0) {
984 viewHolder.status_message.setText(activity.getString(R.string.outgoing_call_duration_timestamp, TimeFrameUtils.resolve(activity, duration), callTime));
985 } else {
986 viewHolder.status_message.setText(activity.getString(R.string.outgoing_call_timestamp, callTime));
987 }
988 }
989 viewHolder.indicatorReceived.setImageResource(RtpSessionStatus.getDrawable(received, rtpSessionStatus.successful, isDarkTheme));
990 viewHolder.indicatorReceived.setAlpha(isDarkTheme ? 0.7f : 0.57f);
991 viewHolder.message_box.setBackgroundResource(isDarkTheme ? R.drawable.date_bubble_grey : R.drawable.date_bubble_white);
992 return view;
993 } else if (type == STATUS) {
994 if ("LOAD_MORE".equals(message.getBody())) {
995 viewHolder.status_message.setVisibility(View.GONE);
996 viewHolder.contact_picture.setVisibility(View.GONE);
997 viewHolder.load_more_messages.setVisibility(View.VISIBLE);
998 viewHolder.load_more_messages.setOnClickListener(v -> loadMoreMessages((Conversation) message.getConversation()));
999 } else {
1000 viewHolder.status_message.setVisibility(View.VISIBLE);
1001 viewHolder.load_more_messages.setVisibility(View.GONE);
1002 viewHolder.status_message.setText(message.getBody());
1003 boolean showAvatar;
1004 if (conversation.getMode() == Conversation.MODE_SINGLE) {
1005 showAvatar = true;
1006 AvatarWorkerTask.loadAvatar(message, viewHolder.contact_picture, R.dimen.avatar_on_status_message);
1007 } else if (message.getCounterpart() != null || message.getTrueCounterpart() != null || (message.getCounterparts() != null && message.getCounterparts().size() > 0)) {
1008 showAvatar = true;
1009 AvatarWorkerTask.loadAvatar(message, viewHolder.contact_picture, R.dimen.avatar_on_status_message);
1010 } else {
1011 showAvatar = false;
1012 }
1013 if (showAvatar) {
1014 viewHolder.contact_picture.setAlpha(0.5f);
1015 viewHolder.contact_picture.setVisibility(View.VISIBLE);
1016 } else {
1017 viewHolder.contact_picture.setVisibility(View.GONE);
1018 }
1019 }
1020 return view;
1021 } else {
1022 AvatarWorkerTask.loadAvatar(message, viewHolder.contact_picture, R.dimen.avatar);
1023 }
1024
1025 resetClickListener(viewHolder.message_box, viewHolder.messageBody);
1026
1027 viewHolder.message_box.setOnClickListener(v -> {
1028 if (MessageAdapter.this.mOnMessageBoxClickedListener != null) {
1029 MessageAdapter.this.mOnMessageBoxClickedListener
1030 .onContactPictureClicked(message);
1031 }
1032 });
1033 SwipeDetector swipeDetector = new SwipeDetector((action) -> {
1034 if (action == SwipeDetector.Action.LR && MessageAdapter.this.mOnMessageBoxSwipedListener != null) {
1035 MessageAdapter.this.mOnMessageBoxSwipedListener.onContactPictureClicked(message);
1036 }
1037 });
1038 viewHolder.message_box.setOnTouchListener(swipeDetector);
1039 viewHolder.image.setOnTouchListener(swipeDetector);
1040 viewHolder.time.setOnTouchListener(swipeDetector);
1041
1042 // Treat touch-up as click so we don't have to touch twice
1043 // (touch twice is because it's waiting to see if you double-touch for text selection)
1044 viewHolder.messageBody.setOnTouchListener((v, event) -> {
1045 if (event.getAction() == MotionEvent.ACTION_UP) {
1046 if (MessageAdapter.this.mOnMessageBoxClickedListener != null) {
1047 MessageAdapter.this.mOnMessageBoxClickedListener
1048 .onContactPictureClicked(message);
1049 }
1050 }
1051
1052 swipeDetector.onTouch(v, event);
1053
1054 return false;
1055 });
1056 viewHolder.messageBody.setOnClickListener(v -> {
1057 if (MessageAdapter.this.mOnMessageBoxClickedListener != null) {
1058 MessageAdapter.this.mOnMessageBoxClickedListener
1059 .onContactPictureClicked(message);
1060 }
1061 });
1062 viewHolder.contact_picture.setOnClickListener(v -> {
1063 if (MessageAdapter.this.mOnContactPictureClickedListener != null) {
1064 MessageAdapter.this.mOnContactPictureClickedListener
1065 .onContactPictureClicked(message);
1066 }
1067
1068 });
1069 viewHolder.contact_picture.setOnLongClickListener(v -> {
1070 if (MessageAdapter.this.mOnContactPictureLongClickedListener != null) {
1071 MessageAdapter.this.mOnContactPictureLongClickedListener
1072 .onContactPictureLongClicked(v, message);
1073 return true;
1074 } else {
1075 return false;
1076 }
1077 });
1078 viewHolder.messageBody.setAccessibilityDelegate(null);
1079
1080 final String theme = activity == null ? null : PreferenceManager.getDefaultSharedPreferences(activity).getString(SettingsActivity.THEME, activity.getResources().getString(R.string.theme));
1081 boolean footerWrap = type == SENT && "oledblack".equals(theme);
1082
1083 final Transferable transferable = message.getTransferable();
1084 final boolean unInitiatedButKnownSize = MessageUtils.unInitiatedButKnownSize(message);
1085
1086 final boolean muted = message.getStatus() == Message.STATUS_RECEIVED && conversation.getMode() == Conversation.MODE_MULTI && activity.xmppConnectionService.isMucUserMuted(new MucOptions.User(null, conversation.getJid(), message.getOccupantId(), null, null));
1087 if (muted) {
1088 // Muted MUC participant
1089 displayInfoMessage(viewHolder, "Muted", darkBackground);
1090 } else if (unInitiatedButKnownSize || message.isDeleted() || (transferable != null && transferable.getStatus() != Transferable.STATUS_UPLOADING)) {
1091 if (unInitiatedButKnownSize || (message.isDeleted() && message.getModerated() == null) || transferable != null && transferable.getStatus() == Transferable.STATUS_OFFER) {
1092 displayDownloadableMessage(viewHolder, message, activity.getString(R.string.download_x_file, UIHelper.getFileDescriptionString(activity, message)), darkBackground, type);
1093 } else if (transferable != null && transferable.getStatus() == Transferable.STATUS_OFFER_CHECK_FILESIZE) {
1094 displayDownloadableMessage(viewHolder, message, activity.getString(R.string.check_x_filesize, UIHelper.getFileDescriptionString(activity, message)), darkBackground, type);
1095 } else {
1096 displayInfoMessage(viewHolder, UIHelper.getMessagePreview(activity.xmppConnectionService, message).first, darkBackground, message, type);
1097 }
1098 } else if (message.isFileOrImage() && message.getEncryption() != Message.ENCRYPTION_PGP && message.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED) {
1099 if (message.getFileParams().width > 0 && message.getFileParams().height > 0) {
1100 displayMediaPreviewMessage(viewHolder, message, darkBackground, type);
1101 if (!"oledblack".equals(theme) && viewHolder.image.getLayoutParams().width > metrics.density * 110) {
1102 footerWrap = true;
1103 }
1104 } else if (message.getFileParams().runtime > 0) {
1105 displayAudioMessage(viewHolder, message, darkBackground, type);
1106 } else if ("application/xdc+zip".equals(message.getFileParams().getMediaType()) && message.getConversation() instanceof Conversation && message.getThread() != null && !message.getFileParams().getCids().isEmpty()) {
1107 displayWebxdcMessage(viewHolder, message, darkBackground, type);
1108 } else {
1109 displayOpenableMessage(viewHolder, message, darkBackground, type);
1110 }
1111 } else if (message.getEncryption() == Message.ENCRYPTION_PGP) {
1112 if (account.isPgpDecryptionServiceConnected()) {
1113 if (conversation instanceof Conversation && !account.hasPendingPgpIntent((Conversation) conversation)) {
1114 displayInfoMessage(viewHolder, activity.getString(R.string.message_decrypting), darkBackground);
1115 } else {
1116 displayInfoMessage(viewHolder, activity.getString(R.string.pgp_message), darkBackground);
1117 }
1118 } else {
1119 displayInfoMessage(viewHolder, activity.getString(R.string.install_openkeychain), darkBackground);
1120 viewHolder.message_box.setOnClickListener(this::promptOpenKeychainInstall);
1121 viewHolder.messageBody.setOnClickListener(this::promptOpenKeychainInstall);
1122 }
1123 } else if (message.getEncryption() == Message.ENCRYPTION_DECRYPTION_FAILED) {
1124 displayInfoMessage(viewHolder, activity.getString(R.string.decryption_failed), darkBackground);
1125 } else if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE) {
1126 displayInfoMessage(viewHolder, activity.getString(R.string.not_encrypted_for_this_device), darkBackground);
1127 } else if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL_FAILED) {
1128 displayInfoMessage(viewHolder, activity.getString(R.string.omemo_decryption_failed), darkBackground);
1129 } else {
1130 if (message.isGeoUri()) {
1131 displayLocationMessage(viewHolder, message, darkBackground, type);
1132 } else if (message.treatAsDownloadable()) {
1133 try {
1134 final URI uri = message.getOob();
1135 displayDownloadableMessage(viewHolder,
1136 message,
1137 activity.getString(R.string.check_x_filesize_on_host,
1138 UIHelper.getFileDescriptionString(activity, message),
1139 uri.getHost()),
1140 darkBackground, type);
1141 } catch (Exception e) {
1142 displayDownloadableMessage(viewHolder,
1143 message,
1144 activity.getString(R.string.check_x_filesize,
1145 UIHelper.getFileDescriptionString(activity, message)),
1146 darkBackground, type);
1147 }
1148 } else if (message.bodyIsOnlyEmojis() && message.getType() != Message.TYPE_PRIVATE) {
1149 displayEmojiMessage(viewHolder, getSpannableBody(message), darkBackground);
1150 } else {
1151 displayTextMessage(viewHolder, message, darkBackground, type);
1152 }
1153 }
1154
1155 viewHolder.message_box_inner.setMinimumWidth(footerWrap ? (int) (110 * metrics.density) : 0);
1156 LinearLayout.LayoutParams statusParams = (LinearLayout.LayoutParams) viewHolder.status_line.getLayoutParams();
1157 statusParams.width = footerWrap ? ViewGroup.LayoutParams.MATCH_PARENT : ViewGroup.LayoutParams.WRAP_CONTENT;
1158 viewHolder.status_line.setLayoutParams(statusParams);
1159
1160 if (type == RECEIVED) {
1161 if (!muted && commands != null && conversation instanceof Conversation) {
1162 CommandButtonAdapter adapter = new CommandButtonAdapter(activity);
1163 adapter.addAll(commands);
1164 viewHolder.commands_list.setAdapter(adapter);
1165 viewHolder.commands_list.setVisibility(View.VISIBLE);
1166 viewHolder.commands_list.setOnItemClickListener((p, v, pos, id) -> {
1167 final Element command = adapter.getItem(pos);
1168 activity.startCommand(conversation.getAccount(), command.getAttributeAsJid("jid"), command.getAttribute("node"));
1169 });
1170 } else {
1171 // It's unclear if we can set this to null...
1172 ListAdapter adapter = viewHolder.commands_list.getAdapter();
1173 if (adapter instanceof ArrayAdapter) {
1174 ((ArrayAdapter<?>) adapter).clear();
1175 }
1176 viewHolder.commands_list.setVisibility(View.GONE);
1177 viewHolder.commands_list.setOnItemClickListener(null);
1178 }
1179
1180 if (!mUseGreenBackground) {
1181 viewHolder.message_box.getBackground().setColorFilter(
1182 StyledAttributes.getColor(activity, mUseGreenBackground ? R.attr.message_bubble_received_bg : R.attr.color_background_primary),
1183 PorterDuff.Mode.SRC_ATOP
1184 );
1185 }
1186 if (isInValidSession) {
1187 viewHolder.encryption.setVisibility(View.GONE);
1188 } else {
1189 viewHolder.encryption.setVisibility(View.VISIBLE);
1190 if (omemoEncryption && !message.isTrusted()) {
1191 viewHolder.encryption.setText(R.string.not_trusted);
1192 } else {
1193 viewHolder.encryption.setText(CryptoHelper.encryptionTypeToText(message.getEncryption()));
1194 }
1195 }
1196 }
1197
1198 if (type == RECEIVED || type == SENT) {
1199 String subject = message.getSubject();
1200 if (subject == null && message.getThread() != null) {
1201 subject = ((Conversation) message.getConversation()).getThread(message.getThread().getContent()).getSubject();
1202 }
1203 if (muted || subject == null) {
1204 viewHolder.subject.setVisibility(View.GONE);
1205 } else {
1206 viewHolder.subject.setVisibility(View.VISIBLE);
1207 viewHolder.subject.setText(subject);
1208 }
1209 }
1210
1211 if (darkBackground) {
1212 if (viewHolder.subject != null) viewHolder.subject.setTextAppearance(getContext(), R.style.TextAppearance_Conversations_Caption_OnDark_Bold);
1213 if (viewHolder.encryption != null) viewHolder.encryption.setTextAppearance(getContext(), R.style.TextAppearance_Conversations_Caption_OnDark_Bold);
1214 } else {
1215 if (viewHolder.subject != null) viewHolder.subject.setTextAppearance(getContext(), R.style.TextAppearance_Conversations_Caption_Bold);
1216 if (viewHolder.encryption != null) viewHolder.encryption.setTextAppearance(getContext(), R.style.TextAppearance_Conversations_Caption_Bold);
1217 }
1218
1219 displayStatus(viewHolder, message, type, darkBackground);
1220
1221 viewHolder.messageBody.setAccessibilityDelegate(new View.AccessibilityDelegate() {
1222 @Override
1223 public void sendAccessibilityEvent(View host, int eventType) {
1224 super.sendAccessibilityEvent(host, eventType);
1225 if (eventType == AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED) {
1226 if (viewHolder.messageBody.hasSelection()) {
1227 selectionUuid = message.getUuid();
1228 } else if (message.getUuid() != null && message.getUuid().equals(selectionUuid)) {
1229 selectionUuid = null;
1230 }
1231 }
1232 }
1233 });
1234
1235 return view;
1236 }
1237
1238 private void promptOpenKeychainInstall(View view) {
1239 activity.showInstallPgpDialog();
1240 }
1241
1242 public FileBackend getFileBackend() {
1243 return activity.xmppConnectionService.getFileBackend();
1244 }
1245
1246 public void stopAudioPlayer() {
1247 audioPlayer.stop();
1248 }
1249
1250 public void unregisterListenerInAudioPlayer() {
1251 audioPlayer.unregisterListener();
1252 }
1253
1254 public void startStopPending() {
1255 audioPlayer.startStopPending();
1256 }
1257
1258 public void openDownloadable(Message message) {
1259 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU && ContextCompat.checkSelfPermission(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
1260 ConversationFragment.registerPendingMessage(activity, message);
1261 ActivityCompat.requestPermissions(activity, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, ConversationsActivity.REQUEST_OPEN_MESSAGE);
1262 return;
1263 }
1264 final DownloadableFile file = activity.xmppConnectionService.getFileBackend().getFile(message);
1265 ViewUtil.view(activity, file);
1266 }
1267
1268 private void showLocation(Message message) {
1269 for (Intent intent : GeoHelper.createGeoIntentsFromMessage(activity, message)) {
1270 if (intent.resolveActivity(getContext().getPackageManager()) != null) {
1271 getContext().startActivity(intent);
1272 return;
1273 }
1274 }
1275 Toast.makeText(activity, R.string.no_application_found_to_display_location, Toast.LENGTH_SHORT).show();
1276 }
1277
1278 public void updatePreferences() {
1279 SharedPreferences p = PreferenceManager.getDefaultSharedPreferences(activity);
1280 this.mUseGreenBackground = p.getBoolean("use_green_background", activity.getResources().getBoolean(R.bool.use_green_background));
1281 }
1282
1283
1284 public void setHighlightedTerm(List<String> terms) {
1285 this.highlightedTerm = terms == null ? null : StylingHelper.filterHighlightedWords(terms);
1286 }
1287
1288 public interface OnContactPictureClicked {
1289 void onContactPictureClicked(Message message);
1290 }
1291
1292 public interface OnContactPictureLongClicked {
1293 void onContactPictureLongClicked(View v, Message message);
1294 }
1295
1296 public interface OnInlineImageLongClicked {
1297 boolean onInlineImageLongClicked(Cid cid);
1298 }
1299
1300 private static class ViewHolder {
1301
1302 public Button load_more_messages;
1303 public ImageView edit_indicator;
1304 public RelativeLayout audioPlayer;
1305 protected View status_line;
1306 protected LinearLayout message_box;
1307 protected View message_box_inner;
1308 protected Button download_button;
1309 protected ImageView image;
1310 protected ImageView indicator;
1311 protected ImageView indicatorReceived;
1312 protected TextView time;
1313 protected TextView subject;
1314 protected TextView messageBody;
1315 protected ImageView contact_picture;
1316 protected TextView status_message;
1317 protected TextView encryption;
1318 protected ListView commands_list;
1319 protected GithubIdenticonView thread_identicon;
1320 }
1321
1322 class ThumbnailTask extends AsyncTask<DownloadableFile, Void, Drawable[]> {
1323 @Override
1324 protected Drawable[] doInBackground(DownloadableFile... params) {
1325 if (isCancelled()) return null;
1326
1327 Drawable[] d = new Drawable[params.length];
1328 for (int i = 0; i < params.length; i++) {
1329 try {
1330 d[i] = activity.xmppConnectionService.getFileBackend().getThumbnail(params[i], activity.getResources(), (int) (metrics.density * 288), false);
1331 } catch (final IOException e) {
1332 d[i] = null;
1333 }
1334 }
1335
1336 return d;
1337 }
1338
1339 @Override
1340 protected void onPostExecute(final Drawable[] d) {
1341 if (isCancelled()) return;
1342 activity.xmppConnectionService.updateConversationUi();
1343 }
1344 }
1345}