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