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
253	public interface OnMessageFound {
254		public void onMessageFound(final Message message);
255	}
256
257	public Conversation(final String name, final Account account, final Jid contactJid,
258			final int mode) {
259		this(java.util.UUID.randomUUID().toString(), name, null, account
260				.getUuid(), contactJid, System.currentTimeMillis(),
261				STATUS_AVAILABLE, mode, "");
262		this.account = account;
263	}
264
265	public Conversation(final String uuid, final String name, final String contactUuid,
266			final String accountUuid, final Jid contactJid, final long created, final int status,
267			final int mode, final String attributes) {
268		this.uuid = uuid;
269		this.name = name;
270		this.contactUuid = contactUuid;
271		this.accountUuid = accountUuid;
272		this.contactJid = contactJid;
273		this.created = created;
274		this.status = status;
275		this.mode = mode;
276		try {
277			this.attributes = new JSONObject(attributes == null ? "" : attributes);
278		} catch (JSONException e) {
279			this.attributes = new JSONObject();
280		}
281	}
282
283	public boolean isRead() {
284		return (this.messages.size() == 0) || this.messages.get(this.messages.size() - 1).isRead();
285	}
286
287	public void markRead() {
288		for (int i = this.messages.size() - 1; i >= 0; --i) {
289			if (messages.get(i).isRead()) {
290				break;
291			}
292			this.messages.get(i).markRead();
293		}
294	}
295
296	public Message getLatestMarkableMessage() {
297		for (int i = this.messages.size() - 1; i >= 0; --i) {
298			if (this.messages.get(i).getStatus() <= Message.STATUS_RECEIVED
299					&& this.messages.get(i).markable) {
300				if (this.messages.get(i).isRead()) {
301					return null;
302				} else {
303					return this.messages.get(i);
304				}
305					}
306		}
307		return null;
308	}
309
310	public Message getLatestMessage() {
311		if (this.messages.size() == 0) {
312			Message message = new Message(this, "", Message.ENCRYPTION_NONE);
313			message.setTime(getCreated());
314			return message;
315		} else {
316			Message message = this.messages.get(this.messages.size() - 1);
317			message.setConversation(this);
318			return message;
319		}
320	}
321
322	public String getName() {
323		if (getMode() == MODE_MULTI) {
324			if (getMucOptions().getSubject() != null) {
325				return getMucOptions().getSubject();
326			} else if (bookmark != null && bookmark.getName() != null) {
327				return bookmark.getName();
328			} else {
329				String generatedName = getMucOptions().createNameFromParticipants();
330				if (generatedName != null) {
331					return generatedName;
332				} else {
333					return getJid().getLocalpart();
334				}
335			}
336		} else {
337			return this.getContact().getDisplayName();
338		}
339	}
340
341	public String getAccountUuid() {
342		return this.accountUuid;
343	}
344
345	public Account getAccount() {
346		return this.account;
347	}
348
349	public Contact getContact() {
350		return this.account.getRoster().getContact(this.contactJid);
351	}
352
353	public void setAccount(final Account account) {
354		this.account = account;
355	}
356
357	@Override
358	public Jid getJid() {
359		return this.contactJid;
360	}
361
362	public int getStatus() {
363		return this.status;
364	}
365
366	public long getCreated() {
367		return this.created;
368	}
369
370	public ContentValues getContentValues() {
371		ContentValues values = new ContentValues();
372		values.put(UUID, uuid);
373		values.put(NAME, name);
374		values.put(CONTACT, contactUuid);
375		values.put(ACCOUNT, accountUuid);
376		values.put(CONTACTJID, contactJid.toString());
377		values.put(CREATED, created);
378		values.put(STATUS, status);
379		values.put(MODE, mode);
380		values.put(ATTRIBUTES, attributes.toString());
381		return values;
382	}
383
384	public static Conversation fromCursor(Cursor cursor) {
385		Jid jid;
386		try {
387			jid = Jid.fromString(cursor.getString(cursor.getColumnIndex(CONTACTJID)), true);
388		} catch (final InvalidJidException e) {
389			// Borked DB..
390			jid = null;
391		}
392		return new Conversation(cursor.getString(cursor.getColumnIndex(UUID)),
393				cursor.getString(cursor.getColumnIndex(NAME)),
394				cursor.getString(cursor.getColumnIndex(CONTACT)),
395				cursor.getString(cursor.getColumnIndex(ACCOUNT)),
396				jid,
397				cursor.getLong(cursor.getColumnIndex(CREATED)),
398				cursor.getInt(cursor.getColumnIndex(STATUS)),
399				cursor.getInt(cursor.getColumnIndex(MODE)),
400				cursor.getString(cursor.getColumnIndex(ATTRIBUTES)));
401	}
402
403	public void setStatus(int status) {
404		this.status = status;
405	}
406
407	public int getMode() {
408		return this.mode;
409	}
410
411	public void setMode(int mode) {
412		this.mode = mode;
413	}
414
415	public SessionImpl startOtrSession(String presence, boolean sendStart) {
416		if (this.otrSession != null) {
417			return this.otrSession;
418		} else {
419			final SessionID sessionId = new SessionID(this.getJid().toBareJid().toString(),
420					presence,
421					"xmpp");
422			this.otrSession = new SessionImpl(sessionId, getAccount().getOtrEngine());
423			try {
424				if (sendStart) {
425					this.otrSession.startSession();
426					return this.otrSession;
427				}
428				return this.otrSession;
429			} catch (OtrException e) {
430				return null;
431			}
432		}
433
434	}
435
436	public SessionImpl getOtrSession() {
437		return this.otrSession;
438	}
439
440	public void resetOtrSession() {
441		this.otrFingerprint = null;
442		this.otrSession = null;
443		this.mSmp.hint = null;
444		this.mSmp.secret = null;
445		this.mSmp.status = Smp.STATUS_NONE;
446	}
447
448	public Smp smp() {
449		return mSmp;
450	}
451
452	public void startOtrIfNeeded() {
453		if (this.otrSession != null
454				&& this.otrSession.getSessionStatus() != SessionStatus.ENCRYPTED) {
455			try {
456				this.otrSession.startSession();
457			} catch (OtrException e) {
458				this.resetOtrSession();
459			}
460				}
461	}
462
463	public boolean endOtrIfNeeded() {
464		if (this.otrSession != null) {
465			if (this.otrSession.getSessionStatus() == SessionStatus.ENCRYPTED) {
466				try {
467					this.otrSession.endSession();
468					this.resetOtrSession();
469					return true;
470				} catch (OtrException e) {
471					this.resetOtrSession();
472					return false;
473				}
474			} else {
475				this.resetOtrSession();
476				return false;
477			}
478		} else {
479			return false;
480		}
481	}
482
483	public boolean hasValidOtrSession() {
484		return this.otrSession != null;
485	}
486
487	public synchronized String getOtrFingerprint() {
488		if (this.otrFingerprint == null) {
489			try {
490				if (getOtrSession() == null || getOtrSession().getSessionStatus() != SessionStatus.ENCRYPTED) {
491					return null;
492				}
493				DSAPublicKey remotePubKey = (DSAPublicKey) getOtrSession().getRemotePublicKey();
494				this.otrFingerprint = getAccount().getOtrEngine().getFingerprint(remotePubKey);
495			} catch (final OtrCryptoException | UnsupportedOperationException ignored) {
496				return null;
497			}
498		}
499		return this.otrFingerprint;
500	}
501
502	public boolean verifyOtrFingerprint() {
503		final String fingerprint = getOtrFingerprint();
504		if (fingerprint != null) {
505			getContact().addOtrFingerprint(fingerprint);
506			return true;
507		} else {
508			return false;
509		}
510	}
511
512	public boolean isOtrFingerprintVerified() {
513		return getContact().getOtrFingerprints().contains(getOtrFingerprint());
514	}
515
516	public synchronized MucOptions getMucOptions() {
517		if (this.mucOptions == null) {
518			this.mucOptions = new MucOptions(this);
519		}
520		return this.mucOptions;
521	}
522
523	public void resetMucOptions() {
524		this.mucOptions = null;
525	}
526
527	public void setContactJid(final Jid jid) {
528		this.contactJid = jid;
529	}
530
531	public void setNextCounterpart(Jid jid) {
532		this.nextCounterpart = jid;
533	}
534
535	public Jid getNextCounterpart() {
536		return this.nextCounterpart;
537	}
538
539	public int getLatestEncryption() {
540		int latestEncryption = this.getLatestMessage().getEncryption();
541		if ((latestEncryption == Message.ENCRYPTION_DECRYPTED)
542				|| (latestEncryption == Message.ENCRYPTION_DECRYPTION_FAILED)) {
543			return Message.ENCRYPTION_PGP;
544		} else {
545			return latestEncryption;
546		}
547	}
548
549	public int getNextEncryption(boolean force) {
550		int next = this.getIntAttribute(ATTRIBUTE_NEXT_ENCRYPTION, -1);
551		if (next == -1) {
552			int latest = this.getLatestEncryption();
553			if (latest == Message.ENCRYPTION_NONE) {
554				if (force && getMode() == MODE_SINGLE) {
555					return Message.ENCRYPTION_OTR;
556				} else if (getContact().getPresences().size() == 1) {
557					if (getContact().getOtrFingerprints().size() >= 1) {
558						return Message.ENCRYPTION_OTR;
559					} else {
560						return latest;
561					}
562				} else {
563					return latest;
564				}
565			} else {
566				return latest;
567			}
568		}
569		if (next == Message.ENCRYPTION_NONE && force
570				&& getMode() == MODE_SINGLE) {
571			return Message.ENCRYPTION_OTR;
572		} else {
573			return next;
574		}
575	}
576
577	public void setNextEncryption(int encryption) {
578		this.setAttribute(ATTRIBUTE_NEXT_ENCRYPTION, String.valueOf(encryption));
579	}
580
581	public String getNextMessage() {
582		if (this.nextMessage == null) {
583			return "";
584		} else {
585			return this.nextMessage;
586		}
587	}
588
589	public boolean smpRequested() {
590		return smp().status == Smp.STATUS_CONTACT_REQUESTED;
591	}
592
593	public void setNextMessage(String message) {
594		this.nextMessage = message;
595	}
596
597	public void setSymmetricKey(byte[] key) {
598		this.symmetricKey = key;
599	}
600
601	public byte[] getSymmetricKey() {
602		return this.symmetricKey;
603	}
604
605	public void setBookmark(Bookmark bookmark) {
606		this.bookmark = bookmark;
607		this.bookmark.setConversation(this);
608	}
609
610	public void deregisterWithBookmark() {
611		if (this.bookmark != null) {
612			this.bookmark.setConversation(null);
613		}
614	}
615
616	public Bookmark getBookmark() {
617		return this.bookmark;
618	}
619
620	public boolean hasDuplicateMessage(Message message) {
621		synchronized (this.messages) {
622			for (int i = this.messages.size() - 1; i >= 0; --i) {
623				if (this.messages.get(i).equals(message)) {
624					return true;
625				}
626			}
627		}
628		return false;
629	}
630
631	public Message findSentMessageWithBody(String body) {
632		synchronized (this.messages) {
633			for (int i = this.messages.size() - 1; i >= 0; --i) {
634				Message message = this.messages.get(i);
635				if ((message.getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_SEND) && message.getBody() != null && message.getBody().equals(body)) {
636					return message;
637				}
638			}
639			return null;
640		}
641	}
642
643	public boolean setLastMessageTransmitted(long value) {
644		long before = getLastMessageTransmitted();
645		if (value - before > 1000) {
646			this.setAttribute(ATTRIBUTE_LAST_MESSAGE_TRANSMITTED, String.valueOf(value));
647			return true;
648		} else {
649			return false;
650		}
651	}
652
653	public long getLastMessageTransmitted() {
654		long timestamp = getLongAttribute(ATTRIBUTE_LAST_MESSAGE_TRANSMITTED,0);
655		if (timestamp == 0) {
656			synchronized (this.messages) {
657				for(int i = this.messages.size() - 1; i >= 0; --i) {
658					Message message = this.messages.get(i);
659					if (message.getStatus() == Message.STATUS_RECEIVED) {
660						return message.getTimeSent();
661					}
662				}
663			}
664		}
665		return timestamp;
666	}
667
668	public void setMutedTill(long value) {
669		this.setAttribute(ATTRIBUTE_MUTED_TILL, String.valueOf(value));
670	}
671
672	public boolean isMuted() {
673		return System.currentTimeMillis() < this.getLongAttribute(ATTRIBUTE_MUTED_TILL, 0);
674	}
675
676	public boolean setAttribute(String key, String value) {
677		try {
678			this.attributes.put(key, value);
679			return true;
680		} catch (JSONException e) {
681			return false;
682		}
683	}
684
685	public String getAttribute(String key) {
686		try {
687			return this.attributes.getString(key);
688		} catch (JSONException e) {
689			return null;
690		}
691	}
692
693	public int getIntAttribute(String key, int defaultValue) {
694		String value = this.getAttribute(key);
695		if (value == null) {
696			return defaultValue;
697		} else {
698			try {
699				return Integer.parseInt(value);
700			} catch (NumberFormatException e) {
701				return defaultValue;
702			}
703		}
704	}
705
706	public long getLongAttribute(String key, long defaultValue) {
707		String value = this.getAttribute(key);
708		if (value == null) {
709			return defaultValue;
710		} else {
711			try {
712				return Long.parseLong(value);
713			} catch (NumberFormatException e) {
714				return defaultValue;
715			}
716		}
717	}
718
719	public void add(Message message) {
720		message.setConversation(this);
721		synchronized (this.messages) {
722			this.messages.add(message);
723		}
724	}
725
726	public void addAll(int index, List<Message> messages) {
727		synchronized (this.messages) {
728			this.messages.addAll(index, messages);
729		}
730	}
731
732	public void sort() {
733		synchronized (this.messages) {
734			Collections.sort(this.messages, new Comparator<Message>() {
735				@Override
736				public int compare(Message left, Message right) {
737					if (left.getTimeSent() < right.getTimeSent()) {
738						return -1;
739					} else if (left.getTimeSent() > right.getTimeSent()) {
740						return 1;
741					} else {
742						return 0;
743					}
744				}
745			});
746			for(Message message : this.messages) {
747				message.untie();
748			}
749		}
750	}
751
752	public int unreadCount() {
753		synchronized (this.messages) {
754			int count = 0;
755			for(int i = this.messages.size() - 1; i >= 0; --i) {
756				if (this.messages.get(i).isRead()) {
757					return count;
758				}
759				++count;
760			}
761			return count;
762		}
763	}
764
765	public class Smp {
766		public static final int STATUS_NONE = 0;
767		public static final int STATUS_CONTACT_REQUESTED = 1;
768		public static final int STATUS_WE_REQUESTED = 2;
769		public static final int STATUS_FAILED = 3;
770		public static final int STATUS_VERIFIED = 4;
771
772		public String secret = null;
773		public String hint = null;
774		public int status = 0;
775	}
776}