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 void setDeleted(boolean deleted) {
556 this.deleted = deleted;
557 }
558
559 public void markRead() {
560 this.read = true;
561 }
562
563 public void markUnread() {
564 this.read = false;
565 }
566
567 public void setTime(long time) {
568 this.timeSent = time;
569 }
570
571 public void setTimeReceived(long time) {
572 this.timeReceived = time;
573 }
574
575 public String getEncryptedBody() {
576 return this.encryptedBody;
577 }
578
579 public void setEncryptedBody(String body) {
580 this.encryptedBody = body;
581 }
582
583 public int getType() {
584 return this.type;
585 }
586
587 public void setType(int type) {
588 this.type = type;
589 }
590
591 public boolean isCarbon() {
592 return carbon;
593 }
594
595 public void setCarbon(boolean carbon) {
596 this.carbon = carbon;
597 }
598
599 public void putEdited(String edited, String serverMsgId) {
600 final Edit edit = new Edit(edited, serverMsgId);
601 if (this.edits.size() < 128 && !this.edits.contains(edit)) {
602 this.edits.add(edit);
603 }
604 }
605
606 boolean remoteMsgIdMatchInEdit(String id) {
607 for (Edit edit : this.edits) {
608 if (id.equals(edit.getEditedId())) {
609 return true;
610 }
611 }
612 return false;
613 }
614
615 public String getBodyLanguage() {
616 return this.bodyLanguage;
617 }
618
619 public void setBodyLanguage(String language) {
620 this.bodyLanguage = language;
621 }
622
623 public boolean edited() {
624 return this.edits.size() > 0;
625 }
626
627 public void setTrueCounterpart(Jid trueCounterpart) {
628 this.trueCounterpart = trueCounterpart;
629 }
630
631 public Jid getTrueCounterpart() {
632 return this.trueCounterpart;
633 }
634
635 public Transferable getTransferable() {
636 return this.transferable;
637 }
638
639 public synchronized void setTransferable(Transferable transferable) {
640 this.transferable = transferable;
641 }
642
643 public boolean addReadByMarker(ReadByMarker readByMarker) {
644 if (readByMarker.getRealJid() != null) {
645 if (readByMarker.getRealJid().asBareJid().equals(trueCounterpart)) {
646 return false;
647 }
648 } else if (readByMarker.getFullJid() != null) {
649 if (readByMarker.getFullJid().equals(counterpart)) {
650 return false;
651 }
652 }
653 if (this.readByMarkers.add(readByMarker)) {
654 if (readByMarker.getRealJid() != null && readByMarker.getFullJid() != null) {
655 Iterator<ReadByMarker> iterator = this.readByMarkers.iterator();
656 while (iterator.hasNext()) {
657 ReadByMarker marker = iterator.next();
658 if (marker.getRealJid() == null && readByMarker.getFullJid().equals(marker.getFullJid())) {
659 iterator.remove();
660 }
661 }
662 }
663 return true;
664 } else {
665 return false;
666 }
667 }
668
669 public Set<ReadByMarker> getReadByMarkers() {
670 return ImmutableSet.copyOf(this.readByMarkers);
671 }
672
673 boolean similar(Message message) {
674 if (!isPrivateMessage() && this.serverMsgId != null && message.getServerMsgId() != null) {
675 return this.serverMsgId.equals(message.getServerMsgId()) || Edit.wasPreviouslyEditedServerMsgId(edits, message.getServerMsgId());
676 } else if (Edit.wasPreviouslyEditedServerMsgId(edits, message.getServerMsgId())) {
677 return true;
678 } else if (this.body == null || this.counterpart == null) {
679 return false;
680 } else {
681 String body, otherBody;
682 if (this.hasFileOnRemoteHost()) {
683 body = getFileParams().url;
684 otherBody = message.body == null ? null : message.body.trim();
685 } else {
686 body = this.body;
687 otherBody = message.body;
688 }
689 final boolean matchingCounterpart = this.counterpart.equals(message.getCounterpart());
690 if (message.getRemoteMsgId() != null) {
691 final boolean hasUuid = CryptoHelper.UUID_PATTERN.matcher(message.getRemoteMsgId()).matches();
692 if (hasUuid && matchingCounterpart && Edit.wasPreviouslyEditedRemoteMsgId(edits, message.getRemoteMsgId())) {
693 return true;
694 }
695 return (message.getRemoteMsgId().equals(this.remoteMsgId) || message.getRemoteMsgId().equals(this.uuid))
696 && matchingCounterpart
697 && (body.equals(otherBody) || (message.getEncryption() == Message.ENCRYPTION_PGP && hasUuid));
698 } else {
699 return this.remoteMsgId == null
700 && matchingCounterpart
701 && body.equals(otherBody)
702 && Math.abs(this.getTimeSent() - message.getTimeSent()) < Config.MESSAGE_MERGE_WINDOW * 1000;
703 }
704 }
705 }
706
707 public Message next() {
708 if (this.conversation instanceof Conversation) {
709 final Conversation conversation = (Conversation) this.conversation;
710 synchronized (conversation.messages) {
711 if (this.mNextMessage == null) {
712 int index = conversation.messages.indexOf(this);
713 if (index < 0 || index >= conversation.messages.size() - 1) {
714 this.mNextMessage = null;
715 } else {
716 this.mNextMessage = conversation.messages.get(index + 1);
717 }
718 }
719 return this.mNextMessage;
720 }
721 } else {
722 throw new AssertionError("Calling next should be disabled for stubs");
723 }
724 }
725
726 public Message prev() {
727 if (this.conversation instanceof Conversation) {
728 final Conversation conversation = (Conversation) this.conversation;
729 synchronized (conversation.messages) {
730 if (this.mPreviousMessage == null) {
731 int index = conversation.messages.indexOf(this);
732 if (index <= 0 || index > conversation.messages.size()) {
733 this.mPreviousMessage = null;
734 } else {
735 this.mPreviousMessage = conversation.messages.get(index - 1);
736 }
737 }
738 }
739 return this.mPreviousMessage;
740 } else {
741 throw new AssertionError("Calling prev should be disabled for stubs");
742 }
743 }
744
745 public boolean isLastCorrectableMessage() {
746 Message next = next();
747 while (next != null) {
748 if (next.isEditable()) {
749 return false;
750 }
751 next = next.next();
752 }
753 return isEditable();
754 }
755
756 public boolean isEditable() {
757 return status != STATUS_RECEIVED && !isCarbon() && type != Message.TYPE_RTP_SESSION;
758 }
759
760 public boolean mergeable(final Message message) {
761 return message != null &&
762 (message.getType() == Message.TYPE_TEXT &&
763 this.getTransferable() == null &&
764 message.getTransferable() == null &&
765 message.getEncryption() != Message.ENCRYPTION_PGP &&
766 message.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED &&
767 this.getType() == message.getType() &&
768 this.getSubject() != null &&
769 isStatusMergeable(this.getStatus(), message.getStatus()) &&
770 isEncryptionMergeable(this.getEncryption(),message.getEncryption()) &&
771 this.getCounterpart() != null &&
772 this.getCounterpart().equals(message.getCounterpart()) &&
773 this.edited() == message.edited() &&
774 (message.getTimeSent() - this.getTimeSent()) <= (Config.MESSAGE_MERGE_WINDOW * 1000) &&
775 this.getBody().length() + message.getBody().length() <= Config.MAX_DISPLAY_MESSAGE_CHARS &&
776 !message.isGeoUri() &&
777 !this.isGeoUri() &&
778 !message.isOOb() &&
779 !this.isOOb() &&
780 !message.treatAsDownloadable() &&
781 !this.treatAsDownloadable() &&
782 !message.hasMeCommand() &&
783 !this.hasMeCommand() &&
784 !this.bodyIsOnlyEmojis() &&
785 !message.bodyIsOnlyEmojis() &&
786 ((this.axolotlFingerprint == null && message.axolotlFingerprint == null) || this.axolotlFingerprint.equals(message.getFingerprint())) &&
787 UIHelper.sameDay(message.getTimeSent(), this.getTimeSent()) &&
788 this.getReadByMarkers().equals(message.getReadByMarkers()) &&
789 !this.conversation.getJid().asBareJid().equals(Config.BUG_REPORTS)
790 );
791 }
792
793 private static boolean isStatusMergeable(int a, int b) {
794 return a == b || (
795 (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_UNSEND)
796 || (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_SEND)
797 || (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_WAITING)
798 || (a == Message.STATUS_SEND && b == Message.STATUS_UNSEND)
799 || (a == Message.STATUS_SEND && b == Message.STATUS_WAITING)
800 );
801 }
802
803 private static boolean isEncryptionMergeable(final int a, final int b) {
804 return a == b
805 && Arrays.asList(ENCRYPTION_NONE, ENCRYPTION_DECRYPTED, ENCRYPTION_AXOLOTL)
806 .contains(a);
807 }
808
809 public void setCounterparts(List<MucOptions.User> counterparts) {
810 this.counterparts = counterparts;
811 }
812
813 public List<MucOptions.User> getCounterparts() {
814 return this.counterparts;
815 }
816
817 @Override
818 public int getAvatarBackgroundColor() {
819 if (type == Message.TYPE_STATUS && getCounterparts() != null && getCounterparts().size() > 1) {
820 return Color.TRANSPARENT;
821 } else {
822 return UIHelper.getColorForName(UIHelper.getMessageDisplayName(this));
823 }
824 }
825
826 @Override
827 public String getAvatarName() {
828 return UIHelper.getMessageDisplayName(this);
829 }
830
831 public boolean isOOb() {
832 return oob || getFileParams().url != null;
833 }
834
835 public static class MergeSeparator {
836 }
837
838 public SpannableStringBuilder getSpannableBody(GetThumbnailForCid thumbnailer, Drawable fallbackImg) {
839 final Element html = getHtml();
840 if (html == null || Build.VERSION.SDK_INT < 24) {
841 return new SpannableStringBuilder(MessageUtils.filterLtrRtl(getBody()).trim());
842 } else {
843 SpannableStringBuilder spannable = new SpannableStringBuilder(Html.fromHtml(
844 MessageUtils.filterLtrRtl(html.toString()).trim(),
845 Html.FROM_HTML_MODE_COMPACT,
846 (source) -> {
847 try {
848 if (thumbnailer == null) return fallbackImg;
849 Cid cid = BobTransfer.cid(new URI(source));
850 if (cid == null) return fallbackImg;
851 Drawable thumbnail = thumbnailer.getThumbnail(cid);
852 if (thumbnail == null) return fallbackImg;
853 return thumbnail;
854 } catch (final URISyntaxException e) {
855 return fallbackImg;
856 }
857 },
858 (opening, tag, output, xmlReader) -> {}
859 ));
860
861 // Make images clickable and long-clickable with BetterLinkMovementMethod
862 ImageSpan[] imageSpans = spannable.getSpans(0, spannable.length(), ImageSpan.class);
863 for (ImageSpan span : imageSpans) {
864 final int start = spannable.getSpanStart(span);
865 final int end = spannable.getSpanEnd(span);
866
867 ClickableSpan click_span = new ClickableSpan() {
868 @Override
869 public void onClick(View widget) { }
870 };
871
872 spannable.setSpan(click_span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
873 }
874
875 // https://stackoverflow.com/a/10187511/8611
876 int i = spannable.length();
877 while(--i >= 0 && Character.isWhitespace(spannable.charAt(i))) { }
878 return (SpannableStringBuilder) spannable.subSequence(0, i+1);
879 }
880 }
881
882 public SpannableStringBuilder getMergedBody() {
883 return getMergedBody(null, null);
884 }
885
886 public SpannableStringBuilder getMergedBody(GetThumbnailForCid thumbnailer, Drawable fallbackImg) {
887 SpannableStringBuilder body = getSpannableBody(thumbnailer, fallbackImg);
888 Message current = this;
889 while (current.mergeable(current.next())) {
890 current = current.next();
891 if (current == null) {
892 break;
893 }
894 body.append("\n\n");
895 body.setSpan(new MergeSeparator(), body.length() - 2, body.length(),
896 SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE);
897 body.append(current.getSpannableBody(thumbnailer, fallbackImg));
898 }
899 return body;
900 }
901
902 public boolean hasMeCommand() {
903 return this.body.trim().startsWith(ME_COMMAND);
904 }
905
906 public int getMergedStatus() {
907 int status = this.status;
908 Message current = this;
909 while (current.mergeable(current.next())) {
910 current = current.next();
911 if (current == null) {
912 break;
913 }
914 status = current.status;
915 }
916 return status;
917 }
918
919 public long getMergedTimeSent() {
920 long time = this.timeSent;
921 Message current = this;
922 while (current.mergeable(current.next())) {
923 current = current.next();
924 if (current == null) {
925 break;
926 }
927 time = current.timeSent;
928 }
929 return time;
930 }
931
932 public boolean wasMergedIntoPrevious() {
933 Message prev = this.prev();
934 return prev != null && prev.mergeable(this);
935 }
936
937 public boolean trusted() {
938 Contact contact = this.getContact();
939 return status > STATUS_RECEIVED || (contact != null && (contact.showInContactList() || contact.isSelf()));
940 }
941
942 public boolean fixCounterpart() {
943 final Presences presences = conversation.getContact().getPresences();
944 if (counterpart != null && presences.has(Strings.nullToEmpty(counterpart.getResource()))) {
945 return true;
946 } else if (presences.size() >= 1) {
947 counterpart = PresenceSelector.getNextCounterpart(getContact(), presences.toResourceArray()[0]);
948 return true;
949 } else {
950 counterpart = null;
951 return false;
952 }
953 }
954
955 public void setUuid(String uuid) {
956 this.uuid = uuid;
957 }
958
959 public String getEditedId() {
960 if (edits.size() > 0) {
961 return edits.get(edits.size() - 1).getEditedId();
962 } else {
963 throw new IllegalStateException("Attempting to store unedited message");
964 }
965 }
966
967 public String getEditedIdWireFormat() {
968 if (edits.size() > 0) {
969 return edits.get(Config.USE_LMC_VERSION_1_1 ? 0 : edits.size() - 1).getEditedId();
970 } else {
971 throw new IllegalStateException("Attempting to store unedited message");
972 }
973 }
974
975 public URI getOob() {
976 final String url = getFileParams().url;
977 try {
978 return url == null ? null : new URI(url);
979 } catch (final URISyntaxException e) {
980 return null;
981 }
982 }
983
984 public void clearPayloads() {
985 this.payloads.clear();
986 }
987
988 public void addPayload(Element el) {
989 if (el == null) return;
990
991 this.payloads.add(el);
992 }
993
994 public List<Element> getPayloads() {
995 return new ArrayList<>(this.payloads);
996 }
997
998 public List<Element> getFallbacks() {
999 List<Element> fallbacks = new ArrayList<>();
1000
1001 if (this.payloads == null) return fallbacks;
1002
1003 for (Element el : this.payloads) {
1004 if (el.getName().equals("fallback") && el.getNamespace().equals("urn:xmpp:fallback:0")) {
1005 final String fallbackFor = el.getAttribute("for");
1006 if (fallbackFor == null) continue;
1007 if (fallbackFor.equals("http://jabber.org/protocol/address") || fallbackFor.equals(Namespace.OOB)) {
1008 fallbacks.add(el);
1009 }
1010 }
1011 }
1012
1013 return fallbacks;
1014 }
1015
1016 public Element getHtml() {
1017 if (this.payloads == null) return null;
1018
1019 for (Element el : this.payloads) {
1020 if (el.getName().equals("html") && el.getNamespace().equals("http://jabber.org/protocol/xhtml-im")) {
1021 return el.getChildren().get(0);
1022 }
1023 }
1024
1025 return null;
1026 }
1027
1028 public List<Element> getCommands() {
1029 if (this.payloads == null) return null;
1030
1031 for (Element el : this.payloads) {
1032 if (el.getName().equals("query") && el.getNamespace().equals("http://jabber.org/protocol/disco#items") && el.getAttribute("node").equals("http://jabber.org/protocol/commands")) {
1033 return el.getChildren();
1034 }
1035 }
1036
1037 return null;
1038 }
1039
1040 public String getMimeType() {
1041 String extension;
1042 if (relativeFilePath != null) {
1043 extension = MimeUtils.extractRelevantExtension(relativeFilePath);
1044 } else {
1045 final String url = URL.tryParse(getOob() == null ? body.split("\n")[0] : getOob().toString());
1046 if (url == null) {
1047 return null;
1048 }
1049 extension = MimeUtils.extractRelevantExtension(url);
1050 }
1051 return MimeUtils.guessMimeTypeFromExtension(extension);
1052 }
1053
1054 public synchronized boolean treatAsDownloadable() {
1055 if (treatAsDownloadable == null) {
1056 treatAsDownloadable = MessageUtils.treatAsDownloadable(this.body, isOOb());
1057 }
1058 return treatAsDownloadable;
1059 }
1060
1061 public synchronized boolean bodyIsOnlyEmojis() {
1062 if (isEmojisOnly == null) {
1063 isEmojisOnly = Emoticons.isOnlyEmoji(getBody().replaceAll("\\s", ""));
1064 }
1065 return isEmojisOnly;
1066 }
1067
1068 public synchronized boolean isGeoUri() {
1069 if (isGeoUri == null) {
1070 isGeoUri = GeoHelper.GEO_URI.matcher(body).matches();
1071 }
1072 return isGeoUri;
1073 }
1074
1075 protected List<Element> getSims() {
1076 return payloads.stream().filter(el ->
1077 el.getName().equals("reference") && el.getNamespace().equals("urn:xmpp:reference:0") &&
1078 el.findChild("media-sharing", "urn:xmpp:sims:1") != null
1079 ).collect(Collectors.toList());
1080 }
1081
1082 public synchronized void resetFileParams() {
1083 this.fileParams = null;
1084 }
1085
1086 public synchronized void setFileParams(FileParams fileParams) {
1087 if (fileParams != null && this.fileParams != null && this.fileParams.sims != null && fileParams.sims == null) {
1088 fileParams.sims = this.fileParams.sims;
1089 }
1090 this.fileParams = fileParams;
1091 }
1092
1093 public synchronized FileParams getFileParams() {
1094 if (fileParams == null) {
1095 List<Element> sims = getSims();
1096 fileParams = sims.isEmpty() ? new FileParams(oob ? this.body : "") : new FileParams(sims.get(0));
1097 if (this.transferable != null) {
1098 fileParams.size = this.transferable.getFileSize();
1099 }
1100 }
1101
1102 return fileParams;
1103 }
1104
1105 private static int parseInt(String value) {
1106 try {
1107 return Integer.parseInt(value);
1108 } catch (NumberFormatException e) {
1109 return 0;
1110 }
1111 }
1112
1113 public void untie() {
1114 this.mNextMessage = null;
1115 this.mPreviousMessage = null;
1116 }
1117
1118 public boolean isPrivateMessage() {
1119 return type == TYPE_PRIVATE || type == TYPE_PRIVATE_FILE;
1120 }
1121
1122 public boolean isFileOrImage() {
1123 return type == TYPE_FILE || type == TYPE_IMAGE || type == TYPE_PRIVATE_FILE;
1124 }
1125
1126
1127 public boolean isTypeText() {
1128 return type == TYPE_TEXT || type == TYPE_PRIVATE;
1129 }
1130
1131 public boolean hasFileOnRemoteHost() {
1132 return isFileOrImage() && getFileParams().url != null;
1133 }
1134
1135 public boolean needsUploading() {
1136 return isFileOrImage() && getFileParams().url == null;
1137 }
1138
1139 public static class FileParams {
1140 public String url;
1141 public Long size = null;
1142 public int width = 0;
1143 public int height = 0;
1144 public int runtime = 0;
1145 public Element sims = null;
1146
1147 public FileParams() { }
1148
1149 public FileParams(Element el) {
1150 if (el.getName().equals("x") && el.getNamespace().equals(Namespace.OOB)) {
1151 this.url = el.findChildContent("url", Namespace.OOB);
1152 }
1153 if (el.getName().equals("reference") && el.getNamespace().equals("urn:xmpp:reference:0")) {
1154 sims = el;
1155 final String refUri = el.getAttribute("uri");
1156 if (refUri != null) url = refUri;
1157 final Element mediaSharing = el.findChild("media-sharing", "urn:xmpp:sims:1");
1158 if (mediaSharing != null) {
1159 Element file = mediaSharing.findChild("file", "urn:xmpp:jingle:apps:file-transfer:5");
1160 if (file == null) file = mediaSharing.findChild("file", "urn:xmpp:jingle:apps:file-transfer:4");
1161 if (file == null) file = mediaSharing.findChild("file", "urn:xmpp:jingle:apps:file-transfer:3");
1162 if (file != null) {
1163 String sizeS = file.findChildContent("size", file.getNamespace());
1164 if (sizeS != null) size = new Long(sizeS);
1165 String widthS = file.findChildContent("width", "https://schema.org/");
1166 if (widthS != null) width = parseInt(widthS);
1167 String heightS = file.findChildContent("height", "https://schema.org/");
1168 if (heightS != null) height = parseInt(heightS);
1169 String durationS = file.findChildContent("duration", "https://schema.org/");
1170 if (durationS != null) runtime = (int)(Duration.parse(durationS).toMillis() / 1000L);
1171 }
1172
1173 final Element sources = mediaSharing.findChild("sources", "urn:xmpp:sims:1");
1174 if (sources != null) {
1175 final Element ref = sources.findChild("reference", "urn:xmpp:reference:0");
1176 if (ref != null) url = ref.getAttribute("uri");
1177 }
1178 }
1179 }
1180 }
1181
1182 public FileParams(String ser) {
1183 final String[] parts = ser == null ? new String[0] : ser.split("\\|");
1184 switch (parts.length) {
1185 case 1:
1186 try {
1187 this.size = Long.parseLong(parts[0]);
1188 } catch (final NumberFormatException e) {
1189 this.url = URL.tryParse(parts[0]);
1190 }
1191 break;
1192 case 5:
1193 this.runtime = parseInt(parts[4]);
1194 case 4:
1195 this.width = parseInt(parts[2]);
1196 this.height = parseInt(parts[3]);
1197 case 2:
1198 this.url = URL.tryParse(parts[0]);
1199 this.size = Longs.tryParse(parts[1]);
1200 break;
1201 case 3:
1202 this.size = Longs.tryParse(parts[0]);
1203 this.width = parseInt(parts[1]);
1204 this.height = parseInt(parts[2]);
1205 break;
1206 }
1207 }
1208
1209 public long getSize() {
1210 return size == null ? 0 : size;
1211 }
1212
1213 public String getName() {
1214 Element file = getFileElement();
1215 if (file == null) return null;
1216
1217 return file.findChildContent("name", file.getNamespace());
1218 }
1219
1220 public Element toSims() {
1221 if (sims == null) sims = new Element("reference", "urn:xmpp:reference:0");
1222 sims.setAttribute("type", "data");
1223 Element mediaSharing = sims.findChild("media-sharing", "urn:xmpp:sims:1");
1224 if (mediaSharing == null) mediaSharing = sims.addChild("media-sharing", "urn:xmpp:sims:1");
1225
1226 Element file = mediaSharing.findChild("file", "urn:xmpp:jingle:apps:file-transfer:5");
1227 if (file == null) file = mediaSharing.findChild("file", "urn:xmpp:jingle:apps:file-transfer:4");
1228 if (file == null) file = mediaSharing.findChild("file", "urn:xmpp:jingle:apps:file-transfer:3");
1229 if (file == null) file = mediaSharing.addChild("file", "urn:xmpp:jingle:apps:file-transfer:5");
1230
1231 file.removeChild(file.findChild("size", file.getNamespace()));
1232 if (size != null) file.addChild("size", file.getNamespace()).setContent(size.toString());
1233
1234 file.removeChild(file.findChild("width", "https://schema.org/"));
1235 if (width > 0) file.addChild("width", "https://schema.org/").setContent(String.valueOf(width));
1236
1237 file.removeChild(file.findChild("height", "https://schema.org/"));
1238 if (height > 0) file.addChild("height", "https://schema.org/").setContent(String.valueOf(height));
1239
1240 file.removeChild(file.findChild("duration", "https://schema.org/"));
1241 if (runtime > 0) file.addChild("duration", "https://schema.org/").setContent("PT" + runtime + "S");
1242
1243 if (url != null) {
1244 Element sources = mediaSharing.findChild("sources", mediaSharing.getNamespace());
1245 if (sources == null) sources = mediaSharing.addChild("sources", mediaSharing.getNamespace());
1246
1247 Element source = sources.findChild("reference", "urn:xmpp:reference:0");
1248 if (source == null) source = sources.addChild("reference", "urn:xmpp:reference:0");
1249 source.setAttribute("type", "data");
1250 source.setAttribute("uri", url);
1251 }
1252
1253 return sims;
1254 }
1255
1256 protected Element getFileElement() {
1257 Element file = null;
1258 if (sims == null) return file;
1259
1260 Element mediaSharing = sims.findChild("media-sharing", "urn:xmpp:sims:1");
1261 if (mediaSharing == null) return file;
1262 file = mediaSharing.findChild("file", "urn:xmpp:jingle:apps:file-transfer:5");
1263 if (file == null) file = mediaSharing.findChild("file", "urn:xmpp:jingle:apps:file-transfer:4");
1264 if (file == null) file = mediaSharing.findChild("file", "urn:xmpp:jingle:apps:file-transfer:3");
1265 return file;
1266 }
1267
1268 public List<Cid> getCids() {
1269 List<Cid> cids = new ArrayList<>();
1270 Element file = getFileElement();
1271 if (file == null) return cids;
1272
1273 for (Element child : file.getChildren()) {
1274 if (child.getName().equals("hash") && child.getNamespace().equals("urn:xmpp:hashes:2")) {
1275 try {
1276 cids.add(CryptoHelper.cid(Base64.decode(child.getContent(), Base64.DEFAULT), child.getAttribute("algo")));
1277 } catch (final NoSuchAlgorithmException | IllegalStateException e) { }
1278 }
1279 }
1280
1281 cids.sort((x, y) -> y.getType().compareTo(x.getType()));
1282
1283 return cids;
1284 }
1285
1286 public List<Element> getThumbnails() {
1287 List<Element> thumbs = new ArrayList<>();
1288 Element file = getFileElement();
1289 if (file == null) return thumbs;
1290
1291 for (Element child : file.getChildren()) {
1292 if (child.getName().equals("thumbnail") && child.getNamespace().equals("urn:xmpp:thumbs:1")) {
1293 thumbs.add(child);
1294 }
1295 }
1296
1297 return thumbs;
1298 }
1299
1300 public String toString() {
1301 final StringBuilder builder = new StringBuilder();
1302 if (url != null) builder.append(url);
1303 if (size != null) builder.append('|').append(size.toString());
1304 if (width > 0 || height > 0 || runtime > 0) builder.append('|').append(width);
1305 if (height > 0 || runtime > 0) builder.append('|').append(height);
1306 if (runtime > 0) builder.append('|').append(runtime);
1307 return builder.toString();
1308 }
1309
1310 public boolean equals(Object o) {
1311 if (!(o instanceof FileParams)) return false;
1312 if (url == null) return false;
1313
1314 return url.equals(((FileParams) o).url);
1315 }
1316
1317 public int hashCode() {
1318 return url == null ? super.hashCode() : url.hashCode();
1319 }
1320 }
1321
1322 public void setFingerprint(String fingerprint) {
1323 this.axolotlFingerprint = fingerprint;
1324 }
1325
1326 public String getFingerprint() {
1327 return axolotlFingerprint;
1328 }
1329
1330 public boolean isTrusted() {
1331 final AxolotlService axolotlService = conversation.getAccount().getAxolotlService();
1332 final FingerprintStatus s = axolotlService != null ? axolotlService.getFingerprintTrust(axolotlFingerprint) : null;
1333 return s != null && s.isTrusted();
1334 }
1335
1336 private int getPreviousEncryption() {
1337 for (Message iterator = this.prev(); iterator != null; iterator = iterator.prev()) {
1338 if (iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED) {
1339 continue;
1340 }
1341 return iterator.getEncryption();
1342 }
1343 return ENCRYPTION_NONE;
1344 }
1345
1346 private int getNextEncryption() {
1347 if (this.conversation instanceof Conversation) {
1348 Conversation conversation = (Conversation) this.conversation;
1349 for (Message iterator = this.next(); iterator != null; iterator = iterator.next()) {
1350 if (iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED) {
1351 continue;
1352 }
1353 return iterator.getEncryption();
1354 }
1355 return conversation.getNextEncryption();
1356 } else {
1357 throw new AssertionError("This should never be called since isInValidSession should be disabled for stubs");
1358 }
1359 }
1360
1361 public boolean isValidInSession() {
1362 int pastEncryption = getCleanedEncryption(this.getPreviousEncryption());
1363 int futureEncryption = getCleanedEncryption(this.getNextEncryption());
1364
1365 boolean inUnencryptedSession = pastEncryption == ENCRYPTION_NONE
1366 || futureEncryption == ENCRYPTION_NONE
1367 || pastEncryption != futureEncryption;
1368
1369 return inUnencryptedSession || getCleanedEncryption(this.getEncryption()) == pastEncryption;
1370 }
1371
1372 private static int getCleanedEncryption(int encryption) {
1373 if (encryption == ENCRYPTION_DECRYPTED || encryption == ENCRYPTION_DECRYPTION_FAILED) {
1374 return ENCRYPTION_PGP;
1375 }
1376 if (encryption == ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE || encryption == ENCRYPTION_AXOLOTL_FAILED) {
1377 return ENCRYPTION_AXOLOTL;
1378 }
1379 return encryption;
1380 }
1381
1382 public static boolean configurePrivateMessage(final Message message) {
1383 return configurePrivateMessage(message, false);
1384 }
1385
1386 public static boolean configurePrivateFileMessage(final Message message) {
1387 return configurePrivateMessage(message, true);
1388 }
1389
1390 private static boolean configurePrivateMessage(final Message message, final boolean isFile) {
1391 final Conversation conversation;
1392 if (message.conversation instanceof Conversation) {
1393 conversation = (Conversation) message.conversation;
1394 } else {
1395 return false;
1396 }
1397 if (conversation.getMode() == Conversation.MODE_MULTI) {
1398 final Jid nextCounterpart = conversation.getNextCounterpart();
1399 return configurePrivateMessage(conversation, message, nextCounterpart, isFile);
1400 }
1401 return false;
1402 }
1403
1404 public static boolean configurePrivateMessage(final Message message, final Jid counterpart) {
1405 final Conversation conversation;
1406 if (message.conversation instanceof Conversation) {
1407 conversation = (Conversation) message.conversation;
1408 } else {
1409 return false;
1410 }
1411 return configurePrivateMessage(conversation, message, counterpart, false);
1412 }
1413
1414 private static boolean configurePrivateMessage(final Conversation conversation, final Message message, final Jid counterpart, final boolean isFile) {
1415 if (counterpart == null) {
1416 return false;
1417 }
1418 message.setCounterpart(counterpart);
1419 message.setTrueCounterpart(conversation.getMucOptions().getTrueCounterpart(counterpart));
1420 message.setType(isFile ? Message.TYPE_PRIVATE_FILE : Message.TYPE_PRIVATE);
1421 return true;
1422 }
1423}