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