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