Conversation.java

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