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 (int i = this.messages.size() - 1; i >= 0; --i) {
317 if (this.messages.get(i).isRead()) {
318 break;
319 }
320 this.messages.get(i).markRead();
321 unread.add(this.messages.get(i));
322 }
323 }
324 return unread;
325 }
326
327 public Message getLatestMarkableMessage() {
328 for (int i = this.messages.size() - 1; i >= 0; --i) {
329 if (this.messages.get(i).getStatus() <= Message.STATUS_RECEIVED
330 && this.messages.get(i).markable) {
331 if (this.messages.get(i).isRead()) {
332 return null;
333 } else {
334 return this.messages.get(i);
335 }
336 }
337 }
338 return null;
339 }
340
341 public Message getLatestMessage() {
342 if (this.messages.size() == 0) {
343 Message message = new Message(this, "", Message.ENCRYPTION_NONE);
344 message.setTime(getCreated());
345 return message;
346 } else {
347 Message message = this.messages.get(this.messages.size() - 1);
348 message.setConversation(this);
349 return message;
350 }
351 }
352
353 public String getName() {
354 if (getMode() == MODE_MULTI) {
355 if (getMucOptions().getSubject() != null) {
356 return getMucOptions().getSubject();
357 } else if (bookmark != null && bookmark.getBookmarkName() != null) {
358 return bookmark.getBookmarkName();
359 } else {
360 String generatedName = getMucOptions().createNameFromParticipants();
361 if (generatedName != null) {
362 return generatedName;
363 } else {
364 return getJid().getLocalpart();
365 }
366 }
367 } else {
368 return this.getContact().getDisplayName();
369 }
370 }
371
372 public String getAccountUuid() {
373 return this.accountUuid;
374 }
375
376 public Account getAccount() {
377 return this.account;
378 }
379
380 public Contact getContact() {
381 return this.account.getRoster().getContact(this.contactJid);
382 }
383
384 public void setAccount(final Account account) {
385 this.account = account;
386 }
387
388 @Override
389 public Jid getJid() {
390 return this.contactJid;
391 }
392
393 public int getStatus() {
394 return this.status;
395 }
396
397 public long getCreated() {
398 return this.created;
399 }
400
401 public ContentValues getContentValues() {
402 ContentValues values = new ContentValues();
403 values.put(UUID, uuid);
404 values.put(NAME, name);
405 values.put(CONTACT, contactUuid);
406 values.put(ACCOUNT, accountUuid);
407 values.put(CONTACTJID, contactJid.toString());
408 values.put(CREATED, created);
409 values.put(STATUS, status);
410 values.put(MODE, mode);
411 values.put(ATTRIBUTES, attributes.toString());
412 return values;
413 }
414
415 public static Conversation fromCursor(Cursor cursor) {
416 Jid jid;
417 try {
418 jid = Jid.fromString(cursor.getString(cursor.getColumnIndex(CONTACTJID)), true);
419 } catch (final InvalidJidException e) {
420 // Borked DB..
421 jid = null;
422 }
423 return new Conversation(cursor.getString(cursor.getColumnIndex(UUID)),
424 cursor.getString(cursor.getColumnIndex(NAME)),
425 cursor.getString(cursor.getColumnIndex(CONTACT)),
426 cursor.getString(cursor.getColumnIndex(ACCOUNT)),
427 jid,
428 cursor.getLong(cursor.getColumnIndex(CREATED)),
429 cursor.getInt(cursor.getColumnIndex(STATUS)),
430 cursor.getInt(cursor.getColumnIndex(MODE)),
431 cursor.getString(cursor.getColumnIndex(ATTRIBUTES)));
432 }
433
434 public void setStatus(int status) {
435 this.status = status;
436 }
437
438 public int getMode() {
439 return this.mode;
440 }
441
442 public void setMode(int mode) {
443 this.mode = mode;
444 }
445
446 public SessionImpl startOtrSession(String presence, boolean sendStart) {
447 if (this.otrSession != null) {
448 return this.otrSession;
449 } else {
450 final SessionID sessionId = new SessionID(this.getJid().toBareJid().toString(),
451 presence,
452 "xmpp");
453 this.otrSession = new SessionImpl(sessionId, getAccount().getOtrService());
454 try {
455 if (sendStart) {
456 this.otrSession.startSession();
457 return this.otrSession;
458 }
459 return this.otrSession;
460 } catch (OtrException e) {
461 return null;
462 }
463 }
464
465 }
466
467 public SessionImpl getOtrSession() {
468 return this.otrSession;
469 }
470
471 public void resetOtrSession() {
472 this.otrFingerprint = null;
473 this.otrSession = null;
474 this.mSmp.hint = null;
475 this.mSmp.secret = null;
476 this.mSmp.status = Smp.STATUS_NONE;
477 }
478
479 public Smp smp() {
480 return mSmp;
481 }
482
483 public void startOtrIfNeeded() {
484 if (this.otrSession != null
485 && this.otrSession.getSessionStatus() != SessionStatus.ENCRYPTED) {
486 try {
487 this.otrSession.startSession();
488 } catch (OtrException e) {
489 this.resetOtrSession();
490 }
491 }
492 }
493
494 public boolean endOtrIfNeeded() {
495 if (this.otrSession != null) {
496 if (this.otrSession.getSessionStatus() == SessionStatus.ENCRYPTED) {
497 try {
498 this.otrSession.endSession();
499 this.resetOtrSession();
500 return true;
501 } catch (OtrException e) {
502 this.resetOtrSession();
503 return false;
504 }
505 } else {
506 this.resetOtrSession();
507 return false;
508 }
509 } else {
510 return false;
511 }
512 }
513
514 public boolean hasValidOtrSession() {
515 return this.otrSession != null;
516 }
517
518 public synchronized String getOtrFingerprint() {
519 if (this.otrFingerprint == null) {
520 try {
521 if (getOtrSession() == null || getOtrSession().getSessionStatus() != SessionStatus.ENCRYPTED) {
522 return null;
523 }
524 DSAPublicKey remotePubKey = (DSAPublicKey) getOtrSession().getRemotePublicKey();
525 this.otrFingerprint = getAccount().getOtrService().getFingerprint(remotePubKey);
526 } catch (final OtrCryptoException | UnsupportedOperationException ignored) {
527 return null;
528 }
529 }
530 return this.otrFingerprint;
531 }
532
533 public boolean verifyOtrFingerprint() {
534 final String fingerprint = getOtrFingerprint();
535 if (fingerprint != null) {
536 getContact().addOtrFingerprint(fingerprint);
537 return true;
538 } else {
539 return false;
540 }
541 }
542
543 public boolean isOtrFingerprintVerified() {
544 return getContact().getOtrFingerprints().contains(getOtrFingerprint());
545 }
546
547 /**
548 * short for is Private and Non-anonymous
549 */
550 public boolean isPnNA() {
551 return mode == MODE_SINGLE || (getMucOptions().membersOnly() && getMucOptions().nonanonymous());
552 }
553
554 public synchronized MucOptions getMucOptions() {
555 if (this.mucOptions == null) {
556 this.mucOptions = new MucOptions(this);
557 }
558 return this.mucOptions;
559 }
560
561 public void resetMucOptions() {
562 this.mucOptions = null;
563 }
564
565 public void setContactJid(final Jid jid) {
566 this.contactJid = jid;
567 }
568
569 public void setNextCounterpart(Jid jid) {
570 this.nextCounterpart = jid;
571 }
572
573 public Jid getNextCounterpart() {
574 return this.nextCounterpart;
575 }
576
577 private int getMostRecentlyUsedOutgoingEncryption() {
578 synchronized (this.messages) {
579 for(int i = this.messages.size() -1; i >= 0; --i) {
580 final Message m = this.messages.get(i);
581 if (!m.isCarbon() && m.getStatus() != Message.STATUS_RECEIVED) {
582 final int e = m.getEncryption();
583 if (e == Message.ENCRYPTION_DECRYPTED || e == Message.ENCRYPTION_DECRYPTION_FAILED) {
584 return Message.ENCRYPTION_PGP;
585 } else {
586 return e;
587 }
588 }
589 }
590 }
591 return Message.ENCRYPTION_NONE;
592 }
593
594 private int getMostRecentlyUsedIncomingEncryption() {
595 synchronized (this.messages) {
596 for(int i = this.messages.size() -1; i >= 0; --i) {
597 final Message m = this.messages.get(i);
598 if (m.getStatus() == Message.STATUS_RECEIVED) {
599 final int e = m.getEncryption();
600 if (e == Message.ENCRYPTION_DECRYPTED || e == Message.ENCRYPTION_DECRYPTION_FAILED) {
601 return Message.ENCRYPTION_PGP;
602 } else {
603 return e;
604 }
605 }
606 }
607 }
608 return Message.ENCRYPTION_NONE;
609 }
610
611 public int getNextEncryption() {
612 int next = this.getIntAttribute(ATTRIBUTE_NEXT_ENCRYPTION, -1);
613 if (next == -1) {
614 int outgoing = this.getMostRecentlyUsedOutgoingEncryption();
615 if (outgoing == Message.ENCRYPTION_NONE) {
616 return this.getMostRecentlyUsedIncomingEncryption();
617 } else {
618 return outgoing;
619 }
620 }
621 return next;
622 }
623
624 public void setNextEncryption(int encryption) {
625 this.setAttribute(ATTRIBUTE_NEXT_ENCRYPTION, String.valueOf(encryption));
626 }
627
628 public String getNextMessage() {
629 if (this.nextMessage == null) {
630 return "";
631 } else {
632 return this.nextMessage;
633 }
634 }
635
636 public boolean smpRequested() {
637 return smp().status == Smp.STATUS_CONTACT_REQUESTED;
638 }
639
640 public void setNextMessage(String message) {
641 this.nextMessage = message;
642 }
643
644 public void setSymmetricKey(byte[] key) {
645 this.symmetricKey = key;
646 }
647
648 public byte[] getSymmetricKey() {
649 return this.symmetricKey;
650 }
651
652 public void setBookmark(Bookmark bookmark) {
653 this.bookmark = bookmark;
654 this.bookmark.setConversation(this);
655 }
656
657 public void deregisterWithBookmark() {
658 if (this.bookmark != null) {
659 this.bookmark.setConversation(null);
660 }
661 }
662
663 public Bookmark getBookmark() {
664 return this.bookmark;
665 }
666
667 public boolean hasDuplicateMessage(Message message) {
668 synchronized (this.messages) {
669 for (int i = this.messages.size() - 1; i >= 0; --i) {
670 if (this.messages.get(i).equals(message)) {
671 return true;
672 }
673 }
674 }
675 return false;
676 }
677
678 public Message findSentMessageWithBody(String body) {
679 synchronized (this.messages) {
680 for (int i = this.messages.size() - 1; i >= 0; --i) {
681 Message message = this.messages.get(i);
682 if ((message.getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_SEND) && message.getBody() != null && message.getBody().equals(body)) {
683 return message;
684 }
685 }
686 return null;
687 }
688 }
689
690 public void resetLastMessageTransmitted() {
691 this.setAttribute(ATTRIBUTE_LAST_MESSAGE_TRANSMITTED,String.valueOf(-1));
692 }
693
694 public boolean setLastMessageTransmitted(long value) {
695 long before = getLastMessageTransmitted();
696 if (value - before > 1000) {
697 this.setAttribute(ATTRIBUTE_LAST_MESSAGE_TRANSMITTED, String.valueOf(value));
698 return true;
699 } else {
700 return false;
701 }
702 }
703
704 public long getLastMessageTransmitted() {
705 long timestamp = getLongAttribute(ATTRIBUTE_LAST_MESSAGE_TRANSMITTED,0);
706 if (timestamp == 0) {
707 synchronized (this.messages) {
708 for(int i = this.messages.size() - 1; i >= 0; --i) {
709 Message message = this.messages.get(i);
710 if (message.getStatus() == Message.STATUS_RECEIVED) {
711 return message.getTimeSent();
712 }
713 }
714 }
715 }
716 return timestamp;
717 }
718
719 public void setMutedTill(long value) {
720 this.setAttribute(ATTRIBUTE_MUTED_TILL, String.valueOf(value));
721 }
722
723 public boolean isMuted() {
724 return System.currentTimeMillis() < this.getLongAttribute(ATTRIBUTE_MUTED_TILL, 0);
725 }
726
727 public boolean setAttribute(String key, String value) {
728 try {
729 this.attributes.put(key, value);
730 return true;
731 } catch (JSONException e) {
732 return false;
733 }
734 }
735
736 public String getAttribute(String key) {
737 try {
738 return this.attributes.getString(key);
739 } catch (JSONException e) {
740 return null;
741 }
742 }
743
744 public int getIntAttribute(String key, int defaultValue) {
745 String value = this.getAttribute(key);
746 if (value == null) {
747 return defaultValue;
748 } else {
749 try {
750 return Integer.parseInt(value);
751 } catch (NumberFormatException e) {
752 return defaultValue;
753 }
754 }
755 }
756
757 public long getLongAttribute(String key, long defaultValue) {
758 String value = this.getAttribute(key);
759 if (value == null) {
760 return defaultValue;
761 } else {
762 try {
763 return Long.parseLong(value);
764 } catch (NumberFormatException e) {
765 return defaultValue;
766 }
767 }
768 }
769
770 public void add(Message message) {
771 message.setConversation(this);
772 synchronized (this.messages) {
773 this.messages.add(message);
774 }
775 }
776
777 public void addAll(int index, List<Message> messages) {
778 synchronized (this.messages) {
779 this.messages.addAll(index, messages);
780 }
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}