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