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