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
30public class Conversation extends AbstractEntity implements Blockable {
31 public static final String TABLENAME = "conversations";
32
33 public static final int STATUS_AVAILABLE = 0;
34 public static final int STATUS_ARCHIVED = 1;
35 public static final int STATUS_DELETED = 2;
36
37 public static final int MODE_MULTI = 1;
38 public static final int MODE_SINGLE = 0;
39
40 public static final String NAME = "name";
41 public static final String ACCOUNT = "accountUuid";
42 public static final String CONTACT = "contactUuid";
43 public static final String CONTACTJID = "contactJid";
44 public static final String STATUS = "status";
45 public static final String CREATED = "created";
46 public static final String MODE = "mode";
47 public static final String ATTRIBUTES = "attributes";
48
49 public static final String ATTRIBUTE_NEXT_ENCRYPTION = "next_encryption";
50 public static final String ATTRIBUTE_MUC_PASSWORD = "muc_password";
51 public static final String ATTRIBUTE_MUTED_TILL = "muted_till";
52 public static final String ATTRIBUTE_ALWAYS_NOTIFY = "always_notify";
53 public static final String ATTRIBUTE_CRYPTO_TARGETS = "crypto_targets";
54
55 private String name;
56 private String contactUuid;
57 private String accountUuid;
58 private Jid contactJid;
59 private int status;
60 private long created;
61 private int mode;
62
63 private JSONObject attributes = new JSONObject();
64
65 private Jid nextCounterpart;
66
67 protected final ArrayList<Message> messages = new ArrayList<>();
68 protected Account account = null;
69
70 private transient SessionImpl otrSession;
71
72 private transient String otrFingerprint = null;
73 private Smp mSmp = new Smp();
74
75 private String nextMessage;
76
77 private transient MucOptions mucOptions = null;
78
79 private byte[] symmetricKey;
80
81 private Bookmark bookmark;
82
83 private boolean messagesLeftOnServer = true;
84 private ChatState mOutgoingChatState = Config.DEFAULT_CHATSTATE;
85 private ChatState mIncomingChatState = Config.DEFAULT_CHATSTATE;
86 private String mLastReceivedOtrMessageId = null;
87 private String mFirstMamReference = null;
88 private Message correctingMessage;
89
90 public boolean hasMessagesLeftOnServer() {
91 return messagesLeftOnServer;
92 }
93
94 public void setHasMessagesLeftOnServer(boolean value) {
95 this.messagesLeftOnServer = value;
96 }
97
98 public Message findUnsentMessageWithUuid(String uuid) {
99 synchronized(this.messages) {
100 for (final Message message : this.messages) {
101 final int s = message.getStatus();
102 if ((s == Message.STATUS_UNSEND || s == Message.STATUS_WAITING) && message.getUuid().equals(uuid)) {
103 return message;
104 }
105 }
106 }
107 return null;
108 }
109
110 public void findWaitingMessages(OnMessageFound onMessageFound) {
111 synchronized (this.messages) {
112 for(Message message : this.messages) {
113 if (message.getStatus() == Message.STATUS_WAITING) {
114 onMessageFound.onMessageFound(message);
115 }
116 }
117 }
118 }
119
120 public void findUnreadMessages(OnMessageFound onMessageFound) {
121 synchronized (this.messages) {
122 for(Message message : this.messages) {
123 if (!message.isRead()) {
124 onMessageFound.onMessageFound(message);
125 }
126 }
127 }
128 }
129
130 public void findMessagesWithFiles(final OnMessageFound onMessageFound) {
131 synchronized (this.messages) {
132 for (final Message message : this.messages) {
133 if ((message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE)
134 && message.getEncryption() != Message.ENCRYPTION_PGP) {
135 onMessageFound.onMessageFound(message);
136 }
137 }
138 }
139 }
140
141 public Message findMessageWithFileAndUuid(final String uuid) {
142 synchronized (this.messages) {
143 for (final Message message : this.messages) {
144 if ((message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE)
145 && message.getEncryption() != Message.ENCRYPTION_PGP
146 && message.getUuid().equals(uuid)) {
147 return message;
148 }
149 }
150 }
151 return null;
152 }
153
154 public void clearMessages() {
155 synchronized (this.messages) {
156 this.messages.clear();
157 }
158 }
159
160 public boolean setIncomingChatState(ChatState state) {
161 if (this.mIncomingChatState == state) {
162 return false;
163 }
164 this.mIncomingChatState = state;
165 return true;
166 }
167
168 public ChatState getIncomingChatState() {
169 return this.mIncomingChatState;
170 }
171
172 public boolean setOutgoingChatState(ChatState state) {
173 if (mode == MODE_MULTI) {
174 return false;
175 }
176 if (this.mOutgoingChatState != state) {
177 this.mOutgoingChatState = state;
178 return true;
179 } else {
180 return false;
181 }
182 }
183
184 public ChatState getOutgoingChatState() {
185 return this.mOutgoingChatState;
186 }
187
188 public void trim() {
189 synchronized (this.messages) {
190 final int size = messages.size();
191 final int maxsize = Config.PAGE_SIZE * Config.MAX_NUM_PAGES;
192 if (size > maxsize) {
193 this.messages.subList(0, size - maxsize).clear();
194 }
195 }
196 }
197
198 public void findUnsentMessagesWithEncryption(int encryptionType, OnMessageFound onMessageFound) {
199 synchronized (this.messages) {
200 for (Message message : this.messages) {
201 if ((message.getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_WAITING)
202 && (message.getEncryption() == encryptionType)) {
203 onMessageFound.onMessageFound(message);
204 }
205 }
206 }
207 }
208
209 public void findUnsentTextMessages(OnMessageFound onMessageFound) {
210 synchronized (this.messages) {
211 for (Message message : this.messages) {
212 if (message.getType() != Message.TYPE_IMAGE
213 && message.getStatus() == Message.STATUS_UNSEND) {
214 onMessageFound.onMessageFound(message);
215 }
216 }
217 }
218 }
219
220 public Message findSentMessageWithUuidOrRemoteId(String id) {
221 synchronized (this.messages) {
222 for (Message message : this.messages) {
223 if (id.equals(message.getUuid())
224 || (message.getStatus() >= Message.STATUS_SEND
225 && id.equals(message.getRemoteMsgId()))) {
226 return message;
227 }
228 }
229 }
230 return null;
231 }
232
233 public Message findMessageWithRemoteIdAndCounterpart(String id, Jid counterpart, boolean received, boolean carbon) {
234 synchronized (this.messages) {
235 for(int i = this.messages.size() - 1; i >= 0; --i) {
236 Message message = messages.get(i);
237 if (counterpart.equals(message.getCounterpart())
238 && ((message.getStatus() == Message.STATUS_RECEIVED) == received)
239 && (carbon == message.isCarbon() || received) ) {
240 if (id.equals(message.getRemoteMsgId())) {
241 return message;
242 } else {
243 return null;
244 }
245 }
246 }
247 }
248 return null;
249 }
250
251 public Message findSentMessageWithUuid(String id) {
252 synchronized (this.messages) {
253 for (Message message : this.messages) {
254 if (id.equals(message.getUuid())) {
255 return message;
256 }
257 }
258 }
259 return null;
260 }
261
262 public void populateWithMessages(final List<Message> messages) {
263 synchronized (this.messages) {
264 messages.clear();
265 messages.addAll(this.messages);
266 }
267 for(Iterator<Message> iterator = messages.iterator(); iterator.hasNext();) {
268 if (iterator.next().wasMergedIntoPrevious()) {
269 iterator.remove();
270 }
271 }
272 }
273
274 @Override
275 public boolean isBlocked() {
276 return getContact().isBlocked();
277 }
278
279 @Override
280 public boolean isDomainBlocked() {
281 return getContact().isDomainBlocked();
282 }
283
284 @Override
285 public Jid getBlockedJid() {
286 return getContact().getBlockedJid();
287 }
288
289 public String getLastReceivedOtrMessageId() {
290 return this.mLastReceivedOtrMessageId;
291 }
292
293 public void setLastReceivedOtrMessageId(String id) {
294 this.mLastReceivedOtrMessageId = id;
295 }
296
297 public int countMessages() {
298 synchronized (this.messages) {
299 return this.messages.size();
300 }
301 }
302
303 public void setFirstMamReference(String reference) {
304 this.mFirstMamReference = reference;
305 }
306
307 public String getFirstMamReference() {
308 return this.mFirstMamReference;
309 }
310
311 public void setLastClearHistory(long time) {
312 setAttribute("last_clear_history",String.valueOf(time));
313 }
314
315 public long getLastClearHistory() {
316 return getLongAttribute("last_clear_history", 0);
317 }
318
319 public List<Jid> getAcceptedCryptoTargets() {
320 if (mode == MODE_SINGLE) {
321 return Arrays.asList(getJid().toBareJid());
322 } else {
323 return getJidListAttribute(ATTRIBUTE_CRYPTO_TARGETS);
324 }
325 }
326
327 public void setAcceptedCryptoTargets(List<Jid> acceptedTargets) {
328 setAttribute(ATTRIBUTE_CRYPTO_TARGETS, acceptedTargets);
329 }
330
331 public void setCorrectingMessage(Message correctingMessage) {
332 this.correctingMessage = correctingMessage;
333 }
334
335 public Message getCorrectingMessage() {
336 return this.correctingMessage;
337 }
338
339 public interface OnMessageFound {
340 void onMessageFound(final Message message);
341 }
342
343 public Conversation(final String name, final Account account, final Jid contactJid,
344 final int mode) {
345 this(java.util.UUID.randomUUID().toString(), name, null, account
346 .getUuid(), contactJid, System.currentTimeMillis(),
347 STATUS_AVAILABLE, mode, "");
348 this.account = account;
349 }
350
351 public Conversation(final String uuid, final String name, final String contactUuid,
352 final String accountUuid, final Jid contactJid, final long created, final int status,
353 final int mode, final String attributes) {
354 this.uuid = uuid;
355 this.name = name;
356 this.contactUuid = contactUuid;
357 this.accountUuid = accountUuid;
358 this.contactJid = contactJid;
359 this.created = created;
360 this.status = status;
361 this.mode = mode;
362 try {
363 this.attributes = new JSONObject(attributes == null ? "" : attributes);
364 } catch (JSONException e) {
365 this.attributes = new JSONObject();
366 }
367 }
368
369 public boolean isRead() {
370 return (this.messages.size() == 0) || this.messages.get(this.messages.size() - 1).isRead();
371 }
372
373 public List<Message> markRead() {
374 final List<Message> unread = new ArrayList<>();
375 synchronized (this.messages) {
376 for(Message message : this.messages) {
377 if (!message.isRead()) {
378 message.markRead();
379 unread.add(message);
380 }
381 }
382 }
383 return unread;
384 }
385
386 public Message getLatestMarkableMessage() {
387 for (int i = this.messages.size() - 1; i >= 0; --i) {
388 if (this.messages.get(i).getStatus() <= Message.STATUS_RECEIVED
389 && this.messages.get(i).markable) {
390 if (this.messages.get(i).isRead()) {
391 return null;
392 } else {
393 return this.messages.get(i);
394 }
395 }
396 }
397 return null;
398 }
399
400 public Message getLatestMessage() {
401 if (this.messages.size() == 0) {
402 Message message = new Message(this, "", Message.ENCRYPTION_NONE);
403 message.setTime(getCreated());
404 return message;
405 } else {
406 Message message = this.messages.get(this.messages.size() - 1);
407 message.setConversation(this);
408 return message;
409 }
410 }
411
412 public String getName() {
413 if (getMode() == MODE_MULTI) {
414 if (getMucOptions().getSubject() != null) {
415 return getMucOptions().getSubject();
416 } else if (bookmark != null && bookmark.getBookmarkName() != null) {
417 return bookmark.getBookmarkName();
418 } else {
419 String generatedName = getMucOptions().createNameFromParticipants();
420 if (generatedName != null) {
421 return generatedName;
422 } else {
423 return getJid().getLocalpart();
424 }
425 }
426 } else {
427 return this.getContact().getDisplayName();
428 }
429 }
430
431 public String getAccountUuid() {
432 return this.accountUuid;
433 }
434
435 public Account getAccount() {
436 return this.account;
437 }
438
439 public Contact getContact() {
440 return this.account.getRoster().getContact(this.contactJid);
441 }
442
443 public void setAccount(final Account account) {
444 this.account = account;
445 }
446
447 @Override
448 public Jid getJid() {
449 return this.contactJid;
450 }
451
452 public int getStatus() {
453 return this.status;
454 }
455
456 public long getCreated() {
457 return this.created;
458 }
459
460 public ContentValues getContentValues() {
461 ContentValues values = new ContentValues();
462 values.put(UUID, uuid);
463 values.put(NAME, name);
464 values.put(CONTACT, contactUuid);
465 values.put(ACCOUNT, accountUuid);
466 values.put(CONTACTJID, contactJid.toString());
467 values.put(CREATED, created);
468 values.put(STATUS, status);
469 values.put(MODE, mode);
470 values.put(ATTRIBUTES, attributes.toString());
471 return values;
472 }
473
474 public static Conversation fromCursor(Cursor cursor) {
475 Jid jid;
476 try {
477 jid = Jid.fromString(cursor.getString(cursor.getColumnIndex(CONTACTJID)), true);
478 } catch (final InvalidJidException e) {
479 // Borked DB..
480 jid = null;
481 }
482 return new Conversation(cursor.getString(cursor.getColumnIndex(UUID)),
483 cursor.getString(cursor.getColumnIndex(NAME)),
484 cursor.getString(cursor.getColumnIndex(CONTACT)),
485 cursor.getString(cursor.getColumnIndex(ACCOUNT)),
486 jid,
487 cursor.getLong(cursor.getColumnIndex(CREATED)),
488 cursor.getInt(cursor.getColumnIndex(STATUS)),
489 cursor.getInt(cursor.getColumnIndex(MODE)),
490 cursor.getString(cursor.getColumnIndex(ATTRIBUTES)));
491 }
492
493 public void setStatus(int status) {
494 this.status = status;
495 }
496
497 public int getMode() {
498 return this.mode;
499 }
500
501 public void setMode(int mode) {
502 this.mode = mode;
503 }
504
505 public SessionImpl startOtrSession(String presence, boolean sendStart) {
506 if (this.otrSession != null) {
507 return this.otrSession;
508 } else {
509 final SessionID sessionId = new SessionID(this.getJid().toBareJid().toString(),
510 presence,
511 "xmpp");
512 this.otrSession = new SessionImpl(sessionId, getAccount().getOtrService());
513 try {
514 if (sendStart) {
515 this.otrSession.startSession();
516 return this.otrSession;
517 }
518 return this.otrSession;
519 } catch (OtrException e) {
520 return null;
521 }
522 }
523
524 }
525
526 public SessionImpl getOtrSession() {
527 return this.otrSession;
528 }
529
530 public void resetOtrSession() {
531 this.otrFingerprint = null;
532 this.otrSession = null;
533 this.mSmp.hint = null;
534 this.mSmp.secret = null;
535 this.mSmp.status = Smp.STATUS_NONE;
536 }
537
538 public Smp smp() {
539 return mSmp;
540 }
541
542 public boolean startOtrIfNeeded() {
543 if (this.otrSession != null && this.otrSession.getSessionStatus() != SessionStatus.ENCRYPTED) {
544 try {
545 this.otrSession.startSession();
546 return true;
547 } catch (OtrException e) {
548 this.resetOtrSession();
549 return false;
550 }
551 } else {
552 return true;
553 }
554 }
555
556 public boolean endOtrIfNeeded() {
557 if (this.otrSession != null) {
558 if (this.otrSession.getSessionStatus() == SessionStatus.ENCRYPTED) {
559 try {
560 this.otrSession.endSession();
561 this.resetOtrSession();
562 return true;
563 } catch (OtrException e) {
564 this.resetOtrSession();
565 return false;
566 }
567 } else {
568 this.resetOtrSession();
569 return false;
570 }
571 } else {
572 return false;
573 }
574 }
575
576 public boolean hasValidOtrSession() {
577 return this.otrSession != null;
578 }
579
580 public synchronized String getOtrFingerprint() {
581 if (this.otrFingerprint == null) {
582 try {
583 if (getOtrSession() == null || getOtrSession().getSessionStatus() != SessionStatus.ENCRYPTED) {
584 return null;
585 }
586 DSAPublicKey remotePubKey = (DSAPublicKey) getOtrSession().getRemotePublicKey();
587 this.otrFingerprint = getAccount().getOtrService().getFingerprint(remotePubKey);
588 } catch (final OtrCryptoException | UnsupportedOperationException ignored) {
589 return null;
590 }
591 }
592 return this.otrFingerprint;
593 }
594
595 public boolean verifyOtrFingerprint() {
596 final String fingerprint = getOtrFingerprint();
597 if (fingerprint != null) {
598 getContact().addOtrFingerprint(fingerprint);
599 return true;
600 } else {
601 return false;
602 }
603 }
604
605 public boolean isOtrFingerprintVerified() {
606 return getContact().getOtrFingerprints().contains(getOtrFingerprint());
607 }
608
609 /**
610 * short for is Private and Non-anonymous
611 */
612 private boolean isPnNA() {
613 return mode == MODE_SINGLE || (getMucOptions().membersOnly() && getMucOptions().nonanonymous());
614 }
615
616 public synchronized MucOptions getMucOptions() {
617 if (this.mucOptions == null) {
618 this.mucOptions = new MucOptions(this);
619 }
620 return this.mucOptions;
621 }
622
623 public void resetMucOptions() {
624 this.mucOptions = null;
625 }
626
627 public void setContactJid(final Jid jid) {
628 this.contactJid = jid;
629 }
630
631 public void setNextCounterpart(Jid jid) {
632 this.nextCounterpart = jid;
633 }
634
635 public Jid getNextCounterpart() {
636 return this.nextCounterpart;
637 }
638
639 private int getMostRecentlyUsedOutgoingEncryption() {
640 synchronized (this.messages) {
641 for(int i = this.messages.size() -1; i >= 0; --i) {
642 final Message m = this.messages.get(i);
643 if (!m.isCarbon() && m.getStatus() != Message.STATUS_RECEIVED) {
644 final int e = m.getEncryption();
645 if (e == Message.ENCRYPTION_DECRYPTED || e == Message.ENCRYPTION_DECRYPTION_FAILED) {
646 return Message.ENCRYPTION_PGP;
647 } else {
648 return e;
649 }
650 }
651 }
652 }
653 return Message.ENCRYPTION_NONE;
654 }
655
656 private int getMostRecentlyUsedIncomingEncryption() {
657 synchronized (this.messages) {
658 for(int i = this.messages.size() -1; i >= 0; --i) {
659 final Message m = this.messages.get(i);
660 if (m.getStatus() == Message.STATUS_RECEIVED) {
661 final int e = m.getEncryption();
662 if (e == Message.ENCRYPTION_DECRYPTED || e == Message.ENCRYPTION_DECRYPTION_FAILED) {
663 return Message.ENCRYPTION_PGP;
664 } else {
665 return e;
666 }
667 }
668 }
669 }
670 return Message.ENCRYPTION_NONE;
671 }
672
673 public int getNextEncryption() {
674 final AxolotlService axolotlService = getAccount().getAxolotlService();
675 int next = this.getIntAttribute(ATTRIBUTE_NEXT_ENCRYPTION, -1);
676 if (next == -1) {
677 if (Config.X509_VERIFICATION) {
678 if (axolotlService != null && axolotlService.isConversationAxolotlCapable(this)) {
679 return Message.ENCRYPTION_AXOLOTL;
680 } else {
681 return Message.ENCRYPTION_NONE;
682 }
683 }
684 int outgoing = this.getMostRecentlyUsedOutgoingEncryption();
685 if (outgoing == Message.ENCRYPTION_NONE) {
686 next = this.getMostRecentlyUsedIncomingEncryption();
687 } else {
688 next = outgoing;
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, Config.ALWAYS_NOTIFY_BY_DEFAULT || isPnNA());
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}