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