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