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