Conversation.java

  1package eu.siacs.conversations.entities;
  2
  3import android.content.ContentValues;
  4import android.database.Cursor;
  5import android.os.SystemClock;
  6
  7import net.java.otr4j.OtrException;
  8import net.java.otr4j.crypto.OtrCryptoEngineImpl;
  9import net.java.otr4j.crypto.OtrCryptoException;
 10import net.java.otr4j.session.SessionID;
 11import net.java.otr4j.session.SessionImpl;
 12import net.java.otr4j.session.SessionStatus;
 13
 14import org.json.JSONException;
 15import org.json.JSONObject;
 16
 17import java.security.interfaces.DSAPublicKey;
 18import java.util.ArrayList;
 19import java.util.Collections;
 20import java.util.Comparator;
 21import java.util.List;
 22
 23import eu.siacs.conversations.Config;
 24import eu.siacs.conversations.xmpp.chatstate.ChatState;
 25import eu.siacs.conversations.xmpp.jid.InvalidJidException;
 26import eu.siacs.conversations.xmpp.jid.Jid;
 27
 28public class Conversation extends AbstractEntity implements Blockable {
 29	public static final String TABLENAME = "conversations";
 30
 31	public static final int STATUS_AVAILABLE = 0;
 32	public static final int STATUS_ARCHIVED = 1;
 33	public static final int STATUS_DELETED = 2;
 34
 35	public static final int MODE_MULTI = 1;
 36	public static final int MODE_SINGLE = 0;
 37
 38	public static final String NAME = "name";
 39	public static final String ACCOUNT = "accountUuid";
 40	public static final String CONTACT = "contactUuid";
 41	public static final String CONTACTJID = "contactJid";
 42	public static final String STATUS = "status";
 43	public static final String CREATED = "created";
 44	public static final String MODE = "mode";
 45	public static final String ATTRIBUTES = "attributes";
 46
 47	public static final String ATTRIBUTE_NEXT_ENCRYPTION = "next_encryption";
 48	public static final String ATTRIBUTE_MUC_PASSWORD = "muc_password";
 49	public static final String ATTRIBUTE_MUTED_TILL = "muted_till";
 50	public static final String ATTRIBUTE_LAST_MESSAGE_TRANSMITTED = "last_message_transmitted";
 51
 52	private String name;
 53	private String contactUuid;
 54	private String accountUuid;
 55	private Jid contactJid;
 56	private int status;
 57	private long created;
 58	private int mode;
 59
 60	private JSONObject attributes = new JSONObject();
 61
 62	private Jid nextCounterpart;
 63
 64	protected final ArrayList<Message> messages = new ArrayList<>();
 65	protected Account account = null;
 66
 67	private transient SessionImpl otrSession;
 68
 69	private transient String otrFingerprint = null;
 70	private Smp mSmp = new Smp();
 71
 72	private String nextMessage;
 73
 74	private transient MucOptions mucOptions = null;
 75
 76	private byte[] symmetricKey;
 77
 78	private Bookmark bookmark;
 79
 80	private boolean messagesLeftOnServer = true;
 81	private ChatState mOutgoingChatState = Config.DEFAULT_CHATSTATE;
 82	private ChatState mIncomingChatState = Config.DEFAULT_CHATSTATE;
 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 findUnsentMessagesWithOtrEncryption(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() == Message.ENCRYPTION_OTR)) {
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	}
223
224	@Override
225	public boolean isBlocked() {
226		return getContact().isBlocked();
227	}
228
229	@Override
230	public boolean isDomainBlocked() {
231		return getContact().isDomainBlocked();
232	}
233
234	@Override
235	public Jid getBlockedJid() {
236		return getContact().getBlockedJid();
237	}
238
239
240	public interface OnMessageFound {
241		public void onMessageFound(final Message message);
242	}
243
244	public Conversation(final String name, final Account account, final Jid contactJid,
245			final int mode) {
246		this(java.util.UUID.randomUUID().toString(), name, null, account
247				.getUuid(), contactJid, System.currentTimeMillis(),
248				STATUS_AVAILABLE, mode, "");
249		this.account = account;
250	}
251
252	public Conversation(final String uuid, final String name, final String contactUuid,
253			final String accountUuid, final Jid contactJid, final long created, final int status,
254			final int mode, final String attributes) {
255		this.uuid = uuid;
256		this.name = name;
257		this.contactUuid = contactUuid;
258		this.accountUuid = accountUuid;
259		this.contactJid = contactJid;
260		this.created = created;
261		this.status = status;
262		this.mode = mode;
263		try {
264			this.attributes = new JSONObject(attributes == null ? "" : attributes);
265		} catch (JSONException e) {
266			this.attributes = new JSONObject();
267		}
268	}
269
270	public boolean isRead() {
271		return (this.messages.size() == 0) || this.messages.get(this.messages.size() - 1).isRead();
272	}
273
274	public void markRead() {
275		for (int i = this.messages.size() - 1; i >= 0; --i) {
276			if (messages.get(i).isRead()) {
277				break;
278			}
279			this.messages.get(i).markRead();
280		}
281	}
282
283	public Message getLatestMarkableMessage() {
284		for (int i = this.messages.size() - 1; i >= 0; --i) {
285			if (this.messages.get(i).getStatus() <= Message.STATUS_RECEIVED
286					&& this.messages.get(i).markable) {
287				if (this.messages.get(i).isRead()) {
288					return null;
289				} else {
290					return this.messages.get(i);
291				}
292					}
293		}
294		return null;
295	}
296
297	public Message getLatestMessage() {
298		if (this.messages.size() == 0) {
299			Message message = new Message(this, "", Message.ENCRYPTION_NONE);
300			message.setTime(getCreated());
301			return message;
302		} else {
303			Message message = this.messages.get(this.messages.size() - 1);
304			message.setConversation(this);
305			return message;
306		}
307	}
308
309	public String getName() {
310		if (getMode() == MODE_MULTI) {
311			if (getMucOptions().getSubject() != null) {
312				return getMucOptions().getSubject();
313			} else if (bookmark != null && bookmark.getName() != null) {
314				return bookmark.getName();
315			} else {
316				String generatedName = getMucOptions().createNameFromParticipants();
317				if (generatedName != null) {
318					return generatedName;
319				} else {
320					return getJid().getLocalpart();
321				}
322			}
323		} else {
324			return this.getContact().getDisplayName();
325		}
326	}
327
328	public String getAccountUuid() {
329		return this.accountUuid;
330	}
331
332	public Account getAccount() {
333		return this.account;
334	}
335
336	public Contact getContact() {
337		return this.account.getRoster().getContact(this.contactJid);
338	}
339
340	public void setAccount(final Account account) {
341		this.account = account;
342	}
343
344	@Override
345	public Jid getJid() {
346		return this.contactJid;
347	}
348
349	public int getStatus() {
350		return this.status;
351	}
352
353	public long getCreated() {
354		return this.created;
355	}
356
357	public ContentValues getContentValues() {
358		ContentValues values = new ContentValues();
359		values.put(UUID, uuid);
360		values.put(NAME, name);
361		values.put(CONTACT, contactUuid);
362		values.put(ACCOUNT, accountUuid);
363		values.put(CONTACTJID, contactJid.toString());
364		values.put(CREATED, created);
365		values.put(STATUS, status);
366		values.put(MODE, mode);
367		values.put(ATTRIBUTES, attributes.toString());
368		return values;
369	}
370
371	public static Conversation fromCursor(Cursor cursor) {
372		Jid jid;
373		try {
374			jid = Jid.fromString(cursor.getString(cursor.getColumnIndex(CONTACTJID)));
375		} catch (final InvalidJidException e) {
376			// Borked DB..
377			jid = null;
378		}
379		return new Conversation(cursor.getString(cursor.getColumnIndex(UUID)),
380				cursor.getString(cursor.getColumnIndex(NAME)),
381				cursor.getString(cursor.getColumnIndex(CONTACT)),
382				cursor.getString(cursor.getColumnIndex(ACCOUNT)),
383				jid,
384				cursor.getLong(cursor.getColumnIndex(CREATED)),
385				cursor.getInt(cursor.getColumnIndex(STATUS)),
386				cursor.getInt(cursor.getColumnIndex(MODE)),
387				cursor.getString(cursor.getColumnIndex(ATTRIBUTES)));
388	}
389
390	public void setStatus(int status) {
391		this.status = status;
392	}
393
394	public int getMode() {
395		return this.mode;
396	}
397
398	public void setMode(int mode) {
399		this.mode = mode;
400	}
401
402	public SessionImpl startOtrSession(String presence, boolean sendStart) {
403		if (this.otrSession != null) {
404			return this.otrSession;
405		} else {
406			final SessionID sessionId = new SessionID(this.getJid().toBareJid().toString(),
407					presence,
408					"xmpp");
409			this.otrSession = new SessionImpl(sessionId, getAccount().getOtrEngine());
410			try {
411				if (sendStart) {
412					this.otrSession.startSession();
413					return this.otrSession;
414				}
415				return this.otrSession;
416			} catch (OtrException e) {
417				return null;
418			}
419		}
420
421	}
422
423	public SessionImpl getOtrSession() {
424		return this.otrSession;
425	}
426
427	public void resetOtrSession() {
428		this.otrFingerprint = null;
429		this.otrSession = null;
430		this.mSmp.hint = null;
431		this.mSmp.secret = null;
432		this.mSmp.status = Smp.STATUS_NONE;
433	}
434
435	public Smp smp() {
436		return mSmp;
437	}
438
439	public void startOtrIfNeeded() {
440		if (this.otrSession != null
441				&& this.otrSession.getSessionStatus() != SessionStatus.ENCRYPTED) {
442			try {
443				this.otrSession.startSession();
444			} catch (OtrException e) {
445				this.resetOtrSession();
446			}
447				}
448	}
449
450	public boolean endOtrIfNeeded() {
451		if (this.otrSession != null) {
452			if (this.otrSession.getSessionStatus() == SessionStatus.ENCRYPTED) {
453				try {
454					this.otrSession.endSession();
455					this.resetOtrSession();
456					return true;
457				} catch (OtrException e) {
458					this.resetOtrSession();
459					return false;
460				}
461			} else {
462				this.resetOtrSession();
463				return false;
464			}
465		} else {
466			return false;
467		}
468	}
469
470	public boolean hasValidOtrSession() {
471		return this.otrSession != null;
472	}
473
474	public synchronized String getOtrFingerprint() {
475		if (this.otrFingerprint == null) {
476			try {
477				if (getOtrSession() == null || getOtrSession().getSessionStatus() != SessionStatus.ENCRYPTED) {
478					return null;
479				}
480				DSAPublicKey remotePubKey = (DSAPublicKey) getOtrSession().getRemotePublicKey();
481				this.otrFingerprint = getAccount().getOtrEngine().getFingerprint(remotePubKey);
482			} catch (final OtrCryptoException | UnsupportedOperationException ignored) {
483				return null;
484			}
485		}
486		return this.otrFingerprint;
487	}
488
489	public boolean verifyOtrFingerprint() {
490		final String fingerprint = getOtrFingerprint();
491		if (fingerprint != null) {
492			getContact().addOtrFingerprint(fingerprint);
493			return true;
494		} else {
495			return false;
496		}
497	}
498
499	public boolean isOtrFingerprintVerified() {
500		return getContact().getOtrFingerprints().contains(getOtrFingerprint());
501	}
502
503	public synchronized MucOptions getMucOptions() {
504		if (this.mucOptions == null) {
505			this.mucOptions = new MucOptions(this);
506		}
507		return this.mucOptions;
508	}
509
510	public void resetMucOptions() {
511		this.mucOptions = null;
512	}
513
514	public void setContactJid(final Jid jid) {
515		this.contactJid = jid;
516	}
517
518	public void setNextCounterpart(Jid jid) {
519		this.nextCounterpart = jid;
520	}
521
522	public Jid getNextCounterpart() {
523		return this.nextCounterpart;
524	}
525
526	public int getLatestEncryption() {
527		int latestEncryption = this.getLatestMessage().getEncryption();
528		if ((latestEncryption == Message.ENCRYPTION_DECRYPTED)
529				|| (latestEncryption == Message.ENCRYPTION_DECRYPTION_FAILED)) {
530			return Message.ENCRYPTION_PGP;
531		} else {
532			return latestEncryption;
533		}
534	}
535
536	public int getNextEncryption(boolean force) {
537		int next = this.getIntAttribute(ATTRIBUTE_NEXT_ENCRYPTION, -1);
538		if (next == -1) {
539			int latest = this.getLatestEncryption();
540			if (latest == Message.ENCRYPTION_NONE) {
541				if (force && getMode() == MODE_SINGLE) {
542					return Message.ENCRYPTION_OTR;
543				} else if (getContact().getPresences().size() == 1) {
544					if (getContact().getOtrFingerprints().size() >= 1) {
545						return Message.ENCRYPTION_OTR;
546					} else {
547						return latest;
548					}
549				} else {
550					return latest;
551				}
552			} else {
553				return latest;
554			}
555		}
556		if (next == Message.ENCRYPTION_NONE && force
557				&& getMode() == MODE_SINGLE) {
558			return Message.ENCRYPTION_OTR;
559		} else {
560			return next;
561		}
562	}
563
564	public void setNextEncryption(int encryption) {
565		this.setAttribute(ATTRIBUTE_NEXT_ENCRYPTION, String.valueOf(encryption));
566	}
567
568	public String getNextMessage() {
569		if (this.nextMessage == null) {
570			return "";
571		} else {
572			return this.nextMessage;
573		}
574	}
575
576	public boolean smpRequested() {
577		return smp().status == Smp.STATUS_CONTACT_REQUESTED;
578	}
579
580	public void setNextMessage(String message) {
581		this.nextMessage = message;
582	}
583
584	public void setSymmetricKey(byte[] key) {
585		this.symmetricKey = key;
586	}
587
588	public byte[] getSymmetricKey() {
589		return this.symmetricKey;
590	}
591
592	public void setBookmark(Bookmark bookmark) {
593		this.bookmark = bookmark;
594		this.bookmark.setConversation(this);
595	}
596
597	public void deregisterWithBookmark() {
598		if (this.bookmark != null) {
599			this.bookmark.setConversation(null);
600		}
601	}
602
603	public Bookmark getBookmark() {
604		return this.bookmark;
605	}
606
607	public boolean hasDuplicateMessage(Message message) {
608		synchronized (this.messages) {
609			for (int i = this.messages.size() - 1; i >= 0; --i) {
610				if (this.messages.get(i).equals(message)) {
611					return true;
612				}
613			}
614		}
615		return false;
616	}
617
618	public Message findSentMessageWithBody(String body) {
619		synchronized (this.messages) {
620			for (int i = this.messages.size() - 1; i >= 0; --i) {
621				Message message = this.messages.get(i);
622				if ((message.getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_SEND) && message.getBody() != null && message.getBody().equals(body)) {
623					return message;
624				}
625			}
626			return null;
627		}
628	}
629
630	public boolean setLastMessageTransmitted(long value) {
631		long before = getLastMessageTransmitted();
632		if (value - before > 1000) {
633			this.setAttribute(ATTRIBUTE_LAST_MESSAGE_TRANSMITTED, String.valueOf(value));
634			return true;
635		} else {
636			return false;
637		}
638	}
639
640	public long getLastMessageTransmitted() {
641		long timestamp = getLongAttribute(ATTRIBUTE_LAST_MESSAGE_TRANSMITTED,0);
642		if (timestamp == 0) {
643			synchronized (this.messages) {
644				for(int i = this.messages.size() - 1; i >= 0; --i) {
645					Message message = this.messages.get(i);
646					if (message.getStatus() == Message.STATUS_RECEIVED) {
647						return message.getTimeSent();
648					}
649				}
650			}
651		}
652		return timestamp;
653	}
654
655	public void setMutedTill(long value) {
656		this.setAttribute(ATTRIBUTE_MUTED_TILL, String.valueOf(value));
657	}
658
659	public boolean isMuted() {
660		return System.currentTimeMillis() < this.getLongAttribute(ATTRIBUTE_MUTED_TILL, 0);
661	}
662
663	public boolean setAttribute(String key, String value) {
664		try {
665			this.attributes.put(key, value);
666			return true;
667		} catch (JSONException e) {
668			return false;
669		}
670	}
671
672	public String getAttribute(String key) {
673		try {
674			return this.attributes.getString(key);
675		} catch (JSONException e) {
676			return null;
677		}
678	}
679
680	public int getIntAttribute(String key, int defaultValue) {
681		String value = this.getAttribute(key);
682		if (value == null) {
683			return defaultValue;
684		} else {
685			try {
686				return Integer.parseInt(value);
687			} catch (NumberFormatException e) {
688				return defaultValue;
689			}
690		}
691	}
692
693	public long getLongAttribute(String key, long defaultValue) {
694		String value = this.getAttribute(key);
695		if (value == null) {
696			return defaultValue;
697		} else {
698			try {
699				return Long.parseLong(value);
700			} catch (NumberFormatException e) {
701				return defaultValue;
702			}
703		}
704	}
705
706	public void add(Message message) {
707		message.setConversation(this);
708		synchronized (this.messages) {
709			this.messages.add(message);
710		}
711	}
712
713	public void addAll(int index, List<Message> messages) {
714		synchronized (this.messages) {
715			this.messages.addAll(index, messages);
716		}
717	}
718
719	public void sort() {
720		synchronized (this.messages) {
721			Collections.sort(this.messages, new Comparator<Message>() {
722				@Override
723				public int compare(Message left, Message right) {
724					if (left.getTimeSent() < right.getTimeSent()) {
725						return -1;
726					} else if (left.getTimeSent() > right.getTimeSent()) {
727						return 1;
728					} else {
729						return 0;
730					}
731				}
732			});
733			for(Message message : this.messages) {
734				message.untie();
735			}
736		}
737	}
738
739	public class Smp {
740		public static final int STATUS_NONE = 0;
741		public static final int STATUS_CONTACT_REQUESTED = 1;
742		public static final int STATUS_WE_REQUESTED = 2;
743		public static final int STATUS_FAILED = 3;
744		public static final int STATUS_VERIFIED = 4;
745
746		public String secret = null;
747		public String hint = null;
748		public int status = 0;
749	}
750}