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