1package eu.siacs.conversations.entities;
2
3import android.content.ContentValues;
4import android.database.Cursor;
5import android.graphics.drawable.Drawable;
6import android.graphics.Color;
7import android.os.Build;
8import android.text.Html;
9import android.text.SpannableStringBuilder;
10import android.text.Spanned;
11import android.text.style.ImageSpan;
12import android.text.style.ClickableSpan;
13import android.util.Base64;
14import android.util.Log;
15import android.util.Pair;
16import android.view.View;
17
18import com.cheogram.android.BobTransfer;
19import com.cheogram.android.GetThumbnailForCid;
20
21import com.google.common.io.ByteSource;
22import com.google.common.base.Strings;
23import com.google.common.collect.ImmutableSet;
24import com.google.common.primitives.Longs;
25
26import org.json.JSONException;
27
28import java.lang.ref.WeakReference;
29import java.io.IOException;
30import java.net.URI;
31import java.net.URISyntaxException;
32import java.time.Duration;
33import java.security.NoSuchAlgorithmException;
34import java.util.ArrayList;
35import java.util.Arrays;
36import java.util.Iterator;
37import java.util.List;
38import java.util.Set;
39import java.util.stream.Collectors;
40import java.util.concurrent.CopyOnWriteArraySet;
41
42import io.ipfs.cid.Cid;
43
44import eu.siacs.conversations.Config;
45import eu.siacs.conversations.crypto.axolotl.AxolotlService;
46import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
47import eu.siacs.conversations.http.URL;
48import eu.siacs.conversations.services.AvatarService;
49import eu.siacs.conversations.ui.util.PresenceSelector;
50import eu.siacs.conversations.utils.CryptoHelper;
51import eu.siacs.conversations.utils.Emoticons;
52import eu.siacs.conversations.utils.GeoHelper;
53import eu.siacs.conversations.utils.MessageUtils;
54import eu.siacs.conversations.utils.MimeUtils;
55import eu.siacs.conversations.utils.UIHelper;
56import eu.siacs.conversations.xmpp.Jid;
57import eu.siacs.conversations.xml.Element;
58import eu.siacs.conversations.xml.Namespace;
59import eu.siacs.conversations.xml.Tag;
60import eu.siacs.conversations.xml.XmlReader;
61
62public class Message extends AbstractEntity implements AvatarService.Avatarable {
63
64 public static final String TABLENAME = "messages";
65
66 public static final int STATUS_RECEIVED = 0;
67 public static final int STATUS_UNSEND = 1;
68 public static final int STATUS_SEND = 2;
69 public static final int STATUS_SEND_FAILED = 3;
70 public static final int STATUS_WAITING = 5;
71 public static final int STATUS_OFFERED = 6;
72 public static final int STATUS_SEND_RECEIVED = 7;
73 public static final int STATUS_SEND_DISPLAYED = 8;
74
75 public static final int ENCRYPTION_NONE = 0;
76 public static final int ENCRYPTION_PGP = 1;
77 public static final int ENCRYPTION_OTR = 2;
78 public static final int ENCRYPTION_DECRYPTED = 3;
79 public static final int ENCRYPTION_DECRYPTION_FAILED = 4;
80 public static final int ENCRYPTION_AXOLOTL = 5;
81 public static final int ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE = 6;
82 public static final int ENCRYPTION_AXOLOTL_FAILED = 7;
83
84 public static final int TYPE_TEXT = 0;
85 public static final int TYPE_IMAGE = 1;
86 public static final int TYPE_FILE = 2;
87 public static final int TYPE_STATUS = 3;
88 public static final int TYPE_PRIVATE = 4;
89 public static final int TYPE_PRIVATE_FILE = 5;
90 public static final int TYPE_RTP_SESSION = 6;
91
92 public static final String CONVERSATION = "conversationUuid";
93 public static final String COUNTERPART = "counterpart";
94 public static final String TRUE_COUNTERPART = "trueCounterpart";
95 public static final String BODY = "body";
96 public static final String BODY_LANGUAGE = "bodyLanguage";
97 public static final String TIME_SENT = "timeSent";
98 public static final String ENCRYPTION = "encryption";
99 public static final String STATUS = "status";
100 public static final String TYPE = "type";
101 public static final String CARBON = "carbon";
102 public static final String OOB = "oob";
103 public static final String EDITED = "edited";
104 public static final String REMOTE_MSG_ID = "remoteMsgId";
105 public static final String SERVER_MSG_ID = "serverMsgId";
106 public static final String RELATIVE_FILE_PATH = "relativeFilePath";
107 public static final String FINGERPRINT = "axolotl_fingerprint";
108 public static final String READ = "read";
109 public static final String ERROR_MESSAGE = "errorMsg";
110 public static final String READ_BY_MARKERS = "readByMarkers";
111 public static final String MARKABLE = "markable";
112 public static final String DELETED = "deleted";
113 public static final String ME_COMMAND = "/me ";
114
115 public static final String ERROR_MESSAGE_CANCELLED = "eu.siacs.conversations.cancelled";
116
117
118 public boolean markable = false;
119 protected String conversationUuid;
120 protected Jid counterpart;
121 protected Jid trueCounterpart;
122 protected String body;
123 protected String subject;
124 protected String encryptedBody;
125 protected long timeSent;
126 protected long timeReceived;
127 protected int encryption;
128 protected int status;
129 protected int type;
130 protected boolean deleted = false;
131 protected boolean carbon = false;
132 private boolean oob = false;
133 protected List<Element> payloads = new ArrayList<>();
134 protected List<Edit> edits = new ArrayList<>();
135 protected String relativeFilePath;
136 protected boolean read = true;
137 protected String remoteMsgId = null;
138 private String bodyLanguage = null;
139 protected String serverMsgId = null;
140 private final Conversational conversation;
141 protected Transferable transferable = null;
142 private Message mNextMessage = null;
143 private Message mPreviousMessage = null;
144 private String axolotlFingerprint = null;
145 private String errorMessage = null;
146 private Set<ReadByMarker> readByMarkers = new CopyOnWriteArraySet<>();
147
148 private Boolean isGeoUri = null;
149 private Boolean isEmojisOnly = null;
150 private Boolean treatAsDownloadable = null;
151 private FileParams fileParams = null;
152 private List<MucOptions.User> counterparts;
153 private WeakReference<MucOptions.User> user;
154
155 protected Message(Conversational conversation) {
156 this.conversation = conversation;
157 }
158
159 public Message(Conversational conversation, String body, int encryption) {
160 this(conversation, body, encryption, STATUS_UNSEND);
161 }
162
163 public Message(Conversational conversation, String body, int encryption, int status) {
164 this(conversation, java.util.UUID.randomUUID().toString(),
165 conversation.getUuid(),
166 conversation.getJid() == null ? null : conversation.getJid().asBareJid(),
167 null,
168 body,
169 System.currentTimeMillis(),
170 encryption,
171 status,
172 TYPE_TEXT,
173 false,
174 null,
175 null,
176 null,
177 null,
178 true,
179 null,
180 false,
181 null,
182 null,
183 false,
184 false,
185 null,
186 System.currentTimeMillis(),
187 null,
188 null,
189 null);
190 }
191
192 public Message(Conversation conversation, int status, int type, final String remoteMsgId) {
193 this(conversation, java.util.UUID.randomUUID().toString(),
194 conversation.getUuid(),
195 conversation.getJid() == null ? null : conversation.getJid().asBareJid(),
196 null,
197 null,
198 System.currentTimeMillis(),
199 Message.ENCRYPTION_NONE,
200 status,
201 type,
202 false,
203 remoteMsgId,
204 null,
205 null,
206 null,
207 true,
208 null,
209 false,
210 null,
211 null,
212 false,
213 false,
214 null,
215 System.currentTimeMillis(),
216 null,
217 null,
218 null);
219 }
220
221 protected Message(final Conversational conversation, final String uuid, final String conversationUUid, final Jid counterpart,
222 final Jid trueCounterpart, final String body, final long timeSent,
223 final int encryption, final int status, final int type, final boolean carbon,
224 final String remoteMsgId, final String relativeFilePath,
225 final String serverMsgId, final String fingerprint, final boolean read,
226 final String edited, final boolean oob, final String errorMessage, final Set<ReadByMarker> readByMarkers,
227 final boolean markable, final boolean deleted, final String bodyLanguage, final long timeReceived, final String subject, final String fileParams, final List<Element> payloads) {
228 this.conversation = conversation;
229 this.uuid = uuid;
230 this.conversationUuid = conversationUUid;
231 this.counterpart = counterpart;
232 this.trueCounterpart = trueCounterpart;
233 this.body = body == null ? "" : body;
234 this.timeSent = timeSent;
235 this.encryption = encryption;
236 this.status = status;
237 this.type = type;
238 this.carbon = carbon;
239 this.remoteMsgId = remoteMsgId;
240 this.relativeFilePath = relativeFilePath;
241 this.serverMsgId = serverMsgId;
242 this.axolotlFingerprint = fingerprint;
243 this.read = read;
244 this.edits = Edit.fromJson(edited);
245 this.oob = oob;
246 this.errorMessage = errorMessage;
247 this.readByMarkers = readByMarkers == null ? new CopyOnWriteArraySet<>() : readByMarkers;
248 this.markable = markable;
249 this.deleted = deleted;
250 this.bodyLanguage = bodyLanguage;
251 this.timeReceived = timeReceived;
252 this.subject = subject;
253 if (payloads != null) this.payloads = payloads;
254 if (fileParams != null && getSims().isEmpty()) this.fileParams = new FileParams(fileParams);
255 }
256
257 public static Message fromCursor(Cursor cursor, Conversation conversation) throws IOException {
258 String payloadsStr = cursor.getString(cursor.getColumnIndex("payloads"));
259 List<Element> payloads = new ArrayList<>();
260 if (payloadsStr != null) {
261 final XmlReader xmlReader = new XmlReader();
262 xmlReader.setInputStream(ByteSource.wrap(payloadsStr.getBytes()).openStream());
263 Tag tag;
264 while ((tag = xmlReader.readTag()) != null) {
265 payloads.add(xmlReader.readElement(tag));
266 }
267 }
268
269 return new Message(conversation,
270 cursor.getString(cursor.getColumnIndex(UUID)),
271 cursor.getString(cursor.getColumnIndex(CONVERSATION)),
272 fromString(cursor.getString(cursor.getColumnIndex(COUNTERPART))),
273 fromString(cursor.getString(cursor.getColumnIndex(TRUE_COUNTERPART))),
274 cursor.getString(cursor.getColumnIndex(BODY)),
275 cursor.getLong(cursor.getColumnIndex(TIME_SENT)),
276 cursor.getInt(cursor.getColumnIndex(ENCRYPTION)),
277 cursor.getInt(cursor.getColumnIndex(STATUS)),
278 cursor.getInt(cursor.getColumnIndex(TYPE)),
279 cursor.getInt(cursor.getColumnIndex(CARBON)) > 0,
280 cursor.getString(cursor.getColumnIndex(REMOTE_MSG_ID)),
281 cursor.getString(cursor.getColumnIndex(RELATIVE_FILE_PATH)),
282 cursor.getString(cursor.getColumnIndex(SERVER_MSG_ID)),
283 cursor.getString(cursor.getColumnIndex(FINGERPRINT)),
284 cursor.getInt(cursor.getColumnIndex(READ)) > 0,
285 cursor.getString(cursor.getColumnIndex(EDITED)),
286 cursor.getInt(cursor.getColumnIndex(OOB)) > 0,
287 cursor.getString(cursor.getColumnIndex(ERROR_MESSAGE)),
288 ReadByMarker.fromJsonString(cursor.getString(cursor.getColumnIndex(READ_BY_MARKERS))),
289 cursor.getInt(cursor.getColumnIndex(MARKABLE)) > 0,
290 cursor.getInt(cursor.getColumnIndex(DELETED)) > 0,
291 cursor.getString(cursor.getColumnIndex(BODY_LANGUAGE)),
292 cursor.getLong(cursor.getColumnIndex(cursor.isNull(cursor.getColumnIndex("timeReceived")) ? TIME_SENT : "timeReceived")),
293 cursor.getString(cursor.getColumnIndex("subject")),
294 cursor.getString(cursor.getColumnIndex("fileParams")),
295 payloads
296 );
297 }
298
299 private static Jid fromString(String value) {
300 try {
301 if (value != null) {
302 return Jid.of(value);
303 }
304 } catch (IllegalArgumentException e) {
305 return null;
306 }
307 return null;
308 }
309
310 public static Message createStatusMessage(Conversation conversation, String body) {
311 final Message message = new Message(conversation);
312 message.setType(Message.TYPE_STATUS);
313 message.setStatus(Message.STATUS_RECEIVED);
314 message.body = body;
315 return message;
316 }
317
318 public static Message createLoadMoreMessage(Conversation conversation) {
319 final Message message = new Message(conversation);
320 message.setType(Message.TYPE_STATUS);
321 message.body = "LOAD_MORE";
322 return message;
323 }
324
325 public ContentValues getCheogramContentValues() {
326 ContentValues values = new ContentValues();
327 values.put(UUID, uuid);
328 values.put("subject", subject);
329 values.put("fileParams", fileParams == null ? null : fileParams.toString());
330 if (fileParams != null) {
331 List<Element> sims = getSims();
332 if (sims.isEmpty()) {
333 addPayload(fileParams.toSims());
334 } else {
335 sims.get(0).replaceChildren(fileParams.toSims().getChildren());
336 }
337 }
338 values.put("payloads", payloads.size() < 1 ? null : payloads.stream().map(Object::toString).collect(Collectors.joining()));
339 return values;
340 }
341
342 @Override
343 public ContentValues getContentValues() {
344 ContentValues values = new ContentValues();
345 values.put(UUID, uuid);
346 values.put(CONVERSATION, conversationUuid);
347 if (counterpart == null) {
348 values.putNull(COUNTERPART);
349 } else {
350 values.put(COUNTERPART, counterpart.toString());
351 }
352 if (trueCounterpart == null) {
353 values.putNull(TRUE_COUNTERPART);
354 } else {
355 values.put(TRUE_COUNTERPART, trueCounterpart.toString());
356 }
357 values.put(BODY, body.length() > Config.MAX_STORAGE_MESSAGE_CHARS ? body.substring(0, Config.MAX_STORAGE_MESSAGE_CHARS) : body);
358 values.put(TIME_SENT, timeSent);
359 values.put(ENCRYPTION, encryption);
360 values.put(STATUS, status);
361 values.put(TYPE, type);
362 values.put(CARBON, carbon ? 1 : 0);
363 values.put(REMOTE_MSG_ID, remoteMsgId);
364 values.put(RELATIVE_FILE_PATH, relativeFilePath);
365 values.put(SERVER_MSG_ID, serverMsgId);
366 values.put(FINGERPRINT, axolotlFingerprint);
367 values.put(READ, read ? 1 : 0);
368 try {
369 values.put(EDITED, Edit.toJson(edits));
370 } catch (JSONException e) {
371 Log.e(Config.LOGTAG, "error persisting json for edits", e);
372 }
373 values.put(OOB, oob ? 1 : 0);
374 values.put(ERROR_MESSAGE, errorMessage);
375 values.put(READ_BY_MARKERS, ReadByMarker.toJson(readByMarkers).toString());
376 values.put(MARKABLE, markable ? 1 : 0);
377 values.put(DELETED, deleted ? 1 : 0);
378 values.put(BODY_LANGUAGE, bodyLanguage);
379 return values;
380 }
381
382 public String getConversationUuid() {
383 return conversationUuid;
384 }
385
386 public Conversational getConversation() {
387 return this.conversation;
388 }
389
390 public Jid getCounterpart() {
391 return counterpart;
392 }
393
394 public void setCounterpart(final Jid counterpart) {
395 this.counterpart = counterpart;
396 }
397
398 public Contact getContact() {
399 if (this.conversation.getMode() == Conversation.MODE_SINGLE) {
400 if (this.trueCounterpart != null) {
401 return this.conversation.getAccount().getRoster()
402 .getContact(this.trueCounterpart);
403 }
404
405 return this.conversation.getContact();
406 } else {
407 if (this.trueCounterpart == null) {
408 return null;
409 } else {
410 return this.conversation.getAccount().getRoster()
411 .getContactFromContactList(this.trueCounterpart);
412 }
413 }
414 }
415
416 public String getBody() {
417 StringBuilder body = new StringBuilder(this.body);
418
419 List<Element> fallbacks = getFallbacks();
420 List<Pair<Integer, Integer>> spans = new ArrayList<>();
421 for (Element fallback : fallbacks) {
422 for (Element span : fallback.getChildren()) {
423 if (!span.getName().equals("body") && !span.getNamespace().equals("urn:xmpp:fallback:0")) continue;
424 if (span.getAttribute("start") == null || span.getAttribute("end") == null) return "";
425 spans.add(new Pair(parseInt(span.getAttribute("start")), parseInt(span.getAttribute("end"))));
426 }
427 }
428 // Do them in reverse order so that span deletions don't affect the indexes of other spans
429 spans.sort((x, y) -> y.first.compareTo(x.first));
430 try {
431 for (Pair<Integer, Integer> span : spans) {
432 body.delete(span.first, span.second);
433 }
434 } catch (final StringIndexOutOfBoundsException e) { spans.clear(); }
435
436 if (spans.isEmpty() && getOob() != null) {
437 return body.toString().replace(getOob().toString(), "");
438 } else {
439 return body.toString();
440 }
441 }
442
443 public synchronized void setBody(String body) {
444 if (body == null) {
445 throw new Error("You should not set the message body to null");
446 }
447 this.body = body;
448 this.isGeoUri = null;
449 this.isEmojisOnly = null;
450 this.treatAsDownloadable = null;
451 }
452
453 public String getSubject() {
454 return subject;
455 }
456
457 public synchronized void setSubject(String subject) {
458 this.subject = subject;
459 }
460
461 public Element getThread() {
462 if (this.payloads == null) return null;
463
464 for (Element el : this.payloads) {
465 if (el.getName().equals("thread") && el.getNamespace().equals("jabber:client")) {
466 return el;
467 }
468 }
469
470 return null;
471 }
472
473 public void setThread(Element thread) {
474 payloads.removeIf(el -> el.getName().equals("thread") && el.getNamespace().equals("jabber:client"));
475 addPayload(thread);
476 }
477
478 public void setMucUser(MucOptions.User user) {
479 this.user = new WeakReference<>(user);
480 }
481
482 public boolean sameMucUser(Message otherMessage) {
483 final MucOptions.User thisUser = this.user == null ? null : this.user.get();
484 final MucOptions.User otherUser = otherMessage.user == null ? null : otherMessage.user.get();
485 return thisUser != null && thisUser == otherUser;
486 }
487
488 public String getErrorMessage() {
489 return errorMessage;
490 }
491
492 public boolean setErrorMessage(String message) {
493 boolean changed = (message != null && !message.equals(errorMessage))
494 || (message == null && errorMessage != null);
495 this.errorMessage = message;
496 return changed;
497 }
498
499 public long getTimeReceived() {
500 return timeReceived;
501 }
502
503 public long getTimeSent() {
504 return timeSent;
505 }
506
507 public int getEncryption() {
508 return encryption;
509 }
510
511 public void setEncryption(int encryption) {
512 this.encryption = encryption;
513 }
514
515 public int getStatus() {
516 return status;
517 }
518
519 public void setStatus(int status) {
520 this.status = status;
521 }
522
523 public String getRelativeFilePath() {
524 return this.relativeFilePath;
525 }
526
527 public void setRelativeFilePath(String path) {
528 this.relativeFilePath = path;
529 }
530
531 public String getRemoteMsgId() {
532 return this.remoteMsgId;
533 }
534
535 public void setRemoteMsgId(String id) {
536 this.remoteMsgId = id;
537 }
538
539 public String getServerMsgId() {
540 return this.serverMsgId;
541 }
542
543 public void setServerMsgId(String id) {
544 this.serverMsgId = id;
545 }
546
547 public boolean isRead() {
548 return this.read;
549 }
550
551 public boolean isDeleted() {
552 return this.deleted;
553 }
554
555 public Element getModerated() {
556 if (this.payloads == null) return null;
557
558 for (Element el : this.payloads) {
559 if (el.getName().equals("moderated") && el.getNamespace().equals("urn:xmpp:message-moderate:0")) {
560 return el;
561 }
562 }
563
564 return null;
565 }
566
567 public void setDeleted(boolean deleted) {
568 this.deleted = deleted;
569 }
570
571 public void markRead() {
572 this.read = true;
573 }
574
575 public void markUnread() {
576 this.read = false;
577 }
578
579 public void setTime(long time) {
580 this.timeSent = time;
581 }
582
583 public void setTimeReceived(long time) {
584 this.timeReceived = time;
585 }
586
587 public String getEncryptedBody() {
588 return this.encryptedBody;
589 }
590
591 public void setEncryptedBody(String body) {
592 this.encryptedBody = body;
593 }
594
595 public int getType() {
596 return this.type;
597 }
598
599 public void setType(int type) {
600 this.type = type;
601 }
602
603 public boolean isCarbon() {
604 return carbon;
605 }
606
607 public void setCarbon(boolean carbon) {
608 this.carbon = carbon;
609 }
610
611 public void putEdited(String edited, String serverMsgId) {
612 final Edit edit = new Edit(edited, serverMsgId);
613 if (this.edits.size() < 128 && !this.edits.contains(edit)) {
614 this.edits.add(edit);
615 }
616 }
617
618 boolean remoteMsgIdMatchInEdit(String id) {
619 for (Edit edit : this.edits) {
620 if (id.equals(edit.getEditedId())) {
621 return true;
622 }
623 }
624 return false;
625 }
626
627 public String getBodyLanguage() {
628 return this.bodyLanguage;
629 }
630
631 public void setBodyLanguage(String language) {
632 this.bodyLanguage = language;
633 }
634
635 public boolean edited() {
636 return this.edits.size() > 0;
637 }
638
639 public void setTrueCounterpart(Jid trueCounterpart) {
640 this.trueCounterpart = trueCounterpart;
641 }
642
643 public Jid getTrueCounterpart() {
644 return this.trueCounterpart;
645 }
646
647 public Transferable getTransferable() {
648 return this.transferable;
649 }
650
651 public synchronized void setTransferable(Transferable transferable) {
652 this.transferable = transferable;
653 }
654
655 public boolean addReadByMarker(ReadByMarker readByMarker) {
656 if (readByMarker.getRealJid() != null) {
657 if (readByMarker.getRealJid().asBareJid().equals(trueCounterpart)) {
658 return false;
659 }
660 } else if (readByMarker.getFullJid() != null) {
661 if (readByMarker.getFullJid().equals(counterpart)) {
662 return false;
663 }
664 }
665 if (this.readByMarkers.add(readByMarker)) {
666 if (readByMarker.getRealJid() != null && readByMarker.getFullJid() != null) {
667 Iterator<ReadByMarker> iterator = this.readByMarkers.iterator();
668 while (iterator.hasNext()) {
669 ReadByMarker marker = iterator.next();
670 if (marker.getRealJid() == null && readByMarker.getFullJid().equals(marker.getFullJid())) {
671 iterator.remove();
672 }
673 }
674 }
675 return true;
676 } else {
677 return false;
678 }
679 }
680
681 public Set<ReadByMarker> getReadByMarkers() {
682 return ImmutableSet.copyOf(this.readByMarkers);
683 }
684
685 boolean similar(Message message) {
686 if (!isPrivateMessage() && this.serverMsgId != null && message.getServerMsgId() != null) {
687 return this.serverMsgId.equals(message.getServerMsgId()) || Edit.wasPreviouslyEditedServerMsgId(edits, message.getServerMsgId());
688 } else if (Edit.wasPreviouslyEditedServerMsgId(edits, message.getServerMsgId())) {
689 return true;
690 } else if (this.body == null || this.counterpart == null) {
691 return false;
692 } else {
693 String body, otherBody;
694 if (this.hasFileOnRemoteHost()) {
695 body = getFileParams().url;
696 otherBody = message.body == null ? null : message.body.trim();
697 } else {
698 body = this.body;
699 otherBody = message.body;
700 }
701 final boolean matchingCounterpart = this.counterpart.equals(message.getCounterpart());
702 if (message.getRemoteMsgId() != null) {
703 final boolean hasUuid = CryptoHelper.UUID_PATTERN.matcher(message.getRemoteMsgId()).matches();
704 if (hasUuid && matchingCounterpart && Edit.wasPreviouslyEditedRemoteMsgId(edits, message.getRemoteMsgId())) {
705 return true;
706 }
707 return (message.getRemoteMsgId().equals(this.remoteMsgId) || message.getRemoteMsgId().equals(this.uuid))
708 && matchingCounterpart
709 && (body.equals(otherBody) || (message.getEncryption() == Message.ENCRYPTION_PGP && hasUuid));
710 } else {
711 return this.remoteMsgId == null
712 && matchingCounterpart
713 && body.equals(otherBody)
714 && Math.abs(this.getTimeSent() - message.getTimeSent()) < Config.MESSAGE_MERGE_WINDOW * 1000;
715 }
716 }
717 }
718
719 public Message next() {
720 if (this.conversation instanceof Conversation) {
721 final Conversation conversation = (Conversation) this.conversation;
722 synchronized (conversation.messages) {
723 if (this.mNextMessage == null) {
724 int index = conversation.messages.indexOf(this);
725 if (index < 0 || index >= conversation.messages.size() - 1) {
726 this.mNextMessage = null;
727 } else {
728 this.mNextMessage = conversation.messages.get(index + 1);
729 }
730 }
731 return this.mNextMessage;
732 }
733 } else {
734 throw new AssertionError("Calling next should be disabled for stubs");
735 }
736 }
737
738 public Message prev() {
739 if (this.conversation instanceof Conversation) {
740 final Conversation conversation = (Conversation) this.conversation;
741 synchronized (conversation.messages) {
742 if (this.mPreviousMessage == null) {
743 int index = conversation.messages.indexOf(this);
744 if (index <= 0 || index > conversation.messages.size()) {
745 this.mPreviousMessage = null;
746 } else {
747 this.mPreviousMessage = conversation.messages.get(index - 1);
748 }
749 }
750 }
751 return this.mPreviousMessage;
752 } else {
753 throw new AssertionError("Calling prev should be disabled for stubs");
754 }
755 }
756
757 public boolean isLastCorrectableMessage() {
758 Message next = next();
759 while (next != null) {
760 if (next.isEditable()) {
761 return false;
762 }
763 next = next.next();
764 }
765 return isEditable();
766 }
767
768 public boolean isEditable() {
769 return status != STATUS_RECEIVED && !isCarbon() && type != Message.TYPE_RTP_SESSION;
770 }
771
772 public boolean mergeable(final Message message) {
773 return message != null &&
774 (message.getType() == Message.TYPE_TEXT &&
775 this.getTransferable() == null &&
776 message.getTransferable() == null &&
777 message.getEncryption() != Message.ENCRYPTION_PGP &&
778 message.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED &&
779 this.getType() == message.getType() &&
780 this.getSubject() != null &&
781 isStatusMergeable(this.getStatus(), message.getStatus()) &&
782 isEncryptionMergeable(this.getEncryption(),message.getEncryption()) &&
783 this.getCounterpart() != null &&
784 this.getCounterpart().equals(message.getCounterpart()) &&
785 this.edited() == message.edited() &&
786 (message.getTimeSent() - this.getTimeSent()) <= (Config.MESSAGE_MERGE_WINDOW * 1000) &&
787 this.getBody().length() + message.getBody().length() <= Config.MAX_DISPLAY_MESSAGE_CHARS &&
788 !message.isGeoUri() &&
789 !this.isGeoUri() &&
790 !message.isOOb() &&
791 !this.isOOb() &&
792 !message.treatAsDownloadable() &&
793 !this.treatAsDownloadable() &&
794 !message.hasMeCommand() &&
795 !this.hasMeCommand() &&
796 !this.bodyIsOnlyEmojis() &&
797 !message.bodyIsOnlyEmojis() &&
798 ((this.axolotlFingerprint == null && message.axolotlFingerprint == null) || this.axolotlFingerprint.equals(message.getFingerprint())) &&
799 UIHelper.sameDay(message.getTimeSent(), this.getTimeSent()) &&
800 this.getReadByMarkers().equals(message.getReadByMarkers()) &&
801 !this.conversation.getJid().asBareJid().equals(Config.BUG_REPORTS)
802 );
803 }
804
805 private static boolean isStatusMergeable(int a, int b) {
806 return a == b || (
807 (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_UNSEND)
808 || (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_SEND)
809 || (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_WAITING)
810 || (a == Message.STATUS_SEND && b == Message.STATUS_UNSEND)
811 || (a == Message.STATUS_SEND && b == Message.STATUS_WAITING)
812 );
813 }
814
815 private static boolean isEncryptionMergeable(final int a, final int b) {
816 return a == b
817 && Arrays.asList(ENCRYPTION_NONE, ENCRYPTION_DECRYPTED, ENCRYPTION_AXOLOTL)
818 .contains(a);
819 }
820
821 public void setCounterparts(List<MucOptions.User> counterparts) {
822 this.counterparts = counterparts;
823 }
824
825 public List<MucOptions.User> getCounterparts() {
826 return this.counterparts;
827 }
828
829 @Override
830 public int getAvatarBackgroundColor() {
831 if (type == Message.TYPE_STATUS && getCounterparts() != null && getCounterparts().size() > 1) {
832 return Color.TRANSPARENT;
833 } else {
834 return UIHelper.getColorForName(UIHelper.getMessageDisplayName(this));
835 }
836 }
837
838 @Override
839 public String getAvatarName() {
840 return UIHelper.getMessageDisplayName(this);
841 }
842
843 public boolean isOOb() {
844 return oob || getFileParams().url != null;
845 }
846
847 public static class MergeSeparator {
848 }
849
850 public SpannableStringBuilder getSpannableBody(GetThumbnailForCid thumbnailer, Drawable fallbackImg) {
851 final Element html = getHtml();
852 if (html == null || Build.VERSION.SDK_INT < 24) {
853 return new SpannableStringBuilder(MessageUtils.filterLtrRtl(getBody()).trim());
854 } else {
855 SpannableStringBuilder spannable = new SpannableStringBuilder(Html.fromHtml(
856 MessageUtils.filterLtrRtl(html.toString()).trim(),
857 Html.FROM_HTML_MODE_COMPACT,
858 (source) -> {
859 try {
860 if (thumbnailer == null) return fallbackImg;
861 Cid cid = BobTransfer.cid(new URI(source));
862 if (cid == null) return fallbackImg;
863 Drawable thumbnail = thumbnailer.getThumbnail(cid);
864 if (thumbnail == null) return fallbackImg;
865 return thumbnail;
866 } catch (final URISyntaxException e) {
867 return fallbackImg;
868 }
869 },
870 (opening, tag, output, xmlReader) -> {}
871 ));
872
873 // Make images clickable and long-clickable with BetterLinkMovementMethod
874 ImageSpan[] imageSpans = spannable.getSpans(0, spannable.length(), ImageSpan.class);
875 for (ImageSpan span : imageSpans) {
876 final int start = spannable.getSpanStart(span);
877 final int end = spannable.getSpanEnd(span);
878
879 ClickableSpan click_span = new ClickableSpan() {
880 @Override
881 public void onClick(View widget) { }
882 };
883
884 spannable.setSpan(click_span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
885 }
886
887 // https://stackoverflow.com/a/10187511/8611
888 int i = spannable.length();
889 while(--i >= 0 && Character.isWhitespace(spannable.charAt(i))) { }
890 return (SpannableStringBuilder) spannable.subSequence(0, i+1);
891 }
892 }
893
894 public SpannableStringBuilder getMergedBody() {
895 return getMergedBody(null, null);
896 }
897
898 public SpannableStringBuilder getMergedBody(GetThumbnailForCid thumbnailer, Drawable fallbackImg) {
899 SpannableStringBuilder body = getSpannableBody(thumbnailer, fallbackImg);
900 Message current = this;
901 while (current.mergeable(current.next())) {
902 current = current.next();
903 if (current == null) {
904 break;
905 }
906 body.append("\n\n");
907 body.setSpan(new MergeSeparator(), body.length() - 2, body.length(),
908 SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE);
909 body.append(current.getSpannableBody(thumbnailer, fallbackImg));
910 }
911 return body;
912 }
913
914 public boolean hasMeCommand() {
915 return this.body.trim().startsWith(ME_COMMAND);
916 }
917
918 public int getMergedStatus() {
919 int status = this.status;
920 Message current = this;
921 while (current.mergeable(current.next())) {
922 current = current.next();
923 if (current == null) {
924 break;
925 }
926 status = current.status;
927 }
928 return status;
929 }
930
931 public long getMergedTimeSent() {
932 long time = this.timeSent;
933 Message current = this;
934 while (current.mergeable(current.next())) {
935 current = current.next();
936 if (current == null) {
937 break;
938 }
939 time = current.timeSent;
940 }
941 return time;
942 }
943
944 public boolean wasMergedIntoPrevious() {
945 Message prev = this.prev();
946 return prev != null && prev.mergeable(this);
947 }
948
949 public boolean trusted() {
950 Contact contact = this.getContact();
951 return status > STATUS_RECEIVED || (contact != null && (contact.showInContactList() || contact.isSelf()));
952 }
953
954 public boolean fixCounterpart() {
955 final Presences presences = conversation.getContact().getPresences();
956 if (counterpart != null && presences.has(Strings.nullToEmpty(counterpart.getResource()))) {
957 return true;
958 } else if (presences.size() >= 1) {
959 counterpart = PresenceSelector.getNextCounterpart(getContact(), presences.toResourceArray()[0]);
960 return true;
961 } else {
962 counterpart = null;
963 return false;
964 }
965 }
966
967 public void setUuid(String uuid) {
968 this.uuid = uuid;
969 }
970
971 public String getEditedId() {
972 if (edits.size() > 0) {
973 return edits.get(edits.size() - 1).getEditedId();
974 } else {
975 throw new IllegalStateException("Attempting to store unedited message");
976 }
977 }
978
979 public String getEditedIdWireFormat() {
980 if (edits.size() > 0) {
981 return edits.get(Config.USE_LMC_VERSION_1_1 ? 0 : edits.size() - 1).getEditedId();
982 } else {
983 throw new IllegalStateException("Attempting to store unedited message");
984 }
985 }
986
987 public URI getOob() {
988 final String url = getFileParams().url;
989 try {
990 return url == null ? null : new URI(url);
991 } catch (final URISyntaxException e) {
992 return null;
993 }
994 }
995
996 public void clearPayloads() {
997 this.payloads.clear();
998 }
999
1000 public void addPayload(Element el) {
1001 if (el == null) return;
1002
1003 this.payloads.add(el);
1004 }
1005
1006 public List<Element> getPayloads() {
1007 return new ArrayList<>(this.payloads);
1008 }
1009
1010 public List<Element> getFallbacks() {
1011 List<Element> fallbacks = new ArrayList<>();
1012
1013 if (this.payloads == null) return fallbacks;
1014
1015 for (Element el : this.payloads) {
1016 if (el.getName().equals("fallback") && el.getNamespace().equals("urn:xmpp:fallback:0")) {
1017 final String fallbackFor = el.getAttribute("for");
1018 if (fallbackFor == null) continue;
1019 if (fallbackFor.equals("http://jabber.org/protocol/address") || fallbackFor.equals(Namespace.OOB)) {
1020 fallbacks.add(el);
1021 }
1022 }
1023 }
1024
1025 return fallbacks;
1026 }
1027
1028 public Element getHtml() {
1029 if (this.payloads == null) return null;
1030
1031 for (Element el : this.payloads) {
1032 if (el.getName().equals("html") && el.getNamespace().equals("http://jabber.org/protocol/xhtml-im")) {
1033 return el.getChildren().get(0);
1034 }
1035 }
1036
1037 return null;
1038 }
1039
1040 public List<Element> getCommands() {
1041 if (this.payloads == null) return null;
1042
1043 for (Element el : this.payloads) {
1044 if (el.getName().equals("query") && el.getNamespace().equals("http://jabber.org/protocol/disco#items") && el.getAttribute("node").equals("http://jabber.org/protocol/commands")) {
1045 return el.getChildren();
1046 }
1047 }
1048
1049 return null;
1050 }
1051
1052 public String getMimeType() {
1053 String extension;
1054 if (relativeFilePath != null) {
1055 extension = MimeUtils.extractRelevantExtension(relativeFilePath);
1056 } else {
1057 final String url = URL.tryParse(getOob() == null ? body.split("\n")[0] : getOob().toString());
1058 if (url == null) {
1059 return null;
1060 }
1061 extension = MimeUtils.extractRelevantExtension(url);
1062 }
1063 return MimeUtils.guessMimeTypeFromExtension(extension);
1064 }
1065
1066 public synchronized boolean treatAsDownloadable() {
1067 if (treatAsDownloadable == null) {
1068 treatAsDownloadable = MessageUtils.treatAsDownloadable(this.body, isOOb());
1069 }
1070 return treatAsDownloadable;
1071 }
1072
1073 public synchronized boolean bodyIsOnlyEmojis() {
1074 if (isEmojisOnly == null) {
1075 isEmojisOnly = Emoticons.isOnlyEmoji(getBody().replaceAll("\\s", ""));
1076 }
1077 return isEmojisOnly;
1078 }
1079
1080 public synchronized boolean isGeoUri() {
1081 if (isGeoUri == null) {
1082 isGeoUri = GeoHelper.GEO_URI.matcher(body).matches();
1083 }
1084 return isGeoUri;
1085 }
1086
1087 protected List<Element> getSims() {
1088 return payloads.stream().filter(el ->
1089 el.getName().equals("reference") && el.getNamespace().equals("urn:xmpp:reference:0") &&
1090 el.findChild("media-sharing", "urn:xmpp:sims:1") != null
1091 ).collect(Collectors.toList());
1092 }
1093
1094 public synchronized void resetFileParams() {
1095 this.fileParams = null;
1096 }
1097
1098 public synchronized void setFileParams(FileParams fileParams) {
1099 if (fileParams != null && this.fileParams != null && this.fileParams.sims != null && fileParams.sims == null) {
1100 fileParams.sims = this.fileParams.sims;
1101 }
1102 this.fileParams = fileParams;
1103 if (fileParams != null && getSims().isEmpty()) {
1104 addPayload(fileParams.toSims());
1105 }
1106 }
1107
1108 public synchronized FileParams getFileParams() {
1109 if (fileParams == null) {
1110 List<Element> sims = getSims();
1111 fileParams = sims.isEmpty() ? new FileParams(oob ? this.body : "") : new FileParams(sims.get(0));
1112 if (this.transferable != null) {
1113 fileParams.size = this.transferable.getFileSize();
1114 }
1115 }
1116
1117 return fileParams;
1118 }
1119
1120 private static int parseInt(String value) {
1121 try {
1122 return Integer.parseInt(value);
1123 } catch (NumberFormatException e) {
1124 return 0;
1125 }
1126 }
1127
1128 public void untie() {
1129 this.mNextMessage = null;
1130 this.mPreviousMessage = null;
1131 }
1132
1133 public boolean isPrivateMessage() {
1134 return type == TYPE_PRIVATE || type == TYPE_PRIVATE_FILE;
1135 }
1136
1137 public boolean isFileOrImage() {
1138 return type == TYPE_FILE || type == TYPE_IMAGE || type == TYPE_PRIVATE_FILE;
1139 }
1140
1141
1142 public boolean isTypeText() {
1143 return type == TYPE_TEXT || type == TYPE_PRIVATE;
1144 }
1145
1146 public boolean hasFileOnRemoteHost() {
1147 return isFileOrImage() && getFileParams().url != null;
1148 }
1149
1150 public boolean needsUploading() {
1151 return isFileOrImage() && getFileParams().url == null;
1152 }
1153
1154 public static class FileParams {
1155 public String url;
1156 public Long size = null;
1157 public int width = 0;
1158 public int height = 0;
1159 public int runtime = 0;
1160 public Element sims = null;
1161
1162 public FileParams() { }
1163
1164 public FileParams(Element el) {
1165 if (el.getName().equals("x") && el.getNamespace().equals(Namespace.OOB)) {
1166 this.url = el.findChildContent("url", Namespace.OOB);
1167 }
1168 if (el.getName().equals("reference") && el.getNamespace().equals("urn:xmpp:reference:0")) {
1169 sims = el;
1170 final String refUri = el.getAttribute("uri");
1171 if (refUri != null) url = refUri;
1172 final Element mediaSharing = el.findChild("media-sharing", "urn:xmpp:sims:1");
1173 if (mediaSharing != null) {
1174 Element file = mediaSharing.findChild("file", "urn:xmpp:jingle:apps:file-transfer:5");
1175 if (file == null) file = mediaSharing.findChild("file", "urn:xmpp:jingle:apps:file-transfer:4");
1176 if (file == null) file = mediaSharing.findChild("file", "urn:xmpp:jingle:apps:file-transfer:3");
1177 if (file != null) {
1178 String sizeS = file.findChildContent("size", file.getNamespace());
1179 if (sizeS != null) size = new Long(sizeS);
1180 String widthS = file.findChildContent("width", "https://schema.org/");
1181 if (widthS != null) width = parseInt(widthS);
1182 String heightS = file.findChildContent("height", "https://schema.org/");
1183 if (heightS != null) height = parseInt(heightS);
1184 String durationS = file.findChildContent("duration", "https://schema.org/");
1185 if (durationS != null) runtime = (int)(Duration.parse(durationS).toMillis() / 1000L);
1186 }
1187
1188 final Element sources = mediaSharing.findChild("sources", "urn:xmpp:sims:1");
1189 if (sources != null) {
1190 final Element ref = sources.findChild("reference", "urn:xmpp:reference:0");
1191 if (ref != null) url = ref.getAttribute("uri");
1192 }
1193 }
1194 }
1195 }
1196
1197 public FileParams(String ser) {
1198 final String[] parts = ser == null ? new String[0] : ser.split("\\|");
1199 switch (parts.length) {
1200 case 1:
1201 try {
1202 this.size = Long.parseLong(parts[0]);
1203 } catch (final NumberFormatException e) {
1204 this.url = URL.tryParse(parts[0]);
1205 }
1206 break;
1207 case 5:
1208 this.runtime = parseInt(parts[4]);
1209 case 4:
1210 this.width = parseInt(parts[2]);
1211 this.height = parseInt(parts[3]);
1212 case 2:
1213 this.url = URL.tryParse(parts[0]);
1214 this.size = Longs.tryParse(parts[1]);
1215 break;
1216 case 3:
1217 this.size = Longs.tryParse(parts[0]);
1218 this.width = parseInt(parts[1]);
1219 this.height = parseInt(parts[2]);
1220 break;
1221 }
1222 }
1223
1224 public long getSize() {
1225 return size == null ? 0 : size;
1226 }
1227
1228 public String getName() {
1229 Element file = getFileElement();
1230 if (file == null) return null;
1231
1232 return file.findChildContent("name", file.getNamespace());
1233 }
1234
1235 public void setName(final String name) {
1236 if (sims == null) toSims();
1237 Element file = getFileElement();
1238
1239 for (Element child : file.getChildren()) {
1240 if (child.getName().equals("name") && child.getNamespace().equals(file.getNamespace())) {
1241 file.removeChild(child);
1242 }
1243 }
1244
1245 if (name != null) {
1246 file.addChild("name", file.getNamespace()).setContent(name);
1247 }
1248 }
1249
1250 public Element toSims() {
1251 if (sims == null) sims = new Element("reference", "urn:xmpp:reference:0");
1252 sims.setAttribute("type", "data");
1253 Element mediaSharing = sims.findChild("media-sharing", "urn:xmpp:sims:1");
1254 if (mediaSharing == null) mediaSharing = sims.addChild("media-sharing", "urn:xmpp:sims:1");
1255
1256 Element file = mediaSharing.findChild("file", "urn:xmpp:jingle:apps:file-transfer:5");
1257 if (file == null) file = mediaSharing.findChild("file", "urn:xmpp:jingle:apps:file-transfer:4");
1258 if (file == null) file = mediaSharing.findChild("file", "urn:xmpp:jingle:apps:file-transfer:3");
1259 if (file == null) file = mediaSharing.addChild("file", "urn:xmpp:jingle:apps:file-transfer:5");
1260
1261 file.removeChild(file.findChild("size", file.getNamespace()));
1262 if (size != null) file.addChild("size", file.getNamespace()).setContent(size.toString());
1263
1264 file.removeChild(file.findChild("width", "https://schema.org/"));
1265 if (width > 0) file.addChild("width", "https://schema.org/").setContent(String.valueOf(width));
1266
1267 file.removeChild(file.findChild("height", "https://schema.org/"));
1268 if (height > 0) file.addChild("height", "https://schema.org/").setContent(String.valueOf(height));
1269
1270 file.removeChild(file.findChild("duration", "https://schema.org/"));
1271 if (runtime > 0) file.addChild("duration", "https://schema.org/").setContent("PT" + runtime + "S");
1272
1273 if (url != null) {
1274 Element sources = mediaSharing.findChild("sources", mediaSharing.getNamespace());
1275 if (sources == null) sources = mediaSharing.addChild("sources", mediaSharing.getNamespace());
1276
1277 Element source = sources.findChild("reference", "urn:xmpp:reference:0");
1278 if (source == null) source = sources.addChild("reference", "urn:xmpp:reference:0");
1279 source.setAttribute("type", "data");
1280 source.setAttribute("uri", url);
1281 }
1282
1283 return sims;
1284 }
1285
1286 protected Element getFileElement() {
1287 Element file = null;
1288 if (sims == null) return file;
1289
1290 Element mediaSharing = sims.findChild("media-sharing", "urn:xmpp:sims:1");
1291 if (mediaSharing == null) return file;
1292 file = mediaSharing.findChild("file", "urn:xmpp:jingle:apps:file-transfer:5");
1293 if (file == null) file = mediaSharing.findChild("file", "urn:xmpp:jingle:apps:file-transfer:4");
1294 if (file == null) file = mediaSharing.findChild("file", "urn:xmpp:jingle:apps:file-transfer:3");
1295 return file;
1296 }
1297
1298 public void setCids(Iterable<Cid> cids) throws NoSuchAlgorithmException {
1299 if (sims == null) toSims();
1300 Element file = getFileElement();
1301
1302 for (Element child : file.getChildren()) {
1303 if (child.getName().equals("hash") && child.getNamespace().equals("urn:xmpp:hashes:2")) {
1304 file.removeChild(child);
1305 }
1306 }
1307
1308 for (Cid cid : cids) {
1309 file.addChild("hash", "urn:xmpp:hashes:2")
1310 .setAttribute("algo", CryptoHelper.multihashAlgo(cid.getType()))
1311 .setContent(Base64.encodeToString(cid.getHash(), Base64.NO_WRAP));
1312 }
1313 }
1314
1315 public List<Cid> getCids() {
1316 List<Cid> cids = new ArrayList<>();
1317 Element file = getFileElement();
1318 if (file == null) return cids;
1319
1320 for (Element child : file.getChildren()) {
1321 if (child.getName().equals("hash") && child.getNamespace().equals("urn:xmpp:hashes:2")) {
1322 try {
1323 cids.add(CryptoHelper.cid(Base64.decode(child.getContent(), Base64.DEFAULT), child.getAttribute("algo")));
1324 } catch (final NoSuchAlgorithmException | IllegalStateException e) { }
1325 }
1326 }
1327
1328 cids.sort((x, y) -> y.getType().compareTo(x.getType()));
1329
1330 return cids;
1331 }
1332
1333 public List<Element> getThumbnails() {
1334 List<Element> thumbs = new ArrayList<>();
1335 Element file = getFileElement();
1336 if (file == null) return thumbs;
1337
1338 for (Element child : file.getChildren()) {
1339 if (child.getName().equals("thumbnail") && child.getNamespace().equals("urn:xmpp:thumbs:1")) {
1340 thumbs.add(child);
1341 }
1342 }
1343
1344 return thumbs;
1345 }
1346
1347 public String toString() {
1348 final StringBuilder builder = new StringBuilder();
1349 if (url != null) builder.append(url);
1350 if (size != null) builder.append('|').append(size.toString());
1351 if (width > 0 || height > 0 || runtime > 0) builder.append('|').append(width);
1352 if (height > 0 || runtime > 0) builder.append('|').append(height);
1353 if (runtime > 0) builder.append('|').append(runtime);
1354 return builder.toString();
1355 }
1356
1357 public boolean equals(Object o) {
1358 if (!(o instanceof FileParams)) return false;
1359 if (url == null) return false;
1360
1361 return url.equals(((FileParams) o).url);
1362 }
1363
1364 public int hashCode() {
1365 return url == null ? super.hashCode() : url.hashCode();
1366 }
1367 }
1368
1369 public void setFingerprint(String fingerprint) {
1370 this.axolotlFingerprint = fingerprint;
1371 }
1372
1373 public String getFingerprint() {
1374 return axolotlFingerprint;
1375 }
1376
1377 public boolean isTrusted() {
1378 final AxolotlService axolotlService = conversation.getAccount().getAxolotlService();
1379 final FingerprintStatus s = axolotlService != null ? axolotlService.getFingerprintTrust(axolotlFingerprint) : null;
1380 return s != null && s.isTrusted();
1381 }
1382
1383 private int getPreviousEncryption() {
1384 for (Message iterator = this.prev(); iterator != null; iterator = iterator.prev()) {
1385 if (iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED) {
1386 continue;
1387 }
1388 return iterator.getEncryption();
1389 }
1390 return ENCRYPTION_NONE;
1391 }
1392
1393 private int getNextEncryption() {
1394 if (this.conversation instanceof Conversation) {
1395 Conversation conversation = (Conversation) this.conversation;
1396 for (Message iterator = this.next(); iterator != null; iterator = iterator.next()) {
1397 if (iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED) {
1398 continue;
1399 }
1400 return iterator.getEncryption();
1401 }
1402 return conversation.getNextEncryption();
1403 } else {
1404 throw new AssertionError("This should never be called since isInValidSession should be disabled for stubs");
1405 }
1406 }
1407
1408 public boolean isValidInSession() {
1409 int pastEncryption = getCleanedEncryption(this.getPreviousEncryption());
1410 int futureEncryption = getCleanedEncryption(this.getNextEncryption());
1411
1412 boolean inUnencryptedSession = pastEncryption == ENCRYPTION_NONE
1413 || futureEncryption == ENCRYPTION_NONE
1414 || pastEncryption != futureEncryption;
1415
1416 return inUnencryptedSession || getCleanedEncryption(this.getEncryption()) == pastEncryption;
1417 }
1418
1419 private static int getCleanedEncryption(int encryption) {
1420 if (encryption == ENCRYPTION_DECRYPTED || encryption == ENCRYPTION_DECRYPTION_FAILED) {
1421 return ENCRYPTION_PGP;
1422 }
1423 if (encryption == ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE || encryption == ENCRYPTION_AXOLOTL_FAILED) {
1424 return ENCRYPTION_AXOLOTL;
1425 }
1426 return encryption;
1427 }
1428
1429 public static boolean configurePrivateMessage(final Message message) {
1430 return configurePrivateMessage(message, false);
1431 }
1432
1433 public static boolean configurePrivateFileMessage(final Message message) {
1434 return configurePrivateMessage(message, true);
1435 }
1436
1437 private static boolean configurePrivateMessage(final Message message, final boolean isFile) {
1438 final Conversation conversation;
1439 if (message.conversation instanceof Conversation) {
1440 conversation = (Conversation) message.conversation;
1441 } else {
1442 return false;
1443 }
1444 if (conversation.getMode() == Conversation.MODE_MULTI) {
1445 final Jid nextCounterpart = conversation.getNextCounterpart();
1446 return configurePrivateMessage(conversation, message, nextCounterpart, isFile);
1447 }
1448 return false;
1449 }
1450
1451 public static boolean configurePrivateMessage(final Message message, final Jid counterpart) {
1452 final Conversation conversation;
1453 if (message.conversation instanceof Conversation) {
1454 conversation = (Conversation) message.conversation;
1455 } else {
1456 return false;
1457 }
1458 return configurePrivateMessage(conversation, message, counterpart, false);
1459 }
1460
1461 private static boolean configurePrivateMessage(final Conversation conversation, final Message message, final Jid counterpart, final boolean isFile) {
1462 if (counterpart == null) {
1463 return false;
1464 }
1465 message.setCounterpart(counterpart);
1466 message.setTrueCounterpart(conversation.getMucOptions().getTrueCounterpart(counterpart));
1467 message.setType(isFile ? Message.TYPE_PRIVATE_FILE : Message.TYPE_PRIVATE);
1468 return true;
1469 }
1470}