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