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