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