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 synchronized (this.messages) {
460 for (int i = this.messages.size() - 1; i >= 0; --i) {
461 final Message message = this.messages.get(i);
462 if (message.getStatus() <= Message.STATUS_RECEIVED && message.markable) {
463 return message.isRead() ? null : message;
464 }
465 }
466 }
467 return null;
468 }
469
470 public Message getLatestMessage() {
471 synchronized (this.messages) {
472 if (this.messages.size() == 0) {
473 Message message = new Message(this, "", Message.ENCRYPTION_NONE);
474 message.setType(Message.TYPE_STATUS);
475 message.setTime(Math.max(getCreated(), getLastClearHistory().getTimestamp()));
476 return message;
477 } else {
478 return this.messages.get(this.messages.size() - 1);
479 }
480 }
481 }
482
483 public String getName() {
484 if (getMode() == MODE_MULTI) {
485 if (getMucOptions().getSubject() != null) {
486 return getMucOptions().getSubject();
487 } else if (bookmark != null
488 && bookmark.getBookmarkName() != null
489 && !bookmark.getBookmarkName().trim().isEmpty()) {
490 return bookmark.getBookmarkName().trim();
491 } else {
492 String generatedName = getMucOptions().createNameFromParticipants();
493 if (generatedName != null) {
494 return generatedName;
495 } else {
496 return getJid().getUnescapedLocalpart();
497 }
498 }
499 } else if (isWithStranger()) {
500 return contactJid.toBareJid().toString();
501 } else {
502 return this.getContact().getDisplayName();
503 }
504 }
505
506 public String getAccountUuid() {
507 return this.accountUuid;
508 }
509
510 public Account getAccount() {
511 return this.account;
512 }
513
514 public Contact getContact() {
515 return this.account.getRoster().getContact(this.contactJid);
516 }
517
518 public void setAccount(final Account account) {
519 this.account = account;
520 }
521
522 @Override
523 public Jid getJid() {
524 return this.contactJid;
525 }
526
527 public int getStatus() {
528 return this.status;
529 }
530
531 public long getCreated() {
532 return this.created;
533 }
534
535 public ContentValues getContentValues() {
536 ContentValues values = new ContentValues();
537 values.put(UUID, uuid);
538 values.put(NAME, name);
539 values.put(CONTACT, contactUuid);
540 values.put(ACCOUNT, accountUuid);
541 values.put(CONTACTJID, contactJid.toPreppedString());
542 values.put(CREATED, created);
543 values.put(STATUS, status);
544 values.put(MODE, mode);
545 values.put(ATTRIBUTES, attributes.toString());
546 return values;
547 }
548
549 public static Conversation fromCursor(Cursor cursor) {
550 Jid jid;
551 try {
552 jid = Jid.fromString(cursor.getString(cursor.getColumnIndex(CONTACTJID)), true);
553 } catch (final InvalidJidException e) {
554 // Borked DB..
555 jid = null;
556 }
557 return new Conversation(cursor.getString(cursor.getColumnIndex(UUID)),
558 cursor.getString(cursor.getColumnIndex(NAME)),
559 cursor.getString(cursor.getColumnIndex(CONTACT)),
560 cursor.getString(cursor.getColumnIndex(ACCOUNT)),
561 jid,
562 cursor.getLong(cursor.getColumnIndex(CREATED)),
563 cursor.getInt(cursor.getColumnIndex(STATUS)),
564 cursor.getInt(cursor.getColumnIndex(MODE)),
565 cursor.getString(cursor.getColumnIndex(ATTRIBUTES)));
566 }
567
568 public void setStatus(int status) {
569 this.status = status;
570 }
571
572 public int getMode() {
573 return this.mode;
574 }
575
576 public void setMode(int mode) {
577 this.mode = mode;
578 }
579
580 public SessionImpl startOtrSession(String presence, boolean sendStart) {
581 if (this.otrSession != null) {
582 return this.otrSession;
583 } else {
584 final SessionID sessionId = new SessionID(this.getJid().toBareJid().toString(),
585 presence,
586 "xmpp");
587 this.otrSession = new SessionImpl(sessionId, getAccount().getOtrService());
588 try {
589 if (sendStart) {
590 this.otrSession.startSession();
591 return this.otrSession;
592 }
593 return this.otrSession;
594 } catch (OtrException e) {
595 return null;
596 }
597 }
598
599 }
600
601 public SessionImpl getOtrSession() {
602 return this.otrSession;
603 }
604
605 public void resetOtrSession() {
606 this.otrFingerprint = null;
607 this.otrSession = null;
608 this.mSmp.hint = null;
609 this.mSmp.secret = null;
610 this.mSmp.status = Smp.STATUS_NONE;
611 }
612
613 public Smp smp() {
614 return mSmp;
615 }
616
617 public boolean startOtrIfNeeded() {
618 if (this.otrSession != null && this.otrSession.getSessionStatus() != SessionStatus.ENCRYPTED) {
619 try {
620 this.otrSession.startSession();
621 return true;
622 } catch (OtrException e) {
623 this.resetOtrSession();
624 return false;
625 }
626 } else {
627 return true;
628 }
629 }
630
631 public boolean endOtrIfNeeded() {
632 if (this.otrSession != null) {
633 if (this.otrSession.getSessionStatus() == SessionStatus.ENCRYPTED) {
634 try {
635 this.otrSession.endSession();
636 this.resetOtrSession();
637 return true;
638 } catch (OtrException e) {
639 this.resetOtrSession();
640 return false;
641 }
642 } else {
643 this.resetOtrSession();
644 return false;
645 }
646 } else {
647 return false;
648 }
649 }
650
651 public boolean hasValidOtrSession() {
652 return this.otrSession != null;
653 }
654
655 public synchronized String getOtrFingerprint() {
656 if (this.otrFingerprint == null) {
657 try {
658 if (getOtrSession() == null || getOtrSession().getSessionStatus() != SessionStatus.ENCRYPTED) {
659 return null;
660 }
661 DSAPublicKey remotePubKey = (DSAPublicKey) getOtrSession().getRemotePublicKey();
662 this.otrFingerprint = getAccount().getOtrService().getFingerprint(remotePubKey).toLowerCase(Locale.US);
663 } catch (final OtrCryptoException | UnsupportedOperationException ignored) {
664 return null;
665 }
666 }
667 return this.otrFingerprint;
668 }
669
670 public boolean verifyOtrFingerprint() {
671 final String fingerprint = getOtrFingerprint();
672 if (fingerprint != null) {
673 getContact().addOtrFingerprint(fingerprint);
674 return true;
675 } else {
676 return false;
677 }
678 }
679
680 public boolean isOtrFingerprintVerified() {
681 return getContact().getOtrFingerprints().contains(getOtrFingerprint());
682 }
683
684 /**
685 * short for is Private and Non-anonymous
686 */
687 private boolean isPnNA() {
688 return mode == MODE_SINGLE || (getMucOptions().membersOnly() && getMucOptions().nonanonymous());
689 }
690
691 public synchronized MucOptions getMucOptions() {
692 if (this.mucOptions == null) {
693 this.mucOptions = new MucOptions(this);
694 }
695 return this.mucOptions;
696 }
697
698 public void resetMucOptions() {
699 this.mucOptions = null;
700 }
701
702 public void setContactJid(final Jid jid) {
703 this.contactJid = jid;
704 }
705
706 public void setNextCounterpart(Jid jid) {
707 this.nextCounterpart = jid;
708 }
709
710 public Jid getNextCounterpart() {
711 return this.nextCounterpart;
712 }
713
714 public int getNextEncryption() {
715 return fixAvailableEncryption(this.getIntAttribute(ATTRIBUTE_NEXT_ENCRYPTION, getDefaultEncryption()));
716 }
717
718 private int fixAvailableEncryption(int selectedEncryption) {
719 switch(selectedEncryption) {
720 case Message.ENCRYPTION_NONE:
721 return Config.supportUnencrypted() ? selectedEncryption : getDefaultEncryption();
722 case Message.ENCRYPTION_AXOLOTL:
723 return Config.supportOmemo() ? selectedEncryption : getDefaultEncryption();
724 case Message.ENCRYPTION_OTR:
725 return Config.supportOtr() ? selectedEncryption : getDefaultEncryption();
726 case Message.ENCRYPTION_PGP:
727 case Message.ENCRYPTION_DECRYPTED:
728 case Message.ENCRYPTION_DECRYPTION_FAILED:
729 return Config.supportOpenPgp() ? Message.ENCRYPTION_PGP : getDefaultEncryption();
730 default:
731 return getDefaultEncryption();
732 }
733 }
734
735 private int getDefaultEncryption() {
736 AxolotlService axolotlService = account.getAxolotlService();
737 if (Config.supportUnencrypted()) {
738 return Message.ENCRYPTION_NONE;
739 } else if (Config.supportOmemo()
740 && (axolotlService != null && axolotlService.isConversationAxolotlCapable(this) || !Config.multipleEncryptionChoices())) {
741 return Message.ENCRYPTION_AXOLOTL;
742 } else if (Config.supportOtr() && mode == MODE_SINGLE) {
743 return Message.ENCRYPTION_OTR;
744 } else if (Config.supportOpenPgp()) {
745 return Message.ENCRYPTION_PGP;
746 } else {
747 return Message.ENCRYPTION_NONE;
748 }
749 }
750
751 public void setNextEncryption(int encryption) {
752 this.setAttribute(ATTRIBUTE_NEXT_ENCRYPTION, String.valueOf(encryption));
753 }
754
755 public String getNextMessage() {
756 if (this.nextMessage == null) {
757 return "";
758 } else {
759 return this.nextMessage;
760 }
761 }
762
763 public boolean smpRequested() {
764 return smp().status == Smp.STATUS_CONTACT_REQUESTED;
765 }
766
767 public void setNextMessage(String message) {
768 this.nextMessage = message;
769 }
770
771 public void setSymmetricKey(byte[] key) {
772 this.symmetricKey = key;
773 }
774
775 public byte[] getSymmetricKey() {
776 return this.symmetricKey;
777 }
778
779 public void setBookmark(Bookmark bookmark) {
780 this.bookmark = bookmark;
781 this.bookmark.setConversation(this);
782 }
783
784 public void deregisterWithBookmark() {
785 if (this.bookmark != null) {
786 this.bookmark.setConversation(null);
787 }
788 this.bookmark = null;
789 }
790
791 public Bookmark getBookmark() {
792 return this.bookmark;
793 }
794
795 public boolean hasDuplicateMessage(Message message) {
796 synchronized (this.messages) {
797 for (int i = this.messages.size() - 1; i >= 0; --i) {
798 if (this.messages.get(i).similar(message)) {
799 return true;
800 }
801 }
802 }
803 return false;
804 }
805
806 public Message findSentMessageWithBody(String body) {
807 synchronized (this.messages) {
808 for (int i = this.messages.size() - 1; i >= 0; --i) {
809 Message message = this.messages.get(i);
810 if (message.getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_SEND) {
811 String otherBody;
812 if (message.hasFileOnRemoteHost()) {
813 otherBody = message.getFileParams().url.toString();
814 } else {
815 otherBody = message.body;
816 }
817 if (otherBody != null && otherBody.equals(body)) {
818 return message;
819 }
820 }
821 }
822 return null;
823 }
824 }
825
826 public MamReference getLastMessageTransmitted() {
827 final MamReference lastClear = getLastClearHistory();
828 MamReference lastReceived = new MamReference(0);
829 synchronized (this.messages) {
830 for(int i = this.messages.size() - 1; i >= 0; --i) {
831 Message message = this.messages.get(i);
832 if (message.getStatus() == Message.STATUS_RECEIVED || message.isCarbon()) {
833 lastReceived = new MamReference(message.getTimeSent(),message.getServerMsgId());
834 break;
835 }
836 }
837 }
838 return MamReference.max(lastClear,lastReceived);
839 }
840
841 public void setMutedTill(long value) {
842 this.setAttribute(ATTRIBUTE_MUTED_TILL, String.valueOf(value));
843 }
844
845 public boolean isMuted() {
846 return System.currentTimeMillis() < this.getLongAttribute(ATTRIBUTE_MUTED_TILL, 0);
847 }
848
849 public boolean alwaysNotify() {
850 return mode == MODE_SINGLE || getBooleanAttribute(ATTRIBUTE_ALWAYS_NOTIFY, Config.ALWAYS_NOTIFY_BY_DEFAULT || isPnNA());
851 }
852
853 public boolean setAttribute(String key, String value) {
854 synchronized (this.attributes) {
855 try {
856 this.attributes.put(key, value);
857 return true;
858 } catch (JSONException e) {
859 return false;
860 }
861 }
862 }
863
864 public boolean setAttribute(String key, List<Jid> jids) {
865 JSONArray array = new JSONArray();
866 for(Jid jid : jids) {
867 array.put(jid.toBareJid().toString());
868 }
869 synchronized (this.attributes) {
870 try {
871 this.attributes.put(key, array);
872 return true;
873 } catch (JSONException e) {
874 e.printStackTrace();
875 return false;
876 }
877 }
878 }
879
880 public String getAttribute(String key) {
881 synchronized (this.attributes) {
882 try {
883 return this.attributes.getString(key);
884 } catch (JSONException e) {
885 return null;
886 }
887 }
888 }
889
890 private List<Jid> getJidListAttribute(String key) {
891 ArrayList<Jid> list = new ArrayList<>();
892 synchronized (this.attributes) {
893 try {
894 JSONArray array = this.attributes.getJSONArray(key);
895 for (int i = 0; i < array.length(); ++i) {
896 try {
897 list.add(Jid.fromString(array.getString(i)));
898 } catch (InvalidJidException e) {
899 //ignored
900 }
901 }
902 } catch (JSONException e) {
903 //ignored
904 }
905 }
906 return list;
907 }
908
909 private int getIntAttribute(String key, int defaultValue) {
910 String value = this.getAttribute(key);
911 if (value == null) {
912 return defaultValue;
913 } else {
914 try {
915 return Integer.parseInt(value);
916 } catch (NumberFormatException e) {
917 return defaultValue;
918 }
919 }
920 }
921
922 public long getLongAttribute(String key, long defaultValue) {
923 String value = this.getAttribute(key);
924 if (value == null) {
925 return defaultValue;
926 } else {
927 try {
928 return Long.parseLong(value);
929 } catch (NumberFormatException e) {
930 return defaultValue;
931 }
932 }
933 }
934
935 private boolean getBooleanAttribute(String key, boolean defaultValue) {
936 String value = this.getAttribute(key);
937 if (value == null) {
938 return defaultValue;
939 } else {
940 return Boolean.parseBoolean(value);
941 }
942 }
943
944 public void add(Message message) {
945 synchronized (this.messages) {
946 this.messages.add(message);
947 }
948 }
949
950 public void prepend(Message message) {
951 synchronized (this.messages) {
952 this.messages.add(0,message);
953 }
954 }
955
956 public void addAll(int index, List<Message> messages) {
957 synchronized (this.messages) {
958 this.messages.addAll(index, messages);
959 }
960 account.getPgpDecryptionService().decrypt(messages);
961 }
962
963 public void expireOldMessages(long timestamp) {
964 synchronized (this.messages) {
965 for(ListIterator<Message> iterator = this.messages.listIterator(); iterator.hasNext();) {
966 if (iterator.next().getTimeSent() < timestamp) {
967 iterator.remove();
968 }
969 }
970 untieMessages();
971 }
972 }
973
974 public void sort() {
975 synchronized (this.messages) {
976 Collections.sort(this.messages, new Comparator<Message>() {
977 @Override
978 public int compare(Message left, Message right) {
979 if (left.getTimeSent() < right.getTimeSent()) {
980 return -1;
981 } else if (left.getTimeSent() > right.getTimeSent()) {
982 return 1;
983 } else {
984 return 0;
985 }
986 }
987 });
988 untieMessages();
989 }
990 }
991
992 private void untieMessages() {
993 for(Message message : this.messages) {
994 message.untie();
995 }
996 }
997
998 public int unreadCount() {
999 synchronized (this.messages) {
1000 int count = 0;
1001 for(int i = this.messages.size() - 1; i >= 0; --i) {
1002 if (this.messages.get(i).isRead()) {
1003 return count;
1004 }
1005 ++count;
1006 }
1007 return count;
1008 }
1009 }
1010
1011 public int receivedMessagesCount() {
1012 int count = 0;
1013 synchronized (this.messages) {
1014 for(Message message : messages) {
1015 if (message.getStatus() == Message.STATUS_RECEIVED) {
1016 ++count;
1017 }
1018 }
1019 }
1020 return count;
1021 }
1022
1023 private int sentMessagesCount() {
1024 int count = 0;
1025 synchronized (this.messages) {
1026 for(Message message : messages) {
1027 if (message.getStatus() != Message.STATUS_RECEIVED) {
1028 ++count;
1029 }
1030 }
1031 }
1032 return count;
1033 }
1034
1035 public boolean isWithStranger() {
1036 return mode == MODE_SINGLE
1037 && !getJid().equals(account.getJid().toDomainJid())
1038 && !getContact().showInRoster()
1039 && sentMessagesCount() == 0;
1040 }
1041
1042 public class Smp {
1043 public static final int STATUS_NONE = 0;
1044 public static final int STATUS_CONTACT_REQUESTED = 1;
1045 public static final int STATUS_WE_REQUESTED = 2;
1046 public static final int STATUS_FAILED = 3;
1047 public static final int STATUS_VERIFIED = 4;
1048
1049 public String secret = null;
1050 public String hint = null;
1051 public int status = 0;
1052 }
1053}