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