Conversation.java

  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(XmppConnectionService service,
244			String presence, boolean sendStart) {
245		if (this.otrSession != null) {
246			return this.otrSession;
247		} else {
248            final SessionID sessionId = new SessionID(this.getContactJid().toBareJid().toString(),
249                    presence,
250                    "xmpp");
251			this.otrSession = new SessionImpl(sessionId, getAccount()
252					.getOtrEngine(service));
253			try {
254				if (sendStart) {
255					this.otrSession.startSession();
256					return this.otrSession;
257				}
258				return this.otrSession;
259			} catch (OtrException e) {
260				return null;
261			}
262		}
263
264	}
265
266	public SessionImpl getOtrSession() {
267		return this.otrSession;
268	}
269
270	public void resetOtrSession() {
271		this.otrFingerprint = null;
272		this.otrSession = null;
273		this.mSmp.hint = null;
274		this.mSmp.secret = null;
275		this.mSmp.status = Smp.STATUS_NONE;
276	}
277
278	public Smp smp() {
279		return mSmp;
280	}
281
282	public void startOtrIfNeeded() {
283		if (this.otrSession != null
284				&& this.otrSession.getSessionStatus() != SessionStatus.ENCRYPTED) {
285			try {
286				this.otrSession.startSession();
287			} catch (OtrException e) {
288				this.resetOtrSession();
289			}
290		}
291	}
292
293	public boolean endOtrIfNeeded() {
294		if (this.otrSession != null) {
295			if (this.otrSession.getSessionStatus() == SessionStatus.ENCRYPTED) {
296				try {
297					this.otrSession.endSession();
298					this.resetOtrSession();
299					return true;
300				} catch (OtrException e) {
301					this.resetOtrSession();
302					return false;
303				}
304			} else {
305				this.resetOtrSession();
306				return false;
307			}
308		} else {
309			return false;
310		}
311	}
312
313	public boolean hasValidOtrSession() {
314		return this.otrSession != null;
315	}
316
317	public String getOtrFingerprint() {
318		if (this.otrFingerprint == null) {
319			try {
320				if (getOtrSession() == null) {
321					return "";
322				}
323				DSAPublicKey remotePubKey = (DSAPublicKey) getOtrSession()
324						.getRemotePublicKey();
325				StringBuilder builder = new StringBuilder(
326						new OtrCryptoEngineImpl().getFingerprint(remotePubKey));
327				builder.insert(8, " ");
328				builder.insert(17, " ");
329				builder.insert(26, " ");
330				builder.insert(35, " ");
331				this.otrFingerprint = builder.toString();
332			} catch (final OtrCryptoException ignored) {
333
334			}
335		}
336		return this.otrFingerprint;
337	}
338
339	public void verifyOtrFingerprint() {
340		getContact().addOtrFingerprint(getOtrFingerprint());
341	}
342
343	public boolean isOtrFingerprintVerified() {
344		return getContact().getOtrFingerprints().contains(getOtrFingerprint());
345	}
346
347	public synchronized MucOptions getMucOptions() {
348		if (this.mucOptions == null) {
349			this.mucOptions = new MucOptions(this);
350		}
351		return this.mucOptions;
352	}
353
354	public void resetMucOptions() {
355		this.mucOptions = null;
356	}
357
358	public void setContactJid(final Jid jid) {
359		this.contactJid = jid;
360	}
361
362	public void setNextCounterpart(Jid jid) {
363		this.nextCounterpart = jid;
364	}
365
366	public Jid getNextCounterpart() {
367		return this.nextCounterpart;
368	}
369
370	public int getLatestEncryption() {
371		int latestEncryption = this.getLatestMessage().getEncryption();
372		if ((latestEncryption == Message.ENCRYPTION_DECRYPTED)
373				|| (latestEncryption == Message.ENCRYPTION_DECRYPTION_FAILED)) {
374			return Message.ENCRYPTION_PGP;
375		} else {
376			return latestEncryption;
377		}
378	}
379
380	public int getNextEncryption(boolean force) {
381		int next = this.getIntAttribute(ATTRIBUTE_NEXT_ENCRYPTION, -1);
382		if (next == -1) {
383			int latest = this.getLatestEncryption();
384			if (latest == Message.ENCRYPTION_NONE) {
385				if (force && getMode() == MODE_SINGLE) {
386					return Message.ENCRYPTION_OTR;
387				} else if (getContact().getPresences().size() == 1) {
388					if (getContact().getOtrFingerprints().size() >= 1) {
389						return Message.ENCRYPTION_OTR;
390					} else {
391						return latest;
392					}
393				} else {
394					return latest;
395				}
396			} else {
397				return latest;
398			}
399		}
400		if (next == Message.ENCRYPTION_NONE && force
401				&& getMode() == MODE_SINGLE) {
402			return Message.ENCRYPTION_OTR;
403		} else {
404			return next;
405		}
406	}
407
408	public void setNextEncryption(int encryption) {
409		this.setAttribute(ATTRIBUTE_NEXT_ENCRYPTION, String.valueOf(encryption));
410	}
411
412	public String getNextMessage() {
413		if (this.nextMessage == null) {
414			return "";
415		} else {
416			return this.nextMessage;
417		}
418	}
419
420	public boolean smpRequested() {
421		return smp().status == Smp.STATUS_CONTACT_REQUESTED;
422	}
423
424	public void setNextMessage(String message) {
425		this.nextMessage = message;
426	}
427
428	public void setSymmetricKey(byte[] key) {
429		this.symmetricKey = key;
430	}
431
432	public byte[] getSymmetricKey() {
433		return this.symmetricKey;
434	}
435
436	public void setBookmark(Bookmark bookmark) {
437		this.bookmark = bookmark;
438		this.bookmark.setConversation(this);
439	}
440
441	public void deregisterWithBookmark() {
442		if (this.bookmark != null) {
443			this.bookmark.setConversation(null);
444		}
445	}
446
447	public Bookmark getBookmark() {
448		return this.bookmark;
449	}
450
451	public boolean hasDuplicateMessage(Message message) {
452		for (int i = this.getMessages().size() - 1; i >= 0; --i) {
453			if (this.messages.get(i).equals(message)) {
454				return true;
455			}
456		}
457		return false;
458	}
459
460	public void setMutedTill(long value) {
461		this.setAttribute(ATTRIBUTE_MUTED_TILL, String.valueOf(value));
462	}
463
464	public boolean isMuted() {
465		return SystemClock.elapsedRealtime() < this.getLongAttribute(
466				ATTRIBUTE_MUTED_TILL, 0);
467	}
468
469	public boolean setAttribute(String key, String value) {
470		try {
471			this.attributes.put(key, value);
472			return true;
473		} catch (JSONException e) {
474			return false;
475		}
476	}
477
478	public String getAttribute(String key) {
479		try {
480			return this.attributes.getString(key);
481		} catch (JSONException e) {
482			return null;
483		}
484	}
485
486	public int getIntAttribute(String key, int defaultValue) {
487		String value = this.getAttribute(key);
488		if (value == null) {
489			return defaultValue;
490		} else {
491			try {
492				return Integer.parseInt(value);
493			} catch (NumberFormatException e) {
494				return defaultValue;
495			}
496		}
497	}
498
499	public long getLongAttribute(String key, long defaultValue) {
500		String value = this.getAttribute(key);
501		if (value == null) {
502			return defaultValue;
503		} else {
504			try {
505				return Long.parseLong(value);
506			} catch (NumberFormatException e) {
507				return defaultValue;
508			}
509		}
510	}
511
512	public void add(Message message) {
513		message.setConversation(this);
514		synchronized (this.messages) {
515			this.messages.add(message);
516		}
517	}
518
519	public void addAll(int index, List<Message> messages) {
520		synchronized (this.messages) {
521			this.messages.addAll(index, messages);
522		}
523	}
524
525	public class Smp {
526		public static final int STATUS_NONE = 0;
527		public static final int STATUS_CONTACT_REQUESTED = 1;
528		public static final int STATUS_WE_REQUESTED = 2;
529		public static final int STATUS_FAILED = 3;
530		public static final int STATUS_VERIFIED = 4;
531
532		public String secret = null;
533		public String hint = null;
534		public int status = 0;
535	}
536}