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