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