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