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