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 @NonNull
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}