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