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