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