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