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