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