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 void startOtrIfNeeded() {
521 if (this.otrSession != null
522 && this.otrSession.getSessionStatus() != SessionStatus.ENCRYPTED) {
523 try {
524 this.otrSession.startSession();
525 } catch (OtrException e) {
526 this.resetOtrSession();
527 }
528 }
529 }
530
531 public boolean endOtrIfNeeded() {
532 if (this.otrSession != null) {
533 if (this.otrSession.getSessionStatus() == SessionStatus.ENCRYPTED) {
534 try {
535 this.otrSession.endSession();
536 this.resetOtrSession();
537 return true;
538 } catch (OtrException e) {
539 this.resetOtrSession();
540 return false;
541 }
542 } else {
543 this.resetOtrSession();
544 return false;
545 }
546 } else {
547 return false;
548 }
549 }
550
551 public boolean hasValidOtrSession() {
552 return this.otrSession != null;
553 }
554
555 public synchronized String getOtrFingerprint() {
556 if (this.otrFingerprint == null) {
557 try {
558 if (getOtrSession() == null || getOtrSession().getSessionStatus() != SessionStatus.ENCRYPTED) {
559 return null;
560 }
561 DSAPublicKey remotePubKey = (DSAPublicKey) getOtrSession().getRemotePublicKey();
562 this.otrFingerprint = getAccount().getOtrService().getFingerprint(remotePubKey);
563 } catch (final OtrCryptoException | UnsupportedOperationException ignored) {
564 return null;
565 }
566 }
567 return this.otrFingerprint;
568 }
569
570 public boolean verifyOtrFingerprint() {
571 final String fingerprint = getOtrFingerprint();
572 if (fingerprint != null) {
573 getContact().addOtrFingerprint(fingerprint);
574 return true;
575 } else {
576 return false;
577 }
578 }
579
580 public boolean isOtrFingerprintVerified() {
581 return getContact().getOtrFingerprints().contains(getOtrFingerprint());
582 }
583
584 /**
585 * short for is Private and Non-anonymous
586 */
587 private boolean isPnNA() {
588 return mode == MODE_SINGLE || (getMucOptions().membersOnly() && getMucOptions().nonanonymous());
589 }
590
591 public synchronized MucOptions getMucOptions() {
592 if (this.mucOptions == null) {
593 this.mucOptions = new MucOptions(this);
594 }
595 return this.mucOptions;
596 }
597
598 public void resetMucOptions() {
599 this.mucOptions = null;
600 }
601
602 public void setContactJid(final Jid jid) {
603 this.contactJid = jid;
604 }
605
606 public void setNextCounterpart(Jid jid) {
607 this.nextCounterpart = jid;
608 }
609
610 public Jid getNextCounterpart() {
611 return this.nextCounterpart;
612 }
613
614 private int getMostRecentlyUsedOutgoingEncryption() {
615 synchronized (this.messages) {
616 for(int i = this.messages.size() -1; i >= 0; --i) {
617 final Message m = this.messages.get(i);
618 if (!m.isCarbon() && m.getStatus() != Message.STATUS_RECEIVED) {
619 final int e = m.getEncryption();
620 if (e == Message.ENCRYPTION_DECRYPTED || e == Message.ENCRYPTION_DECRYPTION_FAILED) {
621 return Message.ENCRYPTION_PGP;
622 } else {
623 return e;
624 }
625 }
626 }
627 }
628 return Message.ENCRYPTION_NONE;
629 }
630
631 private int getMostRecentlyUsedIncomingEncryption() {
632 synchronized (this.messages) {
633 for(int i = this.messages.size() -1; i >= 0; --i) {
634 final Message m = this.messages.get(i);
635 if (m.getStatus() == Message.STATUS_RECEIVED) {
636 final int e = m.getEncryption();
637 if (e == Message.ENCRYPTION_DECRYPTED || e == Message.ENCRYPTION_DECRYPTION_FAILED) {
638 return Message.ENCRYPTION_PGP;
639 } else {
640 return e;
641 }
642 }
643 }
644 }
645 return Message.ENCRYPTION_NONE;
646 }
647
648 public int getNextEncryption() {
649 final AxolotlService axolotlService = getAccount().getAxolotlService();
650 int next = this.getIntAttribute(ATTRIBUTE_NEXT_ENCRYPTION, -1);
651 if (next == -1) {
652 if (Config.X509_VERIFICATION && mode == MODE_SINGLE) {
653 if (axolotlService != null && axolotlService.isContactAxolotlCapable(getContact())) {
654 return Message.ENCRYPTION_AXOLOTL;
655 } else {
656 return Message.ENCRYPTION_NONE;
657 }
658 }
659 int outgoing = this.getMostRecentlyUsedOutgoingEncryption();
660 if (outgoing == Message.ENCRYPTION_NONE) {
661 next = this.getMostRecentlyUsedIncomingEncryption();
662 } else {
663 next = outgoing;
664 }
665 }
666 if (Config.FORCE_E2E_ENCRYPTION && mode == MODE_SINGLE && next <= 0) {
667 if (axolotlService != null && axolotlService.isContactAxolotlCapable(getContact())) {
668 return Message.ENCRYPTION_AXOLOTL;
669 } else {
670 return Message.ENCRYPTION_OTR;
671 }
672 }
673 return next;
674 }
675
676 public void setNextEncryption(int encryption) {
677 this.setAttribute(ATTRIBUTE_NEXT_ENCRYPTION, String.valueOf(encryption));
678 }
679
680 public String getNextMessage() {
681 if (this.nextMessage == null) {
682 return "";
683 } else {
684 return this.nextMessage;
685 }
686 }
687
688 public boolean smpRequested() {
689 return smp().status == Smp.STATUS_CONTACT_REQUESTED;
690 }
691
692 public void setNextMessage(String message) {
693 this.nextMessage = message;
694 }
695
696 public void setSymmetricKey(byte[] key) {
697 this.symmetricKey = key;
698 }
699
700 public byte[] getSymmetricKey() {
701 return this.symmetricKey;
702 }
703
704 public void setBookmark(Bookmark bookmark) {
705 this.bookmark = bookmark;
706 this.bookmark.setConversation(this);
707 }
708
709 public void deregisterWithBookmark() {
710 if (this.bookmark != null) {
711 this.bookmark.setConversation(null);
712 }
713 }
714
715 public Bookmark getBookmark() {
716 return this.bookmark;
717 }
718
719 public boolean hasDuplicateMessage(Message message) {
720 synchronized (this.messages) {
721 for (int i = this.messages.size() - 1; i >= 0; --i) {
722 if (this.messages.get(i).equals(message)) {
723 return true;
724 }
725 }
726 }
727 return false;
728 }
729
730 public Message findSentMessageWithBody(String body) {
731 synchronized (this.messages) {
732 for (int i = this.messages.size() - 1; i >= 0; --i) {
733 Message message = this.messages.get(i);
734 if (message.getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_SEND) {
735 String otherBody;
736 if (message.hasFileOnRemoteHost()) {
737 otherBody = message.getFileParams().url.toString();
738 } else {
739 otherBody = message.body;
740 }
741 if (otherBody != null && otherBody.equals(body)) {
742 return message;
743 }
744 }
745 }
746 return null;
747 }
748 }
749
750 public long getLastMessageTransmitted() {
751 long last_clear = getLastClearHistory();
752 if (last_clear != 0) {
753 return last_clear;
754 }
755 synchronized (this.messages) {
756 for(int i = this.messages.size() - 1; i >= 0; --i) {
757 Message message = this.messages.get(i);
758 if (message.getStatus() == Message.STATUS_RECEIVED || message.isCarbon()) {
759 return message.getTimeSent();
760 }
761 }
762 }
763 return 0;
764 }
765
766 public void setMutedTill(long value) {
767 this.setAttribute(ATTRIBUTE_MUTED_TILL, String.valueOf(value));
768 }
769
770 public boolean isMuted() {
771 return System.currentTimeMillis() < this.getLongAttribute(ATTRIBUTE_MUTED_TILL, 0);
772 }
773
774 public boolean alwaysNotify() {
775 return mode == MODE_SINGLE || getBooleanAttribute(ATTRIBUTE_ALWAYS_NOTIFY, Config.ALWAYS_NOTIFY_BY_DEFAULT || isPnNA());
776 }
777
778 public boolean setAttribute(String key, String value) {
779 try {
780 this.attributes.put(key, value);
781 return true;
782 } catch (JSONException e) {
783 return false;
784 }
785 }
786
787 public String getAttribute(String key) {
788 try {
789 return this.attributes.getString(key);
790 } catch (JSONException e) {
791 return null;
792 }
793 }
794
795 public int getIntAttribute(String key, int defaultValue) {
796 String value = this.getAttribute(key);
797 if (value == null) {
798 return defaultValue;
799 } else {
800 try {
801 return Integer.parseInt(value);
802 } catch (NumberFormatException e) {
803 return defaultValue;
804 }
805 }
806 }
807
808 public long getLongAttribute(String key, long defaultValue) {
809 String value = this.getAttribute(key);
810 if (value == null) {
811 return defaultValue;
812 } else {
813 try {
814 return Long.parseLong(value);
815 } catch (NumberFormatException e) {
816 return defaultValue;
817 }
818 }
819 }
820
821 public boolean getBooleanAttribute(String key, boolean defaultValue) {
822 String value = this.getAttribute(key);
823 if (value == null) {
824 return defaultValue;
825 } else {
826 return Boolean.parseBoolean(value);
827 }
828 }
829
830 public void add(Message message) {
831 message.setConversation(this);
832 synchronized (this.messages) {
833 this.messages.add(message);
834 }
835 }
836
837 public void prepend(Message message) {
838 message.setConversation(this);
839 synchronized (this.messages) {
840 this.messages.add(0,message);
841 }
842 }
843
844 public void addAll(int index, List<Message> messages) {
845 synchronized (this.messages) {
846 this.messages.addAll(index, messages);
847 }
848 account.getPgpDecryptionService().addAll(messages);
849 }
850
851 public void sort() {
852 synchronized (this.messages) {
853 Collections.sort(this.messages, new Comparator<Message>() {
854 @Override
855 public int compare(Message left, Message right) {
856 if (left.getTimeSent() < right.getTimeSent()) {
857 return -1;
858 } else if (left.getTimeSent() > right.getTimeSent()) {
859 return 1;
860 } else {
861 return 0;
862 }
863 }
864 });
865 for(Message message : this.messages) {
866 message.untie();
867 }
868 }
869 }
870
871 public int unreadCount() {
872 synchronized (this.messages) {
873 int count = 0;
874 for(int i = this.messages.size() - 1; i >= 0; --i) {
875 if (this.messages.get(i).isRead()) {
876 return count;
877 }
878 ++count;
879 }
880 return count;
881 }
882 }
883
884 public class Smp {
885 public static final int STATUS_NONE = 0;
886 public static final int STATUS_CONTACT_REQUESTED = 1;
887 public static final int STATUS_WE_REQUESTED = 2;
888 public static final int STATUS_FAILED = 3;
889 public static final int STATUS_VERIFIED = 4;
890
891 public String secret = null;
892 public String hint = null;
893 public int status = 0;
894 }
895}