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 static class MergeSeparator {
733 }
734
735 public SpannableStringBuilder getMergedBody() {
736 SpannableStringBuilder body = new SpannableStringBuilder(MessageUtils.filterLtrRtl(this.body).trim());
737 Message current = this;
738 while (current.mergeable(current.next())) {
739 current = current.next();
740 if (current == null) {
741 break;
742 }
743 body.append("\n\n");
744 body.setSpan(new MergeSeparator(), body.length() - 2, body.length(),
745 SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE);
746 body.append(MessageUtils.filterLtrRtl(current.getBody()).trim());
747 }
748 return body;
749 }
750
751 public boolean hasMeCommand() {
752 return this.body.trim().startsWith(ME_COMMAND);
753 }
754
755 public int getMergedStatus() {
756 int status = this.status;
757 Message current = this;
758 while (current.mergeable(current.next())) {
759 current = current.next();
760 if (current == null) {
761 break;
762 }
763 status = current.status;
764 }
765 return status;
766 }
767
768 public long getMergedTimeSent() {
769 long time = this.timeSent;
770 Message current = this;
771 while (current.mergeable(current.next())) {
772 current = current.next();
773 if (current == null) {
774 break;
775 }
776 time = current.timeSent;
777 }
778 return time;
779 }
780
781 public boolean wasMergedIntoPrevious() {
782 Message prev = this.prev();
783 return prev != null && prev.mergeable(this);
784 }
785
786 public boolean trusted() {
787 Contact contact = this.getContact();
788 return status > STATUS_RECEIVED || (contact != null && (contact.showInContactList() || contact.isSelf()));
789 }
790
791 public boolean fixCounterpart() {
792 final Presences presences = conversation.getContact().getPresences();
793 if (counterpart != null && presences.has(Strings.nullToEmpty(counterpart.getResource()))) {
794 return true;
795 } else if (presences.size() >= 1) {
796 counterpart = PresenceSelector.getNextCounterpart(getContact(), presences.toResourceArray()[0]);
797 return true;
798 } else {
799 counterpart = null;
800 return false;
801 }
802 }
803
804 public void setUuid(String uuid) {
805 this.uuid = uuid;
806 }
807
808 public String getEditedId() {
809 if (edits.size() > 0) {
810 return edits.get(edits.size() - 1).getEditedId();
811 } else {
812 throw new IllegalStateException("Attempting to store unedited message");
813 }
814 }
815
816 public String getEditedIdWireFormat() {
817 if (edits.size() > 0) {
818 return edits.get(Config.USE_LMC_VERSION_1_1 ? 0 : edits.size() - 1).getEditedId();
819 } else {
820 throw new IllegalStateException("Attempting to store unedited message");
821 }
822 }
823
824 public void setOob(boolean isOob) {
825 this.oob = isOob;
826 }
827
828 public String getMimeType() {
829 String extension;
830 if (relativeFilePath != null) {
831 extension = MimeUtils.extractRelevantExtension(relativeFilePath);
832 } else {
833 final String url = URL.tryParse(body.split("\n")[0]);
834 if (url == null) {
835 return null;
836 }
837 extension = MimeUtils.extractRelevantExtension(url);
838 }
839 return MimeUtils.guessMimeTypeFromExtension(extension);
840 }
841
842 public synchronized boolean treatAsDownloadable() {
843 if (treatAsDownloadable == null) {
844 treatAsDownloadable = MessageUtils.treatAsDownloadable(this.body, this.oob);
845 }
846 return treatAsDownloadable;
847 }
848
849 public synchronized boolean bodyIsOnlyEmojis() {
850 if (isEmojisOnly == null) {
851 isEmojisOnly = Emoticons.isOnlyEmoji(body.replaceAll("\\s", ""));
852 }
853 return isEmojisOnly;
854 }
855
856 public synchronized boolean isGeoUri() {
857 if (isGeoUri == null) {
858 isGeoUri = GeoHelper.GEO_URI.matcher(body).matches();
859 }
860 return isGeoUri;
861 }
862
863 public synchronized void resetFileParams() {
864 this.fileParams = null;
865 }
866
867 public synchronized FileParams getFileParams() {
868 if (fileParams == null) {
869 fileParams = new FileParams();
870 if (this.transferable != null) {
871 fileParams.size = this.transferable.getFileSize();
872 }
873 final String[] parts = body == null ? new String[0] : body.split("\\|");
874 switch (parts.length) {
875 case 1:
876 try {
877 fileParams.size = Long.parseLong(parts[0]);
878 } catch (final NumberFormatException e) {
879 fileParams.url = URL.tryParse(parts[0]);
880 }
881 break;
882 case 5:
883 fileParams.runtime = parseInt(parts[4]);
884 case 4:
885 fileParams.width = parseInt(parts[2]);
886 fileParams.height = parseInt(parts[3]);
887 case 2:
888 fileParams.url = URL.tryParse(parts[0]);
889 fileParams.size = Longs.tryParse(parts[1]);
890 break;
891 case 3:
892 fileParams.size = Longs.tryParse(parts[0]);
893 fileParams.width = parseInt(parts[1]);
894 fileParams.height = parseInt(parts[2]);
895 break;
896 }
897 }
898 return fileParams;
899 }
900
901 private static int parseInt(String value) {
902 try {
903 return Integer.parseInt(value);
904 } catch (NumberFormatException e) {
905 return 0;
906 }
907 }
908
909 public void untie() {
910 this.mNextMessage = null;
911 this.mPreviousMessage = null;
912 }
913
914 public boolean isPrivateMessage() {
915 return type == TYPE_PRIVATE || type == TYPE_PRIVATE_FILE;
916 }
917
918 public boolean isFileOrImage() {
919 return type == TYPE_FILE || type == TYPE_IMAGE || type == TYPE_PRIVATE_FILE;
920 }
921
922
923 public boolean isTypeText() {
924 return type == TYPE_TEXT || type == TYPE_PRIVATE;
925 }
926
927 public boolean hasFileOnRemoteHost() {
928 return isFileOrImage() && getFileParams().url != null;
929 }
930
931 public boolean needsUploading() {
932 return isFileOrImage() && getFileParams().url == null;
933 }
934
935 public static class FileParams {
936 public String url;
937 public Long size = null;
938 public int width = 0;
939 public int height = 0;
940 public int runtime = 0;
941
942 public long getSize() {
943 return size == null ? 0 : size;
944 }
945 }
946
947 public void setFingerprint(String fingerprint) {
948 this.axolotlFingerprint = fingerprint;
949 }
950
951 public String getFingerprint() {
952 return axolotlFingerprint;
953 }
954
955 public boolean isTrusted() {
956 final AxolotlService axolotlService = conversation.getAccount().getAxolotlService();
957 final FingerprintStatus s = axolotlService != null ? axolotlService.getFingerprintTrust(axolotlFingerprint) : null;
958 return s != null && s.isTrusted();
959 }
960
961 private int getPreviousEncryption() {
962 for (Message iterator = this.prev(); iterator != null; iterator = iterator.prev()) {
963 if (iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED) {
964 continue;
965 }
966 return iterator.getEncryption();
967 }
968 return ENCRYPTION_NONE;
969 }
970
971 private int getNextEncryption() {
972 if (this.conversation instanceof Conversation) {
973 Conversation conversation = (Conversation) this.conversation;
974 for (Message iterator = this.next(); iterator != null; iterator = iterator.next()) {
975 if (iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED) {
976 continue;
977 }
978 return iterator.getEncryption();
979 }
980 return conversation.getNextEncryption();
981 } else {
982 throw new AssertionError("This should never be called since isInValidSession should be disabled for stubs");
983 }
984 }
985
986 public boolean isValidInSession() {
987 int pastEncryption = getCleanedEncryption(this.getPreviousEncryption());
988 int futureEncryption = getCleanedEncryption(this.getNextEncryption());
989
990 boolean inUnencryptedSession = pastEncryption == ENCRYPTION_NONE
991 || futureEncryption == ENCRYPTION_NONE
992 || pastEncryption != futureEncryption;
993
994 return inUnencryptedSession || getCleanedEncryption(this.getEncryption()) == pastEncryption;
995 }
996
997 private static int getCleanedEncryption(int encryption) {
998 if (encryption == ENCRYPTION_DECRYPTED || encryption == ENCRYPTION_DECRYPTION_FAILED) {
999 return ENCRYPTION_PGP;
1000 }
1001 if (encryption == ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE || encryption == ENCRYPTION_AXOLOTL_FAILED) {
1002 return ENCRYPTION_AXOLOTL;
1003 }
1004 return encryption;
1005 }
1006
1007 public static boolean configurePrivateMessage(final Message message) {
1008 return configurePrivateMessage(message, false);
1009 }
1010
1011 public static boolean configurePrivateFileMessage(final Message message) {
1012 return configurePrivateMessage(message, true);
1013 }
1014
1015 private static boolean configurePrivateMessage(final Message message, final boolean isFile) {
1016 final Conversation conversation;
1017 if (message.conversation instanceof Conversation) {
1018 conversation = (Conversation) message.conversation;
1019 } else {
1020 return false;
1021 }
1022 if (conversation.getMode() == Conversation.MODE_MULTI) {
1023 final Jid nextCounterpart = conversation.getNextCounterpart();
1024 return configurePrivateMessage(conversation, message, nextCounterpart, isFile);
1025 }
1026 return false;
1027 }
1028
1029 public static boolean configurePrivateMessage(final Message message, final Jid counterpart) {
1030 final Conversation conversation;
1031 if (message.conversation instanceof Conversation) {
1032 conversation = (Conversation) message.conversation;
1033 } else {
1034 return false;
1035 }
1036 return configurePrivateMessage(conversation, message, counterpart, false);
1037 }
1038
1039 private static boolean configurePrivateMessage(final Conversation conversation, final Message message, final Jid counterpart, final boolean isFile) {
1040 if (counterpart == null) {
1041 return false;
1042 }
1043 message.setCounterpart(counterpart);
1044 message.setTrueCounterpart(conversation.getMucOptions().getTrueCounterpart(counterpart));
1045 message.setType(isFile ? Message.TYPE_PRIVATE_FILE : Message.TYPE_PRIVATE);
1046 return true;
1047 }
1048}