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