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