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