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