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