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 private 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 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 Conversational 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 if (this.conversation instanceof Conversation) {
512 final Conversation conversation = (Conversation) this.conversation;
513 synchronized (conversation.messages) {
514 if (this.mNextMessage == null) {
515 int index = conversation.messages.indexOf(this);
516 if (index < 0 || index >= conversation.messages.size() - 1) {
517 this.mNextMessage = null;
518 } else {
519 this.mNextMessage = conversation.messages.get(index + 1);
520 }
521 }
522 return this.mNextMessage;
523 }
524 } else {
525 throw new AssertionError("Calling next should be disabled for stubs");
526 }
527 }
528
529 public Message prev() {
530 if (this.conversation instanceof Conversation) {
531 final Conversation conversation = (Conversation) this.conversation;
532 synchronized (conversation.messages) {
533 if (this.mPreviousMessage == null) {
534 int index = conversation.messages.indexOf(this);
535 if (index <= 0 || index > conversation.messages.size()) {
536 this.mPreviousMessage = null;
537 } else {
538 this.mPreviousMessage = conversation.messages.get(index - 1);
539 }
540 }
541 }
542 return this.mPreviousMessage;
543 } else {
544 throw new AssertionError("Calling prev should be disabled for stubs");
545 }
546 }
547
548 public boolean isLastCorrectableMessage() {
549 Message next = next();
550 while (next != null) {
551 if (next.isCorrectable()) {
552 return false;
553 }
554 next = next.next();
555 }
556 return isCorrectable();
557 }
558
559 private boolean isCorrectable() {
560 return getStatus() != STATUS_RECEIVED && !isCarbon();
561 }
562
563 public boolean mergeable(final Message message) {
564 return message != null &&
565 (message.getType() == Message.TYPE_TEXT &&
566 this.getTransferable() == null &&
567 message.getTransferable() == null &&
568 message.getEncryption() != Message.ENCRYPTION_PGP &&
569 message.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED &&
570 this.getType() == message.getType() &&
571 //this.getStatus() == message.getStatus() &&
572 isStatusMergeable(this.getStatus(), message.getStatus()) &&
573 this.getEncryption() == message.getEncryption() &&
574 this.getCounterpart() != null &&
575 this.getCounterpart().equals(message.getCounterpart()) &&
576 this.edited() == message.edited() &&
577 (message.getTimeSent() - this.getTimeSent()) <= (Config.MESSAGE_MERGE_WINDOW * 1000) &&
578 this.getBody().length() + message.getBody().length() <= Config.MAX_DISPLAY_MESSAGE_CHARS &&
579 !message.isGeoUri() &&
580 !this.isGeoUri() &&
581 !message.treatAsDownloadable() &&
582 !this.treatAsDownloadable() &&
583 !message.getBody().startsWith(ME_COMMAND) &&
584 !this.getBody().startsWith(ME_COMMAND) &&
585 !this.bodyIsOnlyEmojis() &&
586 !message.bodyIsOnlyEmojis() &&
587 ((this.axolotlFingerprint == null && message.axolotlFingerprint == null) || this.axolotlFingerprint.equals(message.getFingerprint())) &&
588 UIHelper.sameDay(message.getTimeSent(), this.getTimeSent()) &&
589 this.getReadByMarkers().equals(message.getReadByMarkers()) &&
590 !this.conversation.getJid().asBareJid().equals(Config.BUG_REPORTS)
591 );
592 }
593
594 private static boolean isStatusMergeable(int a, int b) {
595 return a == b || (
596 (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_UNSEND)
597 || (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_SEND)
598 || (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_WAITING)
599 || (a == Message.STATUS_SEND && b == Message.STATUS_UNSEND)
600 || (a == Message.STATUS_SEND && b == Message.STATUS_WAITING)
601 );
602 }
603
604 public void setCounterparts(List<MucOptions.User> counterparts) {
605 this.counterparts = counterparts;
606 }
607
608 public List<MucOptions.User> getCounterparts() {
609 return this.counterparts;
610 }
611
612 public static class MergeSeparator {
613 }
614
615 public SpannableStringBuilder getMergedBody() {
616 SpannableStringBuilder body = new SpannableStringBuilder(this.body.trim());
617 Message current = this;
618 while (current.mergeable(current.next())) {
619 current = current.next();
620 if (current == null) {
621 break;
622 }
623 body.append("\n\n");
624 body.setSpan(new MergeSeparator(), body.length() - 2, body.length(),
625 SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE);
626 body.append(current.getBody().trim());
627 }
628 return body;
629 }
630
631 public boolean hasMeCommand() {
632 return this.body.trim().startsWith(ME_COMMAND);
633 }
634
635 public int getMergedStatus() {
636 int status = this.status;
637 Message current = this;
638 while (current.mergeable(current.next())) {
639 current = current.next();
640 if (current == null) {
641 break;
642 }
643 status = current.status;
644 }
645 return status;
646 }
647
648 public long getMergedTimeSent() {
649 long time = this.timeSent;
650 Message current = this;
651 while (current.mergeable(current.next())) {
652 current = current.next();
653 if (current == null) {
654 break;
655 }
656 time = current.timeSent;
657 }
658 return time;
659 }
660
661 public boolean wasMergedIntoPrevious() {
662 Message prev = this.prev();
663 return prev != null && prev.mergeable(this);
664 }
665
666 public boolean trusted() {
667 Contact contact = this.getContact();
668 return status > STATUS_RECEIVED || (contact != null && (contact.showInRoster() || contact.isSelf()));
669 }
670
671 public boolean fixCounterpart() {
672 Presences presences = conversation.getContact().getPresences();
673 if (counterpart != null && presences.has(counterpart.getResource())) {
674 return true;
675 } else if (presences.size() >= 1) {
676 try {
677 counterpart = Jid.of(conversation.getJid().getLocal(),
678 conversation.getJid().getDomain(),
679 presences.toResourceArray()[0]);
680 return true;
681 } catch (IllegalArgumentException e) {
682 counterpart = null;
683 return false;
684 }
685 } else {
686 counterpart = null;
687 return false;
688 }
689 }
690
691 public void setUuid(String uuid) {
692 this.uuid = uuid;
693 }
694
695 public String getEditedId() {
696 return edited;
697 }
698
699 public void setOob(boolean isOob) {
700 this.oob = isOob;
701 }
702
703 public String getMimeType() {
704 String extension;
705 if (relativeFilePath != null) {
706 extension = MimeUtils.extractRelevantExtension(relativeFilePath);
707 } else {
708 try {
709 final URL url = new URL(body.split("\n")[0]);
710 extension = MimeUtils.extractRelevantExtension(url);
711 } catch (MalformedURLException e) {
712 return null;
713 }
714 }
715 return MimeUtils.guessMimeTypeFromExtension(extension);
716 }
717
718 public synchronized boolean treatAsDownloadable() {
719 if (treatAsDownloadable == null) {
720 try {
721 final String[] lines = body.split("\n");
722 if (lines.length == 0) {
723 treatAsDownloadable = false;
724 return false;
725 }
726 for (String line : lines) {
727 if (line.contains("\\s+")) {
728 treatAsDownloadable = false;
729 return false;
730 }
731 }
732 final URL url = new URL(lines[0]);
733 final String ref = url.getRef();
734 final String protocol = url.getProtocol();
735 final boolean encrypted = ref != null && AesGcmURLStreamHandler.IV_KEY.matcher(ref).matches();
736 final boolean followedByDataUri = lines.length == 2 && lines[1].startsWith("data:");
737 final boolean validAesGcm = AesGcmURLStreamHandler.PROTOCOL_NAME.equalsIgnoreCase(protocol) && encrypted && (lines.length == 1 || followedByDataUri);
738 final boolean validOob = ("http".equalsIgnoreCase(protocol) || "https".equalsIgnoreCase(protocol)) && (oob || encrypted) && lines.length == 1;
739 treatAsDownloadable = validAesGcm || validOob;
740 } catch (MalformedURLException e) {
741 treatAsDownloadable = false;
742 }
743 }
744 return treatAsDownloadable;
745 }
746
747 public synchronized boolean bodyIsOnlyEmojis() {
748 if (isEmojisOnly == null) {
749 isEmojisOnly = Emoticons.isOnlyEmoji(body.replaceAll("\\s", ""));
750 }
751 return isEmojisOnly;
752 }
753
754 public synchronized boolean isGeoUri() {
755 if (isGeoUri == null) {
756 isGeoUri = GeoHelper.GEO_URI.matcher(body).matches();
757 }
758 return isGeoUri;
759 }
760
761 public synchronized void resetFileParams() {
762 this.fileParams = null;
763 }
764
765 public synchronized FileParams getFileParams() {
766 if (fileParams == null) {
767 fileParams = new FileParams();
768 if (this.transferable != null) {
769 fileParams.size = this.transferable.getFileSize();
770 }
771 String parts[] = body == null ? new String[0] : body.split("\\|");
772 switch (parts.length) {
773 case 1:
774 try {
775 fileParams.size = Long.parseLong(parts[0]);
776 } catch (NumberFormatException e) {
777 fileParams.url = parseUrl(parts[0]);
778 }
779 break;
780 case 5:
781 fileParams.runtime = parseInt(parts[4]);
782 case 4:
783 fileParams.width = parseInt(parts[2]);
784 fileParams.height = parseInt(parts[3]);
785 case 2:
786 fileParams.url = parseUrl(parts[0]);
787 fileParams.size = parseLong(parts[1]);
788 break;
789 case 3:
790 fileParams.size = parseLong(parts[0]);
791 fileParams.width = parseInt(parts[1]);
792 fileParams.height = parseInt(parts[2]);
793 break;
794 }
795 }
796 return fileParams;
797 }
798
799 private static long parseLong(String value) {
800 try {
801 return Long.parseLong(value);
802 } catch (NumberFormatException e) {
803 return 0;
804 }
805 }
806
807 private static int parseInt(String value) {
808 try {
809 return Integer.parseInt(value);
810 } catch (NumberFormatException e) {
811 return 0;
812 }
813 }
814
815 private static URL parseUrl(String value) {
816 try {
817 return new URL(value);
818 } catch (MalformedURLException e) {
819 return null;
820 }
821 }
822
823 public void untie() {
824 this.mNextMessage = null;
825 this.mPreviousMessage = null;
826 }
827
828 public boolean isFileOrImage() {
829 return type == TYPE_FILE || type == TYPE_IMAGE;
830 }
831
832 public boolean hasFileOnRemoteHost() {
833 return isFileOrImage() && getFileParams().url != null;
834 }
835
836 public boolean needsUploading() {
837 return isFileOrImage() && getFileParams().url == null;
838 }
839
840 public class FileParams {
841 public URL url;
842 public long size = 0;
843 public int width = 0;
844 public int height = 0;
845 public int runtime = 0;
846 }
847
848 public void setFingerprint(String fingerprint) {
849 this.axolotlFingerprint = fingerprint;
850 }
851
852 public String getFingerprint() {
853 return axolotlFingerprint;
854 }
855
856 public boolean isTrusted() {
857 FingerprintStatus s = conversation.getAccount().getAxolotlService().getFingerprintTrust(axolotlFingerprint);
858 return s != null && s.isTrusted();
859 }
860
861 private int getPreviousEncryption() {
862 for (Message iterator = this.prev(); iterator != null; iterator = iterator.prev()) {
863 if (iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED) {
864 continue;
865 }
866 return iterator.getEncryption();
867 }
868 return ENCRYPTION_NONE;
869 }
870
871 private int getNextEncryption() {
872 if (this.conversation instanceof Conversation) {
873 Conversation conversation = (Conversation) this.conversation;
874 for (Message iterator = this.next(); iterator != null; iterator = iterator.next()) {
875 if (iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED) {
876 continue;
877 }
878 return iterator.getEncryption();
879 }
880 return conversation.getNextEncryption();
881 } else {
882 throw new AssertionError("This should never be called since isInValidSession should be disabled for stubs");
883 }
884 }
885
886 public boolean isValidInSession() {
887 int pastEncryption = getCleanedEncryption(this.getPreviousEncryption());
888 int futureEncryption = getCleanedEncryption(this.getNextEncryption());
889
890 boolean inUnencryptedSession = pastEncryption == ENCRYPTION_NONE
891 || futureEncryption == ENCRYPTION_NONE
892 || pastEncryption != futureEncryption;
893
894 return inUnencryptedSession || getCleanedEncryption(this.getEncryption()) == pastEncryption;
895 }
896
897 private static int getCleanedEncryption(int encryption) {
898 if (encryption == ENCRYPTION_DECRYPTED || encryption == ENCRYPTION_DECRYPTION_FAILED) {
899 return ENCRYPTION_PGP;
900 }
901 if (encryption == ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE) {
902 return ENCRYPTION_AXOLOTL;
903 }
904 return encryption;
905 }
906}