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