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 && getContact().getPresences().allOrNonSupport(AxolotlService.PEP_DEVICE_LIST_NOTIFY)) {
687 return Message.ENCRYPTION_AXOLOTL;
688 }
689 int outgoing = this.getMostRecentlyUsedOutgoingEncryption();
690 if (outgoing == Message.ENCRYPTION_NONE) {
691 next = this.getMostRecentlyUsedIncomingEncryption();
692 } else {
693 next = outgoing;
694 }
695 }
696
697 if (!Config.supportUnencrypted() && next <= 0) {
698 if (Config.supportOmemo()
699 && ((axolotlService != null && axolotlService.isConversationAxolotlCapable(this)) || !Config.multipleEncryptionChoices())) {
700 return Message.ENCRYPTION_AXOLOTL;
701 } else if (Config.supportOtr() && mode == MODE_SINGLE) {
702 return Message.ENCRYPTION_OTR;
703 } else if (Config.supportOpenPgp()
704 && (mode == MODE_SINGLE) || !Config.multipleEncryptionChoices()) {
705 return Message.ENCRYPTION_PGP;
706 }
707 } else if (next == Message.ENCRYPTION_AXOLOTL
708 && (!Config.supportOmemo() || axolotlService == null || !axolotlService.isConversationAxolotlCapable(this))) {
709 next = Message.ENCRYPTION_NONE;
710 }
711 return next;
712 }
713
714 public void setNextEncryption(int encryption) {
715 this.setAttribute(ATTRIBUTE_NEXT_ENCRYPTION, String.valueOf(encryption));
716 }
717
718 public String getNextMessage() {
719 if (this.nextMessage == null) {
720 return "";
721 } else {
722 return this.nextMessage;
723 }
724 }
725
726 public boolean smpRequested() {
727 return smp().status == Smp.STATUS_CONTACT_REQUESTED;
728 }
729
730 public void setNextMessage(String message) {
731 this.nextMessage = message;
732 }
733
734 public void setSymmetricKey(byte[] key) {
735 this.symmetricKey = key;
736 }
737
738 public byte[] getSymmetricKey() {
739 return this.symmetricKey;
740 }
741
742 public void setBookmark(Bookmark bookmark) {
743 this.bookmark = bookmark;
744 this.bookmark.setConversation(this);
745 }
746
747 public void deregisterWithBookmark() {
748 if (this.bookmark != null) {
749 this.bookmark.setConversation(null);
750 }
751 }
752
753 public Bookmark getBookmark() {
754 return this.bookmark;
755 }
756
757 public boolean hasDuplicateMessage(Message message) {
758 synchronized (this.messages) {
759 for (int i = this.messages.size() - 1; i >= 0; --i) {
760 if (this.messages.get(i).equals(message)) {
761 return true;
762 }
763 }
764 }
765 return false;
766 }
767
768 public Message findSentMessageWithBody(String body) {
769 synchronized (this.messages) {
770 for (int i = this.messages.size() - 1; i >= 0; --i) {
771 Message message = this.messages.get(i);
772 if (message.getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_SEND) {
773 String otherBody;
774 if (message.hasFileOnRemoteHost()) {
775 otherBody = message.getFileParams().url.toString();
776 } else {
777 otherBody = message.body;
778 }
779 if (otherBody != null && otherBody.equals(body)) {
780 return message;
781 }
782 }
783 }
784 return null;
785 }
786 }
787
788 public long getLastMessageTransmitted() {
789 long last_clear = getLastClearHistory();
790 if (last_clear != 0) {
791 return last_clear;
792 }
793 synchronized (this.messages) {
794 for(int i = this.messages.size() - 1; i >= 0; --i) {
795 Message message = this.messages.get(i);
796 if (message.getStatus() == Message.STATUS_RECEIVED || message.isCarbon()) {
797 return message.getTimeSent();
798 }
799 }
800 }
801 return 0;
802 }
803
804 public void setMutedTill(long value) {
805 this.setAttribute(ATTRIBUTE_MUTED_TILL, String.valueOf(value));
806 }
807
808 public boolean isMuted() {
809 return System.currentTimeMillis() < this.getLongAttribute(ATTRIBUTE_MUTED_TILL, 0);
810 }
811
812 public boolean alwaysNotify() {
813 return mode == MODE_SINGLE || getBooleanAttribute(ATTRIBUTE_ALWAYS_NOTIFY, true);
814 }
815
816 public boolean setAttribute(String key, String value) {
817 synchronized (this.attributes) {
818 try {
819 this.attributes.put(key, value);
820 return true;
821 } catch (JSONException e) {
822 return false;
823 }
824 }
825 }
826
827 public boolean setAttribute(String key, List<Jid> jids) {
828 JSONArray array = new JSONArray();
829 for(Jid jid : jids) {
830 array.put(jid.toBareJid().toString());
831 }
832 synchronized (this.attributes) {
833 try {
834 this.attributes.put(key, array);
835 return true;
836 } catch (JSONException e) {
837 e.printStackTrace();
838 return false;
839 }
840 }
841 }
842
843 public String getAttribute(String key) {
844 synchronized (this.attributes) {
845 try {
846 return this.attributes.getString(key);
847 } catch (JSONException e) {
848 return null;
849 }
850 }
851 }
852
853 public List<Jid> getJidListAttribute(String key) {
854 ArrayList<Jid> list = new ArrayList<>();
855 synchronized (this.attributes) {
856 try {
857 JSONArray array = this.attributes.getJSONArray(key);
858 for (int i = 0; i < array.length(); ++i) {
859 try {
860 list.add(Jid.fromString(array.getString(i)));
861 } catch (InvalidJidException e) {
862 //ignored
863 }
864 }
865 } catch (JSONException e) {
866 //ignored
867 }
868 }
869 return list;
870 }
871
872 public int getIntAttribute(String key, int defaultValue) {
873 String value = this.getAttribute(key);
874 if (value == null) {
875 return defaultValue;
876 } else {
877 try {
878 return Integer.parseInt(value);
879 } catch (NumberFormatException e) {
880 return defaultValue;
881 }
882 }
883 }
884
885 public long getLongAttribute(String key, long defaultValue) {
886 String value = this.getAttribute(key);
887 if (value == null) {
888 return defaultValue;
889 } else {
890 try {
891 return Long.parseLong(value);
892 } catch (NumberFormatException e) {
893 return defaultValue;
894 }
895 }
896 }
897
898 public boolean getBooleanAttribute(String key, boolean defaultValue) {
899 String value = this.getAttribute(key);
900 if (value == null) {
901 return defaultValue;
902 } else {
903 return Boolean.parseBoolean(value);
904 }
905 }
906
907 public void add(Message message) {
908 message.setConversation(this);
909 synchronized (this.messages) {
910 this.messages.add(message);
911 }
912 }
913
914 public void prepend(Message message) {
915 message.setConversation(this);
916 synchronized (this.messages) {
917 this.messages.add(0,message);
918 }
919 }
920
921 public void addAll(int index, List<Message> messages) {
922 synchronized (this.messages) {
923 this.messages.addAll(index, messages);
924 }
925 account.getPgpDecryptionService().addAll(messages);
926 }
927
928 public void sort() {
929 synchronized (this.messages) {
930 Collections.sort(this.messages, new Comparator<Message>() {
931 @Override
932 public int compare(Message left, Message right) {
933 if (left.getTimeSent() < right.getTimeSent()) {
934 return -1;
935 } else if (left.getTimeSent() > right.getTimeSent()) {
936 return 1;
937 } else {
938 return 0;
939 }
940 }
941 });
942 for(Message message : this.messages) {
943 message.untie();
944 }
945 }
946 }
947
948 public int unreadCount() {
949 synchronized (this.messages) {
950 int count = 0;
951 for(int i = this.messages.size() - 1; i >= 0; --i) {
952 if (this.messages.get(i).isRead()) {
953 return count;
954 }
955 ++count;
956 }
957 return count;
958 }
959 }
960
961 public class Smp {
962 public static final int STATUS_NONE = 0;
963 public static final int STATUS_CONTACT_REQUESTED = 1;
964 public static final int STATUS_WE_REQUESTED = 2;
965 public static final int STATUS_FAILED = 3;
966 public static final int STATUS_VERIFIED = 4;
967
968 public String secret = null;
969 public String hint = null;
970 public int status = 0;
971 }
972}