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