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