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(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}