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