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