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 && mode == MODE_SINGLE) {
663				if (axolotlService != null && axolotlService.isContactAxolotlCapable(getContact())) {
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		if (Config.FORCE_E2E_ENCRYPTION && mode == MODE_SINGLE && next <= 0) {
677			if (axolotlService != null && axolotlService.isContactAxolotlCapable(getContact())) {
678				return Message.ENCRYPTION_AXOLOTL;
679			} else {
680				return Message.ENCRYPTION_OTR;
681			}
682		}
683		return next;
684	}
685
686	public void setNextEncryption(int encryption) {
687		this.setAttribute(ATTRIBUTE_NEXT_ENCRYPTION, String.valueOf(encryption));
688	}
689
690	public String getNextMessage() {
691		if (this.nextMessage == null) {
692			return "";
693		} else {
694			return this.nextMessage;
695		}
696	}
697
698	public boolean smpRequested() {
699		return smp().status == Smp.STATUS_CONTACT_REQUESTED;
700	}
701
702	public void setNextMessage(String message) {
703		this.nextMessage = message;
704	}
705
706	public void setSymmetricKey(byte[] key) {
707		this.symmetricKey = key;
708	}
709
710	public byte[] getSymmetricKey() {
711		return this.symmetricKey;
712	}
713
714	public void setBookmark(Bookmark bookmark) {
715		this.bookmark = bookmark;
716		this.bookmark.setConversation(this);
717	}
718
719	public void deregisterWithBookmark() {
720		if (this.bookmark != null) {
721			this.bookmark.setConversation(null);
722		}
723	}
724
725	public Bookmark getBookmark() {
726		return this.bookmark;
727	}
728
729	public boolean hasDuplicateMessage(Message message) {
730		synchronized (this.messages) {
731			for (int i = this.messages.size() - 1; i >= 0; --i) {
732				if (this.messages.get(i).equals(message)) {
733					return true;
734				}
735			}
736		}
737		return false;
738	}
739
740	public Message findSentMessageWithBody(String body) {
741		synchronized (this.messages) {
742			for (int i = this.messages.size() - 1; i >= 0; --i) {
743				Message message = this.messages.get(i);
744				if (message.getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_SEND) {
745					String otherBody;
746					if (message.hasFileOnRemoteHost()) {
747						otherBody = message.getFileParams().url.toString();
748					} else {
749						otherBody = message.body;
750					}
751					if (otherBody != null && otherBody.equals(body)) {
752						return message;
753					}
754				}
755			}
756			return null;
757		}
758	}
759
760	public long getLastMessageTransmitted() {
761		long last_clear = getLastClearHistory();
762		if (last_clear != 0) {
763			return last_clear;
764		}
765		synchronized (this.messages) {
766			for(int i = this.messages.size() - 1; i >= 0; --i) {
767				Message message = this.messages.get(i);
768				if (message.getStatus() == Message.STATUS_RECEIVED || message.isCarbon()) {
769					return message.getTimeSent();
770				}
771			}
772		}
773		return 0;
774	}
775
776	public void setMutedTill(long value) {
777		this.setAttribute(ATTRIBUTE_MUTED_TILL, String.valueOf(value));
778	}
779
780	public boolean isMuted() {
781		return System.currentTimeMillis() < this.getLongAttribute(ATTRIBUTE_MUTED_TILL, 0);
782	}
783
784	public boolean alwaysNotify() {
785		return mode == MODE_SINGLE || getBooleanAttribute(ATTRIBUTE_ALWAYS_NOTIFY, Config.ALWAYS_NOTIFY_BY_DEFAULT || isPnNA());
786	}
787
788	public boolean setAttribute(String key, String value) {
789		try {
790			this.attributes.put(key, value);
791			return true;
792		} catch (JSONException e) {
793			return false;
794		}
795	}
796
797	public String getAttribute(String key) {
798		try {
799			return this.attributes.getString(key);
800		} catch (JSONException e) {
801			return null;
802		}
803	}
804
805	public int getIntAttribute(String key, int defaultValue) {
806		String value = this.getAttribute(key);
807		if (value == null) {
808			return defaultValue;
809		} else {
810			try {
811				return Integer.parseInt(value);
812			} catch (NumberFormatException e) {
813				return defaultValue;
814			}
815		}
816	}
817
818	public long getLongAttribute(String key, long defaultValue) {
819		String value = this.getAttribute(key);
820		if (value == null) {
821			return defaultValue;
822		} else {
823			try {
824				return Long.parseLong(value);
825			} catch (NumberFormatException e) {
826				return defaultValue;
827			}
828		}
829	}
830
831	public boolean getBooleanAttribute(String key, boolean defaultValue) {
832		String value = this.getAttribute(key);
833		if (value == null) {
834			return defaultValue;
835		} else {
836			return Boolean.parseBoolean(value);
837		}
838	}
839
840	public void add(Message message) {
841		message.setConversation(this);
842		synchronized (this.messages) {
843			this.messages.add(message);
844		}
845	}
846
847	public void prepend(Message message) {
848		message.setConversation(this);
849		synchronized (this.messages) {
850			this.messages.add(0,message);
851		}
852	}
853
854	public void addAll(int index, List<Message> messages) {
855		synchronized (this.messages) {
856			this.messages.addAll(index, messages);
857		}
858		account.getPgpDecryptionService().addAll(messages);
859	}
860
861	public void sort() {
862		synchronized (this.messages) {
863			Collections.sort(this.messages, new Comparator<Message>() {
864				@Override
865				public int compare(Message left, Message right) {
866					if (left.getTimeSent() < right.getTimeSent()) {
867						return -1;
868					} else if (left.getTimeSent() > right.getTimeSent()) {
869						return 1;
870					} else {
871						return 0;
872					}
873				}
874			});
875			for(Message message : this.messages) {
876				message.untie();
877			}
878		}
879	}
880
881	public int unreadCount() {
882		synchronized (this.messages) {
883			int count = 0;
884			for(int i = this.messages.size() - 1; i >= 0; --i) {
885				if (this.messages.get(i).isRead()) {
886					return count;
887				}
888				++count;
889			}
890			return count;
891		}
892	}
893
894	public class Smp {
895		public static final int STATUS_NONE = 0;
896		public static final int STATUS_CONTACT_REQUESTED = 1;
897		public static final int STATUS_WE_REQUESTED = 2;
898		public static final int STATUS_FAILED = 3;
899		public static final int STATUS_VERIFIED = 4;
900
901		public String secret = null;
902		public String hint = null;
903		public int status = 0;
904	}
905}