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