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