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