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