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;
11
12import org.json.JSONException;
13
14import java.lang.ref.WeakReference;
15import java.net.MalformedURLException;
16import java.net.URL;
17import java.util.ArrayList;
18import java.util.Collections;
19import java.util.HashSet;
20import java.util.Iterator;
21import java.util.List;
22import java.util.Set;
23
24import eu.siacs.conversations.Config;
25import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
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 HashSet<>();
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 HashSet<>() : 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.toString();
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.isCorrectable()) {
617 return false;
618 }
619 next = next.next();
620 }
621 return isCorrectable();
622 }
623
624 private boolean isCorrectable() {
625 return getStatus() != STATUS_RECEIVED && !isCarbon();
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 public boolean isOOb() {
689 return oob;
690 }
691
692 public static class MergeSeparator {
693 }
694
695 public SpannableStringBuilder getMergedBody() {
696 SpannableStringBuilder body = new SpannableStringBuilder(MessageUtils.filterLtrRtl(this.body).trim());
697 Message current = this;
698 while (current.mergeable(current.next())) {
699 current = current.next();
700 if (current == null) {
701 break;
702 }
703 body.append("\n\n");
704 body.setSpan(new MergeSeparator(), body.length() - 2, body.length(),
705 SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE);
706 body.append(MessageUtils.filterLtrRtl(current.getBody()).trim());
707 }
708 return body;
709 }
710
711 public boolean hasMeCommand() {
712 return this.body.trim().startsWith(ME_COMMAND);
713 }
714
715 public int getMergedStatus() {
716 int status = this.status;
717 Message current = this;
718 while (current.mergeable(current.next())) {
719 current = current.next();
720 if (current == null) {
721 break;
722 }
723 status = current.status;
724 }
725 return status;
726 }
727
728 public long getMergedTimeSent() {
729 long time = this.timeSent;
730 Message current = this;
731 while (current.mergeable(current.next())) {
732 current = current.next();
733 if (current == null) {
734 break;
735 }
736 time = current.timeSent;
737 }
738 return time;
739 }
740
741 public boolean wasMergedIntoPrevious() {
742 Message prev = this.prev();
743 return prev != null && prev.mergeable(this);
744 }
745
746 public boolean trusted() {
747 Contact contact = this.getContact();
748 return status > STATUS_RECEIVED || (contact != null && (contact.showInContactList() || contact.isSelf()));
749 }
750
751 public boolean fixCounterpart() {
752 final Presences presences = conversation.getContact().getPresences();
753 if (counterpart != null && presences.has(Strings.nullToEmpty(counterpart.getResource()))) {
754 return true;
755 } else if (presences.size() >= 1) {
756 counterpart = PresenceSelector.getNextCounterpart(getContact(),presences.toResourceArray()[0]);
757 return true;
758 } else {
759 counterpart = null;
760 return false;
761 }
762 }
763
764 public void setUuid(String uuid) {
765 this.uuid = uuid;
766 }
767
768 public String getEditedId() {
769 if (edits.size() > 0) {
770 return edits.get(edits.size() - 1).getEditedId();
771 } else {
772 throw new IllegalStateException("Attempting to store unedited message");
773 }
774 }
775
776 public String getEditedIdWireFormat() {
777 if (edits.size() > 0) {
778 return edits.get(Config.USE_LMC_VERSION_1_1 ? 0 : edits.size() - 1).getEditedId();
779 } else {
780 throw new IllegalStateException("Attempting to store unedited message");
781 }
782 }
783
784 public void setOob(boolean isOob) {
785 this.oob = isOob;
786 }
787
788 public String getMimeType() {
789 String extension;
790 if (relativeFilePath != null) {
791 extension = MimeUtils.extractRelevantExtension(relativeFilePath);
792 } else {
793 try {
794 final URL url = new URL(body.split("\n")[0]);
795 extension = MimeUtils.extractRelevantExtension(url);
796 } catch (MalformedURLException e) {
797 return null;
798 }
799 }
800 return MimeUtils.guessMimeTypeFromExtension(extension);
801 }
802
803 public synchronized boolean treatAsDownloadable() {
804 if (treatAsDownloadable == null) {
805 treatAsDownloadable = MessageUtils.treatAsDownloadable(this.body, this.oob);
806 }
807 return treatAsDownloadable;
808 }
809
810 public synchronized boolean bodyIsOnlyEmojis() {
811 if (isEmojisOnly == null) {
812 isEmojisOnly = Emoticons.isOnlyEmoji(body.replaceAll("\\s", ""));
813 }
814 return isEmojisOnly;
815 }
816
817 public synchronized boolean isGeoUri() {
818 if (isGeoUri == null) {
819 isGeoUri = GeoHelper.GEO_URI.matcher(body).matches();
820 }
821 return isGeoUri;
822 }
823
824 public synchronized void resetFileParams() {
825 this.fileParams = null;
826 }
827
828 public synchronized FileParams getFileParams() {
829 if (fileParams == null) {
830 fileParams = new FileParams();
831 if (this.transferable != null) {
832 fileParams.size = this.transferable.getFileSize();
833 }
834 final String[] parts = body == null ? new String[0] : body.split("\\|");
835 switch (parts.length) {
836 case 1:
837 try {
838 fileParams.size = Long.parseLong(parts[0]);
839 } catch (NumberFormatException e) {
840 fileParams.url = parseUrl(parts[0]);
841 }
842 break;
843 case 5:
844 fileParams.runtime = parseInt(parts[4]);
845 case 4:
846 fileParams.width = parseInt(parts[2]);
847 fileParams.height = parseInt(parts[3]);
848 case 2:
849 fileParams.url = parseUrl(parts[0]);
850 fileParams.size = parseLong(parts[1]);
851 break;
852 case 3:
853 fileParams.size = parseLong(parts[0]);
854 fileParams.width = parseInt(parts[1]);
855 fileParams.height = parseInt(parts[2]);
856 break;
857 }
858 }
859 return fileParams;
860 }
861
862 private static long parseLong(String value) {
863 try {
864 return Long.parseLong(value);
865 } catch (NumberFormatException e) {
866 return 0;
867 }
868 }
869
870 private static int parseInt(String value) {
871 try {
872 return Integer.parseInt(value);
873 } catch (NumberFormatException e) {
874 return 0;
875 }
876 }
877
878 private static URL parseUrl(String value) {
879 try {
880 return new URL(value);
881 } catch (MalformedURLException e) {
882 return null;
883 }
884 }
885
886 public void untie() {
887 this.mNextMessage = null;
888 this.mPreviousMessage = null;
889 }
890
891 public boolean isPrivateMessage() {
892 return type == TYPE_PRIVATE || type == TYPE_PRIVATE_FILE;
893 }
894
895 public boolean isFileOrImage() {
896 return type == TYPE_FILE || type == TYPE_IMAGE || type == TYPE_PRIVATE_FILE;
897 }
898
899 public boolean hasFileOnRemoteHost() {
900 return isFileOrImage() && getFileParams().url != null;
901 }
902
903 public boolean needsUploading() {
904 return isFileOrImage() && getFileParams().url == null;
905 }
906
907 public class FileParams {
908 public URL url;
909 public long size = 0;
910 public int width = 0;
911 public int height = 0;
912 public int runtime = 0;
913 }
914
915 public void setFingerprint(String fingerprint) {
916 this.axolotlFingerprint = fingerprint;
917 }
918
919 public String getFingerprint() {
920 return axolotlFingerprint;
921 }
922
923 public boolean isTrusted() {
924 FingerprintStatus s = conversation.getAccount().getAxolotlService().getFingerprintTrust(axolotlFingerprint);
925 return s != null && s.isTrusted();
926 }
927
928 private int getPreviousEncryption() {
929 for (Message iterator = this.prev(); iterator != null; iterator = iterator.prev()) {
930 if (iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED) {
931 continue;
932 }
933 return iterator.getEncryption();
934 }
935 return ENCRYPTION_NONE;
936 }
937
938 private int getNextEncryption() {
939 if (this.conversation instanceof Conversation) {
940 Conversation conversation = (Conversation) this.conversation;
941 for (Message iterator = this.next(); iterator != null; iterator = iterator.next()) {
942 if (iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED) {
943 continue;
944 }
945 return iterator.getEncryption();
946 }
947 return conversation.getNextEncryption();
948 } else {
949 throw new AssertionError("This should never be called since isInValidSession should be disabled for stubs");
950 }
951 }
952
953 public boolean isValidInSession() {
954 int pastEncryption = getCleanedEncryption(this.getPreviousEncryption());
955 int futureEncryption = getCleanedEncryption(this.getNextEncryption());
956
957 boolean inUnencryptedSession = pastEncryption == ENCRYPTION_NONE
958 || futureEncryption == ENCRYPTION_NONE
959 || pastEncryption != futureEncryption;
960
961 return inUnencryptedSession || getCleanedEncryption(this.getEncryption()) == pastEncryption;
962 }
963
964 private static int getCleanedEncryption(int encryption) {
965 if (encryption == ENCRYPTION_DECRYPTED || encryption == ENCRYPTION_DECRYPTION_FAILED) {
966 return ENCRYPTION_PGP;
967 }
968 if (encryption == ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE || encryption == ENCRYPTION_AXOLOTL_FAILED) {
969 return ENCRYPTION_AXOLOTL;
970 }
971 return encryption;
972 }
973
974 public static boolean configurePrivateMessage(final Message message) {
975 return configurePrivateMessage(message, false);
976 }
977
978 public static boolean configurePrivateFileMessage(final Message message) {
979 return configurePrivateMessage(message, true);
980 }
981
982 private static boolean configurePrivateMessage(final Message message, final boolean isFile) {
983 final Conversation conversation;
984 if (message.conversation instanceof Conversation) {
985 conversation = (Conversation) message.conversation;
986 } else {
987 return false;
988 }
989 if (conversation.getMode() == Conversation.MODE_MULTI) {
990 final Jid nextCounterpart = conversation.getNextCounterpart();
991 if (nextCounterpart != null) {
992 message.setCounterpart(nextCounterpart);
993 message.setTrueCounterpart(conversation.getMucOptions().getTrueCounterpart(nextCounterpart));
994 message.setType(isFile ? Message.TYPE_PRIVATE_FILE : Message.TYPE_PRIVATE);
995 return true;
996 }
997 }
998 return false;
999 }
1000}