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