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>, Conversational {
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 String NAME = "name";
40 public static final String ACCOUNT = "accountUuid";
41 public static final String CONTACT = "contactUuid";
42 public static final String CONTACTJID = "contactJid";
43 public static final String STATUS = "status";
44 public static final String CREATED = "created";
45 public static final String MODE = "mode";
46 public static final String ATTRIBUTES = "attributes";
47
48 public static final String ATTRIBUTE_MUTED_TILL = "muted_till";
49 public static final String ATTRIBUTE_ALWAYS_NOTIFY = "always_notify";
50 public static final String ATTRIBUTE_LAST_CLEAR_HISTORY = "last_clear_history";
51 static final String ATTRIBUTE_MUC_PASSWORD = "muc_password";
52 private static final String ATTRIBUTE_NEXT_MESSAGE = "next_message";
53 private static final String ATTRIBUTE_NEXT_MESSAGE_TIMESTAMP = "next_message_timestamp";
54 private static final String ATTRIBUTE_CRYPTO_TARGETS = "crypto_targets";
55 private static final String ATTRIBUTE_NEXT_ENCRYPTION = "next_encryption";
56 public static final String ATTRIBUTE_ALLOW_PM = "allow_pm";
57 public static final String ATTRIBUTE_MEMBERS_ONLY = "members_only";
58 public static final String ATTRIBUTE_MODERATED = "moderated";
59 public static final String ATTRIBUTE_NON_ANONYMOUS = "non_anonymous";
60 protected final ArrayList<Message> messages = new ArrayList<>();
61 public AtomicBoolean messagesLoaded = new AtomicBoolean(true);
62 protected Account account = null;
63 private String draftMessage;
64 private String name;
65 private String contactUuid;
66 private String accountUuid;
67 private Jid contactJid;
68 private int status;
69 private long created;
70 private int mode;
71 private JSONObject attributes = new JSONObject();
72 private Jid nextCounterpart;
73 private transient MucOptions mucOptions = null;
74 private boolean messagesLeftOnServer = true;
75 private ChatState mOutgoingChatState = Config.DEFAULT_CHATSTATE;
76 private ChatState mIncomingChatState = Config.DEFAULT_CHATSTATE;
77 private String mFirstMamReference = null;
78 private Message correctingMessage;
79
80 public Conversation(final String name, final Account account, final Jid contactJid,
81 final int mode) {
82 this(java.util.UUID.randomUUID().toString(), name, null, account
83 .getUuid(), contactJid, System.currentTimeMillis(),
84 STATUS_AVAILABLE, mode, "");
85 this.account = account;
86 }
87
88 public Conversation(final String uuid, final String name, final String contactUuid,
89 final String accountUuid, final Jid contactJid, final long created, final int status,
90 final int mode, final String attributes) {
91 this.uuid = uuid;
92 this.name = name;
93 this.contactUuid = contactUuid;
94 this.accountUuid = accountUuid;
95 this.contactJid = contactJid;
96 this.created = created;
97 this.status = status;
98 this.mode = mode;
99 try {
100 this.attributes = new JSONObject(attributes == null ? "" : attributes);
101 } catch (JSONException e) {
102 this.attributes = new JSONObject();
103 }
104 }
105
106 public static Conversation fromCursor(Cursor cursor) {
107 Jid jid;
108 try {
109 jid = Jid.of(cursor.getString(cursor.getColumnIndex(CONTACTJID)));
110 } catch (final IllegalArgumentException e) {
111 // Borked DB..
112 jid = null;
113 }
114 return new Conversation(cursor.getString(cursor.getColumnIndex(UUID)),
115 cursor.getString(cursor.getColumnIndex(NAME)),
116 cursor.getString(cursor.getColumnIndex(CONTACT)),
117 cursor.getString(cursor.getColumnIndex(ACCOUNT)),
118 jid,
119 cursor.getLong(cursor.getColumnIndex(CREATED)),
120 cursor.getInt(cursor.getColumnIndex(STATUS)),
121 cursor.getInt(cursor.getColumnIndex(MODE)),
122 cursor.getString(cursor.getColumnIndex(ATTRIBUTES)));
123 }
124
125 public boolean hasMessagesLeftOnServer() {
126 return messagesLeftOnServer;
127 }
128
129 public void setHasMessagesLeftOnServer(boolean value) {
130 this.messagesLeftOnServer = value;
131 }
132
133 public Message getFirstUnreadMessage() {
134 Message first = null;
135 synchronized (this.messages) {
136 for (int i = messages.size() - 1; i >= 0; --i) {
137 if (messages.get(i).isRead()) {
138 return first;
139 } else {
140 first = messages.get(i);
141 }
142 }
143 }
144 return first;
145 }
146
147 public Message findUnsentMessageWithUuid(String uuid) {
148 synchronized (this.messages) {
149 for (final Message message : this.messages) {
150 final int s = message.getStatus();
151 if ((s == Message.STATUS_UNSEND || s == Message.STATUS_WAITING) && message.getUuid().equals(uuid)) {
152 return message;
153 }
154 }
155 }
156 return null;
157 }
158
159 public void findWaitingMessages(OnMessageFound onMessageFound) {
160 synchronized (this.messages) {
161 for (Message message : this.messages) {
162 if (message.getStatus() == Message.STATUS_WAITING) {
163 onMessageFound.onMessageFound(message);
164 }
165 }
166 }
167 }
168
169 public void findUnreadMessages(OnMessageFound onMessageFound) {
170 synchronized (this.messages) {
171 for (Message message : this.messages) {
172 if (!message.isRead()) {
173 onMessageFound.onMessageFound(message);
174 }
175 }
176 }
177 }
178
179 public void findMessagesWithFiles(final OnMessageFound onMessageFound) {
180 synchronized (this.messages) {
181 for (final Message message : this.messages) {
182 if ((message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE)
183 && message.getEncryption() != Message.ENCRYPTION_PGP) {
184 onMessageFound.onMessageFound(message);
185 }
186 }
187 }
188 }
189
190 public Message findMessageWithFileAndUuid(final String uuid) {
191 synchronized (this.messages) {
192 for (final Message message : this.messages) {
193 if (message.getUuid().equals(uuid)
194 && message.getEncryption() != Message.ENCRYPTION_PGP
195 && (message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE || message.treatAsDownloadable())) {
196 return message;
197 }
198 }
199 }
200 return null;
201 }
202
203 public void clearMessages() {
204 synchronized (this.messages) {
205 this.messages.clear();
206 }
207 }
208
209 public boolean setIncomingChatState(ChatState state) {
210 if (this.mIncomingChatState == state) {
211 return false;
212 }
213 this.mIncomingChatState = state;
214 return true;
215 }
216
217 public ChatState getIncomingChatState() {
218 return this.mIncomingChatState;
219 }
220
221 public boolean setOutgoingChatState(ChatState state) {
222 if (mode == MODE_SINGLE && !getContact().isSelf() || (isPrivateAndNonAnonymous() && getNextCounterpart() == null)) {
223 if (this.mOutgoingChatState != state) {
224 this.mOutgoingChatState = state;
225 return true;
226 }
227 }
228 return false;
229 }
230
231 public ChatState getOutgoingChatState() {
232 return this.mOutgoingChatState;
233 }
234
235 public void trim() {
236 synchronized (this.messages) {
237 final int size = messages.size();
238 final int maxsize = Config.PAGE_SIZE * Config.MAX_NUM_PAGES;
239 if (size > maxsize) {
240 List<Message> discards = this.messages.subList(0, size - maxsize);
241 final PgpDecryptionService pgpDecryptionService = account.getPgpDecryptionService();
242 if (pgpDecryptionService != null) {
243 pgpDecryptionService.discard(discards);
244 }
245 discards.clear();
246 untieMessages();
247 }
248 }
249 }
250
251 public void findUnsentMessagesWithEncryption(int encryptionType, OnMessageFound onMessageFound) {
252 synchronized (this.messages) {
253 for (Message message : this.messages) {
254 if ((message.getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_WAITING)
255 && (message.getEncryption() == encryptionType)) {
256 onMessageFound.onMessageFound(message);
257 }
258 }
259 }
260 }
261
262 public void findUnsentTextMessages(OnMessageFound onMessageFound) {
263 synchronized (this.messages) {
264 for (Message message : this.messages) {
265 if (message.getType() != Message.TYPE_IMAGE
266 && message.getStatus() == Message.STATUS_UNSEND) {
267 onMessageFound.onMessageFound(message);
268 }
269 }
270 }
271 }
272
273 public Message findSentMessageWithUuidOrRemoteId(String id) {
274 synchronized (this.messages) {
275 for (Message message : this.messages) {
276 if (id.equals(message.getUuid())
277 || (message.getStatus() >= Message.STATUS_SEND
278 && id.equals(message.getRemoteMsgId()))) {
279 return message;
280 }
281 }
282 }
283 return null;
284 }
285
286 public Message findMessageWithRemoteIdAndCounterpart(String id, Jid counterpart, boolean received, boolean carbon) {
287 synchronized (this.messages) {
288 for (int i = this.messages.size() - 1; i >= 0; --i) {
289 Message message = messages.get(i);
290 if (counterpart.equals(message.getCounterpart())
291 && ((message.getStatus() == Message.STATUS_RECEIVED) == received)
292 && (carbon == message.isCarbon() || received)) {
293 if (id.equals(message.getRemoteMsgId()) && !message.isFileOrImage() && !message.treatAsDownloadable()) {
294 return message;
295 } else {
296 return null;
297 }
298 }
299 }
300 }
301 return null;
302 }
303
304 public Message findSentMessageWithUuid(String id) {
305 synchronized (this.messages) {
306 for (Message message : this.messages) {
307 if (id.equals(message.getUuid())) {
308 return message;
309 }
310 }
311 }
312 return null;
313 }
314
315 public Message findMessageWithRemoteId(String id, Jid counterpart) {
316 synchronized (this.messages) {
317 for (Message message : this.messages) {
318 if (counterpart.equals(message.getCounterpart())
319 && (id.equals(message.getRemoteMsgId()) || id.equals(message.getUuid()))) {
320 return message;
321 }
322 }
323 }
324 return null;
325 }
326
327 public boolean hasMessageWithCounterpart(Jid counterpart) {
328 synchronized (this.messages) {
329 for (Message message : this.messages) {
330 if (counterpart.equals(message.getCounterpart())) {
331 return true;
332 }
333 }
334 }
335 return false;
336 }
337
338 public void populateWithMessages(final List<Message> messages) {
339 synchronized (this.messages) {
340 messages.clear();
341 messages.addAll(this.messages);
342 }
343 for (Iterator<Message> iterator = messages.iterator(); iterator.hasNext(); ) {
344 if (iterator.next().wasMergedIntoPrevious()) {
345 iterator.remove();
346 }
347 }
348 }
349
350 @Override
351 public boolean isBlocked() {
352 return getContact().isBlocked();
353 }
354
355 @Override
356 public boolean isDomainBlocked() {
357 return getContact().isDomainBlocked();
358 }
359
360 @Override
361 public Jid getBlockedJid() {
362 return getContact().getBlockedJid();
363 }
364
365 public int countMessages() {
366 synchronized (this.messages) {
367 return this.messages.size();
368 }
369 }
370
371 public String getFirstMamReference() {
372 return this.mFirstMamReference;
373 }
374
375 public void setFirstMamReference(String reference) {
376 this.mFirstMamReference = reference;
377 }
378
379 public void setLastClearHistory(long time, String reference) {
380 if (reference != null) {
381 setAttribute(ATTRIBUTE_LAST_CLEAR_HISTORY, String.valueOf(time) + ":" + reference);
382 } else {
383 setAttribute(ATTRIBUTE_LAST_CLEAR_HISTORY, time);
384 }
385 }
386
387 public MamReference getLastClearHistory() {
388 return MamReference.fromAttribute(getAttribute(ATTRIBUTE_LAST_CLEAR_HISTORY));
389 }
390
391 public List<Jid> getAcceptedCryptoTargets() {
392 if (mode == MODE_SINGLE) {
393 return Collections.singletonList(getJid().asBareJid());
394 } else {
395 return getJidListAttribute(ATTRIBUTE_CRYPTO_TARGETS);
396 }
397 }
398
399 public void setAcceptedCryptoTargets(List<Jid> acceptedTargets) {
400 setAttribute(ATTRIBUTE_CRYPTO_TARGETS, acceptedTargets);
401 }
402
403 public boolean setCorrectingMessage(Message correctingMessage) {
404 this.correctingMessage = correctingMessage;
405 return correctingMessage == null && draftMessage != null;
406 }
407
408 public Message getCorrectingMessage() {
409 return this.correctingMessage;
410 }
411
412 public boolean withSelf() {
413 return getContact().isSelf();
414 }
415
416 @Override
417 public int compareTo(@NonNull Conversation another) {
418 return Long.compare(another.getSortableTime(), getSortableTime());
419 }
420
421 private long getSortableTime() {
422 Draft draft = getDraft();
423 long messageTime = getLatestMessage().getTimeSent();
424 if (draft == null) {
425 return messageTime;
426 } else {
427 return Math.max(messageTime, draft.getTimestamp());
428 }
429 }
430
431 public String getDraftMessage() {
432 return draftMessage;
433 }
434
435 public void setDraftMessage(String draftMessage) {
436 this.draftMessage = draftMessage;
437 }
438
439 public boolean isRead() {
440 return (this.messages.size() == 0) || this.messages.get(this.messages.size() - 1).isRead();
441 }
442
443 public List<Message> markRead(String upToUuid) {
444 final List<Message> unread = new ArrayList<>();
445 synchronized (this.messages) {
446 for (Message message : this.messages) {
447 if (!message.isRead()) {
448 message.markRead();
449 unread.add(message);
450 }
451 if (message.getUuid().equals(upToUuid)) {
452 return unread;
453 }
454 }
455 }
456 return unread;
457 }
458
459 public static Message getLatestMarkableMessage(final List<Message> messages, boolean isPrivateAndNonAnonymousMuc) {
460 for (int i = messages.size() - 1; i >= 0; --i) {
461 final Message message = messages.get(i);
462 if (message.getStatus() <= Message.STATUS_RECEIVED
463 && (message.markable || isPrivateAndNonAnonymousMuc)
464 && message.getType() != Message.TYPE_PRIVATE) {
465 return message;
466 }
467 }
468 return null;
469 }
470
471 public Message getLatestMessage() {
472 synchronized (this.messages) {
473 if (this.messages.size() == 0) {
474 Message message = new Message(this, "", Message.ENCRYPTION_NONE);
475 message.setType(Message.TYPE_STATUS);
476 message.setTime(Math.max(getCreated(), getLastClearHistory().getTimestamp()));
477 return message;
478 } else {
479 return this.messages.get(this.messages.size() - 1);
480 }
481 }
482 }
483
484 public @NonNull CharSequence getName() {
485 if (getMode() == MODE_MULTI) {
486 final String subject = getMucOptions().getSubject();
487 final Bookmark bookmark = getBookmark();
488 final String bookmarkName = bookmark != null ? bookmark.getBookmarkName() : null;
489 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(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 final Contact contact = getContact();
924 return mode == MODE_SINGLE
925 && !contact.isOwnServer()
926 && !contact.showInRoster()
927 && !contact.isSelf()
928 && sentMessagesCount() == 0;
929 }
930
931 public int getReceivedMessagesCountSinceUuid(String uuid) {
932 if (uuid == null) {
933 return 0;
934 }
935 int count = 0;
936 synchronized (this.messages) {
937 for (int i = messages.size() - 1; i >= 0; i--) {
938 final Message message = messages.get(i);
939 if (uuid.equals(message.getUuid())) {
940 return count;
941 }
942 if (message.getStatus() <= Message.STATUS_RECEIVED) {
943 ++count;
944 }
945 }
946 }
947 return 0;
948 }
949
950 public interface OnMessageFound {
951 void onMessageFound(final Message message);
952 }
953
954 public static class Draft {
955 private final String message;
956 private final long timestamp;
957
958 private Draft(String message, long timestamp) {
959 this.message = message;
960 this.timestamp = timestamp;
961 }
962
963 public long getTimestamp() {
964 return timestamp;
965 }
966
967 public String getMessage() {
968 return message;
969 }
970 }
971}