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