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