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