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