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())) {
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 boolean hasMessageWithCounterpart(Jid counterpart) {
293 synchronized (this.messages) {
294 for(Message message : this.messages) {
295 if (counterpart.equals(message.getCounterpart())) {
296 return true;
297 }
298 }
299 }
300 return false;
301 }
302
303 public void populateWithMessages(final List<Message> messages) {
304 synchronized (this.messages) {
305 messages.clear();
306 messages.addAll(this.messages);
307 }
308 for(Iterator<Message> iterator = messages.iterator(); iterator.hasNext();) {
309 if (iterator.next().wasMergedIntoPrevious()) {
310 iterator.remove();
311 }
312 }
313 }
314
315 @Override
316 public boolean isBlocked() {
317 return getContact().isBlocked();
318 }
319
320 @Override
321 public boolean isDomainBlocked() {
322 return getContact().isDomainBlocked();
323 }
324
325 @Override
326 public Jid getBlockedJid() {
327 return getContact().getBlockedJid();
328 }
329
330 public String getLastReceivedOtrMessageId() {
331 return this.mLastReceivedOtrMessageId;
332 }
333
334 public void setLastReceivedOtrMessageId(String id) {
335 this.mLastReceivedOtrMessageId = id;
336 }
337
338 public int countMessages() {
339 synchronized (this.messages) {
340 return this.messages.size();
341 }
342 }
343
344 public void setFirstMamReference(String reference) {
345 this.mFirstMamReference = reference;
346 }
347
348 public String getFirstMamReference() {
349 return this.mFirstMamReference;
350 }
351
352 public void setLastClearHistory(long time) {
353 setAttribute(ATTRIBUTE_LAST_CLEAR_HISTORY,String.valueOf(time));
354 }
355
356 public long getLastClearHistory() {
357 return getLongAttribute(ATTRIBUTE_LAST_CLEAR_HISTORY, 0);
358 }
359
360 public List<Jid> getAcceptedCryptoTargets() {
361 if (mode == MODE_SINGLE) {
362 return Collections.singletonList(getJid().toBareJid());
363 } else {
364 return getJidListAttribute(ATTRIBUTE_CRYPTO_TARGETS);
365 }
366 }
367
368 public void setAcceptedCryptoTargets(List<Jid> acceptedTargets) {
369 setAttribute(ATTRIBUTE_CRYPTO_TARGETS, acceptedTargets);
370 }
371
372 public boolean setCorrectingMessage(Message correctingMessage) {
373 this.correctingMessage = correctingMessage;
374 return correctingMessage == null && draftMessage != null;
375 }
376
377 public Message getCorrectingMessage() {
378 return this.correctingMessage;
379 }
380
381 public boolean withSelf() {
382 return getContact().isSelf();
383 }
384
385 @Override
386 public int compareTo(@NonNull Conversation another) {
387 final Message left = getLatestMessage();
388 final Message right = another.getLatestMessage();
389 if (left.getTimeSent() > right.getTimeSent()) {
390 return -1;
391 } else if (left.getTimeSent() < right.getTimeSent()) {
392 return 1;
393 } else {
394 return 0;
395 }
396 }
397
398 public void setDraftMessage(String draftMessage) {
399 this.draftMessage = draftMessage;
400 }
401
402 public String getDraftMessage() {
403 return draftMessage;
404 }
405
406 public interface OnMessageFound {
407 void onMessageFound(final Message message);
408 }
409
410 public Conversation(final String name, final Account account, final Jid contactJid,
411 final int mode) {
412 this(java.util.UUID.randomUUID().toString(), name, null, account
413 .getUuid(), contactJid, System.currentTimeMillis(),
414 STATUS_AVAILABLE, mode, "");
415 this.account = account;
416 }
417
418 public Conversation(final String uuid, final String name, final String contactUuid,
419 final String accountUuid, final Jid contactJid, final long created, final int status,
420 final int mode, final String attributes) {
421 this.uuid = uuid;
422 this.name = name;
423 this.contactUuid = contactUuid;
424 this.accountUuid = accountUuid;
425 this.contactJid = contactJid;
426 this.created = created;
427 this.status = status;
428 this.mode = mode;
429 try {
430 this.attributes = new JSONObject(attributes == null ? "" : attributes);
431 } catch (JSONException e) {
432 this.attributes = new JSONObject();
433 }
434 }
435
436 public boolean isRead() {
437 return (this.messages.size() == 0) || this.messages.get(this.messages.size() - 1).isRead();
438 }
439
440 public List<Message> markRead() {
441 final List<Message> unread = new ArrayList<>();
442 synchronized (this.messages) {
443 for(Message message : this.messages) {
444 if (!message.isRead()) {
445 message.markRead();
446 unread.add(message);
447 }
448 }
449 }
450 return unread;
451 }
452
453 public Message getLatestMarkableMessage() {
454 for (int i = this.messages.size() - 1; i >= 0; --i) {
455 if (this.messages.get(i).getStatus() <= Message.STATUS_RECEIVED
456 && this.messages.get(i).markable) {
457 if (this.messages.get(i).isRead()) {
458 return null;
459 } else {
460 return this.messages.get(i);
461 }
462 }
463 }
464 return null;
465 }
466
467 public Message getLatestMessage() {
468 if (this.messages.size() == 0) {
469 Message message = new Message(this, "", Message.ENCRYPTION_NONE);
470 message.setType(Message.TYPE_STATUS);
471 message.setTime(Math.max(getCreated(),getLastClearHistory()));
472 return message;
473 } else {
474 Message message = this.messages.get(this.messages.size() - 1);
475 message.setConversation(this);
476 return message;
477 }
478 }
479
480 public String getName() {
481 if (getMode() == MODE_MULTI) {
482 if (getMucOptions().getSubject() != null) {
483 return getMucOptions().getSubject();
484 } else if (bookmark != null
485 && bookmark.getBookmarkName() != null
486 && !bookmark.getBookmarkName().trim().isEmpty()) {
487 return bookmark.getBookmarkName().trim();
488 } else {
489 String generatedName = getMucOptions().createNameFromParticipants();
490 if (generatedName != null) {
491 return generatedName;
492 } else {
493 return getJid().getUnescapedLocalpart();
494 }
495 }
496 } else if (isWithStranger()) {
497 return contactJid.toBareJid().toString();
498 } else {
499 return this.getContact().getDisplayName();
500 }
501 }
502
503 public String getAccountUuid() {
504 return this.accountUuid;
505 }
506
507 public Account getAccount() {
508 return this.account;
509 }
510
511 public Contact getContact() {
512 return this.account.getRoster().getContact(this.contactJid);
513 }
514
515 public void setAccount(final Account account) {
516 this.account = account;
517 }
518
519 @Override
520 public Jid getJid() {
521 return this.contactJid;
522 }
523
524 public int getStatus() {
525 return this.status;
526 }
527
528 public long getCreated() {
529 return this.created;
530 }
531
532 public ContentValues getContentValues() {
533 ContentValues values = new ContentValues();
534 values.put(UUID, uuid);
535 values.put(NAME, name);
536 values.put(CONTACT, contactUuid);
537 values.put(ACCOUNT, accountUuid);
538 values.put(CONTACTJID, contactJid.toPreppedString());
539 values.put(CREATED, created);
540 values.put(STATUS, status);
541 values.put(MODE, mode);
542 values.put(ATTRIBUTES, attributes.toString());
543 return values;
544 }
545
546 public static Conversation fromCursor(Cursor cursor) {
547 Jid jid;
548 try {
549 jid = Jid.fromString(cursor.getString(cursor.getColumnIndex(CONTACTJID)), true);
550 } catch (final InvalidJidException e) {
551 // Borked DB..
552 jid = null;
553 }
554 return new Conversation(cursor.getString(cursor.getColumnIndex(UUID)),
555 cursor.getString(cursor.getColumnIndex(NAME)),
556 cursor.getString(cursor.getColumnIndex(CONTACT)),
557 cursor.getString(cursor.getColumnIndex(ACCOUNT)),
558 jid,
559 cursor.getLong(cursor.getColumnIndex(CREATED)),
560 cursor.getInt(cursor.getColumnIndex(STATUS)),
561 cursor.getInt(cursor.getColumnIndex(MODE)),
562 cursor.getString(cursor.getColumnIndex(ATTRIBUTES)));
563 }
564
565 public void setStatus(int status) {
566 this.status = status;
567 }
568
569 public int getMode() {
570 return this.mode;
571 }
572
573 public void setMode(int mode) {
574 this.mode = mode;
575 }
576
577 public SessionImpl startOtrSession(String presence, boolean sendStart) {
578 if (this.otrSession != null) {
579 return this.otrSession;
580 } else {
581 final SessionID sessionId = new SessionID(this.getJid().toBareJid().toString(),
582 presence,
583 "xmpp");
584 this.otrSession = new SessionImpl(sessionId, getAccount().getOtrService());
585 try {
586 if (sendStart) {
587 this.otrSession.startSession();
588 return this.otrSession;
589 }
590 return this.otrSession;
591 } catch (OtrException e) {
592 return null;
593 }
594 }
595
596 }
597
598 public SessionImpl getOtrSession() {
599 return this.otrSession;
600 }
601
602 public void resetOtrSession() {
603 this.otrFingerprint = null;
604 this.otrSession = null;
605 this.mSmp.hint = null;
606 this.mSmp.secret = null;
607 this.mSmp.status = Smp.STATUS_NONE;
608 }
609
610 public Smp smp() {
611 return mSmp;
612 }
613
614 public boolean startOtrIfNeeded() {
615 if (this.otrSession != null && this.otrSession.getSessionStatus() != SessionStatus.ENCRYPTED) {
616 try {
617 this.otrSession.startSession();
618 return true;
619 } catch (OtrException e) {
620 this.resetOtrSession();
621 return false;
622 }
623 } else {
624 return true;
625 }
626 }
627
628 public boolean endOtrIfNeeded() {
629 if (this.otrSession != null) {
630 if (this.otrSession.getSessionStatus() == SessionStatus.ENCRYPTED) {
631 try {
632 this.otrSession.endSession();
633 this.resetOtrSession();
634 return true;
635 } catch (OtrException e) {
636 this.resetOtrSession();
637 return false;
638 }
639 } else {
640 this.resetOtrSession();
641 return false;
642 }
643 } else {
644 return false;
645 }
646 }
647
648 public boolean hasValidOtrSession() {
649 return this.otrSession != null;
650 }
651
652 public synchronized String getOtrFingerprint() {
653 if (this.otrFingerprint == null) {
654 try {
655 if (getOtrSession() == null || getOtrSession().getSessionStatus() != SessionStatus.ENCRYPTED) {
656 return null;
657 }
658 DSAPublicKey remotePubKey = (DSAPublicKey) getOtrSession().getRemotePublicKey();
659 this.otrFingerprint = getAccount().getOtrService().getFingerprint(remotePubKey).toLowerCase(Locale.US);
660 } catch (final OtrCryptoException | UnsupportedOperationException ignored) {
661 return null;
662 }
663 }
664 return this.otrFingerprint;
665 }
666
667 public boolean verifyOtrFingerprint() {
668 final String fingerprint = getOtrFingerprint();
669 if (fingerprint != null) {
670 getContact().addOtrFingerprint(fingerprint);
671 return true;
672 } else {
673 return false;
674 }
675 }
676
677 public boolean isOtrFingerprintVerified() {
678 return getContact().getOtrFingerprints().contains(getOtrFingerprint());
679 }
680
681 /**
682 * short for is Private and Non-anonymous
683 */
684 private boolean isPnNA() {
685 return mode == MODE_SINGLE || (getMucOptions().membersOnly() && getMucOptions().nonanonymous());
686 }
687
688 public synchronized MucOptions getMucOptions() {
689 if (this.mucOptions == null) {
690 this.mucOptions = new MucOptions(this);
691 }
692 return this.mucOptions;
693 }
694
695 public void resetMucOptions() {
696 this.mucOptions = null;
697 }
698
699 public void setContactJid(final Jid jid) {
700 this.contactJid = jid;
701 }
702
703 public void setNextCounterpart(Jid jid) {
704 this.nextCounterpart = jid;
705 }
706
707 public Jid getNextCounterpart() {
708 return this.nextCounterpart;
709 }
710
711 public int getNextEncryption() {
712 return fixAvailableEncryption(this.getIntAttribute(ATTRIBUTE_NEXT_ENCRYPTION, getDefaultEncryption()));
713 }
714
715 private int fixAvailableEncryption(int selectedEncryption) {
716 switch(selectedEncryption) {
717 case Message.ENCRYPTION_NONE:
718 return Config.supportUnencrypted() ? selectedEncryption : getDefaultEncryption();
719 case Message.ENCRYPTION_AXOLOTL:
720 return Config.supportOmemo() ? selectedEncryption : getDefaultEncryption();
721 case Message.ENCRYPTION_OTR:
722 return Config.supportOtr() ? selectedEncryption : getDefaultEncryption();
723 case Message.ENCRYPTION_PGP:
724 case Message.ENCRYPTION_DECRYPTED:
725 case Message.ENCRYPTION_DECRYPTION_FAILED:
726 return Config.supportOpenPgp() ? Message.ENCRYPTION_PGP : getDefaultEncryption();
727 default:
728 return getDefaultEncryption();
729 }
730 }
731
732 private int getDefaultEncryption() {
733 AxolotlService axolotlService = account.getAxolotlService();
734 if (Config.supportUnencrypted()) {
735 return Message.ENCRYPTION_NONE;
736 } else if (Config.supportOmemo()
737 && (axolotlService != null && axolotlService.isConversationAxolotlCapable(this) || !Config.multipleEncryptionChoices())) {
738 return Message.ENCRYPTION_AXOLOTL;
739 } else if (Config.supportOtr() && mode == MODE_SINGLE) {
740 return Message.ENCRYPTION_OTR;
741 } else if (Config.supportOpenPgp()) {
742 return Message.ENCRYPTION_PGP;
743 } else {
744 return Message.ENCRYPTION_NONE;
745 }
746 }
747
748 public void setNextEncryption(int encryption) {
749 this.setAttribute(ATTRIBUTE_NEXT_ENCRYPTION, String.valueOf(encryption));
750 }
751
752 public String getNextMessage() {
753 if (this.nextMessage == null) {
754 return "";
755 } else {
756 return this.nextMessage;
757 }
758 }
759
760 public boolean smpRequested() {
761 return smp().status == Smp.STATUS_CONTACT_REQUESTED;
762 }
763
764 public void setNextMessage(String message) {
765 this.nextMessage = message;
766 }
767
768 public void setSymmetricKey(byte[] key) {
769 this.symmetricKey = key;
770 }
771
772 public byte[] getSymmetricKey() {
773 return this.symmetricKey;
774 }
775
776 public void setBookmark(Bookmark bookmark) {
777 this.bookmark = bookmark;
778 this.bookmark.setConversation(this);
779 }
780
781 public void deregisterWithBookmark() {
782 if (this.bookmark != null) {
783 this.bookmark.setConversation(null);
784 }
785 this.bookmark = null;
786 }
787
788 public Bookmark getBookmark() {
789 return this.bookmark;
790 }
791
792 public boolean hasDuplicateMessage(Message message) {
793 synchronized (this.messages) {
794 for (int i = this.messages.size() - 1; i >= 0; --i) {
795 if (this.messages.get(i).similar(message)) {
796 return true;
797 }
798 }
799 }
800 return false;
801 }
802
803 public Message findSentMessageWithBody(String body) {
804 synchronized (this.messages) {
805 for (int i = this.messages.size() - 1; i >= 0; --i) {
806 Message message = this.messages.get(i);
807 if (message.getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_SEND) {
808 String otherBody;
809 if (message.hasFileOnRemoteHost()) {
810 otherBody = message.getFileParams().url.toString();
811 } else {
812 otherBody = message.body;
813 }
814 if (otherBody != null && otherBody.equals(body)) {
815 return message;
816 }
817 }
818 }
819 return null;
820 }
821 }
822
823 public long getLastMessageTransmitted() {
824 final long last_clear = getLastClearHistory();
825 long last_received = 0;
826 synchronized (this.messages) {
827 for(int i = this.messages.size() - 1; i >= 0; --i) {
828 Message message = this.messages.get(i);
829 if (message.getStatus() == Message.STATUS_RECEIVED || message.isCarbon()) {
830 last_received = message.getTimeSent();
831 break;
832 }
833 }
834 }
835 return Math.max(last_clear,last_received);
836 }
837
838 public void setMutedTill(long value) {
839 this.setAttribute(ATTRIBUTE_MUTED_TILL, String.valueOf(value));
840 }
841
842 public boolean isMuted() {
843 return System.currentTimeMillis() < this.getLongAttribute(ATTRIBUTE_MUTED_TILL, 0);
844 }
845
846 public boolean alwaysNotify() {
847 return mode == MODE_SINGLE || getBooleanAttribute(ATTRIBUTE_ALWAYS_NOTIFY, Config.ALWAYS_NOTIFY_BY_DEFAULT || isPnNA());
848 }
849
850 public boolean setAttribute(String key, String value) {
851 synchronized (this.attributes) {
852 try {
853 this.attributes.put(key, value);
854 return true;
855 } catch (JSONException e) {
856 return false;
857 }
858 }
859 }
860
861 public boolean setAttribute(String key, List<Jid> jids) {
862 JSONArray array = new JSONArray();
863 for(Jid jid : jids) {
864 array.put(jid.toBareJid().toString());
865 }
866 synchronized (this.attributes) {
867 try {
868 this.attributes.put(key, array);
869 return true;
870 } catch (JSONException e) {
871 e.printStackTrace();
872 return false;
873 }
874 }
875 }
876
877 public String getAttribute(String key) {
878 synchronized (this.attributes) {
879 try {
880 return this.attributes.getString(key);
881 } catch (JSONException e) {
882 return null;
883 }
884 }
885 }
886
887 private List<Jid> getJidListAttribute(String key) {
888 ArrayList<Jid> list = new ArrayList<>();
889 synchronized (this.attributes) {
890 try {
891 JSONArray array = this.attributes.getJSONArray(key);
892 for (int i = 0; i < array.length(); ++i) {
893 try {
894 list.add(Jid.fromString(array.getString(i)));
895 } catch (InvalidJidException e) {
896 //ignored
897 }
898 }
899 } catch (JSONException e) {
900 //ignored
901 }
902 }
903 return list;
904 }
905
906 private int getIntAttribute(String key, int defaultValue) {
907 String value = this.getAttribute(key);
908 if (value == null) {
909 return defaultValue;
910 } else {
911 try {
912 return Integer.parseInt(value);
913 } catch (NumberFormatException e) {
914 return defaultValue;
915 }
916 }
917 }
918
919 public long getLongAttribute(String key, long defaultValue) {
920 String value = this.getAttribute(key);
921 if (value == null) {
922 return defaultValue;
923 } else {
924 try {
925 return Long.parseLong(value);
926 } catch (NumberFormatException e) {
927 return defaultValue;
928 }
929 }
930 }
931
932 private boolean getBooleanAttribute(String key, boolean defaultValue) {
933 String value = this.getAttribute(key);
934 if (value == null) {
935 return defaultValue;
936 } else {
937 return Boolean.parseBoolean(value);
938 }
939 }
940
941 public void add(Message message) {
942 message.setConversation(this);
943 synchronized (this.messages) {
944 this.messages.add(message);
945 }
946 }
947
948 public void prepend(Message message) {
949 message.setConversation(this);
950 synchronized (this.messages) {
951 this.messages.add(0,message);
952 }
953 }
954
955 public void addAll(int index, List<Message> messages) {
956 synchronized (this.messages) {
957 this.messages.addAll(index, messages);
958 }
959 account.getPgpDecryptionService().decrypt(messages);
960 }
961
962 public void expireOldMessages(long timestamp) {
963 synchronized (this.messages) {
964 for(ListIterator<Message> iterator = this.messages.listIterator(); iterator.hasNext();) {
965 if (iterator.next().getTimeSent() < timestamp) {
966 iterator.remove();
967 }
968 }
969 untieMessages();
970 }
971 }
972
973 public void sort() {
974 synchronized (this.messages) {
975 Collections.sort(this.messages, new Comparator<Message>() {
976 @Override
977 public int compare(Message left, Message right) {
978 if (left.getTimeSent() < right.getTimeSent()) {
979 return -1;
980 } else if (left.getTimeSent() > right.getTimeSent()) {
981 return 1;
982 } else {
983 return 0;
984 }
985 }
986 });
987 untieMessages();
988 }
989 }
990
991 private void untieMessages() {
992 for(Message message : this.messages) {
993 message.untie();
994 }
995 }
996
997 public int unreadCount() {
998 synchronized (this.messages) {
999 int count = 0;
1000 for(int i = this.messages.size() - 1; i >= 0; --i) {
1001 if (this.messages.get(i).isRead()) {
1002 return count;
1003 }
1004 ++count;
1005 }
1006 return count;
1007 }
1008 }
1009
1010 private int sentMessagesCount() {
1011 int count = 0;
1012 synchronized (this.messages) {
1013 for(Message message : messages) {
1014 if (message.getStatus() != Message.STATUS_RECEIVED) {
1015 ++count;
1016 }
1017 }
1018 }
1019 return count;
1020 }
1021
1022 public boolean isWithStranger() {
1023 return mode == MODE_SINGLE
1024 && !getJid().equals(account.getJid().toDomainJid())
1025 && !getContact().showInRoster()
1026 && sentMessagesCount() == 0;
1027 }
1028
1029 public class Smp {
1030 public static final int STATUS_NONE = 0;
1031 public static final int STATUS_CONTACT_REQUESTED = 1;
1032 public static final int STATUS_WE_REQUESTED = 2;
1033 public static final int STATUS_FAILED = 3;
1034 public static final int STATUS_VERIFIED = 4;
1035
1036 public String secret = null;
1037 public String hint = null;
1038 public int status = 0;
1039 }
1040}