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