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