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 && message.markable) {
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 if (getMucOptions().getSubject() != null) {
497 return getMucOptions().getSubject();
498 } else if (bookmark != null
499 && bookmark.getBookmarkName() != null
500 && !bookmark.getBookmarkName().trim().isEmpty()) {
501 return bookmark.getBookmarkName().trim();
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 private boolean isPnNA() {
699 return mode == MODE_SINGLE || (getMucOptions().membersOnly() && getMucOptions().nonanonymous());
700 }
701
702 public synchronized MucOptions getMucOptions() {
703 if (this.mucOptions == null) {
704 this.mucOptions = new MucOptions(this);
705 }
706 return this.mucOptions;
707 }
708
709 public void resetMucOptions() {
710 this.mucOptions = null;
711 }
712
713 public void setContactJid(final Jid jid) {
714 this.contactJid = jid;
715 }
716
717 public void setNextCounterpart(Jid jid) {
718 this.nextCounterpart = jid;
719 }
720
721 public Jid getNextCounterpart() {
722 return this.nextCounterpart;
723 }
724
725 public int getNextEncryption() {
726 return fixAvailableEncryption(this.getIntAttribute(ATTRIBUTE_NEXT_ENCRYPTION, getDefaultEncryption()));
727 }
728
729 private int fixAvailableEncryption(int selectedEncryption) {
730 switch(selectedEncryption) {
731 case Message.ENCRYPTION_NONE:
732 return Config.supportUnencrypted() ? selectedEncryption : getDefaultEncryption();
733 case Message.ENCRYPTION_AXOLOTL:
734 return Config.supportOmemo() ? selectedEncryption : getDefaultEncryption();
735 case Message.ENCRYPTION_OTR:
736 return Config.supportOtr() ? selectedEncryption : getDefaultEncryption();
737 case Message.ENCRYPTION_PGP:
738 case Message.ENCRYPTION_DECRYPTED:
739 case Message.ENCRYPTION_DECRYPTION_FAILED:
740 return Config.supportOpenPgp() ? Message.ENCRYPTION_PGP : getDefaultEncryption();
741 default:
742 return getDefaultEncryption();
743 }
744 }
745
746 private int getDefaultEncryption() {
747 AxolotlService axolotlService = account.getAxolotlService();
748 if (Config.supportUnencrypted()) {
749 return Message.ENCRYPTION_NONE;
750 } else if (Config.supportOmemo()
751 && (axolotlService != null && axolotlService.isConversationAxolotlCapable(this) || !Config.multipleEncryptionChoices())) {
752 return Message.ENCRYPTION_AXOLOTL;
753 } else if (Config.supportOtr() && mode == MODE_SINGLE) {
754 return Message.ENCRYPTION_OTR;
755 } else if (Config.supportOpenPgp()) {
756 return Message.ENCRYPTION_PGP;
757 } else {
758 return Message.ENCRYPTION_NONE;
759 }
760 }
761
762 public void setNextEncryption(int encryption) {
763 this.setAttribute(ATTRIBUTE_NEXT_ENCRYPTION, String.valueOf(encryption));
764 }
765
766 public String getNextMessage() {
767 final String nextMessage = getAttribute(ATTRIBUTE_NEXT_MESSAGE);
768 return nextMessage == null ? "" : nextMessage;
769 }
770
771 public boolean smpRequested() {
772 return smp().status == Smp.STATUS_CONTACT_REQUESTED;
773 }
774
775 public boolean setNextMessage(String message) {
776 boolean changed = !getNextMessage().equals(message);
777 this.setAttribute(ATTRIBUTE_NEXT_MESSAGE, message);
778 return changed;
779 }
780
781 public void setSymmetricKey(byte[] key) {
782 this.symmetricKey = key;
783 }
784
785 public byte[] getSymmetricKey() {
786 return this.symmetricKey;
787 }
788
789 public void setBookmark(Bookmark bookmark) {
790 this.bookmark = bookmark;
791 this.bookmark.setConversation(this);
792 }
793
794 public void deregisterWithBookmark() {
795 if (this.bookmark != null) {
796 this.bookmark.setConversation(null);
797 }
798 this.bookmark = null;
799 }
800
801 public Bookmark getBookmark() {
802 return this.bookmark;
803 }
804
805 public boolean hasDuplicateMessage(Message message) {
806 synchronized (this.messages) {
807 for (int i = this.messages.size() - 1; i >= 0; --i) {
808 if (this.messages.get(i).similar(message)) {
809 return true;
810 }
811 }
812 }
813 return false;
814 }
815
816 public Message findSentMessageWithBody(String body) {
817 synchronized (this.messages) {
818 for (int i = this.messages.size() - 1; i >= 0; --i) {
819 Message message = this.messages.get(i);
820 if (message.getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_SEND) {
821 String otherBody;
822 if (message.hasFileOnRemoteHost()) {
823 otherBody = message.getFileParams().url.toString();
824 } else {
825 otherBody = message.body;
826 }
827 if (otherBody != null && otherBody.equals(body)) {
828 return message;
829 }
830 }
831 }
832 return null;
833 }
834 }
835
836 public MamReference getLastMessageTransmitted() {
837 final MamReference lastClear = getLastClearHistory();
838 MamReference lastReceived = new MamReference(0);
839 synchronized (this.messages) {
840 for(int i = this.messages.size() - 1; i >= 0; --i) {
841 Message message = this.messages.get(i);
842 if (message.getStatus() == Message.STATUS_RECEIVED || message.isCarbon() || message.getServerMsgId() != null) {
843 lastReceived = new MamReference(message.getTimeSent(),message.getServerMsgId());
844 break;
845 }
846 }
847 }
848 return MamReference.max(lastClear,lastReceived);
849 }
850
851 public void setMutedTill(long value) {
852 this.setAttribute(ATTRIBUTE_MUTED_TILL, String.valueOf(value));
853 }
854
855 public boolean isMuted() {
856 return System.currentTimeMillis() < this.getLongAttribute(ATTRIBUTE_MUTED_TILL, 0);
857 }
858
859 public boolean alwaysNotify() {
860 return mode == MODE_SINGLE || getBooleanAttribute(ATTRIBUTE_ALWAYS_NOTIFY, Config.ALWAYS_NOTIFY_BY_DEFAULT || isPnNA());
861 }
862
863 public boolean setAttribute(String key, String value) {
864 synchronized (this.attributes) {
865 try {
866 this.attributes.put(key, value == null ? "" : value);
867 return true;
868 } catch (JSONException e) {
869 return false;
870 }
871 }
872 }
873
874 public boolean setAttribute(String key, List<Jid> jids) {
875 JSONArray array = new JSONArray();
876 for(Jid jid : jids) {
877 array.put(jid.toBareJid().toString());
878 }
879 synchronized (this.attributes) {
880 try {
881 this.attributes.put(key, array);
882 return true;
883 } catch (JSONException e) {
884 e.printStackTrace();
885 return false;
886 }
887 }
888 }
889
890 public String getAttribute(String key) {
891 synchronized (this.attributes) {
892 try {
893 return this.attributes.getString(key);
894 } catch (JSONException e) {
895 return null;
896 }
897 }
898 }
899
900 private List<Jid> getJidListAttribute(String key) {
901 ArrayList<Jid> list = new ArrayList<>();
902 synchronized (this.attributes) {
903 try {
904 JSONArray array = this.attributes.getJSONArray(key);
905 for (int i = 0; i < array.length(); ++i) {
906 try {
907 list.add(Jid.fromString(array.getString(i)));
908 } catch (InvalidJidException e) {
909 //ignored
910 }
911 }
912 } catch (JSONException e) {
913 //ignored
914 }
915 }
916 return list;
917 }
918
919 private int getIntAttribute(String key, int defaultValue) {
920 String value = this.getAttribute(key);
921 if (value == null) {
922 return defaultValue;
923 } else {
924 try {
925 return Integer.parseInt(value);
926 } catch (NumberFormatException e) {
927 return defaultValue;
928 }
929 }
930 }
931
932 public long getLongAttribute(String key, long defaultValue) {
933 String value = this.getAttribute(key);
934 if (value == null) {
935 return defaultValue;
936 } else {
937 try {
938 return Long.parseLong(value);
939 } catch (NumberFormatException e) {
940 return defaultValue;
941 }
942 }
943 }
944
945 private boolean getBooleanAttribute(String key, boolean defaultValue) {
946 String value = this.getAttribute(key);
947 if (value == null) {
948 return defaultValue;
949 } else {
950 return Boolean.parseBoolean(value);
951 }
952 }
953
954 public void add(Message message) {
955 synchronized (this.messages) {
956 this.messages.add(message);
957 }
958 }
959
960 public void prepend(Message message) {
961 synchronized (this.messages) {
962 this.messages.add(0,message);
963 }
964 }
965
966 public void addAll(int index, List<Message> messages) {
967 synchronized (this.messages) {
968 this.messages.addAll(index, messages);
969 }
970 account.getPgpDecryptionService().decrypt(messages);
971 }
972
973 public void expireOldMessages(long timestamp) {
974 synchronized (this.messages) {
975 for(ListIterator<Message> iterator = this.messages.listIterator(); iterator.hasNext();) {
976 if (iterator.next().getTimeSent() < timestamp) {
977 iterator.remove();
978 }
979 }
980 untieMessages();
981 }
982 }
983
984 public void sort() {
985 synchronized (this.messages) {
986 Collections.sort(this.messages, new Comparator<Message>() {
987 @Override
988 public int compare(Message left, Message right) {
989 if (left.getTimeSent() < right.getTimeSent()) {
990 return -1;
991 } else if (left.getTimeSent() > right.getTimeSent()) {
992 return 1;
993 } else {
994 return 0;
995 }
996 }
997 });
998 untieMessages();
999 }
1000 }
1001
1002 private void untieMessages() {
1003 for(Message message : this.messages) {
1004 message.untie();
1005 }
1006 }
1007
1008 public int unreadCount() {
1009 synchronized (this.messages) {
1010 int count = 0;
1011 for(int i = this.messages.size() - 1; i >= 0; --i) {
1012 if (this.messages.get(i).isRead()) {
1013 return count;
1014 }
1015 ++count;
1016 }
1017 return count;
1018 }
1019 }
1020
1021 public int receivedMessagesCount() {
1022 int count = 0;
1023 synchronized (this.messages) {
1024 for(Message message : messages) {
1025 if (message.getStatus() == Message.STATUS_RECEIVED) {
1026 ++count;
1027 }
1028 }
1029 }
1030 return count;
1031 }
1032
1033 private int sentMessagesCount() {
1034 int count = 0;
1035 synchronized (this.messages) {
1036 for(Message message : messages) {
1037 if (message.getStatus() != Message.STATUS_RECEIVED) {
1038 ++count;
1039 }
1040 }
1041 }
1042 return count;
1043 }
1044
1045 public boolean isWithStranger() {
1046 return mode == MODE_SINGLE
1047 && !getJid().equals(account.getJid().toDomainJid())
1048 && !getContact().showInRoster()
1049 && sentMessagesCount() == 0;
1050 }
1051
1052 public class Smp {
1053 public static final int STATUS_NONE = 0;
1054 public static final int STATUS_CONTACT_REQUESTED = 1;
1055 public static final int STATUS_WE_REQUESTED = 2;
1056 public static final int STATUS_FAILED = 3;
1057 public static final int STATUS_VERIFIED = 4;
1058
1059 public String secret = null;
1060 public String hint = null;
1061 public int status = 0;
1062 }
1063}