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