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