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