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