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