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