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