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 if (getMucOptions().getSubject() != null) {
499 return getMucOptions().getSubject();
500 } else if (bookmark != null
501 && bookmark.getBookmarkName() != null
502 && !bookmark.getBookmarkName().trim().isEmpty()) {
503 return bookmark.getBookmarkName().trim();
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 boolean hasDuplicateMessage(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 true;
812 }
813 }
814 }
815 return false;
816 }
817
818 public Message findSentMessageWithBody(String body) {
819 synchronized (this.messages) {
820 for (int i = this.messages.size() - 1; i >= 0; --i) {
821 Message message = this.messages.get(i);
822 if (message.getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_SEND) {
823 String otherBody;
824 if (message.hasFileOnRemoteHost()) {
825 otherBody = message.getFileParams().url.toString();
826 } else {
827 otherBody = message.body;
828 }
829 if (otherBody != null && otherBody.equals(body)) {
830 return message;
831 }
832 }
833 }
834 return null;
835 }
836 }
837
838 public MamReference getLastMessageTransmitted() {
839 final MamReference lastClear = getLastClearHistory();
840 MamReference lastReceived = new MamReference(0);
841 synchronized (this.messages) {
842 for(int i = this.messages.size() - 1; i >= 0; --i) {
843 final Message message = this.messages.get(i);
844 if (message.getType() == Message.TYPE_PRIVATE) {
845 continue; //it's unsafe to use private messages as anchor. They could be coming from user archive
846 }
847 if (message.getStatus() == Message.STATUS_RECEIVED || message.isCarbon() || message.getServerMsgId() != null) {
848 lastReceived = new MamReference(message.getTimeSent(),message.getServerMsgId());
849 break;
850 }
851 }
852 }
853 return MamReference.max(lastClear,lastReceived);
854 }
855
856 public void setMutedTill(long value) {
857 this.setAttribute(ATTRIBUTE_MUTED_TILL, String.valueOf(value));
858 }
859
860 public boolean isMuted() {
861 return System.currentTimeMillis() < this.getLongAttribute(ATTRIBUTE_MUTED_TILL, 0);
862 }
863
864 public boolean alwaysNotify() {
865 return mode == MODE_SINGLE || getBooleanAttribute(ATTRIBUTE_ALWAYS_NOTIFY, Config.ALWAYS_NOTIFY_BY_DEFAULT || isPnNA());
866 }
867
868 public boolean setAttribute(String key, String value) {
869 synchronized (this.attributes) {
870 try {
871 this.attributes.put(key, value == null ? "" : value);
872 return true;
873 } catch (JSONException e) {
874 return false;
875 }
876 }
877 }
878
879 public boolean setAttribute(String key, List<Jid> jids) {
880 JSONArray array = new JSONArray();
881 for(Jid jid : jids) {
882 array.put(jid.toBareJid().toString());
883 }
884 synchronized (this.attributes) {
885 try {
886 this.attributes.put(key, array);
887 return true;
888 } catch (JSONException e) {
889 e.printStackTrace();
890 return false;
891 }
892 }
893 }
894
895 public String getAttribute(String key) {
896 synchronized (this.attributes) {
897 try {
898 return this.attributes.getString(key);
899 } catch (JSONException e) {
900 return null;
901 }
902 }
903 }
904
905 private List<Jid> getJidListAttribute(String key) {
906 ArrayList<Jid> list = new ArrayList<>();
907 synchronized (this.attributes) {
908 try {
909 JSONArray array = this.attributes.getJSONArray(key);
910 for (int i = 0; i < array.length(); ++i) {
911 try {
912 list.add(Jid.fromString(array.getString(i)));
913 } catch (InvalidJidException e) {
914 //ignored
915 }
916 }
917 } catch (JSONException e) {
918 //ignored
919 }
920 }
921 return list;
922 }
923
924 private int getIntAttribute(String key, int defaultValue) {
925 String value = this.getAttribute(key);
926 if (value == null) {
927 return defaultValue;
928 } else {
929 try {
930 return Integer.parseInt(value);
931 } catch (NumberFormatException e) {
932 return defaultValue;
933 }
934 }
935 }
936
937 public long getLongAttribute(String key, long defaultValue) {
938 String value = this.getAttribute(key);
939 if (value == null) {
940 return defaultValue;
941 } else {
942 try {
943 return Long.parseLong(value);
944 } catch (NumberFormatException e) {
945 return defaultValue;
946 }
947 }
948 }
949
950 private boolean getBooleanAttribute(String key, boolean defaultValue) {
951 String value = this.getAttribute(key);
952 if (value == null) {
953 return defaultValue;
954 } else {
955 return Boolean.parseBoolean(value);
956 }
957 }
958
959 public void add(Message message) {
960 synchronized (this.messages) {
961 this.messages.add(message);
962 }
963 }
964
965 public void prepend(Message message) {
966 synchronized (this.messages) {
967 this.messages.add(0,message);
968 }
969 }
970
971 public void addAll(int index, List<Message> messages) {
972 synchronized (this.messages) {
973 this.messages.addAll(index, messages);
974 }
975 account.getPgpDecryptionService().decrypt(messages);
976 }
977
978 public void expireOldMessages(long timestamp) {
979 synchronized (this.messages) {
980 for(ListIterator<Message> iterator = this.messages.listIterator(); iterator.hasNext();) {
981 if (iterator.next().getTimeSent() < timestamp) {
982 iterator.remove();
983 }
984 }
985 untieMessages();
986 }
987 }
988
989 public void sort() {
990 synchronized (this.messages) {
991 Collections.sort(this.messages, new Comparator<Message>() {
992 @Override
993 public int compare(Message left, Message right) {
994 if (left.getTimeSent() < right.getTimeSent()) {
995 return -1;
996 } else if (left.getTimeSent() > right.getTimeSent()) {
997 return 1;
998 } else {
999 return 0;
1000 }
1001 }
1002 });
1003 untieMessages();
1004 }
1005 }
1006
1007 private void untieMessages() {
1008 for(Message message : this.messages) {
1009 message.untie();
1010 }
1011 }
1012
1013 public int unreadCount() {
1014 synchronized (this.messages) {
1015 int count = 0;
1016 for(int i = this.messages.size() - 1; i >= 0; --i) {
1017 if (this.messages.get(i).isRead()) {
1018 return count;
1019 }
1020 ++count;
1021 }
1022 return count;
1023 }
1024 }
1025
1026 public int receivedMessagesCount() {
1027 int count = 0;
1028 synchronized (this.messages) {
1029 for(Message message : messages) {
1030 if (message.getStatus() == Message.STATUS_RECEIVED) {
1031 ++count;
1032 }
1033 }
1034 }
1035 return count;
1036 }
1037
1038 private int sentMessagesCount() {
1039 int count = 0;
1040 synchronized (this.messages) {
1041 for(Message message : messages) {
1042 if (message.getStatus() != Message.STATUS_RECEIVED) {
1043 ++count;
1044 }
1045 }
1046 }
1047 return count;
1048 }
1049
1050 public boolean isWithStranger() {
1051 return mode == MODE_SINGLE
1052 && !getJid().equals(account.getJid().toDomainJid())
1053 && !getContact().showInRoster()
1054 && sentMessagesCount() == 0;
1055 }
1056
1057 public class Smp {
1058 public static final int STATUS_NONE = 0;
1059 public static final int STATUS_CONTACT_REQUESTED = 1;
1060 public static final int STATUS_WE_REQUESTED = 2;
1061 public static final int STATUS_FAILED = 3;
1062 public static final int STATUS_VERIFIED = 4;
1063
1064 public String secret = null;
1065 public String hint = null;
1066 public int status = 0;
1067 }
1068}