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