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