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