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