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