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