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(Cursor cursor, 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 boolean remoteMsgIdMatchInEdit(String id) {
493 for (Edit edit : this.edits) {
494 if (id.equals(edit.getEditedId())) {
495 return true;
496 }
497 }
498 return false;
499 }
500
501 public String getBodyLanguage() {
502 return this.bodyLanguage;
503 }
504
505 public void setBodyLanguage(String language) {
506 this.bodyLanguage = language;
507 }
508
509 public boolean edited() {
510 return this.edits.size() > 0;
511 }
512
513 public void setTrueCounterpart(Jid trueCounterpart) {
514 this.trueCounterpart = trueCounterpart;
515 }
516
517 public Jid getTrueCounterpart() {
518 return this.trueCounterpart;
519 }
520
521 public Transferable getTransferable() {
522 return this.transferable;
523 }
524
525 public synchronized void setTransferable(Transferable transferable) {
526 this.fileParams = null;
527 this.transferable = transferable;
528 }
529
530 public boolean addReadByMarker(final ReadByMarker readByMarker) {
531 if (readByMarker.getRealJid() != null) {
532 if (readByMarker.getRealJid().asBareJid().equals(trueCounterpart)) {
533 return false;
534 }
535 } else if (readByMarker.getFullJid() != null) {
536 if (readByMarker.getFullJid().equals(counterpart)) {
537 return false;
538 }
539 }
540 if (this.readByMarkers.add(readByMarker)) {
541 if (readByMarker.getRealJid() != null && readByMarker.getFullJid() != null) {
542 Iterator<ReadByMarker> iterator = this.readByMarkers.iterator();
543 while (iterator.hasNext()) {
544 ReadByMarker marker = iterator.next();
545 if (marker.getRealJid() == null && readByMarker.getFullJid().equals(marker.getFullJid())) {
546 iterator.remove();
547 }
548 }
549 }
550 return true;
551 } else {
552 return false;
553 }
554 }
555
556 public Set<ReadByMarker> getReadByMarkers() {
557 return ImmutableSet.copyOf(this.readByMarkers);
558 }
559
560 public Set<Jid> getReadyByTrue() {
561 return ImmutableSet.copyOf(
562 Collections2.transform(
563 Collections2.filter(this.readByMarkers, m -> m.getRealJid() != null),
564 ReadByMarker::getRealJid));
565 }
566
567 boolean similar(Message message) {
568 if (!isPrivateMessage() && this.serverMsgId != null && message.getServerMsgId() != null) {
569 return this.serverMsgId.equals(message.getServerMsgId()) || Edit.wasPreviouslyEditedServerMsgId(edits, message.getServerMsgId());
570 } else if (Edit.wasPreviouslyEditedServerMsgId(edits, message.getServerMsgId())) {
571 return true;
572 } else if (this.body == null || this.counterpart == null) {
573 return false;
574 } else {
575 String body, otherBody;
576 if (this.hasFileOnRemoteHost()) {
577 body = getFileParams().url;
578 otherBody = message.body == null ? null : message.body.trim();
579 } else {
580 body = this.body;
581 otherBody = message.body;
582 }
583 final boolean matchingCounterpart = this.counterpart.equals(message.getCounterpart());
584 if (message.getRemoteMsgId() != null) {
585 final boolean hasUuid = CryptoHelper.UUID_PATTERN.matcher(message.getRemoteMsgId()).matches();
586 if (hasUuid && matchingCounterpart && Edit.wasPreviouslyEditedRemoteMsgId(edits, message.getRemoteMsgId())) {
587 return true;
588 }
589 return (message.getRemoteMsgId().equals(this.remoteMsgId) || message.getRemoteMsgId().equals(this.uuid))
590 && matchingCounterpart
591 && (body.equals(otherBody) || (message.getEncryption() == Message.ENCRYPTION_PGP && hasUuid));
592 } else {
593 return this.remoteMsgId == null
594 && matchingCounterpart
595 && body.equals(otherBody)
596 && Math.abs(this.getTimeSent() - message.getTimeSent()) < Config.MESSAGE_MERGE_WINDOW * 1000;
597 }
598 }
599 }
600
601 public Message next() {
602 if (this.conversation instanceof Conversation) {
603 final Conversation conversation = (Conversation) this.conversation;
604 synchronized (conversation.messages) {
605 if (this.mNextMessage == null) {
606 int index = conversation.messages.indexOf(this);
607 if (index < 0 || index >= conversation.messages.size() - 1) {
608 this.mNextMessage = null;
609 } else {
610 this.mNextMessage = conversation.messages.get(index + 1);
611 }
612 }
613 return this.mNextMessage;
614 }
615 } else {
616 throw new AssertionError("Calling next should be disabled for stubs");
617 }
618 }
619
620 public Message prev() {
621 if (this.conversation instanceof Conversation) {
622 final Conversation conversation = (Conversation) this.conversation;
623 synchronized (conversation.messages) {
624 if (this.mPreviousMessage == null) {
625 int index = conversation.messages.indexOf(this);
626 if (index <= 0 || index > conversation.messages.size()) {
627 this.mPreviousMessage = null;
628 } else {
629 this.mPreviousMessage = conversation.messages.get(index - 1);
630 }
631 }
632 }
633 return this.mPreviousMessage;
634 } else {
635 throw new AssertionError("Calling prev should be disabled for stubs");
636 }
637 }
638
639 public boolean isLastCorrectableMessage() {
640 Message next = next();
641 while (next != null) {
642 if (next.isEditable()) {
643 return false;
644 }
645 next = next.next();
646 }
647 return isEditable();
648 }
649
650 public boolean isEditable() {
651 return status != STATUS_RECEIVED && !isCarbon() && type != Message.TYPE_RTP_SESSION;
652 }
653
654 public boolean mergeable(final Message message) {
655 return message != null &&
656 (message.getType() == Message.TYPE_TEXT &&
657 this.getTransferable() == null &&
658 message.getTransferable() == null &&
659 message.getEncryption() != Message.ENCRYPTION_PGP &&
660 message.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED &&
661 this.getType() == message.getType() &&
662 isStatusMergeable(this.getStatus(), message.getStatus()) &&
663 isEncryptionMergeable(this.getEncryption(),message.getEncryption()) &&
664 this.getCounterpart() != null &&
665 this.getCounterpart().equals(message.getCounterpart()) &&
666 this.edited() == message.edited() &&
667 (message.getTimeSent() - this.getTimeSent()) <= (Config.MESSAGE_MERGE_WINDOW * 1000) &&
668 this.getBody().length() + message.getBody().length() <= Config.MAX_DISPLAY_MESSAGE_CHARS &&
669 !message.isGeoUri() &&
670 !this.isGeoUri() &&
671 !message.isOOb() &&
672 !this.isOOb() &&
673 !message.treatAsDownloadable() &&
674 !this.treatAsDownloadable() &&
675 !message.hasMeCommand() &&
676 !this.hasMeCommand() &&
677 !this.bodyIsOnlyEmojis() &&
678 !message.bodyIsOnlyEmojis() &&
679 ((this.axolotlFingerprint == null && message.axolotlFingerprint == null) || this.axolotlFingerprint.equals(message.getFingerprint())) &&
680 UIHelper.sameDay(message.getTimeSent(), this.getTimeSent()) &&
681 this.getReadByMarkers().equals(message.getReadByMarkers()) &&
682 !this.conversation.getJid().asBareJid().equals(Config.BUG_REPORTS)
683 );
684 }
685
686 private static boolean isStatusMergeable(int a, int b) {
687 return a == b || (
688 (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_UNSEND)
689 || (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_SEND)
690 || (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_WAITING)
691 || (a == Message.STATUS_SEND && b == Message.STATUS_UNSEND)
692 || (a == Message.STATUS_SEND && b == Message.STATUS_WAITING)
693 );
694 }
695
696 private static boolean isEncryptionMergeable(final int a, final int b) {
697 return a == b
698 && Arrays.asList(ENCRYPTION_NONE, ENCRYPTION_DECRYPTED, ENCRYPTION_AXOLOTL)
699 .contains(a);
700 }
701
702 public void setCounterparts(List<MucOptions.User> counterparts) {
703 this.counterparts = counterparts;
704 }
705
706 public List<MucOptions.User> getCounterparts() {
707 return this.counterparts;
708 }
709
710 @Override
711 public int getAvatarBackgroundColor() {
712 if (type == Message.TYPE_STATUS && getCounterparts() != null && getCounterparts().size() > 1) {
713 return Color.TRANSPARENT;
714 } else {
715 return UIHelper.getColorForName(UIHelper.getMessageDisplayName(this));
716 }
717 }
718
719 @Override
720 public String getAvatarName() {
721 return UIHelper.getMessageDisplayName(this);
722 }
723
724 public boolean isOOb() {
725 return oob;
726 }
727
728 public void setOccupantId(final String id) {
729 this.occupantId = id;
730 }
731
732 public String getOccupantId() {
733 return this.occupantId;
734 }
735
736 public Collection<Reaction> getReactions() {
737 return this.reactions;
738 }
739
740 public Reaction.Aggregated getAggregatedReactions() {
741 return Reaction.aggregated(this.reactions);
742 }
743
744 public void setReactions(final Collection<Reaction> reactions) {
745 this.reactions = reactions;
746 }
747
748 public static class MergeSeparator {
749 }
750
751 public SpannableStringBuilder getMergedBody() {
752 SpannableStringBuilder body = new SpannableStringBuilder(MessageUtils.filterLtrRtl(this.body).trim());
753 Message current = this;
754 while (current.mergeable(current.next())) {
755 current = current.next();
756 if (current == null) {
757 break;
758 }
759 body.append("\n\n");
760 body.setSpan(new MergeSeparator(), body.length() - 2, body.length(),
761 SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE);
762 body.append(MessageUtils.filterLtrRtl(current.getBody()).trim());
763 }
764 return body;
765 }
766
767 public boolean hasMeCommand() {
768 return this.body.trim().startsWith(ME_COMMAND);
769 }
770
771 public int getMergedStatus() {
772 int status = this.status;
773 Message current = this;
774 while (current.mergeable(current.next())) {
775 current = current.next();
776 if (current == null) {
777 break;
778 }
779 status = current.status;
780 }
781 return status;
782 }
783
784 public long getMergedTimeSent() {
785 long time = this.timeSent;
786 Message current = this;
787 while (current.mergeable(current.next())) {
788 current = current.next();
789 if (current == null) {
790 break;
791 }
792 time = current.timeSent;
793 }
794 return time;
795 }
796
797 public boolean wasMergedIntoPrevious() {
798 Message prev = this.prev();
799 return prev != null && prev.mergeable(this);
800 }
801
802 public boolean trusted() {
803 Contact contact = this.getContact();
804 return status > STATUS_RECEIVED || (contact != null && (contact.showInContactList() || contact.isSelf()));
805 }
806
807 public boolean fixCounterpart() {
808 final Presences presences = conversation.getContact().getPresences();
809 if (counterpart != null && presences.has(Strings.nullToEmpty(counterpart.getResource()))) {
810 return true;
811 } else if (presences.size() >= 1) {
812 counterpart = PresenceSelector.getNextCounterpart(getContact(), presences.toResourceArray()[0]);
813 return true;
814 } else {
815 counterpart = null;
816 return false;
817 }
818 }
819
820 public void setUuid(String uuid) {
821 this.uuid = uuid;
822 }
823
824 public String getEditedId() {
825 if (edits.size() > 0) {
826 return edits.get(edits.size() - 1).getEditedId();
827 } else {
828 throw new IllegalStateException("Attempting to store unedited message");
829 }
830 }
831
832 public String getEditedIdWireFormat() {
833 if (edits.size() > 0) {
834 return edits.get(Config.USE_LMC_VERSION_1_1 ? 0 : edits.size() - 1).getEditedId();
835 } else {
836 throw new IllegalStateException("Attempting to store unedited message");
837 }
838 }
839
840 public void setOob(boolean isOob) {
841 this.oob = isOob;
842 }
843
844 public String getMimeType() {
845 String extension;
846 if (relativeFilePath != null) {
847 extension = MimeUtils.extractRelevantExtension(relativeFilePath);
848 } else {
849 final String url = URL.tryParse(body.split("\n")[0]);
850 if (url == null) {
851 return null;
852 }
853 extension = MimeUtils.extractRelevantExtension(url);
854 }
855 return MimeUtils.guessMimeTypeFromExtension(extension);
856 }
857
858 public synchronized boolean treatAsDownloadable() {
859 if (treatAsDownloadable == null) {
860 treatAsDownloadable = MessageUtils.treatAsDownloadable(this.body, this.oob);
861 }
862 return treatAsDownloadable;
863 }
864
865 public synchronized boolean bodyIsOnlyEmojis() {
866 if (isEmojisOnly == null) {
867 isEmojisOnly = Emoticons.isOnlyEmoji(body.replaceAll("\\s", ""));
868 }
869 return isEmojisOnly;
870 }
871
872 public synchronized boolean isGeoUri() {
873 if (isGeoUri == null) {
874 isGeoUri = GeoHelper.GEO_URI.matcher(body).matches();
875 }
876 return isGeoUri;
877 }
878
879 public synchronized void resetFileParams() {
880 this.fileParams = null;
881 }
882
883 public synchronized FileParams getFileParams() {
884 if (fileParams == null) {
885 fileParams = new FileParams();
886 if (this.transferable != null) {
887 fileParams.size = this.transferable.getFileSize();
888 }
889 final String[] parts = body == null ? new String[0] : body.split("\\|");
890 switch (parts.length) {
891 case 1:
892 try {
893 fileParams.size = Long.parseLong(parts[0]);
894 } catch (final NumberFormatException e) {
895 fileParams.url = URL.tryParse(parts[0]);
896 }
897 break;
898 case 5:
899 fileParams.runtime = parseInt(parts[4]);
900 case 4:
901 fileParams.width = parseInt(parts[2]);
902 fileParams.height = parseInt(parts[3]);
903 case 2:
904 fileParams.url = URL.tryParse(parts[0]);
905 fileParams.size = Longs.tryParse(parts[1]);
906 break;
907 case 3:
908 fileParams.size = Longs.tryParse(parts[0]);
909 fileParams.width = parseInt(parts[1]);
910 fileParams.height = parseInt(parts[2]);
911 break;
912 }
913 }
914 return fileParams;
915 }
916
917 private static int parseInt(String value) {
918 try {
919 return Integer.parseInt(value);
920 } catch (NumberFormatException e) {
921 return 0;
922 }
923 }
924
925 public void untie() {
926 this.mNextMessage = null;
927 this.mPreviousMessage = null;
928 }
929
930 public boolean isPrivateMessage() {
931 return type == TYPE_PRIVATE || type == TYPE_PRIVATE_FILE;
932 }
933
934 public boolean isFileOrImage() {
935 return type == TYPE_FILE || type == TYPE_IMAGE || type == TYPE_PRIVATE_FILE;
936 }
937
938
939 public boolean isTypeText() {
940 return type == TYPE_TEXT || type == TYPE_PRIVATE;
941 }
942
943 public boolean hasFileOnRemoteHost() {
944 return isFileOrImage() && getFileParams().url != null;
945 }
946
947 public boolean needsUploading() {
948 return isFileOrImage() && getFileParams().url == null;
949 }
950
951 public static class FileParams {
952 public String url;
953 public Long size = null;
954 public int width = 0;
955 public int height = 0;
956 public int runtime = 0;
957
958 public long getSize() {
959 return size == null ? 0 : size;
960 }
961 }
962
963 public void setFingerprint(String fingerprint) {
964 this.axolotlFingerprint = fingerprint;
965 }
966
967 public String getFingerprint() {
968 return axolotlFingerprint;
969 }
970
971 public boolean isTrusted() {
972 final AxolotlService axolotlService = conversation.getAccount().getAxolotlService();
973 final FingerprintStatus s = axolotlService != null ? axolotlService.getFingerprintTrust(axolotlFingerprint) : null;
974 return s != null && s.isTrusted();
975 }
976
977 private int getPreviousEncryption() {
978 for (Message iterator = this.prev(); iterator != null; iterator = iterator.prev()) {
979 if (iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED) {
980 continue;
981 }
982 return iterator.getEncryption();
983 }
984 return ENCRYPTION_NONE;
985 }
986
987 private int getNextEncryption() {
988 if (this.conversation instanceof Conversation) {
989 Conversation conversation = (Conversation) this.conversation;
990 for (Message iterator = this.next(); iterator != null; iterator = iterator.next()) {
991 if (iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED) {
992 continue;
993 }
994 return iterator.getEncryption();
995 }
996 return conversation.getNextEncryption();
997 } else {
998 throw new AssertionError("This should never be called since isInValidSession should be disabled for stubs");
999 }
1000 }
1001
1002 public boolean isValidInSession() {
1003 int pastEncryption = getCleanedEncryption(this.getPreviousEncryption());
1004 int futureEncryption = getCleanedEncryption(this.getNextEncryption());
1005
1006 boolean inUnencryptedSession = pastEncryption == ENCRYPTION_NONE
1007 || futureEncryption == ENCRYPTION_NONE
1008 || pastEncryption != futureEncryption;
1009
1010 return inUnencryptedSession || getCleanedEncryption(this.getEncryption()) == pastEncryption;
1011 }
1012
1013 private static int getCleanedEncryption(int encryption) {
1014 if (encryption == ENCRYPTION_DECRYPTED || encryption == ENCRYPTION_DECRYPTION_FAILED) {
1015 return ENCRYPTION_PGP;
1016 }
1017 if (encryption == ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE || encryption == ENCRYPTION_AXOLOTL_FAILED) {
1018 return ENCRYPTION_AXOLOTL;
1019 }
1020 return encryption;
1021 }
1022
1023 public static boolean configurePrivateMessage(final Message message) {
1024 return configurePrivateMessage(message, false);
1025 }
1026
1027 public static boolean configurePrivateFileMessage(final Message message) {
1028 return configurePrivateMessage(message, true);
1029 }
1030
1031 private static boolean configurePrivateMessage(final Message message, final boolean isFile) {
1032 final Conversation conversation;
1033 if (message.conversation instanceof Conversation) {
1034 conversation = (Conversation) message.conversation;
1035 } else {
1036 return false;
1037 }
1038 if (conversation.getMode() == Conversation.MODE_MULTI) {
1039 final Jid nextCounterpart = conversation.getNextCounterpart();
1040 return configurePrivateMessage(conversation, message, nextCounterpart, isFile);
1041 }
1042 return false;
1043 }
1044
1045 public static boolean configurePrivateMessage(final Message message, final Jid counterpart) {
1046 final Conversation conversation;
1047 if (message.conversation instanceof Conversation) {
1048 conversation = (Conversation) message.conversation;
1049 } else {
1050 return false;
1051 }
1052 return configurePrivateMessage(conversation, message, counterpart, false);
1053 }
1054
1055 private static boolean configurePrivateMessage(final Conversation conversation, final Message message, final Jid counterpart, final boolean isFile) {
1056 if (counterpart == null) {
1057 return false;
1058 }
1059 message.setCounterpart(counterpart);
1060 message.setTrueCounterpart(conversation.getMucOptions().getTrueCounterpart(counterpart));
1061 message.setType(isFile ? Message.TYPE_PRIVATE_FILE : Message.TYPE_PRIVATE);
1062 return true;
1063 }
1064}