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