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