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