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