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 findUnsentMessagesWithEncryption(int encryptionType, 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() == encryptionType)) {
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	/**
523	 * short for is Private and Non-anonymous
524	 */
525	public boolean isPnNA() {
526		return mode == MODE_SINGLE || (getMucOptions().membersOnly() && getMucOptions().nonanonymous());
527	}
528
529	public synchronized MucOptions getMucOptions() {
530		if (this.mucOptions == null) {
531			this.mucOptions = new MucOptions(this);
532		}
533		return this.mucOptions;
534	}
535
536	public void resetMucOptions() {
537		this.mucOptions = null;
538	}
539
540	public void setContactJid(final Jid jid) {
541		this.contactJid = jid;
542	}
543
544	public void setNextCounterpart(Jid jid) {
545		this.nextCounterpart = jid;
546	}
547
548	public Jid getNextCounterpart() {
549		return this.nextCounterpart;
550	}
551
552	private int getMostRecentlyUsedOutgoingEncryption() {
553		synchronized (this.messages) {
554			for(int i = this.messages.size() -1; i >= 0; --i) {
555				final Message m = this.messages.get(i);
556				if (!m.isCarbon() && m.getStatus() != Message.STATUS_RECEIVED) {
557					final int e = m.getEncryption();
558					if (e == Message.ENCRYPTION_DECRYPTED || e == Message.ENCRYPTION_DECRYPTION_FAILED) {
559						return Message.ENCRYPTION_PGP;
560					} else {
561						return e;
562					}
563				}
564			}
565		}
566		return Message.ENCRYPTION_NONE;
567	}
568
569	private int getMostRecentlyUsedIncomingEncryption() {
570		synchronized (this.messages) {
571			for(int i = this.messages.size() -1; i >= 0; --i) {
572				final Message m = this.messages.get(i);
573				if (m.getStatus() == Message.STATUS_RECEIVED) {
574					final int e = m.getEncryption();
575					if (e == Message.ENCRYPTION_DECRYPTED || e == Message.ENCRYPTION_DECRYPTION_FAILED) {
576						return Message.ENCRYPTION_PGP;
577					} else {
578						return e;
579					}
580				}
581			}
582		}
583		return Message.ENCRYPTION_NONE;
584	}
585
586	public int getNextEncryption() {
587		int next = this.getIntAttribute(ATTRIBUTE_NEXT_ENCRYPTION, -1);
588		if (next == -1) {
589			int outgoing = this.getMostRecentlyUsedOutgoingEncryption();
590			if (outgoing == Message.ENCRYPTION_NONE) {
591				return this.getMostRecentlyUsedIncomingEncryption();
592			} else {
593				return outgoing;
594			}
595		}
596		return next;
597	}
598
599	public void setNextEncryption(int encryption) {
600		this.setAttribute(ATTRIBUTE_NEXT_ENCRYPTION, String.valueOf(encryption));
601	}
602
603	public String getNextMessage() {
604		if (this.nextMessage == null) {
605			return "";
606		} else {
607			return this.nextMessage;
608		}
609	}
610
611	public boolean smpRequested() {
612		return smp().status == Smp.STATUS_CONTACT_REQUESTED;
613	}
614
615	public void setNextMessage(String message) {
616		this.nextMessage = message;
617	}
618
619	public void setSymmetricKey(byte[] key) {
620		this.symmetricKey = key;
621	}
622
623	public byte[] getSymmetricKey() {
624		return this.symmetricKey;
625	}
626
627	public void setBookmark(Bookmark bookmark) {
628		this.bookmark = bookmark;
629		this.bookmark.setConversation(this);
630	}
631
632	public void deregisterWithBookmark() {
633		if (this.bookmark != null) {
634			this.bookmark.setConversation(null);
635		}
636	}
637
638	public Bookmark getBookmark() {
639		return this.bookmark;
640	}
641
642	public boolean hasDuplicateMessage(Message message) {
643		synchronized (this.messages) {
644			for (int i = this.messages.size() - 1; i >= 0; --i) {
645				if (this.messages.get(i).equals(message)) {
646					return true;
647				}
648			}
649		}
650		return false;
651	}
652
653	public Message findSentMessageWithBody(String body) {
654		synchronized (this.messages) {
655			for (int i = this.messages.size() - 1; i >= 0; --i) {
656				Message message = this.messages.get(i);
657				if ((message.getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_SEND) && message.getBody() != null && message.getBody().equals(body)) {
658					return message;
659				}
660			}
661			return null;
662		}
663	}
664
665	public boolean setLastMessageTransmitted(long value) {
666		long before = getLastMessageTransmitted();
667		if (value - before > 1000) {
668			this.setAttribute(ATTRIBUTE_LAST_MESSAGE_TRANSMITTED, String.valueOf(value));
669			return true;
670		} else {
671			return false;
672		}
673	}
674
675	public long getLastMessageTransmitted() {
676		long timestamp = getLongAttribute(ATTRIBUTE_LAST_MESSAGE_TRANSMITTED,0);
677		if (timestamp == 0) {
678			synchronized (this.messages) {
679				for(int i = this.messages.size() - 1; i >= 0; --i) {
680					Message message = this.messages.get(i);
681					if (message.getStatus() == Message.STATUS_RECEIVED) {
682						return message.getTimeSent();
683					}
684				}
685			}
686		}
687		return timestamp;
688	}
689
690	public void setMutedTill(long value) {
691		this.setAttribute(ATTRIBUTE_MUTED_TILL, String.valueOf(value));
692	}
693
694	public boolean isMuted() {
695		return System.currentTimeMillis() < this.getLongAttribute(ATTRIBUTE_MUTED_TILL, 0);
696	}
697
698	public boolean setAttribute(String key, String value) {
699		try {
700			this.attributes.put(key, value);
701			return true;
702		} catch (JSONException e) {
703			return false;
704		}
705	}
706
707	public String getAttribute(String key) {
708		try {
709			return this.attributes.getString(key);
710		} catch (JSONException e) {
711			return null;
712		}
713	}
714
715	public int getIntAttribute(String key, int defaultValue) {
716		String value = this.getAttribute(key);
717		if (value == null) {
718			return defaultValue;
719		} else {
720			try {
721				return Integer.parseInt(value);
722			} catch (NumberFormatException e) {
723				return defaultValue;
724			}
725		}
726	}
727
728	public long getLongAttribute(String key, long defaultValue) {
729		String value = this.getAttribute(key);
730		if (value == null) {
731			return defaultValue;
732		} else {
733			try {
734				return Long.parseLong(value);
735			} catch (NumberFormatException e) {
736				return defaultValue;
737			}
738		}
739	}
740
741	public void add(Message message) {
742		message.setConversation(this);
743		synchronized (this.messages) {
744			this.messages.add(message);
745		}
746	}
747
748	public void addAll(int index, List<Message> messages) {
749		synchronized (this.messages) {
750			this.messages.addAll(index, messages);
751		}
752	}
753
754	public void sort() {
755		synchronized (this.messages) {
756			Collections.sort(this.messages, new Comparator<Message>() {
757				@Override
758				public int compare(Message left, Message right) {
759					if (left.getTimeSent() < right.getTimeSent()) {
760						return -1;
761					} else if (left.getTimeSent() > right.getTimeSent()) {
762						return 1;
763					} else {
764						return 0;
765					}
766				}
767			});
768			for(Message message : this.messages) {
769				message.untie();
770			}
771		}
772	}
773
774	public int unreadCount() {
775		synchronized (this.messages) {
776			int count = 0;
777			for(int i = this.messages.size() - 1; i >= 0; --i) {
778				if (this.messages.get(i).isRead()) {
779					return count;
780				}
781				++count;
782			}
783			return count;
784		}
785	}
786
787	public class Smp {
788		public static final int STATUS_NONE = 0;
789		public static final int STATUS_CONTACT_REQUESTED = 1;
790		public static final int STATUS_WE_REQUESTED = 2;
791		public static final int STATUS_FAILED = 3;
792		public static final int STATUS_VERIFIED = 4;
793
794		public String secret = null;
795		public String hint = null;
796		public int status = 0;
797	}
798}