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