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 final 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 String getName() {
152		if (getMode() == MODE_MULTI && getMucOptions().getSubject() != null) {
153			return getMucOptions().getSubject();
154		} else if (getMode() == MODE_MULTI && bookmark != null
155				&& bookmark.getName() != null) {
156			return bookmark.getName();
157		} else {
158			return this.getContact().getDisplayName();
159		}
160	}
161
162	public String getProfilePhotoString() {
163		return this.getContact().getProfilePhoto();
164	}
165
166	public String getAccountUuid() {
167		return this.accountUuid;
168	}
169
170	public Account getAccount() {
171		return this.account;
172	}
173
174	public Contact getContact() {
175		return this.account.getRoster().getContact(this.contactJid);
176	}
177
178	public void setAccount(Account account) {
179		this.account = account;
180	}
181
182	public Jid getContactJid() {
183		return this.contactJid;
184	}
185
186	public int getStatus() {
187		return this.status;
188	}
189
190	public long getCreated() {
191		return this.created;
192	}
193
194	public ContentValues getContentValues() {
195		ContentValues values = new ContentValues();
196		values.put(UUID, uuid);
197		values.put(NAME, name);
198		values.put(CONTACT, contactUuid);
199		values.put(ACCOUNT, accountUuid);
200		values.put(CONTACTJID, contactJid.toString());
201		values.put(CREATED, created);
202		values.put(STATUS, status);
203		values.put(MODE, mode);
204		values.put(ATTRIBUTES, attributes.toString());
205		return values;
206	}
207
208	public static Conversation fromCursor(Cursor cursor) {
209        Jid jid;
210        try {
211            jid = Jid.fromString(cursor.getString(cursor.getColumnIndex(CONTACTJID)));
212        } catch (final InvalidJidException e) {
213            // Borked DB..
214            jid = null;
215        }
216        return new Conversation(cursor.getString(cursor.getColumnIndex(UUID)),
217				cursor.getString(cursor.getColumnIndex(NAME)),
218				cursor.getString(cursor.getColumnIndex(CONTACT)),
219				cursor.getString(cursor.getColumnIndex(ACCOUNT)),
220				jid,
221				cursor.getLong(cursor.getColumnIndex(CREATED)),
222				cursor.getInt(cursor.getColumnIndex(STATUS)),
223				cursor.getInt(cursor.getColumnIndex(MODE)),
224				cursor.getString(cursor.getColumnIndex(ATTRIBUTES)));
225	}
226
227	public void setStatus(int status) {
228		this.status = status;
229	}
230
231	public int getMode() {
232		return this.mode;
233	}
234
235	public void setMode(int mode) {
236		this.mode = mode;
237	}
238
239	public SessionImpl startOtrSession(String presence, boolean sendStart) {
240		if (this.otrSession != null) {
241			return this.otrSession;
242		} else {
243            final SessionID sessionId = new SessionID(this.getContactJid().toBareJid().toString(),
244                    presence,
245                    "xmpp");
246			this.otrSession = new SessionImpl(sessionId, getAccount().getOtrEngine());
247			try {
248				if (sendStart) {
249					this.otrSession.startSession();
250					return this.otrSession;
251				}
252				return this.otrSession;
253			} catch (OtrException e) {
254				return null;
255			}
256		}
257
258	}
259
260	public SessionImpl getOtrSession() {
261		return this.otrSession;
262	}
263
264	public void resetOtrSession() {
265		this.otrFingerprint = null;
266		this.otrSession = null;
267		this.mSmp.hint = null;
268		this.mSmp.secret = null;
269		this.mSmp.status = Smp.STATUS_NONE;
270	}
271
272	public Smp smp() {
273		return mSmp;
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 void verifyOtrFingerprint() {
334		getContact().addOtrFingerprint(getOtrFingerprint());
335	}
336
337	public boolean isOtrFingerprintVerified() {
338		return getContact().getOtrFingerprints().contains(getOtrFingerprint());
339	}
340
341	public synchronized MucOptions getMucOptions() {
342		if (this.mucOptions == null) {
343			this.mucOptions = new MucOptions(this);
344		}
345		return this.mucOptions;
346	}
347
348	public void resetMucOptions() {
349		this.mucOptions = null;
350	}
351
352	public void setContactJid(final Jid jid) {
353		this.contactJid = jid;
354	}
355
356	public void setNextCounterpart(Jid jid) {
357		this.nextCounterpart = jid;
358	}
359
360	public Jid getNextCounterpart() {
361		return this.nextCounterpart;
362	}
363
364	public int getLatestEncryption() {
365		int latestEncryption = this.getLatestMessage().getEncryption();
366		if ((latestEncryption == Message.ENCRYPTION_DECRYPTED)
367				|| (latestEncryption == Message.ENCRYPTION_DECRYPTION_FAILED)) {
368			return Message.ENCRYPTION_PGP;
369		} else {
370			return latestEncryption;
371		}
372	}
373
374	public int getNextEncryption(boolean force) {
375		int next = this.getIntAttribute(ATTRIBUTE_NEXT_ENCRYPTION, -1);
376		if (next == -1) {
377			int latest = this.getLatestEncryption();
378			if (latest == Message.ENCRYPTION_NONE) {
379				if (force && getMode() == MODE_SINGLE) {
380					return Message.ENCRYPTION_OTR;
381				} else if (getContact().getPresences().size() == 1) {
382					if (getContact().getOtrFingerprints().size() >= 1) {
383						return Message.ENCRYPTION_OTR;
384					} else {
385						return latest;
386					}
387				} else {
388					return latest;
389				}
390			} else {
391				return latest;
392			}
393		}
394		if (next == Message.ENCRYPTION_NONE && force
395				&& getMode() == MODE_SINGLE) {
396			return Message.ENCRYPTION_OTR;
397		} else {
398			return next;
399		}
400	}
401
402	public void setNextEncryption(int encryption) {
403		this.setAttribute(ATTRIBUTE_NEXT_ENCRYPTION, String.valueOf(encryption));
404	}
405
406	public String getNextMessage() {
407		if (this.nextMessage == null) {
408			return "";
409		} else {
410			return this.nextMessage;
411		}
412	}
413
414	public boolean smpRequested() {
415		return smp().status == Smp.STATUS_CONTACT_REQUESTED;
416	}
417
418	public void setNextMessage(String message) {
419		this.nextMessage = message;
420	}
421
422	public void setSymmetricKey(byte[] key) {
423		this.symmetricKey = key;
424	}
425
426	public byte[] getSymmetricKey() {
427		return this.symmetricKey;
428	}
429
430	public void setBookmark(Bookmark bookmark) {
431		this.bookmark = bookmark;
432		this.bookmark.setConversation(this);
433	}
434
435	public void deregisterWithBookmark() {
436		if (this.bookmark != null) {
437			this.bookmark.setConversation(null);
438		}
439	}
440
441	public Bookmark getBookmark() {
442		return this.bookmark;
443	}
444
445	public boolean hasDuplicateMessage(Message message) {
446		for (int i = this.getMessages().size() - 1; i >= 0; --i) {
447			if (this.messages.get(i).equals(message)) {
448				return true;
449			}
450		}
451		return false;
452	}
453
454	public void setMutedTill(long value) {
455		this.setAttribute(ATTRIBUTE_MUTED_TILL, String.valueOf(value));
456	}
457
458	public boolean isMuted() {
459		return SystemClock.elapsedRealtime() < this.getLongAttribute(
460				ATTRIBUTE_MUTED_TILL, 0);
461	}
462
463	public boolean setAttribute(String key, String value) {
464		try {
465			this.attributes.put(key, value);
466			return true;
467		} catch (JSONException e) {
468			return false;
469		}
470	}
471
472	public String getAttribute(String key) {
473		try {
474			return this.attributes.getString(key);
475		} catch (JSONException e) {
476			return null;
477		}
478	}
479
480	public int getIntAttribute(String key, int defaultValue) {
481		String value = this.getAttribute(key);
482		if (value == null) {
483			return defaultValue;
484		} else {
485			try {
486				return Integer.parseInt(value);
487			} catch (NumberFormatException e) {
488				return defaultValue;
489			}
490		}
491	}
492
493	public long getLongAttribute(String key, long defaultValue) {
494		String value = this.getAttribute(key);
495		if (value == null) {
496			return defaultValue;
497		} else {
498			try {
499				return Long.parseLong(value);
500			} catch (NumberFormatException e) {
501				return defaultValue;
502			}
503		}
504	}
505
506	public void add(Message message) {
507		message.setConversation(this);
508		synchronized (this.messages) {
509			this.messages.add(message);
510		}
511	}
512
513	public void addAll(int index, List<Message> messages) {
514		synchronized (this.messages) {
515			this.messages.addAll(index, messages);
516		}
517	}
518
519	public class Smp {
520		public static final int STATUS_NONE = 0;
521		public static final int STATUS_CONTACT_REQUESTED = 1;
522		public static final int STATUS_WE_REQUESTED = 2;
523		public static final int STATUS_FAILED = 3;
524		public static final int STATUS_VERIFIED = 4;
525
526		public String secret = null;
527		public String hint = null;
528		public int status = 0;
529	}
530}