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