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