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