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 void startOtrIfNeeded() {
521		if (this.otrSession != null
522				&& this.otrSession.getSessionStatus() != SessionStatus.ENCRYPTED) {
523			try {
524				this.otrSession.startSession();
525			} catch (OtrException e) {
526				this.resetOtrSession();
527			}
528				}
529	}
530
531	public boolean endOtrIfNeeded() {
532		if (this.otrSession != null) {
533			if (this.otrSession.getSessionStatus() == SessionStatus.ENCRYPTED) {
534				try {
535					this.otrSession.endSession();
536					this.resetOtrSession();
537					return true;
538				} catch (OtrException e) {
539					this.resetOtrSession();
540					return false;
541				}
542			} else {
543				this.resetOtrSession();
544				return false;
545			}
546		} else {
547			return false;
548		}
549	}
550
551	public boolean hasValidOtrSession() {
552		return this.otrSession != null;
553	}
554
555	public synchronized String getOtrFingerprint() {
556		if (this.otrFingerprint == null) {
557			try {
558				if (getOtrSession() == null || getOtrSession().getSessionStatus() != SessionStatus.ENCRYPTED) {
559					return null;
560				}
561				DSAPublicKey remotePubKey = (DSAPublicKey) getOtrSession().getRemotePublicKey();
562				this.otrFingerprint = getAccount().getOtrService().getFingerprint(remotePubKey);
563			} catch (final OtrCryptoException | UnsupportedOperationException ignored) {
564				return null;
565			}
566		}
567		return this.otrFingerprint;
568	}
569
570	public boolean verifyOtrFingerprint() {
571		final String fingerprint = getOtrFingerprint();
572		if (fingerprint != null) {
573			getContact().addOtrFingerprint(fingerprint);
574			return true;
575		} else {
576			return false;
577		}
578	}
579
580	public boolean isOtrFingerprintVerified() {
581		return getContact().getOtrFingerprints().contains(getOtrFingerprint());
582	}
583
584	/**
585	 * short for is Private and Non-anonymous
586	 */
587	private boolean isPnNA() {
588		return mode == MODE_SINGLE || (getMucOptions().membersOnly() && getMucOptions().nonanonymous());
589	}
590
591	public synchronized MucOptions getMucOptions() {
592		if (this.mucOptions == null) {
593			this.mucOptions = new MucOptions(this);
594		}
595		return this.mucOptions;
596	}
597
598	public void resetMucOptions() {
599		this.mucOptions = null;
600	}
601
602	public void setContactJid(final Jid jid) {
603		this.contactJid = jid;
604	}
605
606	public void setNextCounterpart(Jid jid) {
607		this.nextCounterpart = jid;
608	}
609
610	public Jid getNextCounterpart() {
611		return this.nextCounterpart;
612	}
613
614	private int getMostRecentlyUsedOutgoingEncryption() {
615		synchronized (this.messages) {
616			for(int i = this.messages.size() -1; i >= 0; --i) {
617				final Message m = this.messages.get(i);
618				if (!m.isCarbon() && m.getStatus() != Message.STATUS_RECEIVED) {
619					final int e = m.getEncryption();
620					if (e == Message.ENCRYPTION_DECRYPTED || e == Message.ENCRYPTION_DECRYPTION_FAILED) {
621						return Message.ENCRYPTION_PGP;
622					} else {
623						return e;
624					}
625				}
626			}
627		}
628		return Message.ENCRYPTION_NONE;
629	}
630
631	private int getMostRecentlyUsedIncomingEncryption() {
632		synchronized (this.messages) {
633			for(int i = this.messages.size() -1; i >= 0; --i) {
634				final Message m = this.messages.get(i);
635				if (m.getStatus() == Message.STATUS_RECEIVED) {
636					final int e = m.getEncryption();
637					if (e == Message.ENCRYPTION_DECRYPTED || e == Message.ENCRYPTION_DECRYPTION_FAILED) {
638						return Message.ENCRYPTION_PGP;
639					} else {
640						return e;
641					}
642				}
643			}
644		}
645		return Message.ENCRYPTION_NONE;
646	}
647
648	public int getNextEncryption() {
649		final AxolotlService axolotlService = getAccount().getAxolotlService();
650		int next = this.getIntAttribute(ATTRIBUTE_NEXT_ENCRYPTION, -1);
651		if (next == -1) {
652			if (Config.X509_VERIFICATION && mode == MODE_SINGLE) {
653				if (axolotlService != null && axolotlService.isContactAxolotlCapable(getContact())) {
654					return Message.ENCRYPTION_AXOLOTL;
655				} else {
656					return Message.ENCRYPTION_NONE;
657				}
658			}
659			int outgoing = this.getMostRecentlyUsedOutgoingEncryption();
660			if (outgoing == Message.ENCRYPTION_NONE) {
661				next = this.getMostRecentlyUsedIncomingEncryption();
662			} else {
663				next = outgoing;
664			}
665		}
666		if (Config.FORCE_E2E_ENCRYPTION && mode == MODE_SINGLE && next <= 0) {
667			if (axolotlService != null && axolotlService.isContactAxolotlCapable(getContact())) {
668				return Message.ENCRYPTION_AXOLOTL;
669			} else {
670				return Message.ENCRYPTION_OTR;
671			}
672		}
673		return next;
674	}
675
676	public void setNextEncryption(int encryption) {
677		this.setAttribute(ATTRIBUTE_NEXT_ENCRYPTION, String.valueOf(encryption));
678	}
679
680	public String getNextMessage() {
681		if (this.nextMessage == null) {
682			return "";
683		} else {
684			return this.nextMessage;
685		}
686	}
687
688	public boolean smpRequested() {
689		return smp().status == Smp.STATUS_CONTACT_REQUESTED;
690	}
691
692	public void setNextMessage(String message) {
693		this.nextMessage = message;
694	}
695
696	public void setSymmetricKey(byte[] key) {
697		this.symmetricKey = key;
698	}
699
700	public byte[] getSymmetricKey() {
701		return this.symmetricKey;
702	}
703
704	public void setBookmark(Bookmark bookmark) {
705		this.bookmark = bookmark;
706		this.bookmark.setConversation(this);
707	}
708
709	public void deregisterWithBookmark() {
710		if (this.bookmark != null) {
711			this.bookmark.setConversation(null);
712		}
713	}
714
715	public Bookmark getBookmark() {
716		return this.bookmark;
717	}
718
719	public boolean hasDuplicateMessage(Message message) {
720		synchronized (this.messages) {
721			for (int i = this.messages.size() - 1; i >= 0; --i) {
722				if (this.messages.get(i).equals(message)) {
723					return true;
724				}
725			}
726		}
727		return false;
728	}
729
730	public Message findSentMessageWithBody(String body) {
731		synchronized (this.messages) {
732			for (int i = this.messages.size() - 1; i >= 0; --i) {
733				Message message = this.messages.get(i);
734				if (message.getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_SEND) {
735					String otherBody;
736					if (message.hasFileOnRemoteHost()) {
737						otherBody = message.getFileParams().url.toString();
738					} else {
739						otherBody = message.body;
740					}
741					if (otherBody != null && otherBody.equals(body)) {
742						return message;
743					}
744				}
745			}
746			return null;
747		}
748	}
749
750	public long getLastMessageTransmitted() {
751		long last_clear = getLastClearHistory();
752		if (last_clear != 0) {
753			return last_clear;
754		}
755		synchronized (this.messages) {
756			for(int i = this.messages.size() - 1; i >= 0; --i) {
757				Message message = this.messages.get(i);
758				if (message.getStatus() == Message.STATUS_RECEIVED || message.isCarbon()) {
759					return message.getTimeSent();
760				}
761			}
762		}
763		return 0;
764	}
765
766	public void setMutedTill(long value) {
767		this.setAttribute(ATTRIBUTE_MUTED_TILL, String.valueOf(value));
768	}
769
770	public boolean isMuted() {
771		return System.currentTimeMillis() < this.getLongAttribute(ATTRIBUTE_MUTED_TILL, 0);
772	}
773
774	public boolean alwaysNotify() {
775		return mode == MODE_SINGLE || getBooleanAttribute(ATTRIBUTE_ALWAYS_NOTIFY, Config.ALWAYS_NOTIFY_BY_DEFAULT || isPnNA());
776	}
777
778	public boolean setAttribute(String key, String value) {
779		try {
780			this.attributes.put(key, value);
781			return true;
782		} catch (JSONException e) {
783			return false;
784		}
785	}
786
787	public String getAttribute(String key) {
788		try {
789			return this.attributes.getString(key);
790		} catch (JSONException e) {
791			return null;
792		}
793	}
794
795	public int getIntAttribute(String key, int defaultValue) {
796		String value = this.getAttribute(key);
797		if (value == null) {
798			return defaultValue;
799		} else {
800			try {
801				return Integer.parseInt(value);
802			} catch (NumberFormatException e) {
803				return defaultValue;
804			}
805		}
806	}
807
808	public long getLongAttribute(String key, long defaultValue) {
809		String value = this.getAttribute(key);
810		if (value == null) {
811			return defaultValue;
812		} else {
813			try {
814				return Long.parseLong(value);
815			} catch (NumberFormatException e) {
816				return defaultValue;
817			}
818		}
819	}
820
821	public boolean getBooleanAttribute(String key, boolean defaultValue) {
822		String value = this.getAttribute(key);
823		if (value == null) {
824			return defaultValue;
825		} else {
826			return Boolean.parseBoolean(value);
827		}
828	}
829
830	public void add(Message message) {
831		message.setConversation(this);
832		synchronized (this.messages) {
833			this.messages.add(message);
834		}
835	}
836
837	public void prepend(Message message) {
838		message.setConversation(this);
839		synchronized (this.messages) {
840			this.messages.add(0,message);
841		}
842	}
843
844	public void addAll(int index, List<Message> messages) {
845		synchronized (this.messages) {
846			this.messages.addAll(index, messages);
847		}
848		account.getPgpDecryptionService().addAll(messages);
849	}
850
851	public void sort() {
852		synchronized (this.messages) {
853			Collections.sort(this.messages, new Comparator<Message>() {
854				@Override
855				public int compare(Message left, Message right) {
856					if (left.getTimeSent() < right.getTimeSent()) {
857						return -1;
858					} else if (left.getTimeSent() > right.getTimeSent()) {
859						return 1;
860					} else {
861						return 0;
862					}
863				}
864			});
865			for(Message message : this.messages) {
866				message.untie();
867			}
868		}
869	}
870
871	public int unreadCount() {
872		synchronized (this.messages) {
873			int count = 0;
874			for(int i = this.messages.size() - 1; i >= 0; --i) {
875				if (this.messages.get(i).isRead()) {
876					return count;
877				}
878				++count;
879			}
880			return count;
881		}
882	}
883
884	public class Smp {
885		public static final int STATUS_NONE = 0;
886		public static final int STATUS_CONTACT_REQUESTED = 1;
887		public static final int STATUS_WE_REQUESTED = 2;
888		public static final int STATUS_FAILED = 3;
889		public static final int STATUS_VERIFIED = 4;
890
891		public String secret = null;
892		public String hint = null;
893		public int status = 0;
894	}
895}