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