1package eu.siacs.conversations.entities;
2
3import android.content.ContentValues;
4import android.database.Cursor;
5import android.graphics.Color;
6import android.text.SpannableStringBuilder;
7import android.util.Log;
8
9import com.google.common.base.Strings;
10import com.google.common.collect.Collections2;
11import com.google.common.collect.ImmutableSet;
12import com.google.common.primitives.Longs;
13
14import org.json.JSONException;
15
16import java.lang.ref.WeakReference;
17import java.util.ArrayList;
18import java.util.Arrays;
19import java.util.Collection;
20import java.util.Collections;
21import java.util.Iterator;
22import java.util.List;
23import java.util.Set;
24import java.util.concurrent.CopyOnWriteArraySet;
25
26import eu.siacs.conversations.Config;
27import eu.siacs.conversations.crypto.axolotl.AxolotlService;
28import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
29import eu.siacs.conversations.http.URL;
30import eu.siacs.conversations.services.AvatarService;
31import eu.siacs.conversations.ui.util.PresenceSelector;
32import eu.siacs.conversations.utils.CryptoHelper;
33import eu.siacs.conversations.utils.Emoticons;
34import eu.siacs.conversations.utils.GeoHelper;
35import eu.siacs.conversations.utils.MessageUtils;
36import eu.siacs.conversations.utils.MimeUtils;
37import eu.siacs.conversations.utils.UIHelper;
38import eu.siacs.conversations.xmpp.Jid;
39
40public class Message extends AbstractEntity implements AvatarService.Avatarable {
41
42 public static final String TABLENAME = "messages";
43
44 public static final int STATUS_RECEIVED = 0;
45 public static final int STATUS_UNSEND = 1;
46 public static final int STATUS_SEND = 2;
47 public static final int STATUS_SEND_FAILED = 3;
48 public static final int STATUS_WAITING = 5;
49 public static final int STATUS_OFFERED = 6;
50 public static final int STATUS_SEND_RECEIVED = 7;
51 public static final int STATUS_SEND_DISPLAYED = 8;
52
53 public static final int ENCRYPTION_NONE = 0;
54 public static final int ENCRYPTION_PGP = 1;
55 public static final int ENCRYPTION_OTR = 2;
56 public static final int ENCRYPTION_DECRYPTED = 3;
57 public static final int ENCRYPTION_DECRYPTION_FAILED = 4;
58 public static final int ENCRYPTION_AXOLOTL = 5;
59 public static final int ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE = 6;
60 public static final int ENCRYPTION_AXOLOTL_FAILED = 7;
61
62 public static final int TYPE_TEXT = 0;
63 public static final int TYPE_IMAGE = 1;
64 public static final int TYPE_FILE = 2;
65 public static final int TYPE_STATUS = 3;
66 public static final int TYPE_PRIVATE = 4;
67 public static final int TYPE_PRIVATE_FILE = 5;
68 public static final int TYPE_RTP_SESSION = 6;
69
70 public static final String CONVERSATION = "conversationUuid";
71 public static final String COUNTERPART = "counterpart";
72 public static final String TRUE_COUNTERPART = "trueCounterpart";
73 public static final String BODY = "body";
74 public static final String BODY_LANGUAGE = "bodyLanguage";
75 public static final String TIME_SENT = "timeSent";
76 public static final String ENCRYPTION = "encryption";
77 public static final String STATUS = "status";
78 public static final String TYPE = "type";
79 public static final String CARBON = "carbon";
80 public static final String OOB = "oob";
81 public static final String EDITED = "edited";
82 public static final String REMOTE_MSG_ID = "remoteMsgId";
83 public static final String SERVER_MSG_ID = "serverMsgId";
84 public static final String RELATIVE_FILE_PATH = "relativeFilePath";
85 public static final String FINGERPRINT = "axolotl_fingerprint";
86 public static final String READ = "read";
87 public static final String ERROR_MESSAGE = "errorMsg";
88 public static final String READ_BY_MARKERS = "readByMarkers";
89 public static final String MARKABLE = "markable";
90 public static final String DELETED = "deleted";
91 public static final String OCCUPANT_ID = "occupantId";
92 public static final String REACTIONS = "reactions";
93 public static final String ME_COMMAND = "/me ";
94
95 public static final String ERROR_MESSAGE_CANCELLED = "eu.siacs.conversations.cancelled";
96
97
98 public boolean markable = false;
99 protected String conversationUuid;
100 protected Jid counterpart;
101 protected Jid trueCounterpart;
102 protected String body;
103 protected String encryptedBody;
104 protected long timeSent;
105 protected int encryption;
106 protected int status;
107 protected int type;
108 protected boolean deleted = false;
109 protected boolean carbon = false;
110 protected boolean oob = false;
111 protected List<Edit> edits = new ArrayList<>();
112 protected String relativeFilePath;
113 protected boolean read = true;
114 protected String remoteMsgId = null;
115 private String bodyLanguage = null;
116 protected String serverMsgId = null;
117 private final Conversational conversation;
118 protected Transferable transferable = null;
119 private Message mNextMessage = null;
120 private Message mPreviousMessage = null;
121 private String axolotlFingerprint = null;
122 private String errorMessage = null;
123 private Set<ReadByMarker> readByMarkers = new CopyOnWriteArraySet<>();
124 private String occupantId;
125 private Collection<Reaction> reactions = Collections.emptyList();
126
127 private Boolean isGeoUri = null;
128 private Boolean isEmojisOnly = null;
129 private Boolean treatAsDownloadable = null;
130 private FileParams fileParams = null;
131 private List<MucOptions.User> counterparts;
132 private WeakReference<MucOptions.User> user;
133
134 protected Message(Conversational conversation) {
135 this.conversation = conversation;
136 }
137
138 public Message(Conversational conversation, String body, int encryption) {
139 this(conversation, body, encryption, STATUS_UNSEND);
140 }
141
142 public Message(Conversational conversation, String body, int encryption, int status) {
143 this(conversation, java.util.UUID.randomUUID().toString(),
144 conversation.getUuid(),
145 conversation.getJid() == null ? null : conversation.getJid().asBareJid(),
146 null,
147 body,
148 System.currentTimeMillis(),
149 encryption,
150 status,
151 TYPE_TEXT,
152 false,
153 null,
154 null,
155 null,
156 null,
157 true,
158 null,
159 false,
160 null,
161 null,
162 false,
163 false,
164 null,
165 null,
166 Collections.emptyList());
167 }
168
169 public Message(Conversation conversation, int status, int type, final String remoteMsgId) {
170 this(conversation, java.util.UUID.randomUUID().toString(),
171 conversation.getUuid(),
172 conversation.getJid() == null ? null : conversation.getJid().asBareJid(),
173 null,
174 null,
175 System.currentTimeMillis(),
176 Message.ENCRYPTION_NONE,
177 status,
178 type,
179 false,
180 remoteMsgId,
181 null,
182 null,
183 null,
184 true,
185 null,
186 false,
187 null,
188 null,
189 false,
190 false,
191 null,
192 null,
193 Collections.emptyList());
194 }
195
196 protected Message(final Conversational conversation, final String uuid, final String conversationUUid, final Jid counterpart,
197 final Jid trueCounterpart, final String body, final long timeSent,
198 final int encryption, final int status, final int type, final boolean carbon,
199 final String remoteMsgId, final String relativeFilePath,
200 final String serverMsgId, final String fingerprint, final boolean read,
201 final String edited, final boolean oob, final String errorMessage, final Set<ReadByMarker> readByMarkers,
202 final boolean markable, final boolean deleted, final String bodyLanguage, final String occupantId, final Collection<Reaction> reactions) {
203 this.conversation = conversation;
204 this.uuid = uuid;
205 this.conversationUuid = conversationUUid;
206 this.counterpart = counterpart;
207 this.trueCounterpart = trueCounterpart;
208 this.body = body == null ? "" : body;
209 this.timeSent = timeSent;
210 this.encryption = encryption;
211 this.status = status;
212 this.type = type;
213 this.carbon = carbon;
214 this.remoteMsgId = remoteMsgId;
215 this.relativeFilePath = relativeFilePath;
216 this.serverMsgId = serverMsgId;
217 this.axolotlFingerprint = fingerprint;
218 this.read = read;
219 this.edits = Edit.fromJson(edited);
220 this.oob = oob;
221 this.errorMessage = errorMessage;
222 this.readByMarkers = readByMarkers == null ? new CopyOnWriteArraySet<>() : readByMarkers;
223 this.markable = markable;
224 this.deleted = deleted;
225 this.bodyLanguage = bodyLanguage;
226 this.occupantId = occupantId;
227 this.reactions = reactions;
228 }
229
230 public static Message fromCursor(final Cursor cursor, final Conversation conversation) {
231 return new Message(conversation,
232 cursor.getString(cursor.getColumnIndexOrThrow(UUID)),
233 cursor.getString(cursor.getColumnIndexOrThrow(CONVERSATION)),
234 fromString(cursor.getString(cursor.getColumnIndexOrThrow(COUNTERPART))),
235 fromString(cursor.getString(cursor.getColumnIndexOrThrow(TRUE_COUNTERPART))),
236 cursor.getString(cursor.getColumnIndexOrThrow(BODY)),
237 cursor.getLong(cursor.getColumnIndexOrThrow(TIME_SENT)),
238 cursor.getInt(cursor.getColumnIndexOrThrow(ENCRYPTION)),
239 cursor.getInt(cursor.getColumnIndexOrThrow(STATUS)),
240 cursor.getInt(cursor.getColumnIndexOrThrow(TYPE)),
241 cursor.getInt(cursor.getColumnIndexOrThrow(CARBON)) > 0,
242 cursor.getString(cursor.getColumnIndexOrThrow(REMOTE_MSG_ID)),
243 cursor.getString(cursor.getColumnIndexOrThrow(RELATIVE_FILE_PATH)),
244 cursor.getString(cursor.getColumnIndexOrThrow(SERVER_MSG_ID)),
245 cursor.getString(cursor.getColumnIndexOrThrow(FINGERPRINT)),
246 cursor.getInt(cursor.getColumnIndexOrThrow(READ)) > 0,
247 cursor.getString(cursor.getColumnIndexOrThrow(EDITED)),
248 cursor.getInt(cursor.getColumnIndexOrThrow(OOB)) > 0,
249 cursor.getString(cursor.getColumnIndexOrThrow(ERROR_MESSAGE)),
250 ReadByMarker.fromJsonString(cursor.getString(cursor.getColumnIndexOrThrow(READ_BY_MARKERS))),
251 cursor.getInt(cursor.getColumnIndexOrThrow(MARKABLE)) > 0,
252 cursor.getInt(cursor.getColumnIndexOrThrow(DELETED)) > 0,
253 cursor.getString(cursor.getColumnIndexOrThrow(BODY_LANGUAGE)),
254 cursor.getString(cursor.getColumnIndexOrThrow(OCCUPANT_ID)),
255 Reaction.fromString(cursor.getString(cursor.getColumnIndexOrThrow(REACTIONS)))
256
257 );
258 }
259
260 private static Jid fromString(String value) {
261 try {
262 if (value != null) {
263 return Jid.of(value);
264 }
265 } catch (IllegalArgumentException e) {
266 return null;
267 }
268 return null;
269 }
270
271 public static Message createStatusMessage(Conversation conversation, String body) {
272 final Message message = new Message(conversation);
273 message.setType(Message.TYPE_STATUS);
274 message.setStatus(Message.STATUS_RECEIVED);
275 message.body = body;
276 return message;
277 }
278
279 public static Message createLoadMoreMessage(Conversation conversation) {
280 final Message message = new Message(conversation);
281 message.setType(Message.TYPE_STATUS);
282 message.body = "LOAD_MORE";
283 return message;
284 }
285
286 @Override
287 public ContentValues getContentValues() {
288 final var values = new ContentValues();
289 values.put(UUID, uuid);
290 values.put(CONVERSATION, conversationUuid);
291 if (counterpart == null) {
292 values.putNull(COUNTERPART);
293 } else {
294 values.put(COUNTERPART, counterpart.toString());
295 }
296 if (trueCounterpart == null) {
297 values.putNull(TRUE_COUNTERPART);
298 } else {
299 values.put(TRUE_COUNTERPART, trueCounterpart.toString());
300 }
301 values.put(BODY, body.length() > Config.MAX_STORAGE_MESSAGE_CHARS ? body.substring(0, Config.MAX_STORAGE_MESSAGE_CHARS) : body);
302 values.put(TIME_SENT, timeSent);
303 values.put(ENCRYPTION, encryption);
304 values.put(STATUS, status);
305 values.put(TYPE, type);
306 values.put(CARBON, carbon ? 1 : 0);
307 values.put(REMOTE_MSG_ID, remoteMsgId);
308 values.put(RELATIVE_FILE_PATH, relativeFilePath);
309 values.put(SERVER_MSG_ID, serverMsgId);
310 values.put(FINGERPRINT, axolotlFingerprint);
311 values.put(READ, read ? 1 : 0);
312 try {
313 values.put(EDITED, Edit.toJson(edits));
314 } catch (JSONException e) {
315 Log.e(Config.LOGTAG, "error persisting json for edits", e);
316 }
317 values.put(OOB, oob ? 1 : 0);
318 values.put(ERROR_MESSAGE, errorMessage);
319 values.put(READ_BY_MARKERS, ReadByMarker.toJson(readByMarkers).toString());
320 values.put(MARKABLE, markable ? 1 : 0);
321 values.put(DELETED, deleted ? 1 : 0);
322 values.put(BODY_LANGUAGE, bodyLanguage);
323 values.put(OCCUPANT_ID, occupantId);
324 values.put(REACTIONS, Reaction.toString(this.reactions));
325 return values;
326 }
327
328 public String getConversationUuid() {
329 return conversationUuid;
330 }
331
332 public Conversational getConversation() {
333 return this.conversation;
334 }
335
336 public Jid getCounterpart() {
337 return counterpart;
338 }
339
340 public void setCounterpart(final Jid counterpart) {
341 this.counterpart = counterpart;
342 }
343
344 public Contact getContact() {
345 if (this.conversation.getMode() == Conversation.MODE_SINGLE) {
346 return this.conversation.getContact();
347 } else {
348 if (this.trueCounterpart == null) {
349 return null;
350 } else {
351 return this.conversation.getAccount().getRoster()
352 .getContactFromContactList(this.trueCounterpart);
353 }
354 }
355 }
356
357 public String getBody() {
358 return body;
359 }
360
361 public synchronized void setBody(String body) {
362 if (body == null) {
363 throw new Error("You should not set the message body to null");
364 }
365 this.body = body;
366 this.isGeoUri = null;
367 this.isEmojisOnly = null;
368 this.treatAsDownloadable = null;
369 this.fileParams = null;
370 }
371
372 public void setMucUser(MucOptions.User user) {
373 this.user = new WeakReference<>(user);
374 }
375
376 public boolean sameMucUser(Message otherMessage) {
377 final MucOptions.User thisUser = this.user == null ? null : this.user.get();
378 final MucOptions.User otherUser = otherMessage.user == null ? null : otherMessage.user.get();
379 return thisUser != null && thisUser == otherUser;
380 }
381
382 public String getErrorMessage() {
383 return errorMessage;
384 }
385
386 public boolean setErrorMessage(String message) {
387 boolean changed = (message != null && !message.equals(errorMessage))
388 || (message == null && errorMessage != null);
389 this.errorMessage = message;
390 return changed;
391 }
392
393 public long getTimeSent() {
394 return timeSent;
395 }
396
397 public int getEncryption() {
398 return encryption;
399 }
400
401 public void setEncryption(int encryption) {
402 this.encryption = encryption;
403 }
404
405 public int getStatus() {
406 return status;
407 }
408
409 public void setStatus(int status) {
410 this.status = status;
411 }
412
413 public String getRelativeFilePath() {
414 return this.relativeFilePath;
415 }
416
417 public void setRelativeFilePath(String path) {
418 this.relativeFilePath = path;
419 }
420
421 public String getRemoteMsgId() {
422 return this.remoteMsgId;
423 }
424
425 public void setRemoteMsgId(String id) {
426 this.remoteMsgId = id;
427 }
428
429 public String getServerMsgId() {
430 return this.serverMsgId;
431 }
432
433 public void setServerMsgId(String id) {
434 this.serverMsgId = id;
435 }
436
437 public boolean isRead() {
438 return this.read;
439 }
440
441 public boolean isDeleted() {
442 return this.deleted;
443 }
444
445 public void setDeleted(boolean deleted) {
446 this.deleted = deleted;
447 }
448
449 public void markRead() {
450 this.read = true;
451 }
452
453 public void markUnread() {
454 this.read = false;
455 }
456
457 public void setTime(long time) {
458 this.timeSent = time;
459 }
460
461 public String getEncryptedBody() {
462 return this.encryptedBody;
463 }
464
465 public void setEncryptedBody(String body) {
466 this.encryptedBody = body;
467 }
468
469 public int getType() {
470 return this.type;
471 }
472
473 public void setType(int type) {
474 this.type = type;
475 }
476
477 public boolean isCarbon() {
478 return carbon;
479 }
480
481 public void setCarbon(boolean carbon) {
482 this.carbon = carbon;
483 }
484
485 public void putEdited(String edited, String serverMsgId) {
486 final Edit edit = new Edit(edited, serverMsgId);
487 if (this.edits.size() < 128 && !this.edits.contains(edit)) {
488 this.edits.add(edit);
489 }
490 }
491
492 public String getBodyLanguage() {
493 return this.bodyLanguage;
494 }
495
496 public void setBodyLanguage(String language) {
497 this.bodyLanguage = language;
498 }
499
500 public boolean edited() {
501 return !this.edits.isEmpty();
502 }
503
504 public void setTrueCounterpart(Jid trueCounterpart) {
505 this.trueCounterpart = trueCounterpart;
506 }
507
508 public Jid getTrueCounterpart() {
509 return this.trueCounterpart;
510 }
511
512 public Transferable getTransferable() {
513 return this.transferable;
514 }
515
516 public synchronized void setTransferable(Transferable transferable) {
517 this.fileParams = null;
518 this.transferable = transferable;
519 }
520
521 public boolean addReadByMarker(final ReadByMarker readByMarker) {
522 if (readByMarker.getRealJid() != null) {
523 if (readByMarker.getRealJid().asBareJid().equals(trueCounterpart)) {
524 return false;
525 }
526 } else if (readByMarker.getFullJid() != null) {
527 if (readByMarker.getFullJid().equals(counterpart)) {
528 return false;
529 }
530 }
531 if (this.readByMarkers.add(readByMarker)) {
532 if (readByMarker.getRealJid() != null && readByMarker.getFullJid() != null) {
533 Iterator<ReadByMarker> iterator = this.readByMarkers.iterator();
534 while (iterator.hasNext()) {
535 ReadByMarker marker = iterator.next();
536 if (marker.getRealJid() == null && readByMarker.getFullJid().equals(marker.getFullJid())) {
537 iterator.remove();
538 }
539 }
540 }
541 return true;
542 } else {
543 return false;
544 }
545 }
546
547 public Set<ReadByMarker> getReadByMarkers() {
548 return ImmutableSet.copyOf(this.readByMarkers);
549 }
550
551 public Set<Jid> getReadyByTrue() {
552 return ImmutableSet.copyOf(
553 Collections2.transform(
554 Collections2.filter(this.readByMarkers, m -> m.getRealJid() != null),
555 ReadByMarker::getRealJid));
556 }
557
558 boolean similar(Message message) {
559 if (!isPrivateMessage() && this.serverMsgId != null && message.getServerMsgId() != null) {
560 return this.serverMsgId.equals(message.getServerMsgId()) || Edit.wasPreviouslyEditedServerMsgId(edits, message.getServerMsgId());
561 } else if (Edit.wasPreviouslyEditedServerMsgId(edits, message.getServerMsgId())) {
562 return true;
563 } else if (this.body == null || this.counterpart == null) {
564 return false;
565 } else {
566 String body, otherBody;
567 if (this.hasFileOnRemoteHost()) {
568 body = getFileParams().url;
569 otherBody = message.body == null ? null : message.body.trim();
570 } else {
571 body = this.body;
572 otherBody = message.body;
573 }
574 final boolean matchingCounterpart = this.counterpart.equals(message.getCounterpart());
575 if (message.getRemoteMsgId() != null) {
576 final boolean hasUuid = CryptoHelper.UUID_PATTERN.matcher(message.getRemoteMsgId()).matches();
577 if (hasUuid && matchingCounterpart && Edit.wasPreviouslyEditedRemoteMsgId(edits, message.getRemoteMsgId())) {
578 return true;
579 }
580 return (message.getRemoteMsgId().equals(this.remoteMsgId) || message.getRemoteMsgId().equals(this.uuid))
581 && matchingCounterpart
582 && (body.equals(otherBody) || (message.getEncryption() == Message.ENCRYPTION_PGP && hasUuid));
583 } else {
584 return this.remoteMsgId == null
585 && matchingCounterpart
586 && body.equals(otherBody)
587 && Math.abs(this.getTimeSent() - message.getTimeSent()) < Config.MESSAGE_MERGE_WINDOW * 1000;
588 }
589 }
590 }
591
592 public Message next() {
593 if (this.conversation instanceof Conversation) {
594 final Conversation conversation = (Conversation) this.conversation;
595 synchronized (conversation.messages) {
596 if (this.mNextMessage == null) {
597 int index = conversation.messages.indexOf(this);
598 if (index < 0 || index >= conversation.messages.size() - 1) {
599 this.mNextMessage = null;
600 } else {
601 this.mNextMessage = conversation.messages.get(index + 1);
602 }
603 }
604 return this.mNextMessage;
605 }
606 } else {
607 throw new AssertionError("Calling next should be disabled for stubs");
608 }
609 }
610
611 public Message prev() {
612 if (this.conversation instanceof Conversation) {
613 final Conversation conversation = (Conversation) this.conversation;
614 synchronized (conversation.messages) {
615 if (this.mPreviousMessage == null) {
616 int index = conversation.messages.indexOf(this);
617 if (index <= 0 || index > conversation.messages.size()) {
618 this.mPreviousMessage = null;
619 } else {
620 this.mPreviousMessage = conversation.messages.get(index - 1);
621 }
622 }
623 }
624 return this.mPreviousMessage;
625 } else {
626 throw new AssertionError("Calling prev should be disabled for stubs");
627 }
628 }
629
630 public boolean isLastCorrectableMessage() {
631 Message next = next();
632 while (next != null) {
633 if (next.isEditable()) {
634 return false;
635 }
636 next = next.next();
637 }
638 return isEditable();
639 }
640
641 public boolean isEditable() {
642 return status != STATUS_RECEIVED && !isCarbon() && type != Message.TYPE_RTP_SESSION;
643 }
644
645 public boolean mergeable(final Message message) {
646 return message != null &&
647 (message.getType() == Message.TYPE_TEXT &&
648 this.getTransferable() == null &&
649 message.getTransferable() == null &&
650 message.getEncryption() != Message.ENCRYPTION_PGP &&
651 message.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED &&
652 this.getType() == message.getType() &&
653 this.isReactionsEmpty() &&
654 message.isReactionsEmpty() &&
655 isStatusMergeable(this.getStatus(), message.getStatus()) &&
656 isEncryptionMergeable(this.getEncryption(),message.getEncryption()) &&
657 this.getCounterpart() != null &&
658 this.getCounterpart().equals(message.getCounterpart()) &&
659 this.edited() == message.edited() &&
660 (message.getTimeSent() - this.getTimeSent()) <= (Config.MESSAGE_MERGE_WINDOW * 1000) &&
661 this.getBody().length() + message.getBody().length() <= Config.MAX_DISPLAY_MESSAGE_CHARS &&
662 !message.isGeoUri() &&
663 !this.isGeoUri() &&
664 !message.isOOb() &&
665 !this.isOOb() &&
666 !message.treatAsDownloadable() &&
667 !this.treatAsDownloadable() &&
668 !message.hasMeCommand() &&
669 !this.hasMeCommand() &&
670 !this.bodyIsOnlyEmojis() &&
671 !message.bodyIsOnlyEmojis() &&
672 ((this.axolotlFingerprint == null && message.axolotlFingerprint == null) || this.axolotlFingerprint.equals(message.getFingerprint())) &&
673 UIHelper.sameDay(message.getTimeSent(), this.getTimeSent()) &&
674 this.getReadByMarkers().equals(message.getReadByMarkers()) &&
675 !this.conversation.getJid().asBareJid().equals(Config.BUG_REPORTS)
676 );
677 }
678
679 private static boolean isStatusMergeable(int a, int b) {
680 return a == b || (
681 (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_UNSEND)
682 || (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_SEND)
683 || (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_WAITING)
684 || (a == Message.STATUS_SEND && b == Message.STATUS_UNSEND)
685 || (a == Message.STATUS_SEND && b == Message.STATUS_WAITING)
686 );
687 }
688
689 private static boolean isEncryptionMergeable(final int a, final int b) {
690 return a == b
691 && Arrays.asList(ENCRYPTION_NONE, ENCRYPTION_DECRYPTED, ENCRYPTION_AXOLOTL)
692 .contains(a);
693 }
694
695 public void setCounterparts(List<MucOptions.User> counterparts) {
696 this.counterparts = counterparts;
697 }
698
699 public List<MucOptions.User> getCounterparts() {
700 return this.counterparts;
701 }
702
703 @Override
704 public int getAvatarBackgroundColor() {
705 if (type == Message.TYPE_STATUS && getCounterparts() != null && getCounterparts().size() > 1) {
706 return Color.TRANSPARENT;
707 } else {
708 return UIHelper.getColorForName(UIHelper.getMessageDisplayName(this));
709 }
710 }
711
712 @Override
713 public String getAvatarName() {
714 return UIHelper.getMessageDisplayName(this);
715 }
716
717 public boolean isOOb() {
718 return oob;
719 }
720
721 public void setOccupantId(final String id) {
722 this.occupantId = id;
723 }
724
725 public String getOccupantId() {
726 return this.occupantId;
727 }
728
729 public Collection<Reaction> getReactions() {
730 return this.reactions;
731 }
732
733 public boolean isReactionsEmpty() {
734 return this.reactions.isEmpty();
735 }
736
737 public Reaction.Aggregated getAggregatedReactions() {
738 return Reaction.aggregated(this.reactions);
739 }
740
741 public void setReactions(final Collection<Reaction> reactions) {
742 this.reactions = reactions;
743 }
744
745 public static class MergeSeparator {
746 }
747
748 public SpannableStringBuilder getMergedBody() {
749 SpannableStringBuilder body = new SpannableStringBuilder(MessageUtils.filterLtrRtl(this.body).trim());
750 Message current = this;
751 while (current.mergeable(current.next())) {
752 current = current.next();
753 if (current == null) {
754 break;
755 }
756 body.append("\n\n");
757 body.setSpan(new MergeSeparator(), body.length() - 2, body.length(),
758 SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE);
759 body.append(MessageUtils.filterLtrRtl(current.getBody()).trim());
760 }
761 return body;
762 }
763
764 public boolean hasMeCommand() {
765 return this.body.trim().startsWith(ME_COMMAND);
766 }
767
768 public int getMergedStatus() {
769 int status = this.status;
770 Message current = this;
771 while (current.mergeable(current.next())) {
772 current = current.next();
773 if (current == null) {
774 break;
775 }
776 status = current.status;
777 }
778 return status;
779 }
780
781 public long getMergedTimeSent() {
782 long time = this.timeSent;
783 Message current = this;
784 while (current.mergeable(current.next())) {
785 current = current.next();
786 if (current == null) {
787 break;
788 }
789 time = current.timeSent;
790 }
791 return time;
792 }
793
794 public boolean wasMergedIntoPrevious() {
795 Message prev = this.prev();
796 return prev != null && prev.mergeable(this);
797 }
798
799 public boolean trusted() {
800 Contact contact = this.getContact();
801 return status > STATUS_RECEIVED || (contact != null && (contact.showInContactList() || contact.isSelf()));
802 }
803
804 public boolean fixCounterpart() {
805 final Presences presences = conversation.getContact().getPresences();
806 if (counterpart != null && presences.has(Strings.nullToEmpty(counterpart.getResource()))) {
807 return true;
808 } else if (presences.size() >= 1) {
809 counterpart = PresenceSelector.getNextCounterpart(getContact(), presences.toResourceArray()[0]);
810 return true;
811 } else {
812 counterpart = null;
813 return false;
814 }
815 }
816
817 public void setUuid(String uuid) {
818 this.uuid = uuid;
819 }
820
821 public String getEditedId() {
822 if (this.edits.isEmpty()) {
823 throw new IllegalStateException("Attempting to access unedited message");
824 }
825 return edits.get(edits.size() - 1).getEditedId();
826 }
827
828 public String getEditedIdWireFormat() {
829 if (this.edits.isEmpty()) {
830 throw new IllegalStateException("Attempting to access unedited message");
831 }
832 return edits.get(0).getEditedId();
833 }
834
835 public void setOob(boolean isOob) {
836 this.oob = isOob;
837 }
838
839 public String getMimeType() {
840 String extension;
841 if (relativeFilePath != null) {
842 extension = MimeUtils.extractRelevantExtension(relativeFilePath);
843 } else {
844 final String url = URL.tryParse(body.split("\n")[0]);
845 if (url == null) {
846 return null;
847 }
848 extension = MimeUtils.extractRelevantExtension(url);
849 }
850 return MimeUtils.guessMimeTypeFromExtension(extension);
851 }
852
853 public synchronized boolean treatAsDownloadable() {
854 if (treatAsDownloadable == null) {
855 treatAsDownloadable = MessageUtils.treatAsDownloadable(this.body, this.oob);
856 }
857 return treatAsDownloadable;
858 }
859
860 public synchronized boolean bodyIsOnlyEmojis() {
861 if (isEmojisOnly == null) {
862 isEmojisOnly = Emoticons.isOnlyEmoji(body.replaceAll("\\s", ""));
863 }
864 return isEmojisOnly;
865 }
866
867 public synchronized boolean isGeoUri() {
868 if (isGeoUri == null) {
869 isGeoUri = GeoHelper.GEO_URI.matcher(body).matches();
870 }
871 return isGeoUri;
872 }
873
874 public synchronized void resetFileParams() {
875 this.fileParams = null;
876 }
877
878 public synchronized FileParams getFileParams() {
879 if (fileParams == null) {
880 fileParams = new FileParams();
881 if (this.transferable != null) {
882 fileParams.size = this.transferable.getFileSize();
883 }
884 final String[] parts = body == null ? new String[0] : body.split("\\|");
885 switch (parts.length) {
886 case 1:
887 try {
888 fileParams.size = Long.parseLong(parts[0]);
889 } catch (final NumberFormatException e) {
890 fileParams.url = URL.tryParse(parts[0]);
891 }
892 break;
893 case 5:
894 fileParams.runtime = parseInt(parts[4]);
895 case 4:
896 fileParams.width = parseInt(parts[2]);
897 fileParams.height = parseInt(parts[3]);
898 case 2:
899 fileParams.url = URL.tryParse(parts[0]);
900 fileParams.size = Longs.tryParse(parts[1]);
901 break;
902 case 3:
903 fileParams.size = Longs.tryParse(parts[0]);
904 fileParams.width = parseInt(parts[1]);
905 fileParams.height = parseInt(parts[2]);
906 break;
907 }
908 }
909 return fileParams;
910 }
911
912 private static int parseInt(String value) {
913 try {
914 return Integer.parseInt(value);
915 } catch (NumberFormatException e) {
916 return 0;
917 }
918 }
919
920 public void untie() {
921 this.mNextMessage = null;
922 this.mPreviousMessage = null;
923 }
924
925 public boolean isPrivateMessage() {
926 return type == TYPE_PRIVATE || type == TYPE_PRIVATE_FILE;
927 }
928
929 public boolean isFileOrImage() {
930 return type == TYPE_FILE || type == TYPE_IMAGE || type == TYPE_PRIVATE_FILE;
931 }
932
933
934 public boolean isTypeText() {
935 return type == TYPE_TEXT || type == TYPE_PRIVATE;
936 }
937
938 public boolean hasFileOnRemoteHost() {
939 return isFileOrImage() && getFileParams().url != null;
940 }
941
942 public boolean needsUploading() {
943 return isFileOrImage() && getFileParams().url == null;
944 }
945
946 public static class FileParams {
947 public String url;
948 public Long size = null;
949 public int width = 0;
950 public int height = 0;
951 public int runtime = 0;
952
953 public long getSize() {
954 return size == null ? 0 : size;
955 }
956 }
957
958 public void setFingerprint(String fingerprint) {
959 this.axolotlFingerprint = fingerprint;
960 }
961
962 public String getFingerprint() {
963 return axolotlFingerprint;
964 }
965
966 public boolean isTrusted() {
967 final AxolotlService axolotlService = conversation.getAccount().getAxolotlService();
968 final FingerprintStatus s = axolotlService != null ? axolotlService.getFingerprintTrust(axolotlFingerprint) : null;
969 return s != null && s.isTrusted();
970 }
971
972 private int getPreviousEncryption() {
973 for (Message iterator = this.prev(); iterator != null; iterator = iterator.prev()) {
974 if (iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED) {
975 continue;
976 }
977 return iterator.getEncryption();
978 }
979 return ENCRYPTION_NONE;
980 }
981
982 private int getNextEncryption() {
983 if (this.conversation instanceof Conversation) {
984 Conversation conversation = (Conversation) this.conversation;
985 for (Message iterator = this.next(); iterator != null; iterator = iterator.next()) {
986 if (iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED) {
987 continue;
988 }
989 return iterator.getEncryption();
990 }
991 return conversation.getNextEncryption();
992 } else {
993 throw new AssertionError("This should never be called since isInValidSession should be disabled for stubs");
994 }
995 }
996
997 public boolean isValidInSession() {
998 int pastEncryption = getCleanedEncryption(this.getPreviousEncryption());
999 int futureEncryption = getCleanedEncryption(this.getNextEncryption());
1000
1001 boolean inUnencryptedSession = pastEncryption == ENCRYPTION_NONE
1002 || futureEncryption == ENCRYPTION_NONE
1003 || pastEncryption != futureEncryption;
1004
1005 return inUnencryptedSession || getCleanedEncryption(this.getEncryption()) == pastEncryption;
1006 }
1007
1008 private static int getCleanedEncryption(int encryption) {
1009 if (encryption == ENCRYPTION_DECRYPTED || encryption == ENCRYPTION_DECRYPTION_FAILED) {
1010 return ENCRYPTION_PGP;
1011 }
1012 if (encryption == ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE || encryption == ENCRYPTION_AXOLOTL_FAILED) {
1013 return ENCRYPTION_AXOLOTL;
1014 }
1015 return encryption;
1016 }
1017
1018 public static boolean configurePrivateMessage(final Message message) {
1019 return configurePrivateMessage(message, false);
1020 }
1021
1022 public static boolean configurePrivateFileMessage(final Message message) {
1023 return configurePrivateMessage(message, true);
1024 }
1025
1026 private static boolean configurePrivateMessage(final Message message, final boolean isFile) {
1027 final Conversation conversation;
1028 if (message.conversation instanceof Conversation) {
1029 conversation = (Conversation) message.conversation;
1030 } else {
1031 return false;
1032 }
1033 if (conversation.getMode() == Conversation.MODE_MULTI) {
1034 final Jid nextCounterpart = conversation.getNextCounterpart();
1035 return configurePrivateMessage(conversation, message, nextCounterpart, isFile);
1036 }
1037 return false;
1038 }
1039
1040 public static boolean configurePrivateMessage(final Message message, final Jid counterpart) {
1041 final Conversation conversation;
1042 if (message.conversation instanceof Conversation) {
1043 conversation = (Conversation) message.conversation;
1044 } else {
1045 return false;
1046 }
1047 return configurePrivateMessage(conversation, message, counterpart, false);
1048 }
1049
1050 private static boolean configurePrivateMessage(final Conversation conversation, final Message message, final Jid counterpart, final boolean isFile) {
1051 if (counterpart == null) {
1052 return false;
1053 }
1054 message.setCounterpart(counterpart);
1055 message.setTrueCounterpart(conversation.getMucOptions().getTrueCounterpart(counterpart));
1056 message.setType(isFile ? Message.TYPE_PRIVATE_FILE : Message.TYPE_PRIVATE);
1057 return true;
1058 }
1059}