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