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