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