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