Conversation.java

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