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