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