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