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