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