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
437 && bookmark.getBookmarkName() != null
438 && !bookmark.getBookmarkName().trim().isEmpty()) {
439 return bookmark.getBookmarkName().trim();
440 } else {
441 String generatedName = getMucOptions().createNameFromParticipants();
442 if (generatedName != null) {
443 return generatedName;
444 } else {
445 return getJid().getLocalpart();
446 }
447 }
448 } else {
449 return this.getContact().getDisplayName();
450 }
451 }
452
453 public String getAccountUuid() {
454 return this.accountUuid;
455 }
456
457 public Account getAccount() {
458 return this.account;
459 }
460
461 public Contact getContact() {
462 return this.account.getRoster().getContact(this.contactJid);
463 }
464
465 public void setAccount(final Account account) {
466 this.account = account;
467 }
468
469 @Override
470 public Jid getJid() {
471 return this.contactJid;
472 }
473
474 public int getStatus() {
475 return this.status;
476 }
477
478 public long getCreated() {
479 return this.created;
480 }
481
482 public ContentValues getContentValues() {
483 ContentValues values = new ContentValues();
484 values.put(UUID, uuid);
485 values.put(NAME, name);
486 values.put(CONTACT, contactUuid);
487 values.put(ACCOUNT, accountUuid);
488 values.put(CONTACTJID, contactJid.toString());
489 values.put(CREATED, created);
490 values.put(STATUS, status);
491 values.put(MODE, mode);
492 values.put(ATTRIBUTES, attributes.toString());
493 return values;
494 }
495
496 public static Conversation fromCursor(Cursor cursor) {
497 Jid jid;
498 try {
499 jid = Jid.fromString(cursor.getString(cursor.getColumnIndex(CONTACTJID)), true);
500 } catch (final InvalidJidException e) {
501 // Borked DB..
502 jid = null;
503 }
504 return new Conversation(cursor.getString(cursor.getColumnIndex(UUID)),
505 cursor.getString(cursor.getColumnIndex(NAME)),
506 cursor.getString(cursor.getColumnIndex(CONTACT)),
507 cursor.getString(cursor.getColumnIndex(ACCOUNT)),
508 jid,
509 cursor.getLong(cursor.getColumnIndex(CREATED)),
510 cursor.getInt(cursor.getColumnIndex(STATUS)),
511 cursor.getInt(cursor.getColumnIndex(MODE)),
512 cursor.getString(cursor.getColumnIndex(ATTRIBUTES)));
513 }
514
515 public void setStatus(int status) {
516 this.status = status;
517 }
518
519 public int getMode() {
520 return this.mode;
521 }
522
523 public void setMode(int mode) {
524 this.mode = mode;
525 }
526
527 public SessionImpl startOtrSession(String presence, boolean sendStart) {
528 if (this.otrSession != null) {
529 return this.otrSession;
530 } else {
531 final SessionID sessionId = new SessionID(this.getJid().toBareJid().toString(),
532 presence,
533 "xmpp");
534 this.otrSession = new SessionImpl(sessionId, getAccount().getOtrService());
535 try {
536 if (sendStart) {
537 this.otrSession.startSession();
538 return this.otrSession;
539 }
540 return this.otrSession;
541 } catch (OtrException e) {
542 return null;
543 }
544 }
545
546 }
547
548 public SessionImpl getOtrSession() {
549 return this.otrSession;
550 }
551
552 public void resetOtrSession() {
553 this.otrFingerprint = null;
554 this.otrSession = null;
555 this.mSmp.hint = null;
556 this.mSmp.secret = null;
557 this.mSmp.status = Smp.STATUS_NONE;
558 }
559
560 public Smp smp() {
561 return mSmp;
562 }
563
564 public boolean startOtrIfNeeded() {
565 if (this.otrSession != null && this.otrSession.getSessionStatus() != SessionStatus.ENCRYPTED) {
566 try {
567 this.otrSession.startSession();
568 return true;
569 } catch (OtrException e) {
570 this.resetOtrSession();
571 return false;
572 }
573 } else {
574 return true;
575 }
576 }
577
578 public boolean endOtrIfNeeded() {
579 if (this.otrSession != null) {
580 if (this.otrSession.getSessionStatus() == SessionStatus.ENCRYPTED) {
581 try {
582 this.otrSession.endSession();
583 this.resetOtrSession();
584 return true;
585 } catch (OtrException e) {
586 this.resetOtrSession();
587 return false;
588 }
589 } else {
590 this.resetOtrSession();
591 return false;
592 }
593 } else {
594 return false;
595 }
596 }
597
598 public boolean hasValidOtrSession() {
599 return this.otrSession != null;
600 }
601
602 public synchronized String getOtrFingerprint() {
603 if (this.otrFingerprint == null) {
604 try {
605 if (getOtrSession() == null || getOtrSession().getSessionStatus() != SessionStatus.ENCRYPTED) {
606 return null;
607 }
608 DSAPublicKey remotePubKey = (DSAPublicKey) getOtrSession().getRemotePublicKey();
609 this.otrFingerprint = getAccount().getOtrService().getFingerprint(remotePubKey);
610 } catch (final OtrCryptoException | UnsupportedOperationException ignored) {
611 return null;
612 }
613 }
614 return this.otrFingerprint;
615 }
616
617 public boolean verifyOtrFingerprint() {
618 final String fingerprint = getOtrFingerprint();
619 if (fingerprint != null) {
620 getContact().addOtrFingerprint(fingerprint);
621 return true;
622 } else {
623 return false;
624 }
625 }
626
627 public boolean isOtrFingerprintVerified() {
628 return getContact().getOtrFingerprints().contains(getOtrFingerprint());
629 }
630
631 /**
632 * short for is Private and Non-anonymous
633 */
634 private boolean isPnNA() {
635 return mode == MODE_SINGLE || (getMucOptions().membersOnly() && getMucOptions().nonanonymous());
636 }
637
638 public synchronized MucOptions getMucOptions() {
639 if (this.mucOptions == null) {
640 this.mucOptions = new MucOptions(this);
641 }
642 return this.mucOptions;
643 }
644
645 public void resetMucOptions() {
646 this.mucOptions = null;
647 }
648
649 public void setContactJid(final Jid jid) {
650 this.contactJid = jid;
651 }
652
653 public void setNextCounterpart(Jid jid) {
654 this.nextCounterpart = jid;
655 }
656
657 public Jid getNextCounterpart() {
658 return this.nextCounterpart;
659 }
660
661 private int getMostRecentlyUsedIncomingEncryption() {
662 synchronized (this.messages) {
663 for(int i = this.messages.size() -1; i >= 0; --i) {
664 final Message m = this.messages.get(i);
665 if (m.getStatus() == Message.STATUS_RECEIVED) {
666 final int e = m.getEncryption();
667 if (e == Message.ENCRYPTION_DECRYPTED || e == Message.ENCRYPTION_DECRYPTION_FAILED) {
668 return Message.ENCRYPTION_PGP;
669 } else {
670 return e;
671 }
672 }
673 }
674 }
675 return Message.ENCRYPTION_NONE;
676 }
677
678 public int getNextEncryption() {
679 final AxolotlService axolotlService = getAccount().getAxolotlService();
680 int next = this.getIntAttribute(ATTRIBUTE_NEXT_ENCRYPTION, -1);
681 if (next == -1) {
682 if (Config.supportOmemo()
683 && axolotlService != null
684 && mode == MODE_SINGLE
685 && axolotlService.isConversationAxolotlCapable(this)
686 && getAccount().getSelfContact().getPresences().allOrNonSupport(AxolotlService.PEP_DEVICE_LIST_NOTIFY)
687 && getContact().getPresences().allOrNonSupport(AxolotlService.PEP_DEVICE_LIST_NOTIFY)) {
688 return Message.ENCRYPTION_AXOLOTL;
689 } else {
690 next = this.getMostRecentlyUsedIncomingEncryption();
691 }
692 }
693
694 if (!Config.supportUnencrypted() && next <= 0) {
695 if (Config.supportOmemo()
696 && ((axolotlService != null && axolotlService.isConversationAxolotlCapable(this)) || !Config.multipleEncryptionChoices())) {
697 return Message.ENCRYPTION_AXOLOTL;
698 } else if (Config.supportOtr() && mode == MODE_SINGLE) {
699 return Message.ENCRYPTION_OTR;
700 } else if (Config.supportOpenPgp()
701 && (mode == MODE_SINGLE) || !Config.multipleEncryptionChoices()) {
702 return Message.ENCRYPTION_PGP;
703 }
704 } else if (next == Message.ENCRYPTION_AXOLOTL
705 && (!Config.supportOmemo() || axolotlService == null || !axolotlService.isConversationAxolotlCapable(this))) {
706 next = Message.ENCRYPTION_NONE;
707 }
708 return next;
709 }
710
711 public void setNextEncryption(int encryption) {
712 this.setAttribute(ATTRIBUTE_NEXT_ENCRYPTION, String.valueOf(encryption));
713 }
714
715 public String getNextMessage() {
716 if (this.nextMessage == null) {
717 return "";
718 } else {
719 return this.nextMessage;
720 }
721 }
722
723 public boolean smpRequested() {
724 return smp().status == Smp.STATUS_CONTACT_REQUESTED;
725 }
726
727 public void setNextMessage(String message) {
728 this.nextMessage = message;
729 }
730
731 public void setSymmetricKey(byte[] key) {
732 this.symmetricKey = key;
733 }
734
735 public byte[] getSymmetricKey() {
736 return this.symmetricKey;
737 }
738
739 public void setBookmark(Bookmark bookmark) {
740 this.bookmark = bookmark;
741 this.bookmark.setConversation(this);
742 }
743
744 public void deregisterWithBookmark() {
745 if (this.bookmark != null) {
746 this.bookmark.setConversation(null);
747 }
748 }
749
750 public Bookmark getBookmark() {
751 return this.bookmark;
752 }
753
754 public boolean hasDuplicateMessage(Message message) {
755 synchronized (this.messages) {
756 for (int i = this.messages.size() - 1; i >= 0; --i) {
757 if (this.messages.get(i).equals(message)) {
758 return true;
759 }
760 }
761 }
762 return false;
763 }
764
765 public Message findSentMessageWithBody(String body) {
766 synchronized (this.messages) {
767 for (int i = this.messages.size() - 1; i >= 0; --i) {
768 Message message = this.messages.get(i);
769 if (message.getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_SEND) {
770 String otherBody;
771 if (message.hasFileOnRemoteHost()) {
772 otherBody = message.getFileParams().url.toString();
773 } else {
774 otherBody = message.body;
775 }
776 if (otherBody != null && otherBody.equals(body)) {
777 return message;
778 }
779 }
780 }
781 return null;
782 }
783 }
784
785 public long getLastMessageTransmitted() {
786 long last_clear = getLastClearHistory();
787 if (last_clear != 0) {
788 return last_clear;
789 }
790 synchronized (this.messages) {
791 for(int i = this.messages.size() - 1; i >= 0; --i) {
792 Message message = this.messages.get(i);
793 if (message.getStatus() == Message.STATUS_RECEIVED || message.isCarbon()) {
794 return message.getTimeSent();
795 }
796 }
797 }
798 return 0;
799 }
800
801 public void setMutedTill(long value) {
802 this.setAttribute(ATTRIBUTE_MUTED_TILL, String.valueOf(value));
803 }
804
805 public boolean isMuted() {
806 return System.currentTimeMillis() < this.getLongAttribute(ATTRIBUTE_MUTED_TILL, 0);
807 }
808
809 public boolean alwaysNotify() {
810 return mode == MODE_SINGLE || getBooleanAttribute(ATTRIBUTE_ALWAYS_NOTIFY, Config.ALWAYS_NOTIFY_BY_DEFAULT || isPnNA());
811 }
812
813 public boolean setAttribute(String key, String value) {
814 synchronized (this.attributes) {
815 try {
816 this.attributes.put(key, value);
817 return true;
818 } catch (JSONException e) {
819 return false;
820 }
821 }
822 }
823
824 public boolean setAttribute(String key, List<Jid> jids) {
825 JSONArray array = new JSONArray();
826 for(Jid jid : jids) {
827 array.put(jid.toBareJid().toString());
828 }
829 synchronized (this.attributes) {
830 try {
831 this.attributes.put(key, array);
832 return true;
833 } catch (JSONException e) {
834 e.printStackTrace();
835 return false;
836 }
837 }
838 }
839
840 public String getAttribute(String key) {
841 synchronized (this.attributes) {
842 try {
843 return this.attributes.getString(key);
844 } catch (JSONException e) {
845 return null;
846 }
847 }
848 }
849
850 public List<Jid> getJidListAttribute(String key) {
851 ArrayList<Jid> list = new ArrayList<>();
852 synchronized (this.attributes) {
853 try {
854 JSONArray array = this.attributes.getJSONArray(key);
855 for (int i = 0; i < array.length(); ++i) {
856 try {
857 list.add(Jid.fromString(array.getString(i)));
858 } catch (InvalidJidException e) {
859 //ignored
860 }
861 }
862 } catch (JSONException e) {
863 //ignored
864 }
865 }
866 return list;
867 }
868
869 public int getIntAttribute(String key, int defaultValue) {
870 String value = this.getAttribute(key);
871 if (value == null) {
872 return defaultValue;
873 } else {
874 try {
875 return Integer.parseInt(value);
876 } catch (NumberFormatException e) {
877 return defaultValue;
878 }
879 }
880 }
881
882 public long getLongAttribute(String key, long defaultValue) {
883 String value = this.getAttribute(key);
884 if (value == null) {
885 return defaultValue;
886 } else {
887 try {
888 return Long.parseLong(value);
889 } catch (NumberFormatException e) {
890 return defaultValue;
891 }
892 }
893 }
894
895 public boolean getBooleanAttribute(String key, boolean defaultValue) {
896 String value = this.getAttribute(key);
897 if (value == null) {
898 return defaultValue;
899 } else {
900 return Boolean.parseBoolean(value);
901 }
902 }
903
904 public void add(Message message) {
905 message.setConversation(this);
906 synchronized (this.messages) {
907 this.messages.add(message);
908 }
909 }
910
911 public void prepend(Message message) {
912 message.setConversation(this);
913 synchronized (this.messages) {
914 this.messages.add(0,message);
915 }
916 }
917
918 public void addAll(int index, List<Message> messages) {
919 synchronized (this.messages) {
920 this.messages.addAll(index, messages);
921 }
922 account.getPgpDecryptionService().addAll(messages);
923 }
924
925 public void sort() {
926 synchronized (this.messages) {
927 Collections.sort(this.messages, new Comparator<Message>() {
928 @Override
929 public int compare(Message left, Message right) {
930 if (left.getTimeSent() < right.getTimeSent()) {
931 return -1;
932 } else if (left.getTimeSent() > right.getTimeSent()) {
933 return 1;
934 } else {
935 return 0;
936 }
937 }
938 });
939 for(Message message : this.messages) {
940 message.untie();
941 }
942 }
943 }
944
945 public int unreadCount() {
946 synchronized (this.messages) {
947 int count = 0;
948 for(int i = this.messages.size() - 1; i >= 0; --i) {
949 if (this.messages.get(i).isRead()) {
950 return count;
951 }
952 ++count;
953 }
954 return count;
955 }
956 }
957
958 public class Smp {
959 public static final int STATUS_NONE = 0;
960 public static final int STATUS_CONTACT_REQUESTED = 1;
961 public static final int STATUS_WE_REQUESTED = 2;
962 public static final int STATUS_FAILED = 3;
963 public static final int STATUS_VERIFIED = 4;
964
965 public String secret = null;
966 public String hint = null;
967 public int status = 0;
968 }
969}