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 String getOtrFingerprint() {
440		if (this.otrFingerprint == null) {
441			try {
442				if (getOtrSession() == null) {
443					return "";
444				}
445				DSAPublicKey remotePubKey = (DSAPublicKey) getOtrSession()
446					.getRemotePublicKey();
447				StringBuilder builder = new StringBuilder(
448						new OtrCryptoEngineImpl().getFingerprint(remotePubKey));
449				builder.insert(8, " ");
450				builder.insert(17, " ");
451				builder.insert(26, " ");
452				builder.insert(35, " ");
453				this.otrFingerprint = builder.toString();
454			} catch (final OtrCryptoException ignored) {
455
456			}
457		}
458		return this.otrFingerprint;
459	}
460
461	public void verifyOtrFingerprint() {
462		getContact().addOtrFingerprint(getOtrFingerprint());
463	}
464
465	public boolean isOtrFingerprintVerified() {
466		return getContact().getOtrFingerprints().contains(getOtrFingerprint());
467	}
468
469	public synchronized MucOptions getMucOptions() {
470		if (this.mucOptions == null) {
471			this.mucOptions = new MucOptions(this);
472		}
473		return this.mucOptions;
474	}
475
476	public void resetMucOptions() {
477		this.mucOptions = null;
478	}
479
480	public void setContactJid(final Jid jid) {
481		this.contactJid = jid;
482	}
483
484	public void setNextCounterpart(Jid jid) {
485		this.nextCounterpart = jid;
486	}
487
488	public Jid getNextCounterpart() {
489		return this.nextCounterpart;
490	}
491
492	public int getLatestEncryption() {
493		int latestEncryption = this.getLatestMessage().getEncryption();
494		if ((latestEncryption == Message.ENCRYPTION_DECRYPTED)
495				|| (latestEncryption == Message.ENCRYPTION_DECRYPTION_FAILED)) {
496			return Message.ENCRYPTION_PGP;
497		} else {
498			return latestEncryption;
499		}
500	}
501
502	public int getNextEncryption(boolean force) {
503		int next = this.getIntAttribute(ATTRIBUTE_NEXT_ENCRYPTION, -1);
504		if (next == -1) {
505			int latest = this.getLatestEncryption();
506			if (latest == Message.ENCRYPTION_NONE) {
507				if (force && getMode() == MODE_SINGLE) {
508					return Message.ENCRYPTION_OTR;
509				} else if (getContact().getPresences().size() == 1) {
510					if (getContact().getOtrFingerprints().size() >= 1) {
511						return Message.ENCRYPTION_OTR;
512					} else {
513						return latest;
514					}
515				} else {
516					return latest;
517				}
518			} else {
519				return latest;
520			}
521		}
522		if (next == Message.ENCRYPTION_NONE && force
523				&& getMode() == MODE_SINGLE) {
524			return Message.ENCRYPTION_OTR;
525		} else {
526			return next;
527		}
528	}
529
530	public void setNextEncryption(int encryption) {
531		this.setAttribute(ATTRIBUTE_NEXT_ENCRYPTION, String.valueOf(encryption));
532	}
533
534	public String getNextMessage() {
535		if (this.nextMessage == null) {
536			return "";
537		} else {
538			return this.nextMessage;
539		}
540	}
541
542	public boolean smpRequested() {
543		return smp().status == Smp.STATUS_CONTACT_REQUESTED;
544	}
545
546	public void setNextMessage(String message) {
547		this.nextMessage = message;
548	}
549
550	public void setSymmetricKey(byte[] key) {
551		this.symmetricKey = key;
552	}
553
554	public byte[] getSymmetricKey() {
555		return this.symmetricKey;
556	}
557
558	public void setBookmark(Bookmark bookmark) {
559		this.bookmark = bookmark;
560		this.bookmark.setConversation(this);
561	}
562
563	public void deregisterWithBookmark() {
564		if (this.bookmark != null) {
565			this.bookmark.setConversation(null);
566		}
567	}
568
569	public Bookmark getBookmark() {
570		return this.bookmark;
571	}
572
573	public boolean hasDuplicateMessage(Message message) {
574		synchronized (this.messages) {
575			for (int i = this.messages.size() - 1; i >= 0; --i) {
576				if (this.messages.get(i).equals(message)) {
577					return true;
578				}
579			}
580		}
581		return false;
582	}
583
584	public Message findSentMessageWithBody(String body) {
585		synchronized (this.messages) {
586			for (int i = this.messages.size() - 1; i >= 0; --i) {
587				Message message = this.messages.get(i);
588				if ((message.getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_SEND) && message.getBody() != null && message.getBody().equals(body)) {
589					return message;
590				}
591			}
592			return null;
593		}
594	}
595
596	public boolean setLastMessageTransmitted(long value) {
597		long before = getLastMessageTransmitted();
598		if (value - before > 1000) {
599			this.setAttribute(ATTRIBUTE_LAST_MESSAGE_TRANSMITTED, String.valueOf(value));
600			return true;
601		} else {
602			return false;
603		}
604	}
605
606	public long getLastMessageTransmitted() {
607		long timestamp = getLongAttribute(ATTRIBUTE_LAST_MESSAGE_TRANSMITTED,0);
608		if (timestamp == 0) {
609			synchronized (this.messages) {
610				for(int i = this.messages.size() - 1; i >= 0; --i) {
611					Message message = this.messages.get(i);
612					if (message.getStatus() == Message.STATUS_RECEIVED) {
613						return message.getTimeSent();
614					}
615				}
616			}
617		}
618		return timestamp;
619	}
620
621	public void setMutedTill(long value) {
622		this.setAttribute(ATTRIBUTE_MUTED_TILL, String.valueOf(value));
623	}
624
625	public boolean isMuted() {
626		return SystemClock.elapsedRealtime() < this.getLongAttribute(
627				ATTRIBUTE_MUTED_TILL, 0);
628	}
629
630	public boolean setAttribute(String key, String value) {
631		try {
632			this.attributes.put(key, value);
633			return true;
634		} catch (JSONException e) {
635			return false;
636		}
637	}
638
639	public String getAttribute(String key) {
640		try {
641			return this.attributes.getString(key);
642		} catch (JSONException e) {
643			return null;
644		}
645	}
646
647	public int getIntAttribute(String key, int defaultValue) {
648		String value = this.getAttribute(key);
649		if (value == null) {
650			return defaultValue;
651		} else {
652			try {
653				return Integer.parseInt(value);
654			} catch (NumberFormatException e) {
655				return defaultValue;
656			}
657		}
658	}
659
660	public long getLongAttribute(String key, long defaultValue) {
661		String value = this.getAttribute(key);
662		if (value == null) {
663			return defaultValue;
664		} else {
665			try {
666				return Long.parseLong(value);
667			} catch (NumberFormatException e) {
668				return defaultValue;
669			}
670		}
671	}
672
673	public void add(Message message) {
674		message.setConversation(this);
675		synchronized (this.messages) {
676			this.messages.add(message);
677		}
678	}
679
680	public void addAll(int index, List<Message> messages) {
681		synchronized (this.messages) {
682			this.messages.addAll(index, messages);
683		}
684	}
685
686	public void sort() {
687		synchronized (this.messages) {
688			Collections.sort(this.messages, new Comparator<Message>() {
689				@Override
690				public int compare(Message left, Message right) {
691					if (left.getTimeSent() < right.getTimeSent()) {
692						return -1;
693					} else if (left.getTimeSent() > right.getTimeSent()) {
694						return 1;
695					} else {
696						return 0;
697					}
698				}
699			});
700			for(Message message : this.messages) {
701				message.untie();
702			}
703		}
704	}
705
706	public class Smp {
707		public static final int STATUS_NONE = 0;
708		public static final int STATUS_CONTACT_REQUESTED = 1;
709		public static final int STATUS_WE_REQUESTED = 2;
710		public static final int STATUS_FAILED = 3;
711		public static final int STATUS_FINISHED = 4;
712
713		public String secret = null;
714		public String hint = null;
715		public int status = 0;
716	}
717}