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