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