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