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