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