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