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