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