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 String 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.asBareJid().toString();
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 (axolotlService != null && axolotlService.isConversationAxolotlCapable(this)) {
604 defaultEncryption = Message.ENCRYPTION_AXOLOTL;
605 } else {
606 defaultEncryption = Message.ENCRYPTION_NONE;
607 }
608 int encryption = this.getIntAttribute(ATTRIBUTE_NEXT_ENCRYPTION, defaultEncryption);
609 if (encryption == Message.ENCRYPTION_OTR) {
610 return defaultEncryption;
611 } else {
612 return encryption;
613 }
614 }
615
616 public void setNextEncryption(int encryption) {
617 this.setAttribute(ATTRIBUTE_NEXT_ENCRYPTION, String.valueOf(encryption));
618 }
619
620 public String getNextMessage() {
621 final String nextMessage = getAttribute(ATTRIBUTE_NEXT_MESSAGE);
622 return nextMessage == null ? "" : nextMessage;
623 }
624
625 public boolean setNextMessage(String message) {
626 boolean changed = !getNextMessage().equals(message);
627 this.setAttribute(ATTRIBUTE_NEXT_MESSAGE, message);
628 return changed;
629 }
630
631 public Bookmark getBookmark() {
632 return this.account.getBookmark(this.contactJid);
633 }
634
635 public Message findDuplicateMessage(Message message) {
636 synchronized (this.messages) {
637 for (int i = this.messages.size() - 1; i >= 0; --i) {
638 if (this.messages.get(i).similar(message)) {
639 return this.messages.get(i);
640 }
641 }
642 }
643 return null;
644 }
645
646 public boolean hasDuplicateMessage(Message message) {
647 return findDuplicateMessage(message) != null;
648 }
649
650 public Message findSentMessageWithBody(String body) {
651 synchronized (this.messages) {
652 for (int i = this.messages.size() - 1; i >= 0; --i) {
653 Message message = this.messages.get(i);
654 if (message.getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_SEND) {
655 String otherBody;
656 if (message.hasFileOnRemoteHost()) {
657 otherBody = message.getFileParams().url.toString();
658 } else {
659 otherBody = message.body;
660 }
661 if (otherBody != null && otherBody.equals(body)) {
662 return message;
663 }
664 }
665 }
666 return null;
667 }
668 }
669
670 public MamReference getLastMessageTransmitted() {
671 final MamReference lastClear = getLastClearHistory();
672 MamReference lastReceived = new MamReference(0);
673 synchronized (this.messages) {
674 for(int i = this.messages.size() - 1; i >= 0; --i) {
675 final Message message = this.messages.get(i);
676 if (message.getType() == Message.TYPE_PRIVATE) {
677 continue; //it's unsafe to use private messages as anchor. They could be coming from user archive
678 }
679 if (message.getStatus() == Message.STATUS_RECEIVED || message.isCarbon() || message.getServerMsgId() != null) {
680 lastReceived = new MamReference(message.getTimeSent(),message.getServerMsgId());
681 break;
682 }
683 }
684 }
685 return MamReference.max(lastClear,lastReceived);
686 }
687
688 public void setMutedTill(long value) {
689 this.setAttribute(ATTRIBUTE_MUTED_TILL, String.valueOf(value));
690 }
691
692 public boolean isMuted() {
693 return System.currentTimeMillis() < this.getLongAttribute(ATTRIBUTE_MUTED_TILL, 0);
694 }
695
696 public boolean alwaysNotify() {
697 return mode == MODE_SINGLE || getBooleanAttribute(ATTRIBUTE_ALWAYS_NOTIFY, Config.ALWAYS_NOTIFY_BY_DEFAULT || isPrivateAndNonAnonymous());
698 }
699
700 public boolean setAttribute(String key, String value) {
701 synchronized (this.attributes) {
702 try {
703 this.attributes.put(key, value == null ? "" : value);
704 return true;
705 } catch (JSONException e) {
706 return false;
707 }
708 }
709 }
710
711 public boolean setAttribute(String key, List<Jid> jids) {
712 JSONArray array = new JSONArray();
713 for(Jid jid : jids) {
714 array.put(jid.asBareJid().toString());
715 }
716 synchronized (this.attributes) {
717 try {
718 this.attributes.put(key, array);
719 return true;
720 } catch (JSONException e) {
721 e.printStackTrace();
722 return false;
723 }
724 }
725 }
726
727 public String getAttribute(String key) {
728 synchronized (this.attributes) {
729 try {
730 return this.attributes.getString(key);
731 } catch (JSONException e) {
732 return null;
733 }
734 }
735 }
736
737 private List<Jid> getJidListAttribute(String key) {
738 ArrayList<Jid> list = new ArrayList<>();
739 synchronized (this.attributes) {
740 try {
741 JSONArray array = this.attributes.getJSONArray(key);
742 for (int i = 0; i < array.length(); ++i) {
743 try {
744 list.add(Jid.of(array.getString(i)));
745 } catch (IllegalArgumentException e) {
746 //ignored
747 }
748 }
749 } catch (JSONException e) {
750 //ignored
751 }
752 }
753 return list;
754 }
755
756 private int getIntAttribute(String key, int defaultValue) {
757 String value = this.getAttribute(key);
758 if (value == null) {
759 return defaultValue;
760 } else {
761 try {
762 return Integer.parseInt(value);
763 } catch (NumberFormatException e) {
764 return defaultValue;
765 }
766 }
767 }
768
769 public long getLongAttribute(String key, long defaultValue) {
770 String value = this.getAttribute(key);
771 if (value == null) {
772 return defaultValue;
773 } else {
774 try {
775 return Long.parseLong(value);
776 } catch (NumberFormatException e) {
777 return defaultValue;
778 }
779 }
780 }
781
782 private boolean getBooleanAttribute(String key, boolean defaultValue) {
783 String value = this.getAttribute(key);
784 if (value == null) {
785 return defaultValue;
786 } else {
787 return Boolean.parseBoolean(value);
788 }
789 }
790
791 public void add(Message message) {
792 synchronized (this.messages) {
793 this.messages.add(message);
794 }
795 }
796
797 public void prepend(int offset, Message message) {
798 synchronized (this.messages) {
799 this.messages.add(Math.min(offset,this.messages.size()),message);
800 }
801 }
802
803 public void addAll(int index, List<Message> messages) {
804 synchronized (this.messages) {
805 this.messages.addAll(index, messages);
806 }
807 account.getPgpDecryptionService().decrypt(messages);
808 }
809
810 public void expireOldMessages(long timestamp) {
811 synchronized (this.messages) {
812 for(ListIterator<Message> iterator = this.messages.listIterator(); iterator.hasNext();) {
813 if (iterator.next().getTimeSent() < timestamp) {
814 iterator.remove();
815 }
816 }
817 untieMessages();
818 }
819 }
820
821 public void sort() {
822 synchronized (this.messages) {
823 Collections.sort(this.messages, (left, right) -> {
824 if (left.getTimeSent() < right.getTimeSent()) {
825 return -1;
826 } else if (left.getTimeSent() > right.getTimeSent()) {
827 return 1;
828 } else {
829 return 0;
830 }
831 });
832 untieMessages();
833 }
834 }
835
836 private void untieMessages() {
837 for(Message message : this.messages) {
838 message.untie();
839 }
840 }
841
842 public int unreadCount() {
843 synchronized (this.messages) {
844 int count = 0;
845 for(int i = this.messages.size() - 1; i >= 0; --i) {
846 if (this.messages.get(i).isRead()) {
847 return count;
848 }
849 ++count;
850 }
851 return count;
852 }
853 }
854
855 public int receivedMessagesCount() {
856 int count = 0;
857 synchronized (this.messages) {
858 for(Message message : messages) {
859 if (message.getStatus() == Message.STATUS_RECEIVED) {
860 ++count;
861 }
862 }
863 }
864 return count;
865 }
866
867 private int sentMessagesCount() {
868 int count = 0;
869 synchronized (this.messages) {
870 for(Message message : messages) {
871 if (message.getStatus() != Message.STATUS_RECEIVED) {
872 ++count;
873 }
874 }
875 }
876 return count;
877 }
878
879 public boolean isWithStranger() {
880 return mode == MODE_SINGLE
881 && !getJid().equals(Jid.ofDomain(account.getJid().getDomain()))
882 && !getContact().showInRoster()
883 && sentMessagesCount() == 0;
884 }
885
886 public class Smp {
887 public static final int STATUS_NONE = 0;
888 public static final int STATUS_CONTACT_REQUESTED = 1;
889 public static final int STATUS_WE_REQUESTED = 2;
890 public static final int STATUS_FAILED = 3;
891 public static final int STATUS_VERIFIED = 4;
892
893 public String secret = null;
894 public String hint = null;
895 public int status = 0;
896 }
897}