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