1package eu.siacs.conversations.entities;
2
3import android.content.ContentValues;
4import android.database.Cursor;
5
6import net.java.otr4j.OtrException;
7import net.java.otr4j.crypto.OtrCryptoException;
8import net.java.otr4j.session.SessionID;
9import net.java.otr4j.session.SessionImpl;
10import net.java.otr4j.session.SessionStatus;
11
12import org.json.JSONException;
13import org.json.JSONObject;
14
15import java.security.interfaces.DSAPublicKey;
16import java.util.ArrayList;
17import java.util.Collections;
18import java.util.Comparator;
19import java.util.Iterator;
20import java.util.List;
21
22import eu.siacs.conversations.Config;
23import eu.siacs.conversations.xmpp.chatstate.ChatState;
24import eu.siacs.conversations.xmpp.jid.InvalidJidException;
25import eu.siacs.conversations.xmpp.jid.Jid;
26
27public class Conversation extends AbstractEntity implements Blockable {
28 public static final String TABLENAME = "conversations";
29
30 public static final int STATUS_AVAILABLE = 0;
31 public static final int STATUS_ARCHIVED = 1;
32 public static final int STATUS_DELETED = 2;
33
34 public static final int MODE_MULTI = 1;
35 public static final int MODE_SINGLE = 0;
36
37 public static final String NAME = "name";
38 public static final String ACCOUNT = "accountUuid";
39 public static final String CONTACT = "contactUuid";
40 public static final String CONTACTJID = "contactJid";
41 public static final String STATUS = "status";
42 public static final String CREATED = "created";
43 public static final String MODE = "mode";
44 public static final String ATTRIBUTES = "attributes";
45
46 public static final String ATTRIBUTE_NEXT_ENCRYPTION = "next_encryption";
47 public static final String ATTRIBUTE_MUC_PASSWORD = "muc_password";
48 public static final String ATTRIBUTE_MUTED_TILL = "muted_till";
49 public static final String ATTRIBUTE_LAST_MESSAGE_TRANSMITTED = "last_message_transmitted";
50
51 private String name;
52 private String contactUuid;
53 private String accountUuid;
54 private Jid contactJid;
55 private int status;
56 private long created;
57 private int mode;
58
59 private JSONObject attributes = new JSONObject();
60
61 private Jid nextCounterpart;
62
63 protected final ArrayList<Message> messages = new ArrayList<>();
64 protected Account account = null;
65
66 private transient SessionImpl otrSession;
67
68 private transient String otrFingerprint = null;
69 private Smp mSmp = new Smp();
70
71 private String nextMessage;
72
73 private transient MucOptions mucOptions = null;
74
75 private byte[] symmetricKey;
76
77 private Bookmark bookmark;
78
79 private boolean messagesLeftOnServer = true;
80 private ChatState mOutgoingChatState = Config.DEFAULT_CHATSTATE;
81 private ChatState mIncomingChatState = Config.DEFAULT_CHATSTATE;
82 private String mLastReceivedOtrMessageId = null;
83
84 public boolean hasMessagesLeftOnServer() {
85 return messagesLeftOnServer;
86 }
87
88 public void setHasMessagesLeftOnServer(boolean value) {
89 this.messagesLeftOnServer = value;
90 }
91
92 public Message findUnsentMessageWithUuid(String uuid) {
93 synchronized(this.messages) {
94 for (final Message message : this.messages) {
95 final int s = message.getStatus();
96 if ((s == Message.STATUS_UNSEND || s == Message.STATUS_WAITING) && message.getUuid().equals(uuid)) {
97 return message;
98 }
99 }
100 }
101 return null;
102 }
103
104 public void findWaitingMessages(OnMessageFound onMessageFound) {
105 synchronized (this.messages) {
106 for(Message message : this.messages) {
107 if (message.getStatus() == Message.STATUS_WAITING) {
108 onMessageFound.onMessageFound(message);
109 }
110 }
111 }
112 }
113
114 public void findUnreadMessages(OnMessageFound onMessageFound) {
115 synchronized (this.messages) {
116 for(Message message : this.messages) {
117 if (!message.isRead()) {
118 onMessageFound.onMessageFound(message);
119 }
120 }
121 }
122 }
123
124 public void findMessagesWithFiles(final OnMessageFound onMessageFound) {
125 synchronized (this.messages) {
126 for (final Message message : this.messages) {
127 if ((message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE)
128 && message.getEncryption() != Message.ENCRYPTION_PGP) {
129 onMessageFound.onMessageFound(message);
130 }
131 }
132 }
133 }
134
135 public Message findMessageWithFileAndUuid(final String uuid) {
136 synchronized (this.messages) {
137 for (final Message message : this.messages) {
138 if ((message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE)
139 && message.getEncryption() != Message.ENCRYPTION_PGP
140 && message.getUuid().equals(uuid)) {
141 return message;
142 }
143 }
144 }
145 return null;
146 }
147
148 public void clearMessages() {
149 synchronized (this.messages) {
150 this.messages.clear();
151 }
152 }
153
154 public boolean setIncomingChatState(ChatState state) {
155 if (this.mIncomingChatState == state) {
156 return false;
157 }
158 this.mIncomingChatState = state;
159 return true;
160 }
161
162 public ChatState getIncomingChatState() {
163 return this.mIncomingChatState;
164 }
165
166 public boolean setOutgoingChatState(ChatState state) {
167 if (mode == MODE_MULTI) {
168 return false;
169 }
170 if (this.mOutgoingChatState != state) {
171 this.mOutgoingChatState = state;
172 return true;
173 } else {
174 return false;
175 }
176 }
177
178 public ChatState getOutgoingChatState() {
179 return this.mOutgoingChatState;
180 }
181
182 public void trim() {
183 synchronized (this.messages) {
184 final int size = messages.size();
185 final int maxsize = Config.PAGE_SIZE * Config.MAX_NUM_PAGES;
186 if (size > maxsize) {
187 this.messages.subList(0, size - maxsize).clear();
188 }
189 }
190 }
191
192 public void findUnsentMessagesWithEncryption(int encryptionType, OnMessageFound onMessageFound) {
193 synchronized (this.messages) {
194 for (Message message : this.messages) {
195 if ((message.getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_WAITING)
196 && (message.getEncryption() == encryptionType)) {
197 onMessageFound.onMessageFound(message);
198 }
199 }
200 }
201 }
202
203 public void findUnsentTextMessages(OnMessageFound onMessageFound) {
204 synchronized (this.messages) {
205 for (Message message : this.messages) {
206 if (message.getType() != Message.TYPE_IMAGE
207 && message.getStatus() == Message.STATUS_UNSEND) {
208 onMessageFound.onMessageFound(message);
209 }
210 }
211 }
212 }
213
214 public Message findSentMessageWithUuidOrRemoteId(String id) {
215 synchronized (this.messages) {
216 for (Message message : this.messages) {
217 if (id.equals(message.getUuid())
218 || (message.getStatus() >= Message.STATUS_SEND
219 && id.equals(message.getRemoteMsgId()))) {
220 return message;
221 }
222 }
223 }
224 return null;
225 }
226
227 public Message findSentMessageWithUuid(String id) {
228 synchronized (this.messages) {
229 for (Message message : this.messages) {
230 if (id.equals(message.getUuid())) {
231 return message;
232 }
233 }
234 }
235 return null;
236 }
237
238 public void populateWithMessages(final List<Message> messages) {
239 synchronized (this.messages) {
240 messages.clear();
241 messages.addAll(this.messages);
242 }
243 for(Iterator<Message> iterator = messages.iterator(); iterator.hasNext();) {
244 if (iterator.next().wasMergedIntoPrevious()) {
245 iterator.remove();
246 }
247 }
248 }
249
250 @Override
251 public boolean isBlocked() {
252 return getContact().isBlocked();
253 }
254
255 @Override
256 public boolean isDomainBlocked() {
257 return getContact().isDomainBlocked();
258 }
259
260 @Override
261 public Jid getBlockedJid() {
262 return getContact().getBlockedJid();
263 }
264
265 public String getLastReceivedOtrMessageId() {
266 return this.mLastReceivedOtrMessageId;
267 }
268
269 public void setLastReceivedOtrMessageId(String id) {
270 this.mLastReceivedOtrMessageId = id;
271 }
272
273 public int countMessages() {
274 synchronized (this.messages) {
275 return this.messages.size();
276 }
277 }
278
279 public interface OnMessageFound {
280 void onMessageFound(final Message message);
281 }
282
283 public Conversation(final String name, final Account account, final Jid contactJid,
284 final int mode) {
285 this(java.util.UUID.randomUUID().toString(), name, null, account
286 .getUuid(), contactJid, System.currentTimeMillis(),
287 STATUS_AVAILABLE, mode, "");
288 this.account = account;
289 }
290
291 public Conversation(final String uuid, final String name, final String contactUuid,
292 final String accountUuid, final Jid contactJid, final long created, final int status,
293 final int mode, final String attributes) {
294 this.uuid = uuid;
295 this.name = name;
296 this.contactUuid = contactUuid;
297 this.accountUuid = accountUuid;
298 this.contactJid = contactJid;
299 this.created = created;
300 this.status = status;
301 this.mode = mode;
302 try {
303 this.attributes = new JSONObject(attributes == null ? "" : attributes);
304 } catch (JSONException e) {
305 this.attributes = new JSONObject();
306 }
307 }
308
309 public boolean isRead() {
310 return (this.messages.size() == 0) || this.messages.get(this.messages.size() - 1).isRead();
311 }
312
313 public List<Message> markRead() {
314 final List<Message> unread = new ArrayList<>();
315 synchronized (this.messages) {
316 for(Message message : this.messages) {
317 if (!message.isRead()) {
318 message.markRead();
319 unread.add(message);
320 }
321 }
322 }
323 return unread;
324 }
325
326 public Message getLatestMarkableMessage() {
327 for (int i = this.messages.size() - 1; i >= 0; --i) {
328 if (this.messages.get(i).getStatus() <= Message.STATUS_RECEIVED
329 && this.messages.get(i).markable) {
330 if (this.messages.get(i).isRead()) {
331 return null;
332 } else {
333 return this.messages.get(i);
334 }
335 }
336 }
337 return null;
338 }
339
340 public Message getLatestMessage() {
341 if (this.messages.size() == 0) {
342 Message message = new Message(this, "", Message.ENCRYPTION_NONE);
343 message.setTime(getCreated());
344 return message;
345 } else {
346 Message message = this.messages.get(this.messages.size() - 1);
347 message.setConversation(this);
348 return message;
349 }
350 }
351
352 public String getName() {
353 if (getMode() == MODE_MULTI) {
354 if (getMucOptions().getSubject() != null) {
355 return getMucOptions().getSubject();
356 } else if (bookmark != null && bookmark.getBookmarkName() != null) {
357 return bookmark.getBookmarkName();
358 } else {
359 String generatedName = getMucOptions().createNameFromParticipants();
360 if (generatedName != null) {
361 return generatedName;
362 } else {
363 return getJid().getLocalpart();
364 }
365 }
366 } else {
367 return this.getContact().getDisplayName();
368 }
369 }
370
371 public String getAccountUuid() {
372 return this.accountUuid;
373 }
374
375 public Account getAccount() {
376 return this.account;
377 }
378
379 public Contact getContact() {
380 return this.account.getRoster().getContact(this.contactJid);
381 }
382
383 public void setAccount(final Account account) {
384 this.account = account;
385 }
386
387 @Override
388 public Jid getJid() {
389 return this.contactJid;
390 }
391
392 public int getStatus() {
393 return this.status;
394 }
395
396 public long getCreated() {
397 return this.created;
398 }
399
400 public ContentValues getContentValues() {
401 ContentValues values = new ContentValues();
402 values.put(UUID, uuid);
403 values.put(NAME, name);
404 values.put(CONTACT, contactUuid);
405 values.put(ACCOUNT, accountUuid);
406 values.put(CONTACTJID, contactJid.toString());
407 values.put(CREATED, created);
408 values.put(STATUS, status);
409 values.put(MODE, mode);
410 values.put(ATTRIBUTES, attributes.toString());
411 return values;
412 }
413
414 public static Conversation fromCursor(Cursor cursor) {
415 Jid jid;
416 try {
417 jid = Jid.fromString(cursor.getString(cursor.getColumnIndex(CONTACTJID)), true);
418 } catch (final InvalidJidException e) {
419 // Borked DB..
420 jid = null;
421 }
422 return new Conversation(cursor.getString(cursor.getColumnIndex(UUID)),
423 cursor.getString(cursor.getColumnIndex(NAME)),
424 cursor.getString(cursor.getColumnIndex(CONTACT)),
425 cursor.getString(cursor.getColumnIndex(ACCOUNT)),
426 jid,
427 cursor.getLong(cursor.getColumnIndex(CREATED)),
428 cursor.getInt(cursor.getColumnIndex(STATUS)),
429 cursor.getInt(cursor.getColumnIndex(MODE)),
430 cursor.getString(cursor.getColumnIndex(ATTRIBUTES)));
431 }
432
433 public void setStatus(int status) {
434 this.status = status;
435 }
436
437 public int getMode() {
438 return this.mode;
439 }
440
441 public void setMode(int mode) {
442 this.mode = mode;
443 }
444
445 public SessionImpl startOtrSession(String presence, boolean sendStart) {
446 if (this.otrSession != null) {
447 return this.otrSession;
448 } else {
449 final SessionID sessionId = new SessionID(this.getJid().toBareJid().toString(),
450 presence,
451 "xmpp");
452 this.otrSession = new SessionImpl(sessionId, getAccount().getOtrService());
453 try {
454 if (sendStart) {
455 this.otrSession.startSession();
456 return this.otrSession;
457 }
458 return this.otrSession;
459 } catch (OtrException e) {
460 return null;
461 }
462 }
463
464 }
465
466 public SessionImpl getOtrSession() {
467 return this.otrSession;
468 }
469
470 public void resetOtrSession() {
471 this.otrFingerprint = null;
472 this.otrSession = null;
473 this.mSmp.hint = null;
474 this.mSmp.secret = null;
475 this.mSmp.status = Smp.STATUS_NONE;
476 }
477
478 public Smp smp() {
479 return mSmp;
480 }
481
482 public void startOtrIfNeeded() {
483 if (this.otrSession != null
484 && this.otrSession.getSessionStatus() != SessionStatus.ENCRYPTED) {
485 try {
486 this.otrSession.startSession();
487 } catch (OtrException e) {
488 this.resetOtrSession();
489 }
490 }
491 }
492
493 public boolean endOtrIfNeeded() {
494 if (this.otrSession != null) {
495 if (this.otrSession.getSessionStatus() == SessionStatus.ENCRYPTED) {
496 try {
497 this.otrSession.endSession();
498 this.resetOtrSession();
499 return true;
500 } catch (OtrException e) {
501 this.resetOtrSession();
502 return false;
503 }
504 } else {
505 this.resetOtrSession();
506 return false;
507 }
508 } else {
509 return false;
510 }
511 }
512
513 public boolean hasValidOtrSession() {
514 return this.otrSession != null;
515 }
516
517 public synchronized String getOtrFingerprint() {
518 if (this.otrFingerprint == null) {
519 try {
520 if (getOtrSession() == null || getOtrSession().getSessionStatus() != SessionStatus.ENCRYPTED) {
521 return null;
522 }
523 DSAPublicKey remotePubKey = (DSAPublicKey) getOtrSession().getRemotePublicKey();
524 this.otrFingerprint = getAccount().getOtrService().getFingerprint(remotePubKey);
525 } catch (final OtrCryptoException | UnsupportedOperationException ignored) {
526 return null;
527 }
528 }
529 return this.otrFingerprint;
530 }
531
532 public boolean verifyOtrFingerprint() {
533 final String fingerprint = getOtrFingerprint();
534 if (fingerprint != null) {
535 getContact().addOtrFingerprint(fingerprint);
536 return true;
537 } else {
538 return false;
539 }
540 }
541
542 public boolean isOtrFingerprintVerified() {
543 return getContact().getOtrFingerprints().contains(getOtrFingerprint());
544 }
545
546 /**
547 * short for is Private and Non-anonymous
548 */
549 public boolean isPnNA() {
550 return mode == MODE_SINGLE || (getMucOptions().membersOnly() && getMucOptions().nonanonymous());
551 }
552
553 public synchronized MucOptions getMucOptions() {
554 if (this.mucOptions == null) {
555 this.mucOptions = new MucOptions(this);
556 }
557 return this.mucOptions;
558 }
559
560 public void resetMucOptions() {
561 this.mucOptions = null;
562 }
563
564 public void setContactJid(final Jid jid) {
565 this.contactJid = jid;
566 }
567
568 public void setNextCounterpart(Jid jid) {
569 this.nextCounterpart = jid;
570 }
571
572 public Jid getNextCounterpart() {
573 return this.nextCounterpart;
574 }
575
576 private int getMostRecentlyUsedOutgoingEncryption() {
577 synchronized (this.messages) {
578 for(int i = this.messages.size() -1; i >= 0; --i) {
579 final Message m = this.messages.get(i);
580 if (!m.isCarbon() && m.getStatus() != Message.STATUS_RECEIVED) {
581 final int e = m.getEncryption();
582 if (e == Message.ENCRYPTION_DECRYPTED || e == Message.ENCRYPTION_DECRYPTION_FAILED) {
583 return Message.ENCRYPTION_PGP;
584 } else {
585 return e;
586 }
587 }
588 }
589 }
590 return Message.ENCRYPTION_NONE;
591 }
592
593 private int getMostRecentlyUsedIncomingEncryption() {
594 synchronized (this.messages) {
595 for(int i = this.messages.size() -1; i >= 0; --i) {
596 final Message m = this.messages.get(i);
597 if (m.getStatus() == Message.STATUS_RECEIVED) {
598 final int e = m.getEncryption();
599 if (e == Message.ENCRYPTION_DECRYPTED || e == Message.ENCRYPTION_DECRYPTION_FAILED) {
600 return Message.ENCRYPTION_PGP;
601 } else {
602 return e;
603 }
604 }
605 }
606 }
607 return Message.ENCRYPTION_NONE;
608 }
609
610 public int getNextEncryption() {
611 int next = this.getIntAttribute(ATTRIBUTE_NEXT_ENCRYPTION, -1);
612 if (next == -1) {
613 int outgoing = this.getMostRecentlyUsedOutgoingEncryption();
614 if (outgoing == Message.ENCRYPTION_NONE) {
615 return this.getMostRecentlyUsedIncomingEncryption();
616 } else {
617 return outgoing;
618 }
619 }
620 return next;
621 }
622
623 public void setNextEncryption(int encryption) {
624 this.setAttribute(ATTRIBUTE_NEXT_ENCRYPTION, String.valueOf(encryption));
625 }
626
627 public String getNextMessage() {
628 if (this.nextMessage == null) {
629 return "";
630 } else {
631 return this.nextMessage;
632 }
633 }
634
635 public boolean smpRequested() {
636 return smp().status == Smp.STATUS_CONTACT_REQUESTED;
637 }
638
639 public void setNextMessage(String message) {
640 this.nextMessage = message;
641 }
642
643 public void setSymmetricKey(byte[] key) {
644 this.symmetricKey = key;
645 }
646
647 public byte[] getSymmetricKey() {
648 return this.symmetricKey;
649 }
650
651 public void setBookmark(Bookmark bookmark) {
652 this.bookmark = bookmark;
653 this.bookmark.setConversation(this);
654 }
655
656 public void deregisterWithBookmark() {
657 if (this.bookmark != null) {
658 this.bookmark.setConversation(null);
659 }
660 }
661
662 public Bookmark getBookmark() {
663 return this.bookmark;
664 }
665
666 public boolean hasDuplicateMessage(Message message) {
667 synchronized (this.messages) {
668 for (int i = this.messages.size() - 1; i >= 0; --i) {
669 if (this.messages.get(i).equals(message)) {
670 return true;
671 }
672 }
673 }
674 return false;
675 }
676
677 public Message findSentMessageWithBody(String body) {
678 synchronized (this.messages) {
679 for (int i = this.messages.size() - 1; i >= 0; --i) {
680 Message message = this.messages.get(i);
681 if ((message.getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_SEND) && message.getBody() != null && message.getBody().equals(body)) {
682 return message;
683 }
684 }
685 return null;
686 }
687 }
688
689 public void resetLastMessageTransmitted() {
690 this.setAttribute(ATTRIBUTE_LAST_MESSAGE_TRANSMITTED,String.valueOf(-1));
691 }
692
693 public boolean setLastMessageTransmitted(long value) {
694 long before = getLastMessageTransmitted();
695 if (value - before > 1000) {
696 this.setAttribute(ATTRIBUTE_LAST_MESSAGE_TRANSMITTED, String.valueOf(value));
697 return true;
698 } else {
699 return false;
700 }
701 }
702
703 public long getLastMessageTransmitted() {
704 long timestamp = getLongAttribute(ATTRIBUTE_LAST_MESSAGE_TRANSMITTED,0);
705 if (timestamp == 0) {
706 synchronized (this.messages) {
707 for(int i = this.messages.size() - 1; i >= 0; --i) {
708 Message message = this.messages.get(i);
709 if (message.getStatus() == Message.STATUS_RECEIVED) {
710 return message.getTimeSent();
711 }
712 }
713 }
714 }
715 return timestamp;
716 }
717
718 public void setMutedTill(long value) {
719 this.setAttribute(ATTRIBUTE_MUTED_TILL, String.valueOf(value));
720 }
721
722 public boolean isMuted() {
723 return System.currentTimeMillis() < this.getLongAttribute(ATTRIBUTE_MUTED_TILL, 0);
724 }
725
726 public boolean setAttribute(String key, String value) {
727 try {
728 this.attributes.put(key, value);
729 return true;
730 } catch (JSONException e) {
731 return false;
732 }
733 }
734
735 public String getAttribute(String key) {
736 try {
737 return this.attributes.getString(key);
738 } catch (JSONException e) {
739 return null;
740 }
741 }
742
743 public int getIntAttribute(String key, int defaultValue) {
744 String value = this.getAttribute(key);
745 if (value == null) {
746 return defaultValue;
747 } else {
748 try {
749 return Integer.parseInt(value);
750 } catch (NumberFormatException e) {
751 return defaultValue;
752 }
753 }
754 }
755
756 public long getLongAttribute(String key, long defaultValue) {
757 String value = this.getAttribute(key);
758 if (value == null) {
759 return defaultValue;
760 } else {
761 try {
762 return Long.parseLong(value);
763 } catch (NumberFormatException e) {
764 return defaultValue;
765 }
766 }
767 }
768
769 public void add(Message message) {
770 message.setConversation(this);
771 synchronized (this.messages) {
772 this.messages.add(message);
773 }
774 }
775
776 public void addAll(int index, List<Message> messages) {
777 synchronized (this.messages) {
778 this.messages.addAll(index, messages);
779 }
780 account.getPgpDecryptionService().addAll(messages);
781 }
782
783 public void sort() {
784 synchronized (this.messages) {
785 Collections.sort(this.messages, new Comparator<Message>() {
786 @Override
787 public int compare(Message left, Message right) {
788 if (left.getTimeSent() < right.getTimeSent()) {
789 return -1;
790 } else if (left.getTimeSent() > right.getTimeSent()) {
791 return 1;
792 } else {
793 return 0;
794 }
795 }
796 });
797 for(Message message : this.messages) {
798 message.untie();
799 }
800 }
801 }
802
803 public int unreadCount() {
804 synchronized (this.messages) {
805 int count = 0;
806 for(int i = this.messages.size() - 1; i >= 0; --i) {
807 if (this.messages.get(i).isRead()) {
808 return count;
809 }
810 ++count;
811 }
812 return count;
813 }
814 }
815
816 public class Smp {
817 public static final int STATUS_NONE = 0;
818 public static final int STATUS_CONTACT_REQUESTED = 1;
819 public static final int STATUS_WE_REQUESTED = 2;
820 public static final int STATUS_FAILED = 3;
821 public static final int STATUS_VERIFIED = 4;
822
823 public String secret = null;
824 public String hint = null;
825 public int status = 0;
826 }
827}