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