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.crypto.axolotl.XmppAxolotlSession;
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() == Decision.NEVER &&
496 this.treatAsDownloadable() == Decision.NEVER &&
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 public enum Decision {
607 MUST,
608 SHOULD,
609 NEVER,
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 Decision treatAsDownloadable() {
655 if (body.trim().contains(" ")) {
656 return Decision.NEVER;
657 }
658 try {
659 URL url = new URL(body);
660 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 if ("omemo".equalsIgnoreCase(protocol) && encrypted) {
664 return Decision.MUST;
665 }
666 if (!url.getProtocol().equalsIgnoreCase("http") && !url.getProtocol().equalsIgnoreCase("https")) {
667 return Decision.NEVER;
668 } else if (oob) {
669 return Decision.MUST;
670 }
671 String extension = extractRelevantExtension(url);
672 if (extension == null) {
673 return Decision.NEVER;
674 }
675
676 if (encrypted) {
677 return Decision.MUST;
678 } else if (Transferable.VALID_IMAGE_EXTENSIONS.contains(extension)
679 || Transferable.WELL_KNOWN_EXTENSIONS.contains(extension)) {
680 return Decision.SHOULD;
681 } else {
682 return Decision.NEVER;
683 }
684
685 } catch (MalformedURLException e) {
686 return Decision.NEVER;
687 }
688 }
689
690 public boolean bodyIsHeart() {
691 return body != null && UIHelper.HEARTS.contains(body.trim());
692 }
693
694 public FileParams getFileParams() {
695 FileParams params = getLegacyFileParams();
696 if (params != null) {
697 return params;
698 }
699 params = new FileParams();
700 if (this.transferable != null) {
701 params.size = this.transferable.getFileSize();
702 }
703 if (body == null) {
704 return params;
705 }
706 String parts[] = body.split("\\|");
707 switch (parts.length) {
708 case 1:
709 try {
710 params.size = Long.parseLong(parts[0]);
711 } catch (NumberFormatException e) {
712 try {
713 params.url = new URL(parts[0]);
714 } catch (MalformedURLException e1) {
715 params.url = null;
716 }
717 }
718 break;
719 case 2:
720 case 4:
721 try {
722 params.url = new URL(parts[0]);
723 } catch (MalformedURLException e1) {
724 params.url = null;
725 }
726 try {
727 params.size = Long.parseLong(parts[1]);
728 } catch (NumberFormatException e) {
729 params.size = 0;
730 }
731 try {
732 params.width = Integer.parseInt(parts[2]);
733 } catch (NumberFormatException | ArrayIndexOutOfBoundsException e) {
734 params.width = 0;
735 }
736 try {
737 params.height = Integer.parseInt(parts[3]);
738 } catch (NumberFormatException | ArrayIndexOutOfBoundsException e) {
739 params.height = 0;
740 }
741 break;
742 case 3:
743 try {
744 params.size = Long.parseLong(parts[0]);
745 } catch (NumberFormatException e) {
746 params.size = 0;
747 }
748 try {
749 params.width = Integer.parseInt(parts[1]);
750 } catch (NumberFormatException e) {
751 params.width = 0;
752 }
753 try {
754 params.height = Integer.parseInt(parts[2]);
755 } catch (NumberFormatException e) {
756 params.height = 0;
757 }
758 break;
759 }
760 return params;
761 }
762
763 public FileParams getLegacyFileParams() {
764 FileParams params = new FileParams();
765 if (body == null) {
766 return params;
767 }
768 String parts[] = body.split(",");
769 if (parts.length == 3) {
770 try {
771 params.size = Long.parseLong(parts[0]);
772 } catch (NumberFormatException e) {
773 return null;
774 }
775 try {
776 params.width = Integer.parseInt(parts[1]);
777 } catch (NumberFormatException e) {
778 return null;
779 }
780 try {
781 params.height = Integer.parseInt(parts[2]);
782 } catch (NumberFormatException e) {
783 return null;
784 }
785 return params;
786 } else {
787 return null;
788 }
789 }
790
791 public void untie() {
792 this.mNextMessage = null;
793 this.mPreviousMessage = null;
794 }
795
796 public boolean isFileOrImage() {
797 return type == TYPE_FILE || type == TYPE_IMAGE;
798 }
799
800 public boolean hasFileOnRemoteHost() {
801 return isFileOrImage() && getFileParams().url != null;
802 }
803
804 public boolean needsUploading() {
805 return isFileOrImage() && getFileParams().url == null;
806 }
807
808 public class FileParams {
809 public URL url;
810 public long size = 0;
811 public int width = 0;
812 public int height = 0;
813 }
814
815 public void setFingerprint(String fingerprint) {
816 this.axolotlFingerprint = fingerprint;
817 }
818
819 public String getFingerprint() {
820 return axolotlFingerprint;
821 }
822
823 public boolean isTrusted() {
824 FingerprintStatus s = conversation.getAccount().getAxolotlService().getFingerprintTrust(axolotlFingerprint);
825 return s != null && s.isTrusted();
826 }
827
828 private int getPreviousEncryption() {
829 for (Message iterator = this.prev(); iterator != null; iterator = iterator.prev()){
830 if( iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED ) {
831 continue;
832 }
833 return iterator.getEncryption();
834 }
835 return ENCRYPTION_NONE;
836 }
837
838 private int getNextEncryption() {
839 for (Message iterator = this.next(); iterator != null; iterator = iterator.next()){
840 if( iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED ) {
841 continue;
842 }
843 return iterator.getEncryption();
844 }
845 return conversation.getNextEncryption();
846 }
847
848 public boolean isValidInSession() {
849 int pastEncryption = getCleanedEncryption(this.getPreviousEncryption());
850 int futureEncryption = getCleanedEncryption(this.getNextEncryption());
851
852 boolean inUnencryptedSession = pastEncryption == ENCRYPTION_NONE
853 || futureEncryption == ENCRYPTION_NONE
854 || pastEncryption != futureEncryption;
855
856 return inUnencryptedSession || getCleanedEncryption(this.getEncryption()) == pastEncryption;
857 }
858
859 private static int getCleanedEncryption(int encryption) {
860 if (encryption == ENCRYPTION_DECRYPTED || encryption == ENCRYPTION_DECRYPTION_FAILED) {
861 return ENCRYPTION_PGP;
862 }
863 return encryption;
864 }
865}