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