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 }
786
787 public Bookmark getBookmark() {
788 return this.bookmark;
789 }
790
791 public boolean hasDuplicateMessage(Message message) {
792 synchronized (this.messages) {
793 for (int i = this.messages.size() - 1; i >= 0; --i) {
794 if (this.messages.get(i).similar(message)) {
795 return true;
796 }
797 }
798 }
799 return false;
800 }
801
802 public Message findSentMessageWithBody(String body) {
803 synchronized (this.messages) {
804 for (int i = this.messages.size() - 1; i >= 0; --i) {
805 Message message = this.messages.get(i);
806 if (message.getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_SEND) {
807 String otherBody;
808 if (message.hasFileOnRemoteHost()) {
809 otherBody = message.getFileParams().url.toString();
810 } else {
811 otherBody = message.body;
812 }
813 if (otherBody != null && otherBody.equals(body)) {
814 return message;
815 }
816 }
817 }
818 return null;
819 }
820 }
821
822 public long getLastMessageTransmitted() {
823 final long last_clear = getLastClearHistory();
824 long last_received = 0;
825 synchronized (this.messages) {
826 for(int i = this.messages.size() - 1; i >= 0; --i) {
827 Message message = this.messages.get(i);
828 if (message.getStatus() == Message.STATUS_RECEIVED || message.isCarbon()) {
829 last_received = message.getTimeSent();
830 break;
831 }
832 }
833 }
834 return Math.max(last_clear,last_received);
835 }
836
837 public void setMutedTill(long value) {
838 this.setAttribute(ATTRIBUTE_MUTED_TILL, String.valueOf(value));
839 }
840
841 public boolean isMuted() {
842 return System.currentTimeMillis() < this.getLongAttribute(ATTRIBUTE_MUTED_TILL, 0);
843 }
844
845 public boolean alwaysNotify() {
846 return mode == MODE_SINGLE || getBooleanAttribute(ATTRIBUTE_ALWAYS_NOTIFY, Config.ALWAYS_NOTIFY_BY_DEFAULT || isPnNA());
847 }
848
849 public boolean setAttribute(String key, String value) {
850 synchronized (this.attributes) {
851 try {
852 this.attributes.put(key, value);
853 return true;
854 } catch (JSONException e) {
855 return false;
856 }
857 }
858 }
859
860 public boolean setAttribute(String key, List<Jid> jids) {
861 JSONArray array = new JSONArray();
862 for(Jid jid : jids) {
863 array.put(jid.toBareJid().toString());
864 }
865 synchronized (this.attributes) {
866 try {
867 this.attributes.put(key, array);
868 return true;
869 } catch (JSONException e) {
870 e.printStackTrace();
871 return false;
872 }
873 }
874 }
875
876 public String getAttribute(String key) {
877 synchronized (this.attributes) {
878 try {
879 return this.attributes.getString(key);
880 } catch (JSONException e) {
881 return null;
882 }
883 }
884 }
885
886 private List<Jid> getJidListAttribute(String key) {
887 ArrayList<Jid> list = new ArrayList<>();
888 synchronized (this.attributes) {
889 try {
890 JSONArray array = this.attributes.getJSONArray(key);
891 for (int i = 0; i < array.length(); ++i) {
892 try {
893 list.add(Jid.fromString(array.getString(i)));
894 } catch (InvalidJidException e) {
895 //ignored
896 }
897 }
898 } catch (JSONException e) {
899 //ignored
900 }
901 }
902 return list;
903 }
904
905 private int getIntAttribute(String key, int defaultValue) {
906 String value = this.getAttribute(key);
907 if (value == null) {
908 return defaultValue;
909 } else {
910 try {
911 return Integer.parseInt(value);
912 } catch (NumberFormatException e) {
913 return defaultValue;
914 }
915 }
916 }
917
918 public long getLongAttribute(String key, long defaultValue) {
919 String value = this.getAttribute(key);
920 if (value == null) {
921 return defaultValue;
922 } else {
923 try {
924 return Long.parseLong(value);
925 } catch (NumberFormatException e) {
926 return defaultValue;
927 }
928 }
929 }
930
931 private boolean getBooleanAttribute(String key, boolean defaultValue) {
932 String value = this.getAttribute(key);
933 if (value == null) {
934 return defaultValue;
935 } else {
936 return Boolean.parseBoolean(value);
937 }
938 }
939
940 public void add(Message message) {
941 message.setConversation(this);
942 synchronized (this.messages) {
943 this.messages.add(message);
944 }
945 }
946
947 public void prepend(Message message) {
948 message.setConversation(this);
949 synchronized (this.messages) {
950 this.messages.add(0,message);
951 }
952 }
953
954 public void addAll(int index, List<Message> messages) {
955 synchronized (this.messages) {
956 this.messages.addAll(index, messages);
957 }
958 account.getPgpDecryptionService().decrypt(messages);
959 }
960
961 public void expireOldMessages(long timestamp) {
962 synchronized (this.messages) {
963 for(ListIterator<Message> iterator = this.messages.listIterator(); iterator.hasNext();) {
964 if (iterator.next().getTimeSent() < timestamp) {
965 iterator.remove();
966 }
967 }
968 untieMessages();
969 }
970 }
971
972 public void sort() {
973 synchronized (this.messages) {
974 Collections.sort(this.messages, new Comparator<Message>() {
975 @Override
976 public int compare(Message left, Message right) {
977 if (left.getTimeSent() < right.getTimeSent()) {
978 return -1;
979 } else if (left.getTimeSent() > right.getTimeSent()) {
980 return 1;
981 } else {
982 return 0;
983 }
984 }
985 });
986 untieMessages();
987 }
988 }
989
990 private void untieMessages() {
991 for(Message message : this.messages) {
992 message.untie();
993 }
994 }
995
996 public int unreadCount() {
997 synchronized (this.messages) {
998 int count = 0;
999 for(int i = this.messages.size() - 1; i >= 0; --i) {
1000 if (this.messages.get(i).isRead()) {
1001 return count;
1002 }
1003 ++count;
1004 }
1005 return count;
1006 }
1007 }
1008
1009 private int sentMessagesCount() {
1010 int count = 0;
1011 synchronized (this.messages) {
1012 for(Message message : messages) {
1013 if (message.getStatus() != Message.STATUS_RECEIVED) {
1014 ++count;
1015 }
1016 }
1017 }
1018 return count;
1019 }
1020
1021 public boolean isWithStranger() {
1022 return mode == MODE_SINGLE
1023 && !getJid().equals(account.getJid().toDomainJid())
1024 && !getContact().showInRoster()
1025 && sentMessagesCount() == 0;
1026 }
1027
1028 public class Smp {
1029 public static final int STATUS_NONE = 0;
1030 public static final int STATUS_CONTACT_REQUESTED = 1;
1031 public static final int STATUS_WE_REQUESTED = 2;
1032 public static final int STATUS_FAILED = 3;
1033 public static final int STATUS_VERIFIED = 4;
1034
1035 public String secret = null;
1036 public String hint = null;
1037 public int status = 0;
1038 }
1039}