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 Conversational 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 protected Message(Conversational conversation) {
109 this.conversation = conversation;
110 }
111
112 public Message(Conversational conversation, String body, int encryption) {
113 this(conversation, body, encryption, STATUS_UNSEND);
114 }
115
116 public Message(Conversational 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 protected Message(final Conversational 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 @Override
233 public ContentValues getContentValues() {
234 ContentValues values = new ContentValues();
235 values.put(UUID, uuid);
236 values.put(CONVERSATION, conversationUuid);
237 if (counterpart == null) {
238 values.putNull(COUNTERPART);
239 } else {
240 values.put(COUNTERPART, counterpart.toString());
241 }
242 if (trueCounterpart == null) {
243 values.putNull(TRUE_COUNTERPART);
244 } else {
245 values.put(TRUE_COUNTERPART, trueCounterpart.toString());
246 }
247 values.put(BODY, body.length() > Config.MAX_STORAGE_MESSAGE_CHARS ? body.substring(0, Config.MAX_STORAGE_MESSAGE_CHARS) : body);
248 values.put(TIME_SENT, timeSent);
249 values.put(ENCRYPTION, encryption);
250 values.put(STATUS, status);
251 values.put(TYPE, type);
252 values.put(CARBON, carbon ? 1 : 0);
253 values.put(REMOTE_MSG_ID, remoteMsgId);
254 values.put(RELATIVE_FILE_PATH, relativeFilePath);
255 values.put(SERVER_MSG_ID, serverMsgId);
256 values.put(FINGERPRINT, axolotlFingerprint);
257 values.put(READ, read ? 1 : 0);
258 values.put(EDITED, edited);
259 values.put(OOB, oob ? 1 : 0);
260 values.put(ERROR_MESSAGE, errorMessage);
261 values.put(READ_BY_MARKERS, ReadByMarker.toJson(readByMarkers).toString());
262 values.put(MARKABLE, markable ? 1 : 0);
263 return values;
264 }
265
266 public String getConversationUuid() {
267 return conversationUuid;
268 }
269
270 public Conversational getConversation() {
271 return this.conversation;
272 }
273
274 public Jid getCounterpart() {
275 return counterpart;
276 }
277
278 public void setCounterpart(final Jid counterpart) {
279 this.counterpart = counterpart;
280 }
281
282 public Contact getContact() {
283 if (this.conversation.getMode() == Conversation.MODE_SINGLE) {
284 return this.conversation.getContact();
285 } else {
286 if (this.trueCounterpart == null) {
287 return null;
288 } else {
289 return this.conversation.getAccount().getRoster()
290 .getContactFromRoster(this.trueCounterpart);
291 }
292 }
293 }
294
295 public String getBody() {
296 return body;
297 }
298
299 public synchronized void setBody(String body) {
300 if (body == null) {
301 throw new Error("You should not set the message body to null");
302 }
303 this.body = body;
304 this.isGeoUri = null;
305 this.isEmojisOnly = null;
306 this.treatAsDownloadable = null;
307 this.fileParams = null;
308 }
309
310 public void setMucUser(MucOptions.User user) {
311 this.user = new WeakReference<>(user);
312 }
313
314 public boolean sameMucUser(Message otherMessage) {
315 final MucOptions.User thisUser = this.user == null ? null : this.user.get();
316 final MucOptions.User otherUser = otherMessage.user == null ? null : otherMessage.user.get();
317 return thisUser != null && thisUser == otherUser;
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().asBareJid().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 if (this.conversation instanceof Conversation) {
504 final Conversation conversation = (Conversation) this.conversation;
505 synchronized (conversation.messages) {
506 if (this.mNextMessage == null) {
507 int index = conversation.messages.indexOf(this);
508 if (index < 0 || index >= conversation.messages.size() - 1) {
509 this.mNextMessage = null;
510 } else {
511 this.mNextMessage = conversation.messages.get(index + 1);
512 }
513 }
514 return this.mNextMessage;
515 }
516 } else {
517 throw new AssertionError("Calling next should be disabled for stubs");
518 }
519 }
520
521 public Message prev() {
522 if (this.conversation instanceof Conversation) {
523 final Conversation conversation = (Conversation) this.conversation;
524 synchronized (conversation.messages) {
525 if (this.mPreviousMessage == null) {
526 int index = conversation.messages.indexOf(this);
527 if (index <= 0 || index > conversation.messages.size()) {
528 this.mPreviousMessage = null;
529 } else {
530 this.mPreviousMessage = conversation.messages.get(index - 1);
531 }
532 }
533 }
534 return this.mPreviousMessage;
535 } else {
536 throw new AssertionError("Calling prev should be disabled for stubs");
537 }
538 }
539
540 public boolean isLastCorrectableMessage() {
541 Message next = next();
542 while (next != null) {
543 if (next.isCorrectable()) {
544 return false;
545 }
546 next = next.next();
547 }
548 return isCorrectable();
549 }
550
551 private boolean isCorrectable() {
552 return getStatus() != STATUS_RECEIVED && !isCarbon();
553 }
554
555 public boolean mergeable(final Message message) {
556 return message != null &&
557 (message.getType() == Message.TYPE_TEXT &&
558 this.getTransferable() == null &&
559 message.getTransferable() == null &&
560 message.getEncryption() != Message.ENCRYPTION_PGP &&
561 message.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED &&
562 this.getType() == message.getType() &&
563 //this.getStatus() == message.getStatus() &&
564 isStatusMergeable(this.getStatus(), message.getStatus()) &&
565 this.getEncryption() == message.getEncryption() &&
566 this.getCounterpart() != null &&
567 this.getCounterpart().equals(message.getCounterpart()) &&
568 this.edited() == message.edited() &&
569 (message.getTimeSent() - this.getTimeSent()) <= (Config.MESSAGE_MERGE_WINDOW * 1000) &&
570 this.getBody().length() + message.getBody().length() <= Config.MAX_DISPLAY_MESSAGE_CHARS &&
571 !message.isGeoUri() &&
572 !this.isGeoUri() &&
573 !message.treatAsDownloadable() &&
574 !this.treatAsDownloadable() &&
575 !message.getBody().startsWith(ME_COMMAND) &&
576 !this.getBody().startsWith(ME_COMMAND) &&
577 !this.bodyIsOnlyEmojis() &&
578 !message.bodyIsOnlyEmojis() &&
579 ((this.axolotlFingerprint == null && message.axolotlFingerprint == null) || this.axolotlFingerprint.equals(message.getFingerprint())) &&
580 UIHelper.sameDay(message.getTimeSent(), this.getTimeSent()) &&
581 this.getReadByMarkers().equals(message.getReadByMarkers()) &&
582 !this.conversation.getJid().asBareJid().equals(Config.BUG_REPORTS)
583 );
584 }
585
586 private static boolean isStatusMergeable(int a, int b) {
587 return a == b || (
588 (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_UNSEND)
589 || (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_SEND)
590 || (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_WAITING)
591 || (a == Message.STATUS_SEND && b == Message.STATUS_UNSEND)
592 || (a == Message.STATUS_SEND && b == Message.STATUS_WAITING)
593 );
594 }
595
596 public void setCounterparts(List<MucOptions.User> counterparts) {
597 this.counterparts = counterparts;
598 }
599
600 public List<MucOptions.User> getCounterparts() {
601 return this.counterparts;
602 }
603
604 public static class MergeSeparator {
605 }
606
607 public SpannableStringBuilder getMergedBody() {
608 SpannableStringBuilder body = new SpannableStringBuilder(this.body.trim());
609 Message current = this;
610 while (current.mergeable(current.next())) {
611 current = current.next();
612 if (current == null) {
613 break;
614 }
615 body.append("\n\n");
616 body.setSpan(new MergeSeparator(), body.length() - 2, body.length(),
617 SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE);
618 body.append(current.getBody().trim());
619 }
620 return body;
621 }
622
623 public boolean hasMeCommand() {
624 return this.body.trim().startsWith(ME_COMMAND);
625 }
626
627 public int getMergedStatus() {
628 int status = this.status;
629 Message current = this;
630 while (current.mergeable(current.next())) {
631 current = current.next();
632 if (current == null) {
633 break;
634 }
635 status = current.status;
636 }
637 return status;
638 }
639
640 public long getMergedTimeSent() {
641 long time = this.timeSent;
642 Message current = this;
643 while (current.mergeable(current.next())) {
644 current = current.next();
645 if (current == null) {
646 break;
647 }
648 time = current.timeSent;
649 }
650 return time;
651 }
652
653 public boolean wasMergedIntoPrevious() {
654 Message prev = this.prev();
655 return prev != null && prev.mergeable(this);
656 }
657
658 public boolean trusted() {
659 Contact contact = this.getContact();
660 return status > STATUS_RECEIVED || (contact != null && (contact.showInRoster() || contact.isSelf()));
661 }
662
663 public boolean fixCounterpart() {
664 Presences presences = conversation.getContact().getPresences();
665 if (counterpart != null && presences.has(counterpart.getResource())) {
666 return true;
667 } else if (presences.size() >= 1) {
668 try {
669 counterpart = Jid.of(conversation.getJid().getLocal(),
670 conversation.getJid().getDomain(),
671 presences.toResourceArray()[0]);
672 return true;
673 } catch (IllegalArgumentException e) {
674 counterpart = null;
675 return false;
676 }
677 } else {
678 counterpart = null;
679 return false;
680 }
681 }
682
683 public void setUuid(String uuid) {
684 this.uuid = uuid;
685 }
686
687 public String getEditedId() {
688 return edited;
689 }
690
691 public void setOob(boolean isOob) {
692 this.oob = isOob;
693 }
694
695 public String getMimeType() {
696 String extension;
697 if (relativeFilePath != null) {
698 extension = MimeUtils.extractRelevantExtension(relativeFilePath);
699 } else {
700 try {
701 final URL url = new URL(body.split("\n")[0]);
702 extension = MimeUtils.extractRelevantExtension(url);
703 } catch (MalformedURLException e) {
704 return null;
705 }
706 }
707 return MimeUtils.guessMimeTypeFromExtension(extension);
708 }
709
710 public synchronized boolean treatAsDownloadable() {
711 if (treatAsDownloadable == null) {
712 try {
713 final String[] lines = body.split("\n");
714 if (lines.length == 0) {
715 treatAsDownloadable = false;
716 return false;
717 }
718 for (String line : lines) {
719 if (line.contains("\\s+")) {
720 treatAsDownloadable = false;
721 return false;
722 }
723 }
724 final URL url = new URL(lines[0]);
725 final String ref = url.getRef();
726 final String protocol = url.getProtocol();
727 final boolean encrypted = ref != null && AesGcmURLStreamHandler.IV_KEY.matcher(ref).matches();
728 final boolean followedByDataUri = lines.length == 2 && lines[1].startsWith("data:");
729 final boolean validAesGcm = AesGcmURLStreamHandler.PROTOCOL_NAME.equalsIgnoreCase(protocol) && encrypted && (lines.length == 1 || followedByDataUri);
730 final boolean validOob = ("http".equalsIgnoreCase(protocol) || "https".equalsIgnoreCase(protocol)) && (oob || encrypted) && lines.length == 1;
731 treatAsDownloadable = validAesGcm || validOob;
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 if (this.conversation instanceof Conversation) {
865 Conversation conversation = (Conversation) this.conversation;
866 for (Message iterator = this.next(); iterator != null; iterator = iterator.next()) {
867 if (iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED) {
868 continue;
869 }
870 return iterator.getEncryption();
871 }
872 return conversation.getNextEncryption();
873 } else {
874 throw new AssertionError("This should never be called since isInValidSession should be disabled for stubs");
875 }
876 }
877
878 public boolean isValidInSession() {
879 int pastEncryption = getCleanedEncryption(this.getPreviousEncryption());
880 int futureEncryption = getCleanedEncryption(this.getNextEncryption());
881
882 boolean inUnencryptedSession = pastEncryption == ENCRYPTION_NONE
883 || futureEncryption == ENCRYPTION_NONE
884 || pastEncryption != futureEncryption;
885
886 return inUnencryptedSession || getCleanedEncryption(this.getEncryption()) == pastEncryption;
887 }
888
889 private static int getCleanedEncryption(int encryption) {
890 if (encryption == ENCRYPTION_DECRYPTED || encryption == ENCRYPTION_DECRYPTION_FAILED) {
891 return ENCRYPTION_PGP;
892 }
893 if (encryption == ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE) {
894 return ENCRYPTION_AXOLOTL;
895 }
896 return encryption;
897 }
898}