Conversation.java

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