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