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