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