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