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 }
1104
1105 public synchronized FileParams getFileParams() {
1106 if (fileParams == null) {
1107 List<Element> sims = getSims();
1108 fileParams = sims.isEmpty() ? new FileParams(oob ? this.body : "") : new FileParams(sims.get(0));
1109 if (this.transferable != null) {
1110 fileParams.size = this.transferable.getFileSize();
1111 }
1112 }
1113
1114 return fileParams;
1115 }
1116
1117 private static int parseInt(String value) {
1118 try {
1119 return Integer.parseInt(value);
1120 } catch (NumberFormatException e) {
1121 return 0;
1122 }
1123 }
1124
1125 public void untie() {
1126 this.mNextMessage = null;
1127 this.mPreviousMessage = null;
1128 }
1129
1130 public boolean isPrivateMessage() {
1131 return type == TYPE_PRIVATE || type == TYPE_PRIVATE_FILE;
1132 }
1133
1134 public boolean isFileOrImage() {
1135 return type == TYPE_FILE || type == TYPE_IMAGE || type == TYPE_PRIVATE_FILE;
1136 }
1137
1138
1139 public boolean isTypeText() {
1140 return type == TYPE_TEXT || type == TYPE_PRIVATE;
1141 }
1142
1143 public boolean hasFileOnRemoteHost() {
1144 return isFileOrImage() && getFileParams().url != null;
1145 }
1146
1147 public boolean needsUploading() {
1148 return isFileOrImage() && getFileParams().url == null;
1149 }
1150
1151 public static class FileParams {
1152 public String url;
1153 public Long size = null;
1154 public int width = 0;
1155 public int height = 0;
1156 public int runtime = 0;
1157 public Element sims = null;
1158
1159 public FileParams() { }
1160
1161 public FileParams(Element el) {
1162 if (el.getName().equals("x") && el.getNamespace().equals(Namespace.OOB)) {
1163 this.url = el.findChildContent("url", Namespace.OOB);
1164 }
1165 if (el.getName().equals("reference") && el.getNamespace().equals("urn:xmpp:reference:0")) {
1166 sims = el;
1167 final String refUri = el.getAttribute("uri");
1168 if (refUri != null) url = refUri;
1169 final Element mediaSharing = el.findChild("media-sharing", "urn:xmpp:sims:1");
1170 if (mediaSharing != null) {
1171 Element file = mediaSharing.findChild("file", "urn:xmpp:jingle:apps:file-transfer:5");
1172 if (file == null) file = mediaSharing.findChild("file", "urn:xmpp:jingle:apps:file-transfer:4");
1173 if (file == null) file = mediaSharing.findChild("file", "urn:xmpp:jingle:apps:file-transfer:3");
1174 if (file != null) {
1175 String sizeS = file.findChildContent("size", file.getNamespace());
1176 if (sizeS != null) size = new Long(sizeS);
1177 String widthS = file.findChildContent("width", "https://schema.org/");
1178 if (widthS != null) width = parseInt(widthS);
1179 String heightS = file.findChildContent("height", "https://schema.org/");
1180 if (heightS != null) height = parseInt(heightS);
1181 String durationS = file.findChildContent("duration", "https://schema.org/");
1182 if (durationS != null) runtime = (int)(Duration.parse(durationS).toMillis() / 1000L);
1183 }
1184
1185 final Element sources = mediaSharing.findChild("sources", "urn:xmpp:sims:1");
1186 if (sources != null) {
1187 final Element ref = sources.findChild("reference", "urn:xmpp:reference:0");
1188 if (ref != null) url = ref.getAttribute("uri");
1189 }
1190 }
1191 }
1192 }
1193
1194 public FileParams(String ser) {
1195 final String[] parts = ser == null ? new String[0] : ser.split("\\|");
1196 switch (parts.length) {
1197 case 1:
1198 try {
1199 this.size = Long.parseLong(parts[0]);
1200 } catch (final NumberFormatException e) {
1201 this.url = URL.tryParse(parts[0]);
1202 }
1203 break;
1204 case 5:
1205 this.runtime = parseInt(parts[4]);
1206 case 4:
1207 this.width = parseInt(parts[2]);
1208 this.height = parseInt(parts[3]);
1209 case 2:
1210 this.url = URL.tryParse(parts[0]);
1211 this.size = Longs.tryParse(parts[1]);
1212 break;
1213 case 3:
1214 this.size = Longs.tryParse(parts[0]);
1215 this.width = parseInt(parts[1]);
1216 this.height = parseInt(parts[2]);
1217 break;
1218 }
1219 }
1220
1221 public long getSize() {
1222 return size == null ? 0 : size;
1223 }
1224
1225 public String getName() {
1226 Element file = getFileElement();
1227 if (file == null) return null;
1228
1229 return file.findChildContent("name", file.getNamespace());
1230 }
1231
1232 public Element toSims() {
1233 if (sims == null) sims = new Element("reference", "urn:xmpp:reference:0");
1234 sims.setAttribute("type", "data");
1235 Element mediaSharing = sims.findChild("media-sharing", "urn:xmpp:sims:1");
1236 if (mediaSharing == null) mediaSharing = sims.addChild("media-sharing", "urn:xmpp:sims:1");
1237
1238 Element file = mediaSharing.findChild("file", "urn:xmpp:jingle:apps:file-transfer:5");
1239 if (file == null) file = mediaSharing.findChild("file", "urn:xmpp:jingle:apps:file-transfer:4");
1240 if (file == null) file = mediaSharing.findChild("file", "urn:xmpp:jingle:apps:file-transfer:3");
1241 if (file == null) file = mediaSharing.addChild("file", "urn:xmpp:jingle:apps:file-transfer:5");
1242
1243 file.removeChild(file.findChild("size", file.getNamespace()));
1244 if (size != null) file.addChild("size", file.getNamespace()).setContent(size.toString());
1245
1246 file.removeChild(file.findChild("width", "https://schema.org/"));
1247 if (width > 0) file.addChild("width", "https://schema.org/").setContent(String.valueOf(width));
1248
1249 file.removeChild(file.findChild("height", "https://schema.org/"));
1250 if (height > 0) file.addChild("height", "https://schema.org/").setContent(String.valueOf(height));
1251
1252 file.removeChild(file.findChild("duration", "https://schema.org/"));
1253 if (runtime > 0) file.addChild("duration", "https://schema.org/").setContent("PT" + runtime + "S");
1254
1255 if (url != null) {
1256 Element sources = mediaSharing.findChild("sources", mediaSharing.getNamespace());
1257 if (sources == null) sources = mediaSharing.addChild("sources", mediaSharing.getNamespace());
1258
1259 Element source = sources.findChild("reference", "urn:xmpp:reference:0");
1260 if (source == null) source = sources.addChild("reference", "urn:xmpp:reference:0");
1261 source.setAttribute("type", "data");
1262 source.setAttribute("uri", url);
1263 }
1264
1265 return sims;
1266 }
1267
1268 protected Element getFileElement() {
1269 Element file = null;
1270 if (sims == null) return file;
1271
1272 Element mediaSharing = sims.findChild("media-sharing", "urn:xmpp:sims:1");
1273 if (mediaSharing == null) return file;
1274 file = mediaSharing.findChild("file", "urn:xmpp:jingle:apps:file-transfer:5");
1275 if (file == null) file = mediaSharing.findChild("file", "urn:xmpp:jingle:apps:file-transfer:4");
1276 if (file == null) file = mediaSharing.findChild("file", "urn:xmpp:jingle:apps:file-transfer:3");
1277 return file;
1278 }
1279
1280 public List<Cid> getCids() {
1281 List<Cid> cids = new ArrayList<>();
1282 Element file = getFileElement();
1283 if (file == null) return cids;
1284
1285 for (Element child : file.getChildren()) {
1286 if (child.getName().equals("hash") && child.getNamespace().equals("urn:xmpp:hashes:2")) {
1287 try {
1288 cids.add(CryptoHelper.cid(Base64.decode(child.getContent(), Base64.DEFAULT), child.getAttribute("algo")));
1289 } catch (final NoSuchAlgorithmException | IllegalStateException e) { }
1290 }
1291 }
1292
1293 cids.sort((x, y) -> y.getType().compareTo(x.getType()));
1294
1295 return cids;
1296 }
1297
1298 public List<Element> getThumbnails() {
1299 List<Element> thumbs = new ArrayList<>();
1300 Element file = getFileElement();
1301 if (file == null) return thumbs;
1302
1303 for (Element child : file.getChildren()) {
1304 if (child.getName().equals("thumbnail") && child.getNamespace().equals("urn:xmpp:thumbs:1")) {
1305 thumbs.add(child);
1306 }
1307 }
1308
1309 return thumbs;
1310 }
1311
1312 public String toString() {
1313 final StringBuilder builder = new StringBuilder();
1314 if (url != null) builder.append(url);
1315 if (size != null) builder.append('|').append(size.toString());
1316 if (width > 0 || height > 0 || runtime > 0) builder.append('|').append(width);
1317 if (height > 0 || runtime > 0) builder.append('|').append(height);
1318 if (runtime > 0) builder.append('|').append(runtime);
1319 return builder.toString();
1320 }
1321
1322 public boolean equals(Object o) {
1323 if (!(o instanceof FileParams)) return false;
1324 if (url == null) return false;
1325
1326 return url.equals(((FileParams) o).url);
1327 }
1328
1329 public int hashCode() {
1330 return url == null ? super.hashCode() : url.hashCode();
1331 }
1332 }
1333
1334 public void setFingerprint(String fingerprint) {
1335 this.axolotlFingerprint = fingerprint;
1336 }
1337
1338 public String getFingerprint() {
1339 return axolotlFingerprint;
1340 }
1341
1342 public boolean isTrusted() {
1343 final AxolotlService axolotlService = conversation.getAccount().getAxolotlService();
1344 final FingerprintStatus s = axolotlService != null ? axolotlService.getFingerprintTrust(axolotlFingerprint) : null;
1345 return s != null && s.isTrusted();
1346 }
1347
1348 private int getPreviousEncryption() {
1349 for (Message iterator = this.prev(); iterator != null; iterator = iterator.prev()) {
1350 if (iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED) {
1351 continue;
1352 }
1353 return iterator.getEncryption();
1354 }
1355 return ENCRYPTION_NONE;
1356 }
1357
1358 private int getNextEncryption() {
1359 if (this.conversation instanceof Conversation) {
1360 Conversation conversation = (Conversation) this.conversation;
1361 for (Message iterator = this.next(); iterator != null; iterator = iterator.next()) {
1362 if (iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED) {
1363 continue;
1364 }
1365 return iterator.getEncryption();
1366 }
1367 return conversation.getNextEncryption();
1368 } else {
1369 throw new AssertionError("This should never be called since isInValidSession should be disabled for stubs");
1370 }
1371 }
1372
1373 public boolean isValidInSession() {
1374 int pastEncryption = getCleanedEncryption(this.getPreviousEncryption());
1375 int futureEncryption = getCleanedEncryption(this.getNextEncryption());
1376
1377 boolean inUnencryptedSession = pastEncryption == ENCRYPTION_NONE
1378 || futureEncryption == ENCRYPTION_NONE
1379 || pastEncryption != futureEncryption;
1380
1381 return inUnencryptedSession || getCleanedEncryption(this.getEncryption()) == pastEncryption;
1382 }
1383
1384 private static int getCleanedEncryption(int encryption) {
1385 if (encryption == ENCRYPTION_DECRYPTED || encryption == ENCRYPTION_DECRYPTION_FAILED) {
1386 return ENCRYPTION_PGP;
1387 }
1388 if (encryption == ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE || encryption == ENCRYPTION_AXOLOTL_FAILED) {
1389 return ENCRYPTION_AXOLOTL;
1390 }
1391 return encryption;
1392 }
1393
1394 public static boolean configurePrivateMessage(final Message message) {
1395 return configurePrivateMessage(message, false);
1396 }
1397
1398 public static boolean configurePrivateFileMessage(final Message message) {
1399 return configurePrivateMessage(message, true);
1400 }
1401
1402 private static boolean configurePrivateMessage(final Message message, final boolean isFile) {
1403 final Conversation conversation;
1404 if (message.conversation instanceof Conversation) {
1405 conversation = (Conversation) message.conversation;
1406 } else {
1407 return false;
1408 }
1409 if (conversation.getMode() == Conversation.MODE_MULTI) {
1410 final Jid nextCounterpart = conversation.getNextCounterpart();
1411 return configurePrivateMessage(conversation, message, nextCounterpart, isFile);
1412 }
1413 return false;
1414 }
1415
1416 public static boolean configurePrivateMessage(final Message message, final Jid counterpart) {
1417 final Conversation conversation;
1418 if (message.conversation instanceof Conversation) {
1419 conversation = (Conversation) message.conversation;
1420 } else {
1421 return false;
1422 }
1423 return configurePrivateMessage(conversation, message, counterpart, false);
1424 }
1425
1426 private static boolean configurePrivateMessage(final Conversation conversation, final Message message, final Jid counterpart, final boolean isFile) {
1427 if (counterpart == null) {
1428 return false;
1429 }
1430 message.setCounterpart(counterpart);
1431 message.setTrueCounterpart(conversation.getMucOptions().getTrueCounterpart(counterpart));
1432 message.setType(isFile ? Message.TYPE_PRIVATE_FILE : Message.TYPE_PRIVATE);
1433 return true;
1434 }
1435}