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