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 void clearMessages() {
210 synchronized (this.messages) {
211 this.messages.clear();
212 }
213 }
214
215 public boolean setIncomingChatState(ChatState state) {
216 if (this.mIncomingChatState == state) {
217 return false;
218 }
219 this.mIncomingChatState = state;
220 return true;
221 }
222
223 public ChatState getIncomingChatState() {
224 return this.mIncomingChatState;
225 }
226
227 public boolean setOutgoingChatState(ChatState state) {
228 if (mode == MODE_SINGLE && !getContact().isSelf() || (isPrivateAndNonAnonymous() && getNextCounterpart() == null)) {
229 if (this.mOutgoingChatState != state) {
230 this.mOutgoingChatState = state;
231 return true;
232 }
233 }
234 return false;
235 }
236
237 public ChatState getOutgoingChatState() {
238 return this.mOutgoingChatState;
239 }
240
241 public void trim() {
242 synchronized (this.messages) {
243 final int size = messages.size();
244 final int maxsize = Config.PAGE_SIZE * Config.MAX_NUM_PAGES;
245 if (size > maxsize) {
246 List<Message> discards = this.messages.subList(0, size - maxsize);
247 final PgpDecryptionService pgpDecryptionService = account.getPgpDecryptionService();
248 if (pgpDecryptionService != null) {
249 pgpDecryptionService.discard(discards);
250 }
251 discards.clear();
252 untieMessages();
253 }
254 }
255 }
256
257 public void findUnsentTextMessages(OnMessageFound onMessageFound) {
258 final ArrayList<Message> results = new ArrayList<>();
259 synchronized (this.messages) {
260 for (Message message : this.messages) {
261 if (message.getType() != Message.TYPE_IMAGE && message.getStatus() == Message.STATUS_UNSEND) {
262 results.add(message);
263 }
264 }
265 }
266 for(Message result : results) {
267 onMessageFound.onMessageFound(result);
268 }
269 }
270
271 public Message findSentMessageWithUuidOrRemoteId(String id) {
272 synchronized (this.messages) {
273 for (Message message : this.messages) {
274 if (id.equals(message.getUuid())
275 || (message.getStatus() >= Message.STATUS_SEND
276 && id.equals(message.getRemoteMsgId()))) {
277 return message;
278 }
279 }
280 }
281 return null;
282 }
283
284 public Message findMessageWithRemoteIdAndCounterpart(String id, Jid counterpart, boolean received, boolean carbon) {
285 synchronized (this.messages) {
286 for (int i = this.messages.size() - 1; i >= 0; --i) {
287 Message message = messages.get(i);
288 if (counterpart.equals(message.getCounterpart())
289 && ((message.getStatus() == Message.STATUS_RECEIVED) == received)
290 && (carbon == message.isCarbon() || received)) {
291 if (id.equals(message.getRemoteMsgId()) && !message.isFileOrImage() && !message.treatAsDownloadable()) {
292 return message;
293 } else {
294 return null;
295 }
296 }
297 }
298 }
299 return null;
300 }
301
302 public Message findSentMessageWithUuid(String id) {
303 synchronized (this.messages) {
304 for (Message message : this.messages) {
305 if (id.equals(message.getUuid())) {
306 return message;
307 }
308 }
309 }
310 return null;
311 }
312
313 public Message findMessageWithRemoteId(String id, Jid counterpart) {
314 synchronized (this.messages) {
315 for (Message message : this.messages) {
316 if (counterpart.equals(message.getCounterpart())
317 && (id.equals(message.getRemoteMsgId()) || id.equals(message.getUuid()))) {
318 return message;
319 }
320 }
321 }
322 return null;
323 }
324
325 public boolean hasMessageWithCounterpart(Jid counterpart) {
326 synchronized (this.messages) {
327 for (Message message : this.messages) {
328 if (counterpart.equals(message.getCounterpart())) {
329 return true;
330 }
331 }
332 }
333 return false;
334 }
335
336 public void populateWithMessages(final List<Message> messages) {
337 synchronized (this.messages) {
338 messages.clear();
339 messages.addAll(this.messages);
340 }
341 for (Iterator<Message> iterator = messages.iterator(); iterator.hasNext(); ) {
342 if (iterator.next().wasMergedIntoPrevious()) {
343 iterator.remove();
344 }
345 }
346 }
347
348 @Override
349 public boolean isBlocked() {
350 return getContact().isBlocked();
351 }
352
353 @Override
354 public boolean isDomainBlocked() {
355 return getContact().isDomainBlocked();
356 }
357
358 @Override
359 public Jid getBlockedJid() {
360 return getContact().getBlockedJid();
361 }
362
363 public int countMessages() {
364 synchronized (this.messages) {
365 return this.messages.size();
366 }
367 }
368
369 public String getFirstMamReference() {
370 return this.mFirstMamReference;
371 }
372
373 public void setFirstMamReference(String reference) {
374 this.mFirstMamReference = reference;
375 }
376
377 public void setLastClearHistory(long time, String reference) {
378 if (reference != null) {
379 setAttribute(ATTRIBUTE_LAST_CLEAR_HISTORY, String.valueOf(time) + ":" + reference);
380 } else {
381 setAttribute(ATTRIBUTE_LAST_CLEAR_HISTORY, time);
382 }
383 }
384
385 public MamReference getLastClearHistory() {
386 return MamReference.fromAttribute(getAttribute(ATTRIBUTE_LAST_CLEAR_HISTORY));
387 }
388
389 public List<Jid> getAcceptedCryptoTargets() {
390 if (mode == MODE_SINGLE) {
391 return Collections.singletonList(getJid().asBareJid());
392 } else {
393 return getJidListAttribute(ATTRIBUTE_CRYPTO_TARGETS);
394 }
395 }
396
397 public void setAcceptedCryptoTargets(List<Jid> acceptedTargets) {
398 setAttribute(ATTRIBUTE_CRYPTO_TARGETS, acceptedTargets);
399 }
400
401 public boolean setCorrectingMessage(Message correctingMessage) {
402 this.correctingMessage = correctingMessage;
403 return correctingMessage == null && draftMessage != null;
404 }
405
406 public Message getCorrectingMessage() {
407 return this.correctingMessage;
408 }
409
410 public boolean withSelf() {
411 return getContact().isSelf();
412 }
413
414 @Override
415 public int compareTo(@NonNull Conversation another) {
416 return Long.compare(another.getSortableTime(), getSortableTime());
417 }
418
419 private long getSortableTime() {
420 Draft draft = getDraft();
421 long messageTime = getLatestMessage().getTimeSent();
422 if (draft == null) {
423 return messageTime;
424 } else {
425 return Math.max(messageTime, draft.getTimestamp());
426 }
427 }
428
429 public String getDraftMessage() {
430 return draftMessage;
431 }
432
433 public void setDraftMessage(String draftMessage) {
434 this.draftMessage = draftMessage;
435 }
436
437 public boolean isRead() {
438 return (this.messages.size() == 0) || this.messages.get(this.messages.size() - 1).isRead();
439 }
440
441 public List<Message> markRead(String upToUuid) {
442 final List<Message> unread = new ArrayList<>();
443 synchronized (this.messages) {
444 for (Message message : this.messages) {
445 if (!message.isRead()) {
446 message.markRead();
447 unread.add(message);
448 }
449 if (message.getUuid().equals(upToUuid)) {
450 return unread;
451 }
452 }
453 }
454 return unread;
455 }
456
457 public static Message getLatestMarkableMessage(final List<Message> messages, boolean isPrivateAndNonAnonymousMuc) {
458 for (int i = messages.size() - 1; i >= 0; --i) {
459 final Message message = messages.get(i);
460 if (message.getStatus() <= Message.STATUS_RECEIVED
461 && (message.markable || isPrivateAndNonAnonymousMuc)
462 && message.getType() != Message.TYPE_PRIVATE) {
463 return message;
464 }
465 }
466 return null;
467 }
468
469 public Message getLatestMessage() {
470 synchronized (this.messages) {
471 if (this.messages.size() == 0) {
472 Message message = new Message(this, "", Message.ENCRYPTION_NONE);
473 message.setType(Message.TYPE_STATUS);
474 message.setTime(Math.max(getCreated(), getLastClearHistory().getTimestamp()));
475 return message;
476 } else {
477 return this.messages.get(this.messages.size() - 1);
478 }
479 }
480 }
481
482 public @NonNull CharSequence getName() {
483 if (getMode() == MODE_MULTI) {
484 final String roomName = getMucOptions().getName();
485 final String subject = getMucOptions().getSubject();
486 final Bookmark bookmark = getBookmark();
487 final String bookmarkName = bookmark != null ? bookmark.getBookmarkName() : null;
488 if (printableValue(roomName)) {
489 return roomName;
490 } else if (printableValue(subject)) {
491 return subject;
492 } else if (printableValue(bookmarkName, false)) {
493 return bookmarkName;
494 } else {
495 final String generatedName = getMucOptions().createNameFromParticipants();
496 if (printableValue(generatedName)) {
497 return generatedName;
498 } else {
499 return contactJid.getLocal() != null ? contactJid.getLocal() : contactJid;
500 }
501 }
502 } else if ((QuickConversationsService.isConversations() || !Config.QUICKSY_DOMAIN.equals(contactJid.getDomain())) && isWithStranger()) {
503 return contactJid;
504 } else {
505 return this.getContact().getDisplayName();
506 }
507 }
508
509 public String getAccountUuid() {
510 return this.accountUuid;
511 }
512
513 public Account getAccount() {
514 return this.account;
515 }
516
517 public void setAccount(final Account account) {
518 this.account = account;
519 }
520
521 public Contact getContact() {
522 return this.account.getRoster().getContact(this.contactJid);
523 }
524
525 @Override
526 public Jid getJid() {
527 return this.contactJid;
528 }
529
530 public int getStatus() {
531 return this.status;
532 }
533
534 public void setStatus(int status) {
535 this.status = status;
536 }
537
538 public long getCreated() {
539 return this.created;
540 }
541
542 public ContentValues getContentValues() {
543 ContentValues values = new ContentValues();
544 values.put(UUID, uuid);
545 values.put(NAME, name);
546 values.put(CONTACT, contactUuid);
547 values.put(ACCOUNT, accountUuid);
548 values.put(CONTACTJID, contactJid.toString());
549 values.put(CREATED, created);
550 values.put(STATUS, status);
551 values.put(MODE, mode);
552 values.put(ATTRIBUTES, attributes.toString());
553 return values;
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 /**
565 * short for is Private and Non-anonymous
566 */
567 public boolean isSingleOrPrivateAndNonAnonymous() {
568 return mode == MODE_SINGLE || isPrivateAndNonAnonymous();
569 }
570
571 public boolean isPrivateAndNonAnonymous() {
572 return getMucOptions().isPrivateAndNonAnonymous();
573 }
574
575 public synchronized MucOptions getMucOptions() {
576 if (this.mucOptions == null) {
577 this.mucOptions = new MucOptions(this);
578 }
579 return this.mucOptions;
580 }
581
582 public void resetMucOptions() {
583 this.mucOptions = null;
584 }
585
586 public void setContactJid(final Jid jid) {
587 this.contactJid = jid;
588 }
589
590 public Jid getNextCounterpart() {
591 return this.nextCounterpart;
592 }
593
594 public void setNextCounterpart(Jid jid) {
595 this.nextCounterpart = jid;
596 }
597
598 public int getNextEncryption() {
599 if (!Config.supportOmemo() && !Config.supportOpenPgp()) {
600 return Message.ENCRYPTION_NONE;
601 }
602 if (OmemoSetting.isAlways()) {
603 return suitableForOmemoByDefault(this) ? Message.ENCRYPTION_AXOLOTL : Message.ENCRYPTION_NONE;
604 }
605 final int defaultEncryption;
606 if (suitableForOmemoByDefault(this)) {
607 defaultEncryption = OmemoSetting.getEncryption();
608 } else {
609 defaultEncryption = Message.ENCRYPTION_NONE;
610 }
611 int encryption = this.getIntAttribute(ATTRIBUTE_NEXT_ENCRYPTION, defaultEncryption);
612 if (encryption == Message.ENCRYPTION_OTR || encryption < 0) {
613 return defaultEncryption;
614 } else {
615 return encryption;
616 }
617 }
618
619 private static boolean suitableForOmemoByDefault(final Conversation conversation) {
620 if (conversation.getJid().asBareJid().equals(Config.BUG_REPORTS)) {
621 return false;
622 }
623 if (conversation.getContact().isOwnServer()) {
624 return false;
625 }
626 final String contact = conversation.getJid().getDomain();
627 final String account = conversation.getAccount().getServer();
628 if (Config.OMEMO_EXCEPTIONS.CONTACT_DOMAINS.contains(contact) || Config.OMEMO_EXCEPTIONS.ACCOUNT_DOMAINS.contains(account)) {
629 return false;
630 }
631 final AxolotlService axolotlService = conversation.getAccount().getAxolotlService();
632 return axolotlService != null && axolotlService.isConversationAxolotlCapable(conversation);
633 }
634
635 public void setNextEncryption(int encryption) {
636 this.setAttribute(ATTRIBUTE_NEXT_ENCRYPTION, String.valueOf(encryption));
637 }
638
639 public String getNextMessage() {
640 final String nextMessage = getAttribute(ATTRIBUTE_NEXT_MESSAGE);
641 return nextMessage == null ? "" : nextMessage;
642 }
643
644 public @Nullable
645 Draft getDraft() {
646 long timestamp = getLongAttribute(ATTRIBUTE_NEXT_MESSAGE_TIMESTAMP, 0);
647 if (timestamp > getLatestMessage().getTimeSent()) {
648 String message = getAttribute(ATTRIBUTE_NEXT_MESSAGE);
649 if (!TextUtils.isEmpty(message) && timestamp != 0) {
650 return new Draft(message, timestamp);
651 }
652 }
653 return null;
654 }
655
656 public boolean setNextMessage(final String input) {
657 final String message = input == null || input.trim().isEmpty() ? null : input;
658 boolean changed = !getNextMessage().equals(message);
659 this.setAttribute(ATTRIBUTE_NEXT_MESSAGE, message);
660 if (changed) {
661 this.setAttribute(ATTRIBUTE_NEXT_MESSAGE_TIMESTAMP, message == null ? 0 : System.currentTimeMillis());
662 }
663 return changed;
664 }
665
666 public Bookmark getBookmark() {
667 return this.account.getBookmark(this.contactJid);
668 }
669
670 public Message findDuplicateMessage(Message message) {
671 synchronized (this.messages) {
672 for (int i = this.messages.size() - 1; i >= 0; --i) {
673 if (this.messages.get(i).similar(message)) {
674 return this.messages.get(i);
675 }
676 }
677 }
678 return null;
679 }
680
681 public boolean hasDuplicateMessage(Message message) {
682 return findDuplicateMessage(message) != null;
683 }
684
685 public Message findSentMessageWithBody(String body) {
686 synchronized (this.messages) {
687 for (int i = this.messages.size() - 1; i >= 0; --i) {
688 Message message = this.messages.get(i);
689 if (message.getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_SEND) {
690 String otherBody;
691 if (message.hasFileOnRemoteHost()) {
692 otherBody = message.getFileParams().url.toString();
693 } else {
694 otherBody = message.body;
695 }
696 if (otherBody != null && otherBody.equals(body)) {
697 return message;
698 }
699 }
700 }
701 return null;
702 }
703 }
704
705 public boolean possibleDuplicate(final String serverMsgId, final String remoteMsgId) {
706 if (serverMsgId == null || remoteMsgId == null) {
707 return false;
708 }
709 synchronized (this.messages) {
710 for(Message message : this.messages) {
711 if (serverMsgId.equals(message.getServerMsgId()) || remoteMsgId.equals(message.getRemoteMsgId())) {
712 return true;
713 }
714 }
715 }
716 return false;
717 }
718
719 public MamReference getLastMessageTransmitted() {
720 final MamReference lastClear = getLastClearHistory();
721 MamReference lastReceived = new MamReference(0);
722 synchronized (this.messages) {
723 for (int i = this.messages.size() - 1; i >= 0; --i) {
724 final Message message = this.messages.get(i);
725 if (message.getType() == Message.TYPE_PRIVATE) {
726 continue; //it's unsafe to use private messages as anchor. They could be coming from user archive
727 }
728 if (message.getStatus() == Message.STATUS_RECEIVED || message.isCarbon() || message.getServerMsgId() != null) {
729 lastReceived = new MamReference(message.getTimeSent(), message.getServerMsgId());
730 break;
731 }
732 }
733 }
734 return MamReference.max(lastClear, lastReceived);
735 }
736
737 public void setMutedTill(long value) {
738 this.setAttribute(ATTRIBUTE_MUTED_TILL, String.valueOf(value));
739 }
740
741 public boolean isMuted() {
742 return System.currentTimeMillis() < this.getLongAttribute(ATTRIBUTE_MUTED_TILL, 0);
743 }
744
745 public boolean alwaysNotify() {
746 return mode == MODE_SINGLE || getBooleanAttribute(ATTRIBUTE_ALWAYS_NOTIFY, Config.ALWAYS_NOTIFY_BY_DEFAULT || isPrivateAndNonAnonymous());
747 }
748
749 public boolean setAttribute(String key, boolean value) {
750 boolean prev = getBooleanAttribute(key,false);
751 setAttribute(key,Boolean.toString(value));
752 return prev != value;
753 }
754
755 private boolean setAttribute(String key, long value) {
756 return setAttribute(key, Long.toString(value));
757 }
758
759 public boolean setAttribute(String key, String value) {
760 synchronized (this.attributes) {
761 try {
762 if (value == null) {
763 if (this.attributes.has(key)) {
764 this.attributes.remove(key);
765 return true;
766 } else {
767 return false;
768 }
769 } else {
770 final String prev = this.attributes.optString(key, null);
771 this.attributes.put(key, value);
772 return !value.equals(prev);
773 }
774 } catch (JSONException e) {
775 throw new AssertionError(e);
776 }
777 }
778 }
779
780 public boolean setAttribute(String key, List<Jid> jids) {
781 JSONArray array = new JSONArray();
782 for (Jid jid : jids) {
783 array.put(jid.asBareJid().toString());
784 }
785 synchronized (this.attributes) {
786 try {
787 this.attributes.put(key, array);
788 return true;
789 } catch (JSONException e) {
790 return false;
791 }
792 }
793 }
794
795 public String getAttribute(String key) {
796 synchronized (this.attributes) {
797 return this.attributes.optString(key, null);
798 }
799 }
800
801 private List<Jid> getJidListAttribute(String key) {
802 ArrayList<Jid> list = new ArrayList<>();
803 synchronized (this.attributes) {
804 try {
805 JSONArray array = this.attributes.getJSONArray(key);
806 for (int i = 0; i < array.length(); ++i) {
807 try {
808 list.add(Jid.of(array.getString(i)));
809 } catch (IllegalArgumentException e) {
810 //ignored
811 }
812 }
813 } catch (JSONException e) {
814 //ignored
815 }
816 }
817 return list;
818 }
819
820 private int getIntAttribute(String key, int defaultValue) {
821 String value = this.getAttribute(key);
822 if (value == null) {
823 return defaultValue;
824 } else {
825 try {
826 return Integer.parseInt(value);
827 } catch (NumberFormatException e) {
828 return defaultValue;
829 }
830 }
831 }
832
833 public long getLongAttribute(String key, long defaultValue) {
834 String value = this.getAttribute(key);
835 if (value == null) {
836 return defaultValue;
837 } else {
838 try {
839 return Long.parseLong(value);
840 } catch (NumberFormatException e) {
841 return defaultValue;
842 }
843 }
844 }
845
846 public boolean getBooleanAttribute(String key, boolean defaultValue) {
847 String value = this.getAttribute(key);
848 if (value == null) {
849 return defaultValue;
850 } else {
851 return Boolean.parseBoolean(value);
852 }
853 }
854
855 public void add(Message message) {
856 synchronized (this.messages) {
857 this.messages.add(message);
858 }
859 }
860
861 public void prepend(int offset, Message message) {
862 synchronized (this.messages) {
863 this.messages.add(Math.min(offset, this.messages.size()), message);
864 }
865 }
866
867 public void addAll(int index, List<Message> messages) {
868 synchronized (this.messages) {
869 this.messages.addAll(index, messages);
870 }
871 account.getPgpDecryptionService().decrypt(messages);
872 }
873
874 public void expireOldMessages(long timestamp) {
875 synchronized (this.messages) {
876 for (ListIterator<Message> iterator = this.messages.listIterator(); iterator.hasNext(); ) {
877 if (iterator.next().getTimeSent() < timestamp) {
878 iterator.remove();
879 }
880 }
881 untieMessages();
882 }
883 }
884
885 public void sort() {
886 synchronized (this.messages) {
887 Collections.sort(this.messages, (left, right) -> {
888 if (left.getTimeSent() < right.getTimeSent()) {
889 return -1;
890 } else if (left.getTimeSent() > right.getTimeSent()) {
891 return 1;
892 } else {
893 return 0;
894 }
895 });
896 untieMessages();
897 }
898 }
899
900 private void untieMessages() {
901 for (Message message : this.messages) {
902 message.untie();
903 }
904 }
905
906 public int unreadCount() {
907 synchronized (this.messages) {
908 int count = 0;
909 for (int i = this.messages.size() - 1; i >= 0; --i) {
910 if (this.messages.get(i).isRead()) {
911 return count;
912 }
913 ++count;
914 }
915 return count;
916 }
917 }
918
919 public int receivedMessagesCount() {
920 int count = 0;
921 synchronized (this.messages) {
922 for (Message message : messages) {
923 if (message.getStatus() == Message.STATUS_RECEIVED) {
924 ++count;
925 }
926 }
927 }
928 return count;
929 }
930
931 public int sentMessagesCount() {
932 int count = 0;
933 synchronized (this.messages) {
934 for (Message message : messages) {
935 if (message.getStatus() != Message.STATUS_RECEIVED) {
936 ++count;
937 }
938 }
939 }
940 return count;
941 }
942
943 public boolean isWithStranger() {
944 final Contact contact = getContact();
945 return mode == MODE_SINGLE
946 && !contact.isOwnServer()
947 && !contact.showInContactList()
948 && !contact.isSelf()
949 && !Config.QUICKSY_DOMAIN.equals(contact.getJid().toEscapedString())
950 && sentMessagesCount() == 0;
951 }
952
953 public int getReceivedMessagesCountSinceUuid(String uuid) {
954 if (uuid == null) {
955 return 0;
956 }
957 int count = 0;
958 synchronized (this.messages) {
959 for (int i = messages.size() - 1; i >= 0; i--) {
960 final Message message = messages.get(i);
961 if (uuid.equals(message.getUuid())) {
962 return count;
963 }
964 if (message.getStatus() <= Message.STATUS_RECEIVED) {
965 ++count;
966 }
967 }
968 }
969 return 0;
970 }
971
972 public interface OnMessageFound {
973 void onMessageFound(final Message message);
974 }
975
976 public static class Draft {
977 private final String message;
978 private final long timestamp;
979
980 private Draft(String message, long timestamp) {
981 this.message = message;
982 this.timestamp = timestamp;
983 }
984
985 public long getTimestamp() {
986 return timestamp;
987 }
988
989 public String getMessage() {
990 return message;
991 }
992 }
993}