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() || contact.isSelf()));
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 public String getMimeType() {
684 String extension;
685 if (relativeFilePath != null) {
686 extension = MimeUtils.extractRelevantExtension(relativeFilePath);
687 } else {
688 try {
689 final URL url = new URL(body.split("\n")[0]);
690 extension = MimeUtils.extractRelevantExtension(url);
691 } catch (MalformedURLException e) {
692 return null;
693 }
694 }
695 return MimeUtils.guessMimeTypeFromExtension(extension);
696 }
697
698 public synchronized boolean treatAsDownloadable() {
699 if (treatAsDownloadable == null) {
700 try {
701 final String[] lines = body.split("\n");
702 if (lines.length ==0) {
703 treatAsDownloadable = false;
704 return false;
705 }
706 for(String line : lines) {
707 if (line.contains("\\s+")) {
708 treatAsDownloadable = false;
709 return false;
710 }
711 }
712 final URL url = new URL(lines[0]);
713 final String ref = url.getRef();
714 final String protocol = url.getProtocol();
715 final boolean encrypted = ref != null && AesGcmURLStreamHandler.IV_KEY.matcher(ref).matches();
716 final boolean followedByDataUri = lines.length == 2 && lines[1].startsWith("data:");
717 final boolean validAesGcm = AesGcmURLStreamHandler.PROTOCOL_NAME.equalsIgnoreCase(protocol) && encrypted && (lines.length == 1 || followedByDataUri);
718 final boolean validOob = ("http".equalsIgnoreCase(protocol) || "https".equalsIgnoreCase(protocol)) && (oob || encrypted) && lines.length == 1;
719 treatAsDownloadable = validAesGcm || validOob;
720 } catch (MalformedURLException e) {
721 treatAsDownloadable = false;
722 }
723 }
724 return treatAsDownloadable;
725 }
726
727 public synchronized boolean bodyIsOnlyEmojis() {
728 if (isEmojisOnly == null) {
729 isEmojisOnly = Emoticons.isOnlyEmoji(body.replaceAll("\\s",""));
730 }
731 return isEmojisOnly;
732 }
733
734 public synchronized boolean isGeoUri() {
735 if (isGeoUri == null) {
736 isGeoUri = GeoHelper.GEO_URI.matcher(body).matches();
737 }
738 return isGeoUri;
739 }
740
741 public synchronized void resetFileParams() {
742 this.fileParams = null;
743 }
744
745 public synchronized FileParams getFileParams() {
746 if (fileParams == null) {
747 fileParams = new FileParams();
748 if (this.transferable != null) {
749 fileParams.size = this.transferable.getFileSize();
750 }
751 String parts[] = body == null ? new String[0] : body.split("\\|");
752 switch (parts.length) {
753 case 1:
754 try {
755 fileParams.size = Long.parseLong(parts[0]);
756 } catch (NumberFormatException e) {
757 fileParams.url = parseUrl(parts[0]);
758 }
759 break;
760 case 5:
761 fileParams.runtime = parseInt(parts[4]);
762 case 4:
763 fileParams.width = parseInt(parts[2]);
764 fileParams.height = parseInt(parts[3]);
765 case 2:
766 fileParams.url = parseUrl(parts[0]);
767 fileParams.size = parseLong(parts[1]);
768 break;
769 case 3:
770 fileParams.size = parseLong(parts[0]);
771 fileParams.width = parseInt(parts[1]);
772 fileParams.height = parseInt(parts[2]);
773 break;
774 }
775 }
776 return fileParams;
777 }
778
779 private static long parseLong(String value) {
780 try {
781 return Long.parseLong(value);
782 } catch (NumberFormatException e) {
783 return 0;
784 }
785 }
786
787 private static int parseInt(String value) {
788 try {
789 return Integer.parseInt(value);
790 } catch (NumberFormatException e) {
791 return 0;
792 }
793 }
794
795 private static URL parseUrl(String value) {
796 try {
797 return new URL(value);
798 } catch (MalformedURLException e) {
799 return null;
800 }
801 }
802
803 public void untie() {
804 this.mNextMessage = null;
805 this.mPreviousMessage = null;
806 }
807
808 public boolean isFileOrImage() {
809 return type == TYPE_FILE || type == TYPE_IMAGE;
810 }
811
812 public boolean hasFileOnRemoteHost() {
813 return isFileOrImage() && getFileParams().url != null;
814 }
815
816 public boolean needsUploading() {
817 return isFileOrImage() && getFileParams().url == null;
818 }
819
820 public class FileParams {
821 public URL url;
822 public long size = 0;
823 public int width = 0;
824 public int height = 0;
825 public int runtime = 0;
826 }
827
828 public void setFingerprint(String fingerprint) {
829 this.axolotlFingerprint = fingerprint;
830 }
831
832 public String getFingerprint() {
833 return axolotlFingerprint;
834 }
835
836 public boolean isTrusted() {
837 FingerprintStatus s = conversation.getAccount().getAxolotlService().getFingerprintTrust(axolotlFingerprint);
838 return s != null && s.isTrusted();
839 }
840
841 private int getPreviousEncryption() {
842 for (Message iterator = this.prev(); iterator != null; iterator = iterator.prev()){
843 if( iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED ) {
844 continue;
845 }
846 return iterator.getEncryption();
847 }
848 return ENCRYPTION_NONE;
849 }
850
851 private int getNextEncryption() {
852 for (Message iterator = this.next(); iterator != null; iterator = iterator.next()){
853 if( iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED ) {
854 continue;
855 }
856 return iterator.getEncryption();
857 }
858 return conversation.getNextEncryption();
859 }
860
861 public boolean isValidInSession() {
862 int pastEncryption = getCleanedEncryption(this.getPreviousEncryption());
863 int futureEncryption = getCleanedEncryption(this.getNextEncryption());
864
865 boolean inUnencryptedSession = pastEncryption == ENCRYPTION_NONE
866 || futureEncryption == ENCRYPTION_NONE
867 || pastEncryption != futureEncryption;
868
869 return inUnencryptedSession || getCleanedEncryption(this.getEncryption()) == pastEncryption;
870 }
871
872 private static int getCleanedEncryption(int encryption) {
873 if (encryption == ENCRYPTION_DECRYPTED || encryption == ENCRYPTION_DECRYPTION_FAILED) {
874 return ENCRYPTION_PGP;
875 }
876 return encryption;
877 }
878}