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