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