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