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