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