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