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