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.getName() != null) {
344				return bookmark.getName();
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 boolean setLastMessageTransmitted(long value) {
677		long before = getLastMessageTransmitted();
678		if (value - before > 1000) {
679			this.setAttribute(ATTRIBUTE_LAST_MESSAGE_TRANSMITTED, String.valueOf(value));
680			return true;
681		} else {
682			return false;
683		}
684	}
685
686	public long getLastMessageTransmitted() {
687		long timestamp = getLongAttribute(ATTRIBUTE_LAST_MESSAGE_TRANSMITTED,0);
688		if (timestamp == 0) {
689			synchronized (this.messages) {
690				for(int i = this.messages.size() - 1; i >= 0; --i) {
691					Message message = this.messages.get(i);
692					if (message.getStatus() == Message.STATUS_RECEIVED) {
693						return message.getTimeSent();
694					}
695				}
696			}
697		}
698		return timestamp;
699	}
700
701	public void setMutedTill(long value) {
702		this.setAttribute(ATTRIBUTE_MUTED_TILL, String.valueOf(value));
703	}
704
705	public boolean isMuted() {
706		return System.currentTimeMillis() < this.getLongAttribute(ATTRIBUTE_MUTED_TILL, 0);
707	}
708
709	public boolean setAttribute(String key, String value) {
710		try {
711			this.attributes.put(key, value);
712			return true;
713		} catch (JSONException e) {
714			return false;
715		}
716	}
717
718	public String getAttribute(String key) {
719		try {
720			return this.attributes.getString(key);
721		} catch (JSONException e) {
722			return null;
723		}
724	}
725
726	public int getIntAttribute(String key, int defaultValue) {
727		String value = this.getAttribute(key);
728		if (value == null) {
729			return defaultValue;
730		} else {
731			try {
732				return Integer.parseInt(value);
733			} catch (NumberFormatException e) {
734				return defaultValue;
735			}
736		}
737	}
738
739	public long getLongAttribute(String key, long defaultValue) {
740		String value = this.getAttribute(key);
741		if (value == null) {
742			return defaultValue;
743		} else {
744			try {
745				return Long.parseLong(value);
746			} catch (NumberFormatException e) {
747				return defaultValue;
748			}
749		}
750	}
751
752	public void add(Message message) {
753		message.setConversation(this);
754		synchronized (this.messages) {
755			this.messages.add(message);
756		}
757	}
758
759	public void addAll(int index, List<Message> messages) {
760		synchronized (this.messages) {
761			this.messages.addAll(index, messages);
762		}
763	}
764
765	public void sort() {
766		synchronized (this.messages) {
767			Collections.sort(this.messages, new Comparator<Message>() {
768				@Override
769				public int compare(Message left, Message right) {
770					if (left.getTimeSent() < right.getTimeSent()) {
771						return -1;
772					} else if (left.getTimeSent() > right.getTimeSent()) {
773						return 1;
774					} else {
775						return 0;
776					}
777				}
778			});
779			for(Message message : this.messages) {
780				message.untie();
781			}
782		}
783	}
784
785	public int unreadCount() {
786		synchronized (this.messages) {
787			int count = 0;
788			for(int i = this.messages.size() - 1; i >= 0; --i) {
789				if (this.messages.get(i).isRead()) {
790					return count;
791				}
792				++count;
793			}
794			return count;
795		}
796	}
797
798	public class Smp {
799		public static final int STATUS_NONE = 0;
800		public static final int STATUS_CONTACT_REQUESTED = 1;
801		public static final int STATUS_WE_REQUESTED = 2;
802		public static final int STATUS_FAILED = 3;
803		public static final int STATUS_VERIFIED = 4;
804
805		public String secret = null;
806		public String hint = null;
807		public int status = 0;
808	}
809}