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