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.crypto.axolotl.AxolotlService;
 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_ALWAYS_NOTIFY = "always_notify";
 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	private String mLastReceivedOtrMessageId = null;
 84	private String mFirstMamReference = null;
 85	private Message correctingMessage;
 86
 87	public boolean hasMessagesLeftOnServer() {
 88		return messagesLeftOnServer;
 89	}
 90
 91	public void setHasMessagesLeftOnServer(boolean value) {
 92		this.messagesLeftOnServer = value;
 93	}
 94
 95	public Message findUnsentMessageWithUuid(String uuid) {
 96		synchronized(this.messages) {
 97			for (final Message message : this.messages) {
 98				final int s = message.getStatus();
 99				if ((s == Message.STATUS_UNSEND || s == Message.STATUS_WAITING) && message.getUuid().equals(uuid)) {
100					return message;
101				}
102			}
103		}
104		return null;
105	}
106
107	public void findWaitingMessages(OnMessageFound onMessageFound) {
108		synchronized (this.messages) {
109			for(Message message : this.messages) {
110				if (message.getStatus() == Message.STATUS_WAITING) {
111					onMessageFound.onMessageFound(message);
112				}
113			}
114		}
115	}
116
117	public void findUnreadMessages(OnMessageFound onMessageFound) {
118		synchronized (this.messages) {
119			for(Message message : this.messages) {
120				if (!message.isRead()) {
121					onMessageFound.onMessageFound(message);
122				}
123			}
124		}
125	}
126
127	public void findMessagesWithFiles(final OnMessageFound onMessageFound) {
128		synchronized (this.messages) {
129			for (final Message message : this.messages) {
130				if ((message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE)
131						&& message.getEncryption() != Message.ENCRYPTION_PGP) {
132					onMessageFound.onMessageFound(message);
133						}
134			}
135		}
136	}
137
138	public Message findMessageWithFileAndUuid(final String uuid) {
139		synchronized (this.messages) {
140			for (final Message message : this.messages) {
141				if ((message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE)
142						&& message.getEncryption() != Message.ENCRYPTION_PGP
143						&& message.getUuid().equals(uuid)) {
144					return message;
145				}
146			}
147		}
148		return null;
149	}
150
151	public void clearMessages() {
152		synchronized (this.messages) {
153			this.messages.clear();
154		}
155	}
156
157	public boolean setIncomingChatState(ChatState state) {
158		if (this.mIncomingChatState == state) {
159			return false;
160		}
161		this.mIncomingChatState = state;
162		return true;
163	}
164
165	public ChatState getIncomingChatState() {
166		return this.mIncomingChatState;
167	}
168
169	public boolean setOutgoingChatState(ChatState state) {
170		if (mode == MODE_MULTI) {
171			return false;
172		}
173		if (this.mOutgoingChatState != state) {
174			this.mOutgoingChatState = state;
175			return true;
176		} else {
177			return false;
178		}
179	}
180
181	public ChatState getOutgoingChatState() {
182		return this.mOutgoingChatState;
183	}
184
185	public void trim() {
186		synchronized (this.messages) {
187			final int size = messages.size();
188			final int maxsize = Config.PAGE_SIZE * Config.MAX_NUM_PAGES;
189			if (size > maxsize) {
190				this.messages.subList(0, size - maxsize).clear();
191			}
192		}
193	}
194
195	public void findUnsentMessagesWithEncryption(int encryptionType, OnMessageFound onMessageFound) {
196		synchronized (this.messages) {
197			for (Message message : this.messages) {
198				if ((message.getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_WAITING)
199						&& (message.getEncryption() == encryptionType)) {
200					onMessageFound.onMessageFound(message);
201				}
202			}
203		}
204	}
205
206	public void findUnsentTextMessages(OnMessageFound onMessageFound) {
207		synchronized (this.messages) {
208			for (Message message : this.messages) {
209				if (message.getType() != Message.TYPE_IMAGE
210						&& message.getStatus() == Message.STATUS_UNSEND) {
211					onMessageFound.onMessageFound(message);
212						}
213			}
214		}
215	}
216
217	public Message findSentMessageWithUuidOrRemoteId(String id) {
218		synchronized (this.messages) {
219			for (Message message : this.messages) {
220				if (id.equals(message.getUuid())
221						|| (message.getStatus() >= Message.STATUS_SEND
222						&& id.equals(message.getRemoteMsgId()))) {
223					return message;
224				}
225			}
226		}
227		return null;
228	}
229
230	public Message findMessageWithRemoteIdAndCounterpart(String id, Jid counterpart, boolean received, boolean carbon) {
231		synchronized (this.messages) {
232			for(int i = this.messages.size() - 1; i >= 0; --i) {
233				Message message = messages.get(i);
234				if (counterpart.equals(message.getCounterpart())
235						&& ((message.getStatus() == Message.STATUS_RECEIVED) == received)
236						&& (carbon == message.isCarbon() || received) ) {
237					if (id.equals(message.getRemoteMsgId())) {
238						return message;
239					} else {
240						return null;
241					}
242				}
243			}
244		}
245		return null;
246	}
247
248	public Message findSentMessageWithUuid(String id) {
249		synchronized (this.messages) {
250			for (Message message : this.messages) {
251				if (id.equals(message.getUuid())) {
252					return message;
253				}
254			}
255		}
256		return null;
257	}
258
259	public void populateWithMessages(final List<Message> messages) {
260		synchronized (this.messages) {
261			messages.clear();
262			messages.addAll(this.messages);
263		}
264		for(Iterator<Message> iterator = messages.iterator(); iterator.hasNext();) {
265			if (iterator.next().wasMergedIntoPrevious()) {
266				iterator.remove();
267			}
268		}
269	}
270
271	@Override
272	public boolean isBlocked() {
273		return getContact().isBlocked();
274	}
275
276	@Override
277	public boolean isDomainBlocked() {
278		return getContact().isDomainBlocked();
279	}
280
281	@Override
282	public Jid getBlockedJid() {
283		return getContact().getBlockedJid();
284	}
285
286	public String getLastReceivedOtrMessageId() {
287		return this.mLastReceivedOtrMessageId;
288	}
289
290	public void setLastReceivedOtrMessageId(String id) {
291		this.mLastReceivedOtrMessageId = id;
292	}
293
294	public int countMessages() {
295		synchronized (this.messages) {
296			return this.messages.size();
297		}
298	}
299
300	public void setFirstMamReference(String reference) {
301		this.mFirstMamReference = reference;
302	}
303
304	public String getFirstMamReference() {
305		return this.mFirstMamReference;
306	}
307
308	public void setLastClearHistory(long time) {
309		setAttribute("last_clear_history",String.valueOf(time));
310	}
311
312	public long getLastClearHistory() {
313		return getLongAttribute("last_clear_history", 0);
314	}
315
316	public void setCorrectingMessage(Message correctingMessage) {
317		this.correctingMessage = correctingMessage;
318	}
319
320	public Message getCorrectingMessage() {
321		return this.correctingMessage;
322	}
323
324	public interface OnMessageFound {
325		void onMessageFound(final Message message);
326	}
327
328	public Conversation(final String name, final Account account, final Jid contactJid,
329			final int mode) {
330		this(java.util.UUID.randomUUID().toString(), name, null, account
331				.getUuid(), contactJid, System.currentTimeMillis(),
332				STATUS_AVAILABLE, mode, "");
333		this.account = account;
334	}
335
336	public Conversation(final String uuid, final String name, final String contactUuid,
337			final String accountUuid, final Jid contactJid, final long created, final int status,
338			final int mode, final String attributes) {
339		this.uuid = uuid;
340		this.name = name;
341		this.contactUuid = contactUuid;
342		this.accountUuid = accountUuid;
343		this.contactJid = contactJid;
344		this.created = created;
345		this.status = status;
346		this.mode = mode;
347		try {
348			this.attributes = new JSONObject(attributes == null ? "" : attributes);
349		} catch (JSONException e) {
350			this.attributes = new JSONObject();
351		}
352	}
353
354	public boolean isRead() {
355		return (this.messages.size() == 0) || this.messages.get(this.messages.size() - 1).isRead();
356	}
357
358	public List<Message> markRead() {
359		final List<Message> unread = new ArrayList<>();
360		synchronized (this.messages) {
361			for(Message message : this.messages) {
362				if (!message.isRead()) {
363					message.markRead();
364					unread.add(message);
365				}
366			}
367		}
368		return unread;
369	}
370
371	public Message getLatestMarkableMessage() {
372		for (int i = this.messages.size() - 1; i >= 0; --i) {
373			if (this.messages.get(i).getStatus() <= Message.STATUS_RECEIVED
374					&& this.messages.get(i).markable) {
375				if (this.messages.get(i).isRead()) {
376					return null;
377				} else {
378					return this.messages.get(i);
379				}
380					}
381		}
382		return null;
383	}
384
385	public Message getLatestMessage() {
386		if (this.messages.size() == 0) {
387			Message message = new Message(this, "", Message.ENCRYPTION_NONE);
388			message.setTime(getCreated());
389			return message;
390		} else {
391			Message message = this.messages.get(this.messages.size() - 1);
392			message.setConversation(this);
393			return message;
394		}
395	}
396
397	public String getName() {
398		if (getMode() == MODE_MULTI) {
399			if (getMucOptions().getSubject() != null) {
400				return getMucOptions().getSubject();
401			} else if (bookmark != null && bookmark.getBookmarkName() != null) {
402				return bookmark.getBookmarkName();
403			} else {
404				String generatedName = getMucOptions().createNameFromParticipants();
405				if (generatedName != null) {
406					return generatedName;
407				} else {
408					return getJid().getLocalpart();
409				}
410			}
411		} else {
412			return this.getContact().getDisplayName();
413		}
414	}
415
416	public String getAccountUuid() {
417		return this.accountUuid;
418	}
419
420	public Account getAccount() {
421		return this.account;
422	}
423
424	public Contact getContact() {
425		return this.account.getRoster().getContact(this.contactJid);
426	}
427
428	public void setAccount(final Account account) {
429		this.account = account;
430	}
431
432	@Override
433	public Jid getJid() {
434		return this.contactJid;
435	}
436
437	public int getStatus() {
438		return this.status;
439	}
440
441	public long getCreated() {
442		return this.created;
443	}
444
445	public ContentValues getContentValues() {
446		ContentValues values = new ContentValues();
447		values.put(UUID, uuid);
448		values.put(NAME, name);
449		values.put(CONTACT, contactUuid);
450		values.put(ACCOUNT, accountUuid);
451		values.put(CONTACTJID, contactJid.toString());
452		values.put(CREATED, created);
453		values.put(STATUS, status);
454		values.put(MODE, mode);
455		values.put(ATTRIBUTES, attributes.toString());
456		return values;
457	}
458
459	public static Conversation fromCursor(Cursor cursor) {
460		Jid jid;
461		try {
462			jid = Jid.fromString(cursor.getString(cursor.getColumnIndex(CONTACTJID)), true);
463		} catch (final InvalidJidException e) {
464			// Borked DB..
465			jid = null;
466		}
467		return new Conversation(cursor.getString(cursor.getColumnIndex(UUID)),
468				cursor.getString(cursor.getColumnIndex(NAME)),
469				cursor.getString(cursor.getColumnIndex(CONTACT)),
470				cursor.getString(cursor.getColumnIndex(ACCOUNT)),
471				jid,
472				cursor.getLong(cursor.getColumnIndex(CREATED)),
473				cursor.getInt(cursor.getColumnIndex(STATUS)),
474				cursor.getInt(cursor.getColumnIndex(MODE)),
475				cursor.getString(cursor.getColumnIndex(ATTRIBUTES)));
476	}
477
478	public void setStatus(int status) {
479		this.status = status;
480	}
481
482	public int getMode() {
483		return this.mode;
484	}
485
486	public void setMode(int mode) {
487		this.mode = mode;
488	}
489
490	public SessionImpl startOtrSession(String presence, boolean sendStart) {
491		if (this.otrSession != null) {
492			return this.otrSession;
493		} else {
494			final SessionID sessionId = new SessionID(this.getJid().toBareJid().toString(),
495					presence,
496					"xmpp");
497			this.otrSession = new SessionImpl(sessionId, getAccount().getOtrService());
498			try {
499				if (sendStart) {
500					this.otrSession.startSession();
501					return this.otrSession;
502				}
503				return this.otrSession;
504			} catch (OtrException e) {
505				return null;
506			}
507		}
508
509	}
510
511	public SessionImpl getOtrSession() {
512		return this.otrSession;
513	}
514
515	public void resetOtrSession() {
516		this.otrFingerprint = null;
517		this.otrSession = null;
518		this.mSmp.hint = null;
519		this.mSmp.secret = null;
520		this.mSmp.status = Smp.STATUS_NONE;
521	}
522
523	public Smp smp() {
524		return mSmp;
525	}
526
527	public boolean startOtrIfNeeded() {
528		if (this.otrSession != null && this.otrSession.getSessionStatus() != SessionStatus.ENCRYPTED) {
529			try {
530				this.otrSession.startSession();
531				return true;
532			} catch (OtrException e) {
533				this.resetOtrSession();
534				return false;
535			}
536		} else {
537			return true;
538		}
539	}
540
541	public boolean endOtrIfNeeded() {
542		if (this.otrSession != null) {
543			if (this.otrSession.getSessionStatus() == SessionStatus.ENCRYPTED) {
544				try {
545					this.otrSession.endSession();
546					this.resetOtrSession();
547					return true;
548				} catch (OtrException e) {
549					this.resetOtrSession();
550					return false;
551				}
552			} else {
553				this.resetOtrSession();
554				return false;
555			}
556		} else {
557			return false;
558		}
559	}
560
561	public boolean hasValidOtrSession() {
562		return this.otrSession != null;
563	}
564
565	public synchronized String getOtrFingerprint() {
566		if (this.otrFingerprint == null) {
567			try {
568				if (getOtrSession() == null || getOtrSession().getSessionStatus() != SessionStatus.ENCRYPTED) {
569					return null;
570				}
571				DSAPublicKey remotePubKey = (DSAPublicKey) getOtrSession().getRemotePublicKey();
572				this.otrFingerprint = getAccount().getOtrService().getFingerprint(remotePubKey);
573			} catch (final OtrCryptoException | UnsupportedOperationException ignored) {
574				return null;
575			}
576		}
577		return this.otrFingerprint;
578	}
579
580	public boolean verifyOtrFingerprint() {
581		final String fingerprint = getOtrFingerprint();
582		if (fingerprint != null) {
583			getContact().addOtrFingerprint(fingerprint);
584			return true;
585		} else {
586			return false;
587		}
588	}
589
590	public boolean isOtrFingerprintVerified() {
591		return getContact().getOtrFingerprints().contains(getOtrFingerprint());
592	}
593
594	/**
595	 * short for is Private and Non-anonymous
596	 */
597	private boolean isPnNA() {
598		return mode == MODE_SINGLE || (getMucOptions().membersOnly() && getMucOptions().nonanonymous());
599	}
600
601	public synchronized MucOptions getMucOptions() {
602		if (this.mucOptions == null) {
603			this.mucOptions = new MucOptions(this);
604		}
605		return this.mucOptions;
606	}
607
608	public void resetMucOptions() {
609		this.mucOptions = null;
610	}
611
612	public void setContactJid(final Jid jid) {
613		this.contactJid = jid;
614	}
615
616	public void setNextCounterpart(Jid jid) {
617		this.nextCounterpart = jid;
618	}
619
620	public Jid getNextCounterpart() {
621		return this.nextCounterpart;
622	}
623
624	private int getMostRecentlyUsedOutgoingEncryption() {
625		synchronized (this.messages) {
626			for(int i = this.messages.size() -1; i >= 0; --i) {
627				final Message m = this.messages.get(i);
628				if (!m.isCarbon() && m.getStatus() != Message.STATUS_RECEIVED) {
629					final int e = m.getEncryption();
630					if (e == Message.ENCRYPTION_DECRYPTED || e == Message.ENCRYPTION_DECRYPTION_FAILED) {
631						return Message.ENCRYPTION_PGP;
632					} else {
633						return e;
634					}
635				}
636			}
637		}
638		return Message.ENCRYPTION_NONE;
639	}
640
641	private int getMostRecentlyUsedIncomingEncryption() {
642		synchronized (this.messages) {
643			for(int i = this.messages.size() -1; i >= 0; --i) {
644				final Message m = this.messages.get(i);
645				if (m.getStatus() == Message.STATUS_RECEIVED) {
646					final int e = m.getEncryption();
647					if (e == Message.ENCRYPTION_DECRYPTED || e == Message.ENCRYPTION_DECRYPTION_FAILED) {
648						return Message.ENCRYPTION_PGP;
649					} else {
650						return e;
651					}
652				}
653			}
654		}
655		return Message.ENCRYPTION_NONE;
656	}
657
658	public int getNextEncryption() {
659		final AxolotlService axolotlService = getAccount().getAxolotlService();
660		int next = this.getIntAttribute(ATTRIBUTE_NEXT_ENCRYPTION, -1);
661		if (next == -1) {
662			if (Config.X509_VERIFICATION) {
663				if (axolotlService != null && axolotlService.isConversationAxolotlCapable(this)) {
664					return Message.ENCRYPTION_AXOLOTL;
665				} else {
666					return Message.ENCRYPTION_NONE;
667				}
668			}
669			int outgoing = this.getMostRecentlyUsedOutgoingEncryption();
670			if (outgoing == Message.ENCRYPTION_NONE) {
671				next = this.getMostRecentlyUsedIncomingEncryption();
672			} else {
673				next = outgoing;
674			}
675		}
676
677		if (!Config.supportUnencrypted() && next <= 0) {
678			if (Config.supportOmemo()
679					&& (axolotlService != null && axolotlService.isConversationAxolotlCapable(this) || !Config.multipleEncryptionChoices())) {
680				return Message.ENCRYPTION_AXOLOTL;
681			} else if (Config.supportOtr() && mode == MODE_SINGLE) {
682				return Message.ENCRYPTION_OTR;
683			} else if (Config.supportOpenPgp()
684					&& (mode == MODE_SINGLE) || !Config.multipleEncryptionChoices()) {
685				return Message.ENCRYPTION_PGP;
686			}
687		} else if (next == Message.ENCRYPTION_AXOLOTL
688				&& (!Config.supportOmemo() || axolotlService == null || !axolotlService.isConversationAxolotlCapable(this))) {
689			next = Message.ENCRYPTION_NONE;
690		}
691		return next;
692	}
693
694	public void setNextEncryption(int encryption) {
695		this.setAttribute(ATTRIBUTE_NEXT_ENCRYPTION, String.valueOf(encryption));
696	}
697
698	public String getNextMessage() {
699		if (this.nextMessage == null) {
700			return "";
701		} else {
702			return this.nextMessage;
703		}
704	}
705
706	public boolean smpRequested() {
707		return smp().status == Smp.STATUS_CONTACT_REQUESTED;
708	}
709
710	public void setNextMessage(String message) {
711		this.nextMessage = message;
712	}
713
714	public void setSymmetricKey(byte[] key) {
715		this.symmetricKey = key;
716	}
717
718	public byte[] getSymmetricKey() {
719		return this.symmetricKey;
720	}
721
722	public void setBookmark(Bookmark bookmark) {
723		this.bookmark = bookmark;
724		this.bookmark.setConversation(this);
725	}
726
727	public void deregisterWithBookmark() {
728		if (this.bookmark != null) {
729			this.bookmark.setConversation(null);
730		}
731	}
732
733	public Bookmark getBookmark() {
734		return this.bookmark;
735	}
736
737	public boolean hasDuplicateMessage(Message message) {
738		synchronized (this.messages) {
739			for (int i = this.messages.size() - 1; i >= 0; --i) {
740				if (this.messages.get(i).equals(message)) {
741					return true;
742				}
743			}
744		}
745		return false;
746	}
747
748	public Message findSentMessageWithBody(String body) {
749		synchronized (this.messages) {
750			for (int i = this.messages.size() - 1; i >= 0; --i) {
751				Message message = this.messages.get(i);
752				if (message.getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_SEND) {
753					String otherBody;
754					if (message.hasFileOnRemoteHost()) {
755						otherBody = message.getFileParams().url.toString();
756					} else {
757						otherBody = message.body;
758					}
759					if (otherBody != null && otherBody.equals(body)) {
760						return message;
761					}
762				}
763			}
764			return null;
765		}
766	}
767
768	public long getLastMessageTransmitted() {
769		long last_clear = getLastClearHistory();
770		if (last_clear != 0) {
771			return last_clear;
772		}
773		synchronized (this.messages) {
774			for(int i = this.messages.size() - 1; i >= 0; --i) {
775				Message message = this.messages.get(i);
776				if (message.getStatus() == Message.STATUS_RECEIVED || message.isCarbon()) {
777					return message.getTimeSent();
778				}
779			}
780		}
781		return 0;
782	}
783
784	public void setMutedTill(long value) {
785		this.setAttribute(ATTRIBUTE_MUTED_TILL, String.valueOf(value));
786	}
787
788	public boolean isMuted() {
789		return System.currentTimeMillis() < this.getLongAttribute(ATTRIBUTE_MUTED_TILL, 0);
790	}
791
792	public boolean alwaysNotify() {
793		return mode == MODE_SINGLE || getBooleanAttribute(ATTRIBUTE_ALWAYS_NOTIFY, Config.ALWAYS_NOTIFY_BY_DEFAULT || isPnNA());
794	}
795
796	public boolean setAttribute(String key, String value) {
797		try {
798			this.attributes.put(key, value);
799			return true;
800		} catch (JSONException e) {
801			return false;
802		}
803	}
804
805	public String getAttribute(String key) {
806		try {
807			return this.attributes.getString(key);
808		} catch (JSONException e) {
809			return null;
810		}
811	}
812
813	public int getIntAttribute(String key, int defaultValue) {
814		String value = this.getAttribute(key);
815		if (value == null) {
816			return defaultValue;
817		} else {
818			try {
819				return Integer.parseInt(value);
820			} catch (NumberFormatException e) {
821				return defaultValue;
822			}
823		}
824	}
825
826	public long getLongAttribute(String key, long defaultValue) {
827		String value = this.getAttribute(key);
828		if (value == null) {
829			return defaultValue;
830		} else {
831			try {
832				return Long.parseLong(value);
833			} catch (NumberFormatException e) {
834				return defaultValue;
835			}
836		}
837	}
838
839	public boolean getBooleanAttribute(String key, boolean defaultValue) {
840		String value = this.getAttribute(key);
841		if (value == null) {
842			return defaultValue;
843		} else {
844			return Boolean.parseBoolean(value);
845		}
846	}
847
848	public void add(Message message) {
849		message.setConversation(this);
850		synchronized (this.messages) {
851			this.messages.add(message);
852		}
853	}
854
855	public void prepend(Message message) {
856		message.setConversation(this);
857		synchronized (this.messages) {
858			this.messages.add(0,message);
859		}
860	}
861
862	public void addAll(int index, List<Message> messages) {
863		synchronized (this.messages) {
864			this.messages.addAll(index, messages);
865		}
866		account.getPgpDecryptionService().addAll(messages);
867	}
868
869	public void sort() {
870		synchronized (this.messages) {
871			Collections.sort(this.messages, new Comparator<Message>() {
872				@Override
873				public int compare(Message left, Message right) {
874					if (left.getTimeSent() < right.getTimeSent()) {
875						return -1;
876					} else if (left.getTimeSent() > right.getTimeSent()) {
877						return 1;
878					} else {
879						return 0;
880					}
881				}
882			});
883			for(Message message : this.messages) {
884				message.untie();
885			}
886		}
887	}
888
889	public int unreadCount() {
890		synchronized (this.messages) {
891			int count = 0;
892			for(int i = this.messages.size() - 1; i >= 0; --i) {
893				if (this.messages.get(i).isRead()) {
894					return count;
895				}
896				++count;
897			}
898			return count;
899		}
900	}
901
902	public class Smp {
903		public static final int STATUS_NONE = 0;
904		public static final int STATUS_CONTACT_REQUESTED = 1;
905		public static final int STATUS_WE_REQUESTED = 2;
906		public static final int STATUS_FAILED = 3;
907		public static final int STATUS_VERIFIED = 4;
908
909		public String secret = null;
910		public String hint = null;
911		public int status = 0;
912	}
913}