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