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