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 );
573 }
574
575 private static boolean isStatusMergeable(int a, int b) {
576 return a == b || (
577 (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_UNSEND)
578 || (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_SEND)
579 || (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_WAITING)
580 || (a == Message.STATUS_SEND && b == Message.STATUS_UNSEND)
581 || (a == Message.STATUS_SEND && b == Message.STATUS_WAITING)
582 );
583 }
584
585 public void setCounterparts(List<MucOptions.User> counterparts) {
586 this.counterparts = counterparts;
587 }
588
589 public List<MucOptions.User> getCounterparts() {
590 return this.counterparts;
591 }
592
593 public static class MergeSeparator {}
594
595 public SpannableStringBuilder getMergedBody() {
596 SpannableStringBuilder body = new SpannableStringBuilder(this.body.trim());
597 Message current = this;
598 while (current.mergeable(current.next())) {
599 current = current.next();
600 if (current == null) {
601 break;
602 }
603 body.append("\n\n");
604 body.setSpan(new MergeSeparator(), body.length() - 2, body.length(),
605 SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE);
606 body.append(current.getBody().trim());
607 }
608 return body;
609 }
610
611 public boolean hasMeCommand() {
612 return this.body.trim().startsWith(ME_COMMAND);
613 }
614
615 public int getMergedStatus() {
616 int status = this.status;
617 Message current = this;
618 while(current.mergeable(current.next())) {
619 current = current.next();
620 if (current == null) {
621 break;
622 }
623 status = current.status;
624 }
625 return status;
626 }
627
628 public long getMergedTimeSent() {
629 long time = this.timeSent;
630 Message current = this;
631 while(current.mergeable(current.next())) {
632 current = current.next();
633 if (current == null) {
634 break;
635 }
636 time = current.timeSent;
637 }
638 return time;
639 }
640
641 public boolean wasMergedIntoPrevious() {
642 Message prev = this.prev();
643 return prev != null && prev.mergeable(this);
644 }
645
646 public boolean trusted() {
647 Contact contact = this.getContact();
648 return status > STATUS_RECEIVED || (contact != null && (contact.mutualPresenceSubscription() || contact.isSelf()));
649 }
650
651 public boolean fixCounterpart() {
652 Presences presences = conversation.getContact().getPresences();
653 if (counterpart != null && presences.has(counterpart.getResourcepart())) {
654 return true;
655 } else if (presences.size() >= 1) {
656 try {
657 counterpart = Jid.fromParts(conversation.getJid().getLocalpart(),
658 conversation.getJid().getDomainpart(),
659 presences.toResourceArray()[0]);
660 return true;
661 } catch (InvalidJidException e) {
662 counterpart = null;
663 return false;
664 }
665 } else {
666 counterpart = null;
667 return false;
668 }
669 }
670
671 public void setUuid(String uuid) {
672 this.uuid = uuid;
673 }
674
675 public String getEditedId() {
676 return edited;
677 }
678
679 public void setOob(boolean isOob) {
680 this.oob = isOob;
681 }
682
683 private static String extractRelevantExtension(URL url) {
684 String path = url.getPath();
685 return extractRelevantExtension(path);
686 }
687
688 private static String extractRelevantExtension(String path) {
689 if (path == null || path.isEmpty()) {
690 return null;
691 }
692
693 String filename = path.substring(path.lastIndexOf('/') + 1).toLowerCase();
694 int dotPosition = filename.lastIndexOf(".");
695
696 if (dotPosition != -1) {
697 String extension = filename.substring(dotPosition + 1);
698 // we want the real file extension, not the crypto one
699 if (Transferable.VALID_CRYPTO_EXTENSIONS.contains(extension)) {
700 return extractRelevantExtension(filename.substring(0,dotPosition));
701 } else {
702 return extension;
703 }
704 }
705 return null;
706 }
707
708 public String getMimeType() {
709 if (relativeFilePath != null) {
710 int start = relativeFilePath.lastIndexOf('.') + 1;
711 if (start < relativeFilePath.length()) {
712 return MimeUtils.guessMimeTypeFromExtension(relativeFilePath.substring(start));
713 } else {
714 return null;
715 }
716 } else {
717 try {
718 return MimeUtils.guessMimeTypeFromExtension(extractRelevantExtension(new URL(body.trim())));
719 } catch (MalformedURLException e) {
720 return null;
721 }
722 }
723 }
724
725 public synchronized boolean treatAsDownloadable() {
726 if (treatAsDownloadable == null) {
727 try {
728 final String[] lines = body.split("\n");
729 if (lines.length ==0) {
730 treatAsDownloadable = false;
731 return false;
732 }
733 for(String line : lines) {
734 if (line.contains("\\s+")) {
735 treatAsDownloadable = false;
736 return false;
737 }
738 }
739 final URL url = new URL(lines[0]);
740 final String ref = url.getRef();
741 final String protocol = url.getProtocol();
742 final boolean encrypted = ref != null && AesGcmURLStreamHandler.IV_KEY.matcher(ref).matches();
743 final boolean followedByDataUri = lines.length == 2 && lines[1].startsWith("data:");
744 final boolean validAesGcm = AesGcmURLStreamHandler.PROTOCOL_NAME.equalsIgnoreCase(protocol) && encrypted && (lines.length == 1 || followedByDataUri);
745 final boolean validOob = ("http".equalsIgnoreCase(protocol) || "https".equalsIgnoreCase(protocol)) && (oob || encrypted) && lines.length == 1;
746 treatAsDownloadable = validAesGcm || validOob;
747 } catch (MalformedURLException e) {
748 treatAsDownloadable = false;
749 }
750 }
751 return treatAsDownloadable;
752 }
753
754 public synchronized boolean bodyIsOnlyEmojis() {
755 if (isEmojisOnly == null) {
756 isEmojisOnly = Emoticons.isOnlyEmoji(body.replaceAll("\\s",""));
757 }
758 return isEmojisOnly;
759 }
760
761 public synchronized boolean isGeoUri() {
762 if (isGeoUri == null) {
763 isGeoUri = GeoHelper.GEO_URI.matcher(body).matches();
764 }
765 return isGeoUri;
766 }
767
768 public synchronized void resetFileParams() {
769 this.fileParams = null;
770 }
771
772 public synchronized FileParams getFileParams() {
773 if (fileParams == null) {
774 fileParams = new FileParams();
775 if (this.transferable != null) {
776 fileParams.size = this.transferable.getFileSize();
777 }
778 String parts[] = body == null ? new String[0] : body.split("\\|");
779 switch (parts.length) {
780 case 1:
781 try {
782 fileParams.size = Long.parseLong(parts[0]);
783 } catch (NumberFormatException e) {
784 fileParams.url = parseUrl(parts[0]);
785 }
786 break;
787 case 5:
788 fileParams.runtime = parseInt(parts[4]);
789 case 4:
790 fileParams.width = parseInt(parts[2]);
791 fileParams.height = parseInt(parts[3]);
792 case 2:
793 fileParams.url = parseUrl(parts[0]);
794 fileParams.size = parseLong(parts[1]);
795 break;
796 case 3:
797 fileParams.size = parseLong(parts[0]);
798 fileParams.width = parseInt(parts[1]);
799 fileParams.height = parseInt(parts[2]);
800 break;
801 }
802 }
803 return fileParams;
804 }
805
806 private static long parseLong(String value) {
807 try {
808 return Long.parseLong(value);
809 } catch (NumberFormatException e) {
810 return 0;
811 }
812 }
813
814 private static int parseInt(String value) {
815 try {
816 return Integer.parseInt(value);
817 } catch (NumberFormatException e) {
818 return 0;
819 }
820 }
821
822 private static URL parseUrl(String value) {
823 try {
824 return new URL(value);
825 } catch (MalformedURLException e) {
826 return null;
827 }
828 }
829
830 public void untie() {
831 this.mNextMessage = null;
832 this.mPreviousMessage = null;
833 }
834
835 public boolean isFileOrImage() {
836 return type == TYPE_FILE || type == TYPE_IMAGE;
837 }
838
839 public boolean hasFileOnRemoteHost() {
840 return isFileOrImage() && getFileParams().url != null;
841 }
842
843 public boolean needsUploading() {
844 return isFileOrImage() && getFileParams().url == null;
845 }
846
847 public class FileParams {
848 public URL url;
849 public long size = 0;
850 public int width = 0;
851 public int height = 0;
852 public int runtime = 0;
853 }
854
855 public void setFingerprint(String fingerprint) {
856 this.axolotlFingerprint = fingerprint;
857 }
858
859 public String getFingerprint() {
860 return axolotlFingerprint;
861 }
862
863 public boolean isTrusted() {
864 FingerprintStatus s = conversation.getAccount().getAxolotlService().getFingerprintTrust(axolotlFingerprint);
865 return s != null && s.isTrusted();
866 }
867
868 private int getPreviousEncryption() {
869 for (Message iterator = this.prev(); iterator != null; iterator = iterator.prev()){
870 if( iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED ) {
871 continue;
872 }
873 return iterator.getEncryption();
874 }
875 return ENCRYPTION_NONE;
876 }
877
878 private int getNextEncryption() {
879 for (Message iterator = this.next(); iterator != null; iterator = iterator.next()){
880 if( iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED ) {
881 continue;
882 }
883 return iterator.getEncryption();
884 }
885 return conversation.getNextEncryption();
886 }
887
888 public boolean isValidInSession() {
889 int pastEncryption = getCleanedEncryption(this.getPreviousEncryption());
890 int futureEncryption = getCleanedEncryption(this.getNextEncryption());
891
892 boolean inUnencryptedSession = pastEncryption == ENCRYPTION_NONE
893 || futureEncryption == ENCRYPTION_NONE
894 || pastEncryption != futureEncryption;
895
896 return inUnencryptedSession || getCleanedEncryption(this.getEncryption()) == pastEncryption;
897 }
898
899 private static int getCleanedEncryption(int encryption) {
900 if (encryption == ENCRYPTION_DECRYPTED || encryption == ENCRYPTION_DECRYPTION_FAILED) {
901 return ENCRYPTION_PGP;
902 }
903 return encryption;
904 }
905}