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