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