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