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_UNSEND && b == Message.STATUS_SEND)
510 || (a == Message.STATUS_UNSEND && b == Message.STATUS_SEND_RECEIVED)
511 || (a == Message.STATUS_SEND && b == Message.STATUS_UNSEND)
512 || (a == Message.STATUS_SEND && b == Message.STATUS_SEND_RECEIVED)
513 );
514 }
515
516 public static class MergeSeparator {}
517
518 public SpannableStringBuilder getMergedBody() {
519 SpannableStringBuilder body = new SpannableStringBuilder(this.body.trim());
520 Message current = this;
521 while (current.mergeable(current.next())) {
522 current = current.next();
523 if (current == null) {
524 break;
525 }
526 body.append("\n\n");
527 body.setSpan(new MergeSeparator(), body.length() - 2, body.length(),
528 SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE);
529 body.append(current.getBody().trim());
530 }
531 return body;
532 }
533
534 public boolean hasMeCommand() {
535 return this.body.trim().startsWith(ME_COMMAND);
536 }
537
538 public int getMergedStatus() {
539 int status = this.status;
540 Message current = this;
541 while(current.mergeable(current.next())) {
542 current = current.next();
543 if (current == null) {
544 break;
545 }
546 status = current.status;
547 }
548 return status;
549 }
550
551 public long getMergedTimeSent() {
552 long time = this.timeSent;
553 Message current = this;
554 while(current.mergeable(current.next())) {
555 current = current.next();
556 if (current == null) {
557 break;
558 }
559 time = current.timeSent;
560 }
561 return time;
562 }
563
564 public boolean wasMergedIntoPrevious() {
565 Message prev = this.prev();
566 return prev != null && prev.mergeable(this);
567 }
568
569 public boolean trusted() {
570 Contact contact = this.getContact();
571 return (status > STATUS_RECEIVED || (contact != null && contact.mutualPresenceSubscription()));
572 }
573
574 public boolean fixCounterpart() {
575 Presences presences = conversation.getContact().getPresences();
576 if (counterpart != null && presences.has(counterpart.getResourcepart())) {
577 return true;
578 } else if (presences.size() >= 1) {
579 try {
580 counterpart = Jid.fromParts(conversation.getJid().getLocalpart(),
581 conversation.getJid().getDomainpart(),
582 presences.toResourceArray()[0]);
583 return true;
584 } catch (InvalidJidException e) {
585 counterpart = null;
586 return false;
587 }
588 } else {
589 counterpart = null;
590 return false;
591 }
592 }
593
594 public void setUuid(String uuid) {
595 this.uuid = uuid;
596 }
597
598 public String getEditedId() {
599 return edited;
600 }
601
602 public void setOob(boolean isOob) {
603 this.oob = isOob;
604 }
605
606 private static String extractRelevantExtension(URL url) {
607 String path = url.getPath();
608 return extractRelevantExtension(path);
609 }
610
611 private static String extractRelevantExtension(String path) {
612 if (path == null || path.isEmpty()) {
613 return null;
614 }
615
616 String filename = path.substring(path.lastIndexOf('/') + 1).toLowerCase();
617 int dotPosition = filename.lastIndexOf(".");
618
619 if (dotPosition != -1) {
620 String extension = filename.substring(dotPosition + 1);
621 // we want the real file extension, not the crypto one
622 if (Transferable.VALID_CRYPTO_EXTENSIONS.contains(extension)) {
623 return extractRelevantExtension(filename.substring(0,dotPosition));
624 } else {
625 return extension;
626 }
627 }
628 return null;
629 }
630
631 public String getMimeType() {
632 if (relativeFilePath != null) {
633 int start = relativeFilePath.lastIndexOf('.') + 1;
634 if (start < relativeFilePath.length()) {
635 return MimeUtils.guessMimeTypeFromExtension(relativeFilePath.substring(start));
636 } else {
637 return null;
638 }
639 } else {
640 try {
641 return MimeUtils.guessMimeTypeFromExtension(extractRelevantExtension(new URL(body.trim())));
642 } catch (MalformedURLException e) {
643 return null;
644 }
645 }
646 }
647
648 public boolean treatAsDownloadable() {
649 if (body.trim().contains(" ")) {
650 return false;
651 }
652 try {
653 final URL url = new URL(body);
654 final String ref = url.getRef();
655 final String protocol = url.getProtocol();
656 final boolean encrypted = ref != null && ref.matches("([A-Fa-f0-9]{2}){48}");
657 return (AesGcmURLStreamHandler.PROTOCOL_NAME.equalsIgnoreCase(protocol) && encrypted)
658 || ((protocol.equalsIgnoreCase("http") || protocol.equalsIgnoreCase("https")) && (oob || encrypted));
659
660 } catch (MalformedURLException e) {
661 return false;
662 }
663 }
664
665 public boolean bodyIsHeart() {
666 return body != null && UIHelper.HEARTS.contains(body.trim());
667 }
668
669 public FileParams getFileParams() {
670 FileParams params = getLegacyFileParams();
671 if (params != null) {
672 return params;
673 }
674 params = new FileParams();
675 if (this.transferable != null) {
676 params.size = this.transferable.getFileSize();
677 }
678 if (body == null) {
679 return params;
680 }
681 String parts[] = body.split("\\|");
682 switch (parts.length) {
683 case 1:
684 try {
685 params.size = Long.parseLong(parts[0]);
686 } catch (NumberFormatException e) {
687 try {
688 params.url = new URL(parts[0]);
689 } catch (MalformedURLException e1) {
690 params.url = null;
691 }
692 }
693 break;
694 case 2:
695 case 4:
696 try {
697 params.url = new URL(parts[0]);
698 } catch (MalformedURLException e1) {
699 params.url = null;
700 }
701 try {
702 params.size = Long.parseLong(parts[1]);
703 } catch (NumberFormatException e) {
704 params.size = 0;
705 }
706 try {
707 params.width = Integer.parseInt(parts[2]);
708 } catch (NumberFormatException | ArrayIndexOutOfBoundsException e) {
709 params.width = 0;
710 }
711 try {
712 params.height = Integer.parseInt(parts[3]);
713 } catch (NumberFormatException | ArrayIndexOutOfBoundsException e) {
714 params.height = 0;
715 }
716 break;
717 case 3:
718 try {
719 params.size = Long.parseLong(parts[0]);
720 } catch (NumberFormatException e) {
721 params.size = 0;
722 }
723 try {
724 params.width = Integer.parseInt(parts[1]);
725 } catch (NumberFormatException e) {
726 params.width = 0;
727 }
728 try {
729 params.height = Integer.parseInt(parts[2]);
730 } catch (NumberFormatException e) {
731 params.height = 0;
732 }
733 break;
734 }
735 return params;
736 }
737
738 public FileParams getLegacyFileParams() {
739 FileParams params = new FileParams();
740 if (body == null) {
741 return params;
742 }
743 String parts[] = body.split(",");
744 if (parts.length == 3) {
745 try {
746 params.size = Long.parseLong(parts[0]);
747 } catch (NumberFormatException e) {
748 return null;
749 }
750 try {
751 params.width = Integer.parseInt(parts[1]);
752 } catch (NumberFormatException e) {
753 return null;
754 }
755 try {
756 params.height = Integer.parseInt(parts[2]);
757 } catch (NumberFormatException e) {
758 return null;
759 }
760 return params;
761 } else {
762 return null;
763 }
764 }
765
766 public void untie() {
767 this.mNextMessage = null;
768 this.mPreviousMessage = null;
769 }
770
771 public boolean isFileOrImage() {
772 return type == TYPE_FILE || type == TYPE_IMAGE;
773 }
774
775 public boolean hasFileOnRemoteHost() {
776 return isFileOrImage() && getFileParams().url != null;
777 }
778
779 public boolean needsUploading() {
780 return isFileOrImage() && getFileParams().url == null;
781 }
782
783 public class FileParams {
784 public URL url;
785 public long size = 0;
786 public int width = 0;
787 public int height = 0;
788 }
789
790 public void setFingerprint(String fingerprint) {
791 this.axolotlFingerprint = fingerprint;
792 }
793
794 public String getFingerprint() {
795 return axolotlFingerprint;
796 }
797
798 public boolean isTrusted() {
799 FingerprintStatus s = conversation.getAccount().getAxolotlService().getFingerprintTrust(axolotlFingerprint);
800 return s != null && s.isTrusted();
801 }
802
803 private int getPreviousEncryption() {
804 for (Message iterator = this.prev(); iterator != null; iterator = iterator.prev()){
805 if( iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED ) {
806 continue;
807 }
808 return iterator.getEncryption();
809 }
810 return ENCRYPTION_NONE;
811 }
812
813 private int getNextEncryption() {
814 for (Message iterator = this.next(); iterator != null; iterator = iterator.next()){
815 if( iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED ) {
816 continue;
817 }
818 return iterator.getEncryption();
819 }
820 return conversation.getNextEncryption();
821 }
822
823 public boolean isValidInSession() {
824 int pastEncryption = getCleanedEncryption(this.getPreviousEncryption());
825 int futureEncryption = getCleanedEncryption(this.getNextEncryption());
826
827 boolean inUnencryptedSession = pastEncryption == ENCRYPTION_NONE
828 || futureEncryption == ENCRYPTION_NONE
829 || pastEncryption != futureEncryption;
830
831 return inUnencryptedSession || getCleanedEncryption(this.getEncryption()) == pastEncryption;
832 }
833
834 private static int getCleanedEncryption(int encryption) {
835 if (encryption == ENCRYPTION_DECRYPTED || encryption == ENCRYPTION_DECRYPTION_FAILED) {
836 return ENCRYPTION_PGP;
837 }
838 return encryption;
839 }
840}