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