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