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 findMessageWithUuidOrRemoteId(final String id) {
398 synchronized (this.messages) {
399 for (final Message message : this.messages) {
400 if (id.equals(message.getUuid()) || id.equals(message.getRemoteMsgId())) {
401 return message;
402 }
403 }
404 }
405 return null;
406 }
407
408 public Message findMessageWithRemoteIdAndCounterpart(String id, Jid counterpart, boolean received, boolean carbon) {
409 synchronized (this.messages) {
410 for (int i = this.messages.size() - 1; i >= 0; --i) {
411 final Message message = messages.get(i);
412 final Jid mcp = message.getCounterpart();
413 if (mcp == null) {
414 continue;
415 }
416 if (mcp.equals(counterpart) && ((message.getStatus() == Message.STATUS_RECEIVED) == received)
417 && (carbon == message.isCarbon() || received)) {
418 if (id.equals(message.getRemoteMsgId()) && !message.isFileOrImage() && !message.treatAsDownloadable()) {
419 return message;
420 } else {
421 return null;
422 }
423 }
424 }
425 }
426 return null;
427 }
428
429 public Message findSentMessageWithUuid(String id) {
430 synchronized (this.messages) {
431 for (Message message : this.messages) {
432 if (id.equals(message.getUuid())) {
433 return message;
434 }
435 }
436 }
437 return null;
438 }
439
440 public Message findMessageWithRemoteId(String id, Jid counterpart) {
441 synchronized (this.messages) {
442 for (Message message : this.messages) {
443 if (counterpart.equals(message.getCounterpart())
444 && (id.equals(message.getRemoteMsgId()) || id.equals(message.getUuid()))) {
445 return message;
446 }
447 }
448 }
449 return null;
450 }
451
452 public Message findReceivedWithRemoteId(final String id) {
453 synchronized (this.messages) {
454 for (final Message message : this.messages) {
455 if (message.getStatus() == Message.STATUS_RECEIVED && id.equals(message.getRemoteMsgId())) {
456 return message;
457 }
458 }
459 }
460 return null;
461 }
462
463 public Message findMessageWithServerMsgId(String id) {
464 synchronized (this.messages) {
465 for (Message message : this.messages) {
466 if (id != null && id.equals(message.getServerMsgId())) {
467 return message;
468 }
469 }
470 }
471 return null;
472 }
473
474 public boolean hasMessageWithCounterpart(Jid counterpart) {
475 synchronized (this.messages) {
476 for (Message message : this.messages) {
477 if (counterpart.equals(message.getCounterpart())) {
478 return true;
479 }
480 }
481 }
482 return false;
483 }
484
485 public void populateWithMessages(final List<Message> messages) {
486 synchronized (this.messages) {
487 messages.clear();
488 messages.addAll(this.messages);
489 }
490 for (Iterator<Message> iterator = messages.iterator(); iterator.hasNext(); ) {
491 if (iterator.next().wasMergedIntoPrevious()) {
492 iterator.remove();
493 }
494 }
495 }
496
497 @Override
498 public boolean isBlocked() {
499 return getContact().isBlocked();
500 }
501
502 @Override
503 public boolean isDomainBlocked() {
504 return getContact().isDomainBlocked();
505 }
506
507 @Override
508 public Jid getBlockedJid() {
509 return getContact().getBlockedJid();
510 }
511
512 public int countMessages() {
513 synchronized (this.messages) {
514 return this.messages.size();
515 }
516 }
517
518 public String getFirstMamReference() {
519 return this.mFirstMamReference;
520 }
521
522 public void setFirstMamReference(String reference) {
523 this.mFirstMamReference = reference;
524 }
525
526 public void setLastClearHistory(long time, String reference) {
527 if (reference != null) {
528 setAttribute(ATTRIBUTE_LAST_CLEAR_HISTORY, time + ":" + reference);
529 } else {
530 setAttribute(ATTRIBUTE_LAST_CLEAR_HISTORY, time);
531 }
532 }
533
534 public MamReference getLastClearHistory() {
535 return MamReference.fromAttribute(getAttribute(ATTRIBUTE_LAST_CLEAR_HISTORY));
536 }
537
538 public List<Jid> getAcceptedCryptoTargets() {
539 if (mode == MODE_SINGLE) {
540 return Collections.singletonList(getJid().asBareJid());
541 } else {
542 return getJidListAttribute(ATTRIBUTE_CRYPTO_TARGETS);
543 }
544 }
545
546 public void setAcceptedCryptoTargets(List<Jid> acceptedTargets) {
547 setAttribute(ATTRIBUTE_CRYPTO_TARGETS, acceptedTargets);
548 }
549
550 public boolean setCorrectingMessage(Message correctingMessage) {
551 setAttribute(ATTRIBUTE_CORRECTING_MESSAGE, correctingMessage == null ? null : correctingMessage.getUuid());
552 return correctingMessage == null && draftMessage != null;
553 }
554
555 public Message getCorrectingMessage() {
556 final String uuid = getAttribute(ATTRIBUTE_CORRECTING_MESSAGE);
557 return uuid == null ? null : findSentMessageWithUuid(uuid);
558 }
559
560 public boolean withSelf() {
561 return getContact().isSelf();
562 }
563
564 @Override
565 public int compareTo(@NonNull Conversation another) {
566 return ComparisonChain.start()
567 .compareFalseFirst(another.getBooleanAttribute(ATTRIBUTE_PINNED_ON_TOP, false), getBooleanAttribute(ATTRIBUTE_PINNED_ON_TOP, false))
568 .compare(another.getSortableTime(), getSortableTime())
569 .result();
570 }
571
572 private long getSortableTime() {
573 Draft draft = getDraft();
574 long messageTime = getLatestMessage().getTimeSent();
575 if (draft == null) {
576 return messageTime;
577 } else {
578 return Math.max(messageTime, draft.getTimestamp());
579 }
580 }
581
582 public String getDraftMessage() {
583 return draftMessage;
584 }
585
586 public void setDraftMessage(String draftMessage) {
587 this.draftMessage = draftMessage;
588 }
589
590 public boolean isRead() {
591 synchronized (this.messages) {
592 for(final Message message : Lists.reverse(this.messages)) {
593 if (message.isRead() && message.getType() == Message.TYPE_RTP_SESSION) {
594 continue;
595 }
596 return message.isRead();
597 }
598 return true;
599 }
600 }
601
602 public List<Message> markRead(final String upToUuid) {
603 final ImmutableList.Builder<Message> unread = new ImmutableList.Builder<>();
604 synchronized (this.messages) {
605 for (final Message message : this.messages) {
606 if (!message.isRead()) {
607 message.markRead();
608 unread.add(message);
609 }
610 if (message.getUuid().equals(upToUuid)) {
611 return unread.build();
612 }
613 }
614 }
615 return unread.build();
616 }
617
618 public Message getLatestMessage() {
619 synchronized (this.messages) {
620 if (this.messages.size() == 0) {
621 Message message = new Message(this, "", Message.ENCRYPTION_NONE);
622 message.setType(Message.TYPE_STATUS);
623 message.setTime(Math.max(getCreated(), getLastClearHistory().getTimestamp()));
624 return message;
625 } else {
626 return this.messages.get(this.messages.size() - 1);
627 }
628 }
629 }
630
631 public @NonNull
632 CharSequence getName() {
633 if (getMode() == MODE_MULTI) {
634 final String roomName = getMucOptions().getName();
635 final String subject = getMucOptions().getSubject();
636 final Bookmark bookmark = getBookmark();
637 final String bookmarkName = bookmark != null ? bookmark.getBookmarkName() : null;
638 if (printableValue(roomName)) {
639 return roomName;
640 } else if (printableValue(subject)) {
641 return subject;
642 } else if (printableValue(bookmarkName, false)) {
643 return bookmarkName;
644 } else {
645 final String generatedName = getMucOptions().createNameFromParticipants();
646 if (printableValue(generatedName)) {
647 return generatedName;
648 } else {
649 return contactJid.getLocal() != null ? contactJid.getLocal() : contactJid;
650 }
651 }
652 } else if ((QuickConversationsService.isConversations() || !Config.QUICKSY_DOMAIN.equals(contactJid.getDomain())) && isWithStranger()) {
653 return contactJid;
654 } else {
655 return this.getContact().getDisplayName();
656 }
657 }
658
659 public String getAccountUuid() {
660 return this.accountUuid;
661 }
662
663 public Account getAccount() {
664 return this.account;
665 }
666
667 public void setAccount(final Account account) {
668 this.account = account;
669 }
670
671 public Contact getContact() {
672 return this.account.getRoster().getContact(this.contactJid);
673 }
674
675 @Override
676 public Jid getJid() {
677 return this.contactJid;
678 }
679
680 public int getStatus() {
681 return this.status;
682 }
683
684 public void setStatus(int status) {
685 this.status = status;
686 }
687
688 public long getCreated() {
689 return this.created;
690 }
691
692 public ContentValues getContentValues() {
693 ContentValues values = new ContentValues();
694 values.put(UUID, uuid);
695 values.put(NAME, name);
696 values.put(CONTACT, contactUuid);
697 values.put(ACCOUNT, accountUuid);
698 values.put(CONTACTJID, contactJid.toString());
699 values.put(CREATED, created);
700 values.put(STATUS, status);
701 values.put(MODE, mode);
702 synchronized (this.attributes) {
703 values.put(ATTRIBUTES, attributes.toString());
704 }
705 return values;
706 }
707
708 public int getMode() {
709 return this.mode;
710 }
711
712 public void setMode(int mode) {
713 this.mode = mode;
714 }
715
716 /**
717 * short for is Private and Non-anonymous
718 */
719 public boolean isSingleOrPrivateAndNonAnonymous() {
720 return mode == MODE_SINGLE || isPrivateAndNonAnonymous();
721 }
722
723 public boolean isPrivateAndNonAnonymous() {
724 return getMucOptions().isPrivateAndNonAnonymous();
725 }
726
727 public synchronized MucOptions getMucOptions() {
728 if (this.mucOptions == null) {
729 this.mucOptions = new MucOptions(this);
730 }
731 return this.mucOptions;
732 }
733
734 public void resetMucOptions() {
735 this.mucOptions = null;
736 }
737
738 public void setContactJid(final Jid jid) {
739 this.contactJid = jid;
740 }
741
742 public Jid getNextCounterpart() {
743 return this.nextCounterpart;
744 }
745
746 public void setNextCounterpart(Jid jid) {
747 this.nextCounterpart = jid;
748 }
749
750 public int getNextEncryption() {
751 if (!Config.supportOmemo() && !Config.supportOpenPgp()) {
752 return Message.ENCRYPTION_NONE;
753 }
754 if (OmemoSetting.isAlways()) {
755 return suitableForOmemoByDefault(this) ? Message.ENCRYPTION_AXOLOTL : Message.ENCRYPTION_NONE;
756 }
757 final int defaultEncryption;
758 if (suitableForOmemoByDefault(this)) {
759 defaultEncryption = OmemoSetting.getEncryption();
760 } else {
761 defaultEncryption = Message.ENCRYPTION_NONE;
762 }
763 int encryption = this.getIntAttribute(ATTRIBUTE_NEXT_ENCRYPTION, defaultEncryption);
764 if (encryption == Message.ENCRYPTION_OTR || encryption < 0) {
765 return defaultEncryption;
766 } else {
767 return encryption;
768 }
769 }
770
771 public boolean setNextEncryption(int encryption) {
772 return this.setAttribute(ATTRIBUTE_NEXT_ENCRYPTION, encryption);
773 }
774
775 public String getNextMessage() {
776 final String nextMessage = getAttribute(ATTRIBUTE_NEXT_MESSAGE);
777 return nextMessage == null ? "" : nextMessage;
778 }
779
780 public @Nullable
781 Draft getDraft() {
782 long timestamp = getLongAttribute(ATTRIBUTE_NEXT_MESSAGE_TIMESTAMP, 0);
783 if (timestamp > getLatestMessage().getTimeSent()) {
784 String message = getAttribute(ATTRIBUTE_NEXT_MESSAGE);
785 if (!TextUtils.isEmpty(message) && timestamp != 0) {
786 return new Draft(message, timestamp);
787 }
788 }
789 return null;
790 }
791
792 public boolean setNextMessage(final String input) {
793 final String message = input == null || input.trim().isEmpty() ? null : input;
794 boolean changed = !getNextMessage().equals(message);
795 this.setAttribute(ATTRIBUTE_NEXT_MESSAGE, message);
796 if (changed) {
797 this.setAttribute(ATTRIBUTE_NEXT_MESSAGE_TIMESTAMP, message == null ? 0 : System.currentTimeMillis());
798 }
799 return changed;
800 }
801
802 public Bookmark getBookmark() {
803 return this.account.getBookmark(this.contactJid);
804 }
805
806 public Message findDuplicateMessage(Message message) {
807 synchronized (this.messages) {
808 for (int i = this.messages.size() - 1; i >= 0; --i) {
809 if (this.messages.get(i).similar(message)) {
810 return this.messages.get(i);
811 }
812 }
813 }
814 return null;
815 }
816
817 public boolean hasDuplicateMessage(Message message) {
818 return findDuplicateMessage(message) != null;
819 }
820
821 public Message findSentMessageWithBody(String body) {
822 synchronized (this.messages) {
823 for (int i = this.messages.size() - 1; i >= 0; --i) {
824 Message message = this.messages.get(i);
825 if (message.getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_SEND) {
826 String otherBody;
827 if (message.hasFileOnRemoteHost()) {
828 otherBody = message.getFileParams().url;
829 } else {
830 otherBody = message.body;
831 }
832 if (otherBody != null && otherBody.equals(body)) {
833 return message;
834 }
835 }
836 }
837 return null;
838 }
839 }
840
841 public Message findRtpSession(final String sessionId, final int s) {
842 synchronized (this.messages) {
843 for (int i = this.messages.size() - 1; i >= 0; --i) {
844 final Message message = this.messages.get(i);
845 if ((message.getStatus() == s) && (message.getType() == Message.TYPE_RTP_SESSION) && sessionId.equals(message.getRemoteMsgId())) {
846 return message;
847 }
848 }
849 }
850 return null;
851 }
852
853 public boolean possibleDuplicate(final String serverMsgId, final String remoteMsgId) {
854 if (serverMsgId == null || remoteMsgId == null) {
855 return false;
856 }
857 synchronized (this.messages) {
858 for (Message message : this.messages) {
859 if (serverMsgId.equals(message.getServerMsgId()) || remoteMsgId.equals(message.getRemoteMsgId())) {
860 return true;
861 }
862 }
863 }
864 return false;
865 }
866
867 public MamReference getLastMessageTransmitted() {
868 final MamReference lastClear = getLastClearHistory();
869 MamReference lastReceived = new MamReference(0);
870 synchronized (this.messages) {
871 for (int i = this.messages.size() - 1; i >= 0; --i) {
872 final Message message = this.messages.get(i);
873 if (message.isPrivateMessage()) {
874 continue; //it's unsafe to use private messages as anchor. They could be coming from user archive
875 }
876 if (message.getStatus() == Message.STATUS_RECEIVED || message.isCarbon() || message.getServerMsgId() != null) {
877 lastReceived = new MamReference(message.getTimeSent(), message.getServerMsgId());
878 break;
879 }
880 }
881 }
882 return MamReference.max(lastClear, lastReceived);
883 }
884
885 public void setMutedTill(long value) {
886 this.setAttribute(ATTRIBUTE_MUTED_TILL, String.valueOf(value));
887 }
888
889 public boolean isMuted() {
890 return System.currentTimeMillis() < this.getLongAttribute(ATTRIBUTE_MUTED_TILL, 0);
891 }
892
893 public boolean alwaysNotify() {
894 return mode == MODE_SINGLE || getBooleanAttribute(ATTRIBUTE_ALWAYS_NOTIFY, Config.ALWAYS_NOTIFY_BY_DEFAULT || isPrivateAndNonAnonymous());
895 }
896
897 public boolean setAttribute(String key, boolean value) {
898 return setAttribute(key, String.valueOf(value));
899 }
900
901 private boolean setAttribute(String key, long value) {
902 return setAttribute(key, Long.toString(value));
903 }
904
905 private boolean setAttribute(String key, int value) {
906 return setAttribute(key, String.valueOf(value));
907 }
908
909 public boolean setAttribute(String key, String value) {
910 synchronized (this.attributes) {
911 try {
912 if (value == null) {
913 if (this.attributes.has(key)) {
914 this.attributes.remove(key);
915 return true;
916 } else {
917 return false;
918 }
919 } else {
920 final String prev = this.attributes.optString(key, null);
921 this.attributes.put(key, value);
922 return !value.equals(prev);
923 }
924 } catch (JSONException e) {
925 throw new AssertionError(e);
926 }
927 }
928 }
929
930 public boolean setAttribute(String key, List<Jid> jids) {
931 JSONArray array = new JSONArray();
932 for (Jid jid : jids) {
933 array.put(jid.asBareJid().toString());
934 }
935 synchronized (this.attributes) {
936 try {
937 this.attributes.put(key, array);
938 return true;
939 } catch (JSONException e) {
940 return false;
941 }
942 }
943 }
944
945 public String getAttribute(String key) {
946 synchronized (this.attributes) {
947 return this.attributes.optString(key, null);
948 }
949 }
950
951 private List<Jid> getJidListAttribute(String key) {
952 ArrayList<Jid> list = new ArrayList<>();
953 synchronized (this.attributes) {
954 try {
955 JSONArray array = this.attributes.getJSONArray(key);
956 for (int i = 0; i < array.length(); ++i) {
957 try {
958 list.add(Jid.of(array.getString(i)));
959 } catch (IllegalArgumentException e) {
960 //ignored
961 }
962 }
963 } catch (JSONException e) {
964 //ignored
965 }
966 }
967 return list;
968 }
969
970 private int getIntAttribute(String key, int defaultValue) {
971 String value = this.getAttribute(key);
972 if (value == null) {
973 return defaultValue;
974 } else {
975 try {
976 return Integer.parseInt(value);
977 } catch (NumberFormatException e) {
978 return defaultValue;
979 }
980 }
981 }
982
983 public long getLongAttribute(String key, long defaultValue) {
984 String value = this.getAttribute(key);
985 if (value == null) {
986 return defaultValue;
987 } else {
988 try {
989 return Long.parseLong(value);
990 } catch (NumberFormatException e) {
991 return defaultValue;
992 }
993 }
994 }
995
996 public boolean getBooleanAttribute(String key, boolean defaultValue) {
997 String value = this.getAttribute(key);
998 if (value == null) {
999 return defaultValue;
1000 } else {
1001 return Boolean.parseBoolean(value);
1002 }
1003 }
1004
1005 public void add(Message message) {
1006 synchronized (this.messages) {
1007 this.messages.add(message);
1008 }
1009 }
1010
1011 public void prepend(int offset, Message message) {
1012 synchronized (this.messages) {
1013 this.messages.add(Math.min(offset, this.messages.size()), message);
1014 }
1015 }
1016
1017 public void addAll(int index, List<Message> messages) {
1018 synchronized (this.messages) {
1019 this.messages.addAll(index, messages);
1020 }
1021 account.getPgpDecryptionService().decrypt(messages);
1022 }
1023
1024 public void expireOldMessages(long timestamp) {
1025 synchronized (this.messages) {
1026 for (ListIterator<Message> iterator = this.messages.listIterator(); iterator.hasNext(); ) {
1027 if (iterator.next().getTimeSent() < timestamp) {
1028 iterator.remove();
1029 }
1030 }
1031 untieMessages();
1032 }
1033 }
1034
1035 public void sort() {
1036 synchronized (this.messages) {
1037 Collections.sort(this.messages, (left, right) -> {
1038 if (left.getTimeSent() < right.getTimeSent()) {
1039 return -1;
1040 } else if (left.getTimeSent() > right.getTimeSent()) {
1041 return 1;
1042 } else {
1043 return 0;
1044 }
1045 });
1046 untieMessages();
1047 }
1048 }
1049
1050 private void untieMessages() {
1051 for (Message message : this.messages) {
1052 message.untie();
1053 }
1054 }
1055
1056 public int unreadCount() {
1057 synchronized (this.messages) {
1058 int count = 0;
1059 for(final Message message : Lists.reverse(this.messages)) {
1060 if (message.isRead()) {
1061 if (message.getType() == Message.TYPE_RTP_SESSION) {
1062 continue;
1063 }
1064 return count;
1065 }
1066 ++count;
1067 }
1068 return count;
1069 }
1070 }
1071
1072 public int receivedMessagesCount() {
1073 int count = 0;
1074 synchronized (this.messages) {
1075 for (Message message : messages) {
1076 if (message.getStatus() == Message.STATUS_RECEIVED) {
1077 ++count;
1078 }
1079 }
1080 }
1081 return count;
1082 }
1083
1084 public int sentMessagesCount() {
1085 int count = 0;
1086 synchronized (this.messages) {
1087 for (Message message : messages) {
1088 if (message.getStatus() != Message.STATUS_RECEIVED) {
1089 ++count;
1090 }
1091 }
1092 }
1093 return count;
1094 }
1095
1096 public boolean isWithStranger() {
1097 final Contact contact = getContact();
1098 return mode == MODE_SINGLE
1099 && !contact.isOwnServer()
1100 && !contact.showInContactList()
1101 && !contact.isSelf()
1102 && !(contact.getJid().isDomainJid() && JidHelper.isQuicksyDomain(contact.getJid()))
1103 && sentMessagesCount() == 0;
1104 }
1105
1106 public int getReceivedMessagesCountSinceUuid(String uuid) {
1107 if (uuid == null) {
1108 return 0;
1109 }
1110 int count = 0;
1111 synchronized (this.messages) {
1112 for (int i = messages.size() - 1; i >= 0; i--) {
1113 final Message message = messages.get(i);
1114 if (uuid.equals(message.getUuid())) {
1115 return count;
1116 }
1117 if (message.getStatus() <= Message.STATUS_RECEIVED) {
1118 ++count;
1119 }
1120 }
1121 }
1122 return 0;
1123 }
1124
1125 @Override
1126 public int getAvatarBackgroundColor() {
1127 return UIHelper.getColorForName(getName().toString());
1128 }
1129
1130 @Override
1131 public String getAvatarName() {
1132 return getName().toString();
1133 }
1134
1135 public void setDisplayState(final String stanzaId) {
1136 this.displayState = stanzaId;
1137 }
1138
1139 public String getDisplayState() {
1140 return this.displayState;
1141 }
1142
1143 public interface OnMessageFound {
1144 void onMessageFound(final Message message);
1145 }
1146
1147 public static class Draft {
1148 private final String message;
1149 private final long timestamp;
1150
1151 private Draft(String message, long timestamp) {
1152 this.message = message;
1153 this.timestamp = timestamp;
1154 }
1155
1156 public long getTimestamp() {
1157 return timestamp;
1158 }
1159
1160 public String getMessage() {
1161 return message;
1162 }
1163 }
1164}