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