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