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 findUnsentMessagesWithOtrEncryption(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() == Message.ENCRYPTION_OTR)) {
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 public synchronized MucOptions getMucOptions() {
523 if (this.mucOptions == null) {
524 this.mucOptions = new MucOptions(this);
525 }
526 return this.mucOptions;
527 }
528
529 public void resetMucOptions() {
530 this.mucOptions = null;
531 }
532
533 public void setContactJid(final Jid jid) {
534 this.contactJid = jid;
535 }
536
537 public void setNextCounterpart(Jid jid) {
538 this.nextCounterpart = jid;
539 }
540
541 public Jid getNextCounterpart() {
542 return this.nextCounterpart;
543 }
544
545 public int getLatestEncryption() {
546 int latestEncryption = this.getLatestMessage().getEncryption();
547 if ((latestEncryption == Message.ENCRYPTION_DECRYPTED)
548 || (latestEncryption == Message.ENCRYPTION_DECRYPTION_FAILED)) {
549 return Message.ENCRYPTION_PGP;
550 } else {
551 return latestEncryption;
552 }
553 }
554
555 public int getNextEncryption(boolean force) {
556 int next = this.getIntAttribute(ATTRIBUTE_NEXT_ENCRYPTION, -1);
557 if (next == -1) {
558 int latest = this.getLatestEncryption();
559 if (latest == Message.ENCRYPTION_NONE) {
560 if (force && getMode() == MODE_SINGLE) {
561 return Message.ENCRYPTION_OTR;
562 } else if (getContact().getPresences().size() == 1) {
563 if (getContact().getOtrFingerprints().size() >= 1) {
564 return Message.ENCRYPTION_OTR;
565 } else {
566 return latest;
567 }
568 } else {
569 return latest;
570 }
571 } else {
572 return latest;
573 }
574 }
575 if (next == Message.ENCRYPTION_NONE && force
576 && getMode() == MODE_SINGLE) {
577 return Message.ENCRYPTION_OTR;
578 } else {
579 return next;
580 }
581 }
582
583 public void setNextEncryption(int encryption) {
584 this.setAttribute(ATTRIBUTE_NEXT_ENCRYPTION, String.valueOf(encryption));
585 }
586
587 public String getNextMessage() {
588 if (this.nextMessage == null) {
589 return "";
590 } else {
591 return this.nextMessage;
592 }
593 }
594
595 public boolean smpRequested() {
596 return smp().status == Smp.STATUS_CONTACT_REQUESTED;
597 }
598
599 public void setNextMessage(String message) {
600 this.nextMessage = message;
601 }
602
603 public void setSymmetricKey(byte[] key) {
604 this.symmetricKey = key;
605 }
606
607 public byte[] getSymmetricKey() {
608 return this.symmetricKey;
609 }
610
611 public void setBookmark(Bookmark bookmark) {
612 this.bookmark = bookmark;
613 this.bookmark.setConversation(this);
614 }
615
616 public void deregisterWithBookmark() {
617 if (this.bookmark != null) {
618 this.bookmark.setConversation(null);
619 }
620 }
621
622 public Bookmark getBookmark() {
623 return this.bookmark;
624 }
625
626 public boolean hasDuplicateMessage(Message message) {
627 synchronized (this.messages) {
628 for (int i = this.messages.size() - 1; i >= 0; --i) {
629 if (this.messages.get(i).equals(message)) {
630 return true;
631 }
632 }
633 }
634 return false;
635 }
636
637 public Message findSentMessageWithBody(String body) {
638 synchronized (this.messages) {
639 for (int i = this.messages.size() - 1; i >= 0; --i) {
640 Message message = this.messages.get(i);
641 if ((message.getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_SEND) && message.getBody() != null && message.getBody().equals(body)) {
642 return message;
643 }
644 }
645 return null;
646 }
647 }
648
649 public boolean setLastMessageTransmitted(long value) {
650 long before = getLastMessageTransmitted();
651 if (value - before > 1000) {
652 this.setAttribute(ATTRIBUTE_LAST_MESSAGE_TRANSMITTED, String.valueOf(value));
653 return true;
654 } else {
655 return false;
656 }
657 }
658
659 public long getLastMessageTransmitted() {
660 long timestamp = getLongAttribute(ATTRIBUTE_LAST_MESSAGE_TRANSMITTED,0);
661 if (timestamp == 0) {
662 synchronized (this.messages) {
663 for(int i = this.messages.size() - 1; i >= 0; --i) {
664 Message message = this.messages.get(i);
665 if (message.getStatus() == Message.STATUS_RECEIVED) {
666 return message.getTimeSent();
667 }
668 }
669 }
670 }
671 return timestamp;
672 }
673
674 public void setMutedTill(long value) {
675 this.setAttribute(ATTRIBUTE_MUTED_TILL, String.valueOf(value));
676 }
677
678 public boolean isMuted() {
679 return System.currentTimeMillis() < this.getLongAttribute(ATTRIBUTE_MUTED_TILL, 0);
680 }
681
682 public boolean setAttribute(String key, String value) {
683 try {
684 this.attributes.put(key, value);
685 return true;
686 } catch (JSONException e) {
687 return false;
688 }
689 }
690
691 public String getAttribute(String key) {
692 try {
693 return this.attributes.getString(key);
694 } catch (JSONException e) {
695 return null;
696 }
697 }
698
699 public int getIntAttribute(String key, int defaultValue) {
700 String value = this.getAttribute(key);
701 if (value == null) {
702 return defaultValue;
703 } else {
704 try {
705 return Integer.parseInt(value);
706 } catch (NumberFormatException e) {
707 return defaultValue;
708 }
709 }
710 }
711
712 public long getLongAttribute(String key, long defaultValue) {
713 String value = this.getAttribute(key);
714 if (value == null) {
715 return defaultValue;
716 } else {
717 try {
718 return Long.parseLong(value);
719 } catch (NumberFormatException e) {
720 return defaultValue;
721 }
722 }
723 }
724
725 public void add(Message message) {
726 message.setConversation(this);
727 synchronized (this.messages) {
728 this.messages.add(message);
729 }
730 }
731
732 public void addAll(int index, List<Message> messages) {
733 synchronized (this.messages) {
734 this.messages.addAll(index, messages);
735 }
736 }
737
738 public void sort() {
739 synchronized (this.messages) {
740 Collections.sort(this.messages, new Comparator<Message>() {
741 @Override
742 public int compare(Message left, Message right) {
743 if (left.getTimeSent() < right.getTimeSent()) {
744 return -1;
745 } else if (left.getTimeSent() > right.getTimeSent()) {
746 return 1;
747 } else {
748 return 0;
749 }
750 }
751 });
752 for(Message message : this.messages) {
753 message.untie();
754 }
755 }
756 }
757
758 public int unreadCount() {
759 synchronized (this.messages) {
760 int count = 0;
761 for(int i = this.messages.size() - 1; i >= 0; --i) {
762 if (this.messages.get(i).isRead()) {
763 return count;
764 }
765 ++count;
766 }
767 return count;
768 }
769 }
770
771 public class Smp {
772 public static final int STATUS_NONE = 0;
773 public static final int STATUS_CONTACT_REQUESTED = 1;
774 public static final int STATUS_WE_REQUESTED = 2;
775 public static final int STATUS_FAILED = 3;
776 public static final int STATUS_VERIFIED = 4;
777
778 public String secret = null;
779 public String hint = null;
780 public int status = 0;
781 }
782}