1package eu.siacs.conversations.entities;
2
3import android.content.ContentValues;
4import android.database.Cursor;
5import android.support.annotation.NonNull;
6
7import net.java.otr4j.OtrException;
8import net.java.otr4j.crypto.OtrCryptoException;
9import net.java.otr4j.session.SessionID;
10import net.java.otr4j.session.SessionImpl;
11import net.java.otr4j.session.SessionStatus;
12
13import org.json.JSONArray;
14import org.json.JSONException;
15import org.json.JSONObject;
16
17import java.security.interfaces.DSAPublicKey;
18import java.util.ArrayList;
19import java.util.Collections;
20import java.util.Comparator;
21import java.util.Iterator;
22import java.util.List;
23import java.util.ListIterator;
24import java.util.Locale;
25import java.util.concurrent.atomic.AtomicBoolean;
26
27import eu.siacs.conversations.Config;
28import eu.siacs.conversations.crypto.PgpDecryptionService;
29import eu.siacs.conversations.crypto.axolotl.AxolotlService;
30import eu.siacs.conversations.xmpp.chatstate.ChatState;
31import eu.siacs.conversations.xmpp.jid.InvalidJidException;
32import eu.siacs.conversations.xmpp.jid.Jid;
33import eu.siacs.conversations.xmpp.mam.MamReference;
34
35
36public class Conversation extends AbstractEntity implements Blockable, Comparable<Conversation> {
37 public static final String TABLENAME = "conversations";
38
39 public static final int STATUS_AVAILABLE = 0;
40 public static final int STATUS_ARCHIVED = 1;
41
42 public static final int MODE_MULTI = 1;
43 public static final int MODE_SINGLE = 0;
44
45 public static final String NAME = "name";
46 public static final String ACCOUNT = "accountUuid";
47 public static final String CONTACT = "contactUuid";
48 public static final String CONTACTJID = "contactJid";
49 public static final String STATUS = "status";
50 public static final String CREATED = "created";
51 public static final String MODE = "mode";
52 public static final String ATTRIBUTES = "attributes";
53
54 public static final String ATTRIBUTE_MUTED_TILL = "muted_till";
55 public static final String ATTRIBUTE_ALWAYS_NOTIFY = "always_notify";
56 public static final String ATTRIBUTE_LAST_CLEAR_HISTORY = "last_clear_history";
57 public static final String ATTRIBUTE_NEXT_MESSAGE = "next_message";
58
59 private static final String ATTRIBUTE_CRYPTO_TARGETS = "crypto_targets";
60
61 private static final String ATTRIBUTE_NEXT_ENCRYPTION = "next_encryption";
62 static final String ATTRIBUTE_MUC_PASSWORD = "muc_password";
63
64 private String draftMessage;
65 private String name;
66 private String contactUuid;
67 private String accountUuid;
68 private Jid contactJid;
69 private int status;
70 private long created;
71 private int mode;
72
73 private JSONObject attributes = new JSONObject();
74
75 private Jid nextCounterpart;
76
77 protected final ArrayList<Message> messages = new ArrayList<>();
78 protected Account account = null;
79
80 private transient SessionImpl otrSession;
81
82 private transient String otrFingerprint = null;
83 private Smp mSmp = new Smp();
84
85 private transient MucOptions mucOptions = null;
86
87 private byte[] symmetricKey;
88
89 private Bookmark bookmark;
90
91 private boolean messagesLeftOnServer = true;
92 private ChatState mOutgoingChatState = Config.DEFAULT_CHATSTATE;
93 private ChatState mIncomingChatState = Config.DEFAULT_CHATSTATE;
94 private String mLastReceivedOtrMessageId = null;
95 private String mFirstMamReference = null;
96 private Message correctingMessage;
97 public AtomicBoolean messagesLoaded = new AtomicBoolean(true);
98
99 public boolean hasMessagesLeftOnServer() {
100 return messagesLeftOnServer;
101 }
102
103 public void setHasMessagesLeftOnServer(boolean value) {
104 this.messagesLeftOnServer = value;
105 }
106
107
108 public Message getFirstUnreadMessage() {
109 Message first = null;
110 synchronized (this.messages) {
111 for (int i = messages.size() - 1; i >= 0; --i) {
112 if (messages.get(i).isRead()) {
113 return first;
114 } else {
115 first = messages.get(i);
116 }
117 }
118 }
119 return first;
120 }
121
122 public Message findUnsentMessageWithUuid(String uuid) {
123 synchronized(this.messages) {
124 for (final Message message : this.messages) {
125 final int s = message.getStatus();
126 if ((s == Message.STATUS_UNSEND || s == Message.STATUS_WAITING) && message.getUuid().equals(uuid)) {
127 return message;
128 }
129 }
130 }
131 return null;
132 }
133
134 public void findWaitingMessages(OnMessageFound onMessageFound) {
135 synchronized (this.messages) {
136 for(Message message : this.messages) {
137 if (message.getStatus() == Message.STATUS_WAITING) {
138 onMessageFound.onMessageFound(message);
139 }
140 }
141 }
142 }
143
144 public void findUnreadMessages(OnMessageFound onMessageFound) {
145 synchronized (this.messages) {
146 for(Message message : this.messages) {
147 if (!message.isRead()) {
148 onMessageFound.onMessageFound(message);
149 }
150 }
151 }
152 }
153
154 public void findMessagesWithFiles(final OnMessageFound onMessageFound) {
155 synchronized (this.messages) {
156 for (final Message message : this.messages) {
157 if ((message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE)
158 && message.getEncryption() != Message.ENCRYPTION_PGP) {
159 onMessageFound.onMessageFound(message);
160 }
161 }
162 }
163 }
164
165 public Message findMessageWithFileAndUuid(final String uuid) {
166 synchronized (this.messages) {
167 for (final Message message : this.messages) {
168 if (message.getUuid().equals(uuid)
169 && message.getEncryption() != Message.ENCRYPTION_PGP
170 && (message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE || message.treatAsDownloadable())) {
171 return message;
172 }
173 }
174 }
175 return null;
176 }
177
178 public void clearMessages() {
179 synchronized (this.messages) {
180 this.messages.clear();
181 }
182 }
183
184 public boolean setIncomingChatState(ChatState state) {
185 if (this.mIncomingChatState == state) {
186 return false;
187 }
188 this.mIncomingChatState = state;
189 return true;
190 }
191
192 public ChatState getIncomingChatState() {
193 return this.mIncomingChatState;
194 }
195
196 public boolean setOutgoingChatState(ChatState state) {
197 if (mode == MODE_MULTI && (getNextCounterpart() != null || !isPnNA())) {
198 return false;
199 }
200 if (this.mOutgoingChatState != state) {
201 this.mOutgoingChatState = state;
202 return true;
203 } else {
204 return false;
205 }
206 }
207
208 public ChatState getOutgoingChatState() {
209 return this.mOutgoingChatState;
210 }
211
212 public void trim() {
213 synchronized (this.messages) {
214 final int size = messages.size();
215 final int maxsize = Config.PAGE_SIZE * Config.MAX_NUM_PAGES;
216 if (size > maxsize) {
217 List<Message> discards = this.messages.subList(0, size - maxsize);
218 final PgpDecryptionService pgpDecryptionService = account.getPgpDecryptionService();
219 if (pgpDecryptionService != null) {
220 pgpDecryptionService.discard(discards);
221 }
222 discards.clear();
223 untieMessages();
224 }
225 }
226 }
227
228 public void findUnsentMessagesWithEncryption(int encryptionType, OnMessageFound onMessageFound) {
229 synchronized (this.messages) {
230 for (Message message : this.messages) {
231 if ((message.getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_WAITING)
232 && (message.getEncryption() == encryptionType)) {
233 onMessageFound.onMessageFound(message);
234 }
235 }
236 }
237 }
238
239 public void findUnsentTextMessages(OnMessageFound onMessageFound) {
240 synchronized (this.messages) {
241 for (Message message : this.messages) {
242 if (message.getType() != Message.TYPE_IMAGE
243 && message.getStatus() == Message.STATUS_UNSEND) {
244 onMessageFound.onMessageFound(message);
245 }
246 }
247 }
248 }
249
250 public Message findSentMessageWithUuidOrRemoteId(String id) {
251 synchronized (this.messages) {
252 for (Message message : this.messages) {
253 if (id.equals(message.getUuid())
254 || (message.getStatus() >= Message.STATUS_SEND
255 && id.equals(message.getRemoteMsgId()))) {
256 return message;
257 }
258 }
259 }
260 return null;
261 }
262
263 public Message findMessageWithRemoteIdAndCounterpart(String id, Jid counterpart, boolean received, boolean carbon) {
264 synchronized (this.messages) {
265 for(int i = this.messages.size() - 1; i >= 0; --i) {
266 Message message = messages.get(i);
267 if (counterpart.equals(message.getCounterpart())
268 && ((message.getStatus() == Message.STATUS_RECEIVED) == received)
269 && (carbon == message.isCarbon() || received) ) {
270 if (id.equals(message.getRemoteMsgId()) && !message.isFileOrImage() && !message.treatAsDownloadable()) {
271 return message;
272 } else {
273 return null;
274 }
275 }
276 }
277 }
278 return null;
279 }
280
281 public Message findSentMessageWithUuid(String id) {
282 synchronized (this.messages) {
283 for (Message message : this.messages) {
284 if (id.equals(message.getUuid())) {
285 return message;
286 }
287 }
288 }
289 return null;
290 }
291
292 public Message findMessageWithRemoteId(String id, Jid counterpart) {
293 synchronized (this.messages) {
294 for(Message message : this.messages) {
295 if (counterpart.equals(message.getCounterpart())
296 && (id.equals(message.getRemoteMsgId()) || id.equals(message.getUuid()))) {
297 return message;
298 }
299 }
300 }
301 return null;
302 }
303
304 public boolean hasMessageWithCounterpart(Jid counterpart) {
305 synchronized (this.messages) {
306 for(Message message : this.messages) {
307 if (counterpart.equals(message.getCounterpart())) {
308 return true;
309 }
310 }
311 }
312 return false;
313 }
314
315 public void populateWithMessages(final List<Message> messages) {
316 synchronized (this.messages) {
317 messages.clear();
318 messages.addAll(this.messages);
319 }
320 for(Iterator<Message> iterator = messages.iterator(); iterator.hasNext();) {
321 if (iterator.next().wasMergedIntoPrevious()) {
322 iterator.remove();
323 }
324 }
325 }
326
327 @Override
328 public boolean isBlocked() {
329 return getContact().isBlocked();
330 }
331
332 @Override
333 public boolean isDomainBlocked() {
334 return getContact().isDomainBlocked();
335 }
336
337 @Override
338 public Jid getBlockedJid() {
339 return getContact().getBlockedJid();
340 }
341
342 public String getLastReceivedOtrMessageId() {
343 return this.mLastReceivedOtrMessageId;
344 }
345
346 public void setLastReceivedOtrMessageId(String id) {
347 this.mLastReceivedOtrMessageId = id;
348 }
349
350 public int countMessages() {
351 synchronized (this.messages) {
352 return this.messages.size();
353 }
354 }
355
356 public void setFirstMamReference(String reference) {
357 this.mFirstMamReference = reference;
358 }
359
360 public String getFirstMamReference() {
361 return this.mFirstMamReference;
362 }
363
364 public void setLastClearHistory(long time,String reference) {
365 if (reference != null) {
366 setAttribute(ATTRIBUTE_LAST_CLEAR_HISTORY, String.valueOf(time) + ":" + reference);
367 } else {
368 setAttribute(ATTRIBUTE_LAST_CLEAR_HISTORY, String.valueOf(time));
369 }
370 }
371
372 public MamReference getLastClearHistory() {
373 return MamReference.fromAttribute(getAttribute(ATTRIBUTE_LAST_CLEAR_HISTORY));
374 }
375
376 public List<Jid> getAcceptedCryptoTargets() {
377 if (mode == MODE_SINGLE) {
378 return Collections.singletonList(getJid().toBareJid());
379 } else {
380 return getJidListAttribute(ATTRIBUTE_CRYPTO_TARGETS);
381 }
382 }
383
384 public void setAcceptedCryptoTargets(List<Jid> acceptedTargets) {
385 setAttribute(ATTRIBUTE_CRYPTO_TARGETS, acceptedTargets);
386 }
387
388 public boolean setCorrectingMessage(Message correctingMessage) {
389 this.correctingMessage = correctingMessage;
390 return correctingMessage == null && draftMessage != null;
391 }
392
393 public Message getCorrectingMessage() {
394 return this.correctingMessage;
395 }
396
397 public boolean withSelf() {
398 return getContact().isSelf();
399 }
400
401 @Override
402 public int compareTo(@NonNull Conversation another) {
403 final Message left = getLatestMessage();
404 final Message right = another.getLatestMessage();
405 if (left.getTimeSent() > right.getTimeSent()) {
406 return -1;
407 } else if (left.getTimeSent() < right.getTimeSent()) {
408 return 1;
409 } else {
410 return 0;
411 }
412 }
413
414 public void setDraftMessage(String draftMessage) {
415 this.draftMessage = draftMessage;
416 }
417
418 public String getDraftMessage() {
419 return draftMessage;
420 }
421
422 public interface OnMessageFound {
423 void onMessageFound(final Message message);
424 }
425
426 public Conversation(final String name, final Account account, final Jid contactJid,
427 final int mode) {
428 this(java.util.UUID.randomUUID().toString(), name, null, account
429 .getUuid(), contactJid, System.currentTimeMillis(),
430 STATUS_AVAILABLE, mode, "");
431 this.account = account;
432 }
433
434 public Conversation(final String uuid, final String name, final String contactUuid,
435 final String accountUuid, final Jid contactJid, final long created, final int status,
436 final int mode, final String attributes) {
437 this.uuid = uuid;
438 this.name = name;
439 this.contactUuid = contactUuid;
440 this.accountUuid = accountUuid;
441 this.contactJid = contactJid;
442 this.created = created;
443 this.status = status;
444 this.mode = mode;
445 try {
446 this.attributes = new JSONObject(attributes == null ? "" : attributes);
447 } catch (JSONException e) {
448 this.attributes = new JSONObject();
449 }
450 }
451
452 public boolean isRead() {
453 return (this.messages.size() == 0) || this.messages.get(this.messages.size() - 1).isRead();
454 }
455
456 public List<Message> markRead() {
457 final List<Message> unread = new ArrayList<>();
458 synchronized (this.messages) {
459 for(Message message : this.messages) {
460 if (!message.isRead()) {
461 message.markRead();
462 unread.add(message);
463 }
464 }
465 }
466 return unread;
467 }
468
469 public Message getLatestMarkableMessage() {
470 synchronized (this.messages) {
471 for (int i = this.messages.size() - 1; i >= 0; --i) {
472 final Message message = this.messages.get(i);
473 if (message.getStatus() <= Message.STATUS_RECEIVED
474 && message.markable
475 && message.getType() != Message.TYPE_PRIVATE) {
476 return message.isRead() ? null : message;
477 }
478 }
479 }
480 return null;
481 }
482
483 public Message getLatestMessage() {
484 synchronized (this.messages) {
485 if (this.messages.size() == 0) {
486 Message message = new Message(this, "", Message.ENCRYPTION_NONE);
487 message.setType(Message.TYPE_STATUS);
488 message.setTime(Math.max(getCreated(), getLastClearHistory().getTimestamp()));
489 return message;
490 } else {
491 return this.messages.get(this.messages.size() - 1);
492 }
493 }
494 }
495
496 public String getName() {
497 if (getMode() == MODE_MULTI) {
498 final String subject = getMucOptions().getSubject();
499 final String bookmarkName = bookmark != null ? bookmark.getBookmarkName() : null;
500 if (subject != null && !subject.trim().isEmpty()) {
501 return subject;
502 } else if (bookmarkName != null && !bookmarkName.trim().isEmpty()) {
503 return bookmarkName;
504 } else {
505 String generatedName = getMucOptions().createNameFromParticipants();
506 if (generatedName != null) {
507 return generatedName;
508 } else {
509 return getJid().getUnescapedLocalpart();
510 }
511 }
512 } else if (isWithStranger()) {
513 return contactJid.toBareJid().toString();
514 } else {
515 return this.getContact().getDisplayName();
516 }
517 }
518
519 public String getAccountUuid() {
520 return this.accountUuid;
521 }
522
523 public Account getAccount() {
524 return this.account;
525 }
526
527 public Contact getContact() {
528 return this.account.getRoster().getContact(this.contactJid);
529 }
530
531 public void setAccount(final Account account) {
532 this.account = account;
533 }
534
535 @Override
536 public Jid getJid() {
537 return this.contactJid;
538 }
539
540 public int getStatus() {
541 return this.status;
542 }
543
544 public long getCreated() {
545 return this.created;
546 }
547
548 public ContentValues getContentValues() {
549 ContentValues values = new ContentValues();
550 values.put(UUID, uuid);
551 values.put(NAME, name);
552 values.put(CONTACT, contactUuid);
553 values.put(ACCOUNT, accountUuid);
554 values.put(CONTACTJID, contactJid.toPreppedString());
555 values.put(CREATED, created);
556 values.put(STATUS, status);
557 values.put(MODE, mode);
558 values.put(ATTRIBUTES, attributes.toString());
559 return values;
560 }
561
562 public static Conversation fromCursor(Cursor cursor) {
563 Jid jid;
564 try {
565 jid = Jid.fromString(cursor.getString(cursor.getColumnIndex(CONTACTJID)), true);
566 } catch (final InvalidJidException e) {
567 // Borked DB..
568 jid = null;
569 }
570 return new Conversation(cursor.getString(cursor.getColumnIndex(UUID)),
571 cursor.getString(cursor.getColumnIndex(NAME)),
572 cursor.getString(cursor.getColumnIndex(CONTACT)),
573 cursor.getString(cursor.getColumnIndex(ACCOUNT)),
574 jid,
575 cursor.getLong(cursor.getColumnIndex(CREATED)),
576 cursor.getInt(cursor.getColumnIndex(STATUS)),
577 cursor.getInt(cursor.getColumnIndex(MODE)),
578 cursor.getString(cursor.getColumnIndex(ATTRIBUTES)));
579 }
580
581 public void setStatus(int status) {
582 this.status = status;
583 }
584
585 public int getMode() {
586 return this.mode;
587 }
588
589 public void setMode(int mode) {
590 this.mode = mode;
591 }
592
593 public SessionImpl startOtrSession(String presence, boolean sendStart) {
594 if (this.otrSession != null) {
595 return this.otrSession;
596 } else {
597 final SessionID sessionId = new SessionID(this.getJid().toBareJid().toString(),
598 presence,
599 "xmpp");
600 this.otrSession = new SessionImpl(sessionId, getAccount().getOtrService());
601 try {
602 if (sendStart) {
603 this.otrSession.startSession();
604 return this.otrSession;
605 }
606 return this.otrSession;
607 } catch (OtrException e) {
608 return null;
609 }
610 }
611
612 }
613
614 public SessionImpl getOtrSession() {
615 return this.otrSession;
616 }
617
618 public void resetOtrSession() {
619 this.otrFingerprint = null;
620 this.otrSession = null;
621 this.mSmp.hint = null;
622 this.mSmp.secret = null;
623 this.mSmp.status = Smp.STATUS_NONE;
624 }
625
626 public Smp smp() {
627 return mSmp;
628 }
629
630 public boolean startOtrIfNeeded() {
631 if (this.otrSession != null && this.otrSession.getSessionStatus() != SessionStatus.ENCRYPTED) {
632 try {
633 this.otrSession.startSession();
634 return true;
635 } catch (OtrException e) {
636 this.resetOtrSession();
637 return false;
638 }
639 } else {
640 return true;
641 }
642 }
643
644 public boolean endOtrIfNeeded() {
645 if (this.otrSession != null) {
646 if (this.otrSession.getSessionStatus() == SessionStatus.ENCRYPTED) {
647 try {
648 this.otrSession.endSession();
649 this.resetOtrSession();
650 return true;
651 } catch (OtrException e) {
652 this.resetOtrSession();
653 return false;
654 }
655 } else {
656 this.resetOtrSession();
657 return false;
658 }
659 } else {
660 return false;
661 }
662 }
663
664 public boolean hasValidOtrSession() {
665 return this.otrSession != null;
666 }
667
668 public synchronized String getOtrFingerprint() {
669 if (this.otrFingerprint == null) {
670 try {
671 if (getOtrSession() == null || getOtrSession().getSessionStatus() != SessionStatus.ENCRYPTED) {
672 return null;
673 }
674 DSAPublicKey remotePubKey = (DSAPublicKey) getOtrSession().getRemotePublicKey();
675 this.otrFingerprint = getAccount().getOtrService().getFingerprint(remotePubKey).toLowerCase(Locale.US);
676 } catch (final OtrCryptoException | UnsupportedOperationException ignored) {
677 return null;
678 }
679 }
680 return this.otrFingerprint;
681 }
682
683 public boolean verifyOtrFingerprint() {
684 final String fingerprint = getOtrFingerprint();
685 if (fingerprint != null) {
686 getContact().addOtrFingerprint(fingerprint);
687 return true;
688 } else {
689 return false;
690 }
691 }
692
693 public boolean isOtrFingerprintVerified() {
694 return getContact().getOtrFingerprints().contains(getOtrFingerprint());
695 }
696
697 /**
698 * short for is Private and Non-anonymous
699 */
700 private boolean isPnNA() {
701 return mode == MODE_SINGLE || (getMucOptions().membersOnly() && getMucOptions().nonanonymous());
702 }
703
704 public synchronized MucOptions getMucOptions() {
705 if (this.mucOptions == null) {
706 this.mucOptions = new MucOptions(this);
707 }
708 return this.mucOptions;
709 }
710
711 public void resetMucOptions() {
712 this.mucOptions = null;
713 }
714
715 public void setContactJid(final Jid jid) {
716 this.contactJid = jid;
717 }
718
719 public void setNextCounterpart(Jid jid) {
720 this.nextCounterpart = jid;
721 }
722
723 public Jid getNextCounterpart() {
724 return this.nextCounterpart;
725 }
726
727 public int getNextEncryption() {
728 return fixAvailableEncryption(this.getIntAttribute(ATTRIBUTE_NEXT_ENCRYPTION, getDefaultEncryption()));
729 }
730
731 private int fixAvailableEncryption(int selectedEncryption) {
732 switch(selectedEncryption) {
733 case Message.ENCRYPTION_NONE:
734 return Config.supportUnencrypted() ? selectedEncryption : getDefaultEncryption();
735 case Message.ENCRYPTION_AXOLOTL:
736 return Config.supportOmemo() ? selectedEncryption : getDefaultEncryption();
737 case Message.ENCRYPTION_OTR:
738 return Config.supportOtr() ? selectedEncryption : getDefaultEncryption();
739 case Message.ENCRYPTION_PGP:
740 case Message.ENCRYPTION_DECRYPTED:
741 case Message.ENCRYPTION_DECRYPTION_FAILED:
742 return Config.supportOpenPgp() ? Message.ENCRYPTION_PGP : getDefaultEncryption();
743 default:
744 return getDefaultEncryption();
745 }
746 }
747
748 private int getDefaultEncryption() {
749 AxolotlService axolotlService = account.getAxolotlService();
750 if (Config.supportUnencrypted()) {
751 return Message.ENCRYPTION_NONE;
752 } else if (Config.supportOmemo()
753 && (axolotlService != null && axolotlService.isConversationAxolotlCapable(this) || !Config.multipleEncryptionChoices())) {
754 return Message.ENCRYPTION_AXOLOTL;
755 } else if (Config.supportOtr() && mode == MODE_SINGLE) {
756 return Message.ENCRYPTION_OTR;
757 } else if (Config.supportOpenPgp()) {
758 return Message.ENCRYPTION_PGP;
759 } else {
760 return Message.ENCRYPTION_NONE;
761 }
762 }
763
764 public void setNextEncryption(int encryption) {
765 this.setAttribute(ATTRIBUTE_NEXT_ENCRYPTION, String.valueOf(encryption));
766 }
767
768 public String getNextMessage() {
769 final String nextMessage = getAttribute(ATTRIBUTE_NEXT_MESSAGE);
770 return nextMessage == null ? "" : nextMessage;
771 }
772
773 public boolean smpRequested() {
774 return smp().status == Smp.STATUS_CONTACT_REQUESTED;
775 }
776
777 public boolean setNextMessage(String message) {
778 boolean changed = !getNextMessage().equals(message);
779 this.setAttribute(ATTRIBUTE_NEXT_MESSAGE, message);
780 return changed;
781 }
782
783 public void setSymmetricKey(byte[] key) {
784 this.symmetricKey = key;
785 }
786
787 public byte[] getSymmetricKey() {
788 return this.symmetricKey;
789 }
790
791 public void setBookmark(Bookmark bookmark) {
792 this.bookmark = bookmark;
793 this.bookmark.setConversation(this);
794 }
795
796 public void deregisterWithBookmark() {
797 if (this.bookmark != null) {
798 this.bookmark.setConversation(null);
799 }
800 this.bookmark = null;
801 }
802
803 public Bookmark getBookmark() {
804 return this.bookmark;
805 }
806
807 public Message findDuplicateMessage(Message message) {
808 synchronized (this.messages) {
809 for (int i = this.messages.size() - 1; i >= 0; --i) {
810 if (this.messages.get(i).similar(message)) {
811 return this.messages.get(i);
812 }
813 }
814 }
815 return null;
816 }
817
818 public boolean hasDuplicateMessage(Message message) {
819 return findDuplicateMessage(message) != null;
820 }
821
822 public Message findSentMessageWithBody(String body) {
823 synchronized (this.messages) {
824 for (int i = this.messages.size() - 1; i >= 0; --i) {
825 Message message = this.messages.get(i);
826 if (message.getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_SEND) {
827 String otherBody;
828 if (message.hasFileOnRemoteHost()) {
829 otherBody = message.getFileParams().url.toString();
830 } else {
831 otherBody = message.body;
832 }
833 if (otherBody != null && otherBody.equals(body)) {
834 return message;
835 }
836 }
837 }
838 return null;
839 }
840 }
841
842 public MamReference getLastMessageTransmitted() {
843 final MamReference lastClear = getLastClearHistory();
844 MamReference lastReceived = new MamReference(0);
845 synchronized (this.messages) {
846 for(int i = this.messages.size() - 1; i >= 0; --i) {
847 final Message message = this.messages.get(i);
848 if (message.getType() == Message.TYPE_PRIVATE) {
849 continue; //it's unsafe to use private messages as anchor. They could be coming from user archive
850 }
851 if (message.getStatus() == Message.STATUS_RECEIVED || message.isCarbon() || message.getServerMsgId() != null) {
852 lastReceived = new MamReference(message.getTimeSent(),message.getServerMsgId());
853 break;
854 }
855 }
856 }
857 return MamReference.max(lastClear,lastReceived);
858 }
859
860 public void setMutedTill(long value) {
861 this.setAttribute(ATTRIBUTE_MUTED_TILL, String.valueOf(value));
862 }
863
864 public boolean isMuted() {
865 return System.currentTimeMillis() < this.getLongAttribute(ATTRIBUTE_MUTED_TILL, 0);
866 }
867
868 public boolean alwaysNotify() {
869 return mode == MODE_SINGLE || getBooleanAttribute(ATTRIBUTE_ALWAYS_NOTIFY, Config.ALWAYS_NOTIFY_BY_DEFAULT || isPnNA());
870 }
871
872 public boolean setAttribute(String key, String value) {
873 synchronized (this.attributes) {
874 try {
875 this.attributes.put(key, value == null ? "" : value);
876 return true;
877 } catch (JSONException e) {
878 return false;
879 }
880 }
881 }
882
883 public boolean setAttribute(String key, List<Jid> jids) {
884 JSONArray array = new JSONArray();
885 for(Jid jid : jids) {
886 array.put(jid.toBareJid().toString());
887 }
888 synchronized (this.attributes) {
889 try {
890 this.attributes.put(key, array);
891 return true;
892 } catch (JSONException e) {
893 e.printStackTrace();
894 return false;
895 }
896 }
897 }
898
899 public String getAttribute(String key) {
900 synchronized (this.attributes) {
901 try {
902 return this.attributes.getString(key);
903 } catch (JSONException e) {
904 return null;
905 }
906 }
907 }
908
909 private List<Jid> getJidListAttribute(String key) {
910 ArrayList<Jid> list = new ArrayList<>();
911 synchronized (this.attributes) {
912 try {
913 JSONArray array = this.attributes.getJSONArray(key);
914 for (int i = 0; i < array.length(); ++i) {
915 try {
916 list.add(Jid.fromString(array.getString(i)));
917 } catch (InvalidJidException e) {
918 //ignored
919 }
920 }
921 } catch (JSONException e) {
922 //ignored
923 }
924 }
925 return list;
926 }
927
928 private int getIntAttribute(String key, int defaultValue) {
929 String value = this.getAttribute(key);
930 if (value == null) {
931 return defaultValue;
932 } else {
933 try {
934 return Integer.parseInt(value);
935 } catch (NumberFormatException e) {
936 return defaultValue;
937 }
938 }
939 }
940
941 public long getLongAttribute(String key, long defaultValue) {
942 String value = this.getAttribute(key);
943 if (value == null) {
944 return defaultValue;
945 } else {
946 try {
947 return Long.parseLong(value);
948 } catch (NumberFormatException e) {
949 return defaultValue;
950 }
951 }
952 }
953
954 private boolean getBooleanAttribute(String key, boolean defaultValue) {
955 String value = this.getAttribute(key);
956 if (value == null) {
957 return defaultValue;
958 } else {
959 return Boolean.parseBoolean(value);
960 }
961 }
962
963 public void add(Message message) {
964 synchronized (this.messages) {
965 this.messages.add(message);
966 }
967 }
968
969 public void prepend(Message message) {
970 synchronized (this.messages) {
971 this.messages.add(0,message);
972 }
973 }
974
975 public void addAll(int index, List<Message> messages) {
976 synchronized (this.messages) {
977 this.messages.addAll(index, messages);
978 }
979 account.getPgpDecryptionService().decrypt(messages);
980 }
981
982 public void expireOldMessages(long timestamp) {
983 synchronized (this.messages) {
984 for(ListIterator<Message> iterator = this.messages.listIterator(); iterator.hasNext();) {
985 if (iterator.next().getTimeSent() < timestamp) {
986 iterator.remove();
987 }
988 }
989 untieMessages();
990 }
991 }
992
993 public void sort() {
994 synchronized (this.messages) {
995 Collections.sort(this.messages, new Comparator<Message>() {
996 @Override
997 public int compare(Message left, Message right) {
998 if (left.getTimeSent() < right.getTimeSent()) {
999 return -1;
1000 } else if (left.getTimeSent() > right.getTimeSent()) {
1001 return 1;
1002 } else {
1003 return 0;
1004 }
1005 }
1006 });
1007 untieMessages();
1008 }
1009 }
1010
1011 private void untieMessages() {
1012 for(Message message : this.messages) {
1013 message.untie();
1014 }
1015 }
1016
1017 public int unreadCount() {
1018 synchronized (this.messages) {
1019 int count = 0;
1020 for(int i = this.messages.size() - 1; i >= 0; --i) {
1021 if (this.messages.get(i).isRead()) {
1022 return count;
1023 }
1024 ++count;
1025 }
1026 return count;
1027 }
1028 }
1029
1030 public int receivedMessagesCount() {
1031 int count = 0;
1032 synchronized (this.messages) {
1033 for(Message message : messages) {
1034 if (message.getStatus() == Message.STATUS_RECEIVED) {
1035 ++count;
1036 }
1037 }
1038 }
1039 return count;
1040 }
1041
1042 private int sentMessagesCount() {
1043 int count = 0;
1044 synchronized (this.messages) {
1045 for(Message message : messages) {
1046 if (message.getStatus() != Message.STATUS_RECEIVED) {
1047 ++count;
1048 }
1049 }
1050 }
1051 return count;
1052 }
1053
1054 public boolean isWithStranger() {
1055 return mode == MODE_SINGLE
1056 && !getJid().equals(account.getJid().toDomainJid())
1057 && !getContact().showInRoster()
1058 && sentMessagesCount() == 0;
1059 }
1060
1061 public class Smp {
1062 public static final int STATUS_NONE = 0;
1063 public static final int STATUS_CONTACT_REQUESTED = 1;
1064 public static final int STATUS_WE_REQUESTED = 2;
1065 public static final int STATUS_FAILED = 3;
1066 public static final int STATUS_VERIFIED = 4;
1067
1068 public String secret = null;
1069 public String hint = null;
1070 public int status = 0;
1071 }
1072}