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