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 interface OnMessageFound {
340		void onMessageFound(final Message message);
341	}
342
343	public Conversation(final String name, final Account account, final Jid contactJid,
344			final int mode) {
345		this(java.util.UUID.randomUUID().toString(), name, null, account
346				.getUuid(), contactJid, System.currentTimeMillis(),
347				STATUS_AVAILABLE, mode, "");
348		this.account = account;
349	}
350
351	public Conversation(final String uuid, final String name, final String contactUuid,
352			final String accountUuid, final Jid contactJid, final long created, final int status,
353			final int mode, final String attributes) {
354		this.uuid = uuid;
355		this.name = name;
356		this.contactUuid = contactUuid;
357		this.accountUuid = accountUuid;
358		this.contactJid = contactJid;
359		this.created = created;
360		this.status = status;
361		this.mode = mode;
362		try {
363			this.attributes = new JSONObject(attributes == null ? "" : attributes);
364		} catch (JSONException e) {
365			this.attributes = new JSONObject();
366		}
367	}
368
369	public boolean isRead() {
370		return (this.messages.size() == 0) || this.messages.get(this.messages.size() - 1).isRead();
371	}
372
373	public List<Message> markRead() {
374		final List<Message> unread = new ArrayList<>();
375		synchronized (this.messages) {
376			for(Message message : this.messages) {
377				if (!message.isRead()) {
378					message.markRead();
379					unread.add(message);
380				}
381			}
382		}
383		return unread;
384	}
385
386	public Message getLatestMarkableMessage() {
387		for (int i = this.messages.size() - 1; i >= 0; --i) {
388			if (this.messages.get(i).getStatus() <= Message.STATUS_RECEIVED
389					&& this.messages.get(i).markable) {
390				if (this.messages.get(i).isRead()) {
391					return null;
392				} else {
393					return this.messages.get(i);
394				}
395					}
396		}
397		return null;
398	}
399
400	public Message getLatestMessage() {
401		if (this.messages.size() == 0) {
402			Message message = new Message(this, "", Message.ENCRYPTION_NONE);
403			message.setTime(getCreated());
404			return message;
405		} else {
406			Message message = this.messages.get(this.messages.size() - 1);
407			message.setConversation(this);
408			return message;
409		}
410	}
411
412	public String getName() {
413		if (getMode() == MODE_MULTI) {
414			if (getMucOptions().getSubject() != null) {
415				return getMucOptions().getSubject();
416			} else if (bookmark != null && bookmark.getBookmarkName() != null) {
417				return bookmark.getBookmarkName();
418			} else {
419				String generatedName = getMucOptions().createNameFromParticipants();
420				if (generatedName != null) {
421					return generatedName;
422				} else {
423					return getJid().getLocalpart();
424				}
425			}
426		} else {
427			return this.getContact().getDisplayName();
428		}
429	}
430
431	public String getAccountUuid() {
432		return this.accountUuid;
433	}
434
435	public Account getAccount() {
436		return this.account;
437	}
438
439	public Contact getContact() {
440		return this.account.getRoster().getContact(this.contactJid);
441	}
442
443	public void setAccount(final Account account) {
444		this.account = account;
445	}
446
447	@Override
448	public Jid getJid() {
449		return this.contactJid;
450	}
451
452	public int getStatus() {
453		return this.status;
454	}
455
456	public long getCreated() {
457		return this.created;
458	}
459
460	public ContentValues getContentValues() {
461		ContentValues values = new ContentValues();
462		values.put(UUID, uuid);
463		values.put(NAME, name);
464		values.put(CONTACT, contactUuid);
465		values.put(ACCOUNT, accountUuid);
466		values.put(CONTACTJID, contactJid.toString());
467		values.put(CREATED, created);
468		values.put(STATUS, status);
469		values.put(MODE, mode);
470		values.put(ATTRIBUTES, attributes.toString());
471		return values;
472	}
473
474	public static Conversation fromCursor(Cursor cursor) {
475		Jid jid;
476		try {
477			jid = Jid.fromString(cursor.getString(cursor.getColumnIndex(CONTACTJID)), true);
478		} catch (final InvalidJidException e) {
479			// Borked DB..
480			jid = null;
481		}
482		return new Conversation(cursor.getString(cursor.getColumnIndex(UUID)),
483				cursor.getString(cursor.getColumnIndex(NAME)),
484				cursor.getString(cursor.getColumnIndex(CONTACT)),
485				cursor.getString(cursor.getColumnIndex(ACCOUNT)),
486				jid,
487				cursor.getLong(cursor.getColumnIndex(CREATED)),
488				cursor.getInt(cursor.getColumnIndex(STATUS)),
489				cursor.getInt(cursor.getColumnIndex(MODE)),
490				cursor.getString(cursor.getColumnIndex(ATTRIBUTES)));
491	}
492
493	public void setStatus(int status) {
494		this.status = status;
495	}
496
497	public int getMode() {
498		return this.mode;
499	}
500
501	public void setMode(int mode) {
502		this.mode = mode;
503	}
504
505	public SessionImpl startOtrSession(String presence, boolean sendStart) {
506		if (this.otrSession != null) {
507			return this.otrSession;
508		} else {
509			final SessionID sessionId = new SessionID(this.getJid().toBareJid().toString(),
510					presence,
511					"xmpp");
512			this.otrSession = new SessionImpl(sessionId, getAccount().getOtrService());
513			try {
514				if (sendStart) {
515					this.otrSession.startSession();
516					return this.otrSession;
517				}
518				return this.otrSession;
519			} catch (OtrException e) {
520				return null;
521			}
522		}
523
524	}
525
526	public SessionImpl getOtrSession() {
527		return this.otrSession;
528	}
529
530	public void resetOtrSession() {
531		this.otrFingerprint = null;
532		this.otrSession = null;
533		this.mSmp.hint = null;
534		this.mSmp.secret = null;
535		this.mSmp.status = Smp.STATUS_NONE;
536	}
537
538	public Smp smp() {
539		return mSmp;
540	}
541
542	public boolean startOtrIfNeeded() {
543		if (this.otrSession != null && this.otrSession.getSessionStatus() != SessionStatus.ENCRYPTED) {
544			try {
545				this.otrSession.startSession();
546				return true;
547			} catch (OtrException e) {
548				this.resetOtrSession();
549				return false;
550			}
551		} else {
552			return true;
553		}
554	}
555
556	public boolean endOtrIfNeeded() {
557		if (this.otrSession != null) {
558			if (this.otrSession.getSessionStatus() == SessionStatus.ENCRYPTED) {
559				try {
560					this.otrSession.endSession();
561					this.resetOtrSession();
562					return true;
563				} catch (OtrException e) {
564					this.resetOtrSession();
565					return false;
566				}
567			} else {
568				this.resetOtrSession();
569				return false;
570			}
571		} else {
572			return false;
573		}
574	}
575
576	public boolean hasValidOtrSession() {
577		return this.otrSession != null;
578	}
579
580	public synchronized String getOtrFingerprint() {
581		if (this.otrFingerprint == null) {
582			try {
583				if (getOtrSession() == null || getOtrSession().getSessionStatus() != SessionStatus.ENCRYPTED) {
584					return null;
585				}
586				DSAPublicKey remotePubKey = (DSAPublicKey) getOtrSession().getRemotePublicKey();
587				this.otrFingerprint = getAccount().getOtrService().getFingerprint(remotePubKey);
588			} catch (final OtrCryptoException | UnsupportedOperationException ignored) {
589				return null;
590			}
591		}
592		return this.otrFingerprint;
593	}
594
595	public boolean verifyOtrFingerprint() {
596		final String fingerprint = getOtrFingerprint();
597		if (fingerprint != null) {
598			getContact().addOtrFingerprint(fingerprint);
599			return true;
600		} else {
601			return false;
602		}
603	}
604
605	public boolean isOtrFingerprintVerified() {
606		return getContact().getOtrFingerprints().contains(getOtrFingerprint());
607	}
608
609	/**
610	 * short for is Private and Non-anonymous
611	 */
612	private boolean isPnNA() {
613		return mode == MODE_SINGLE || (getMucOptions().membersOnly() && getMucOptions().nonanonymous());
614	}
615
616	public synchronized MucOptions getMucOptions() {
617		if (this.mucOptions == null) {
618			this.mucOptions = new MucOptions(this);
619		}
620		return this.mucOptions;
621	}
622
623	public void resetMucOptions() {
624		this.mucOptions = null;
625	}
626
627	public void setContactJid(final Jid jid) {
628		this.contactJid = jid;
629	}
630
631	public void setNextCounterpart(Jid jid) {
632		this.nextCounterpart = jid;
633	}
634
635	public Jid getNextCounterpart() {
636		return this.nextCounterpart;
637	}
638
639	private int getMostRecentlyUsedOutgoingEncryption() {
640		synchronized (this.messages) {
641			for(int i = this.messages.size() -1; i >= 0; --i) {
642				final Message m = this.messages.get(i);
643				if (!m.isCarbon() && m.getStatus() != Message.STATUS_RECEIVED) {
644					final int e = m.getEncryption();
645					if (e == Message.ENCRYPTION_DECRYPTED || e == Message.ENCRYPTION_DECRYPTION_FAILED) {
646						return Message.ENCRYPTION_PGP;
647					} else {
648						return e;
649					}
650				}
651			}
652		}
653		return Message.ENCRYPTION_NONE;
654	}
655
656	private int getMostRecentlyUsedIncomingEncryption() {
657		synchronized (this.messages) {
658			for(int i = this.messages.size() -1; i >= 0; --i) {
659				final Message m = this.messages.get(i);
660				if (m.getStatus() == Message.STATUS_RECEIVED) {
661					final int e = m.getEncryption();
662					if (e == Message.ENCRYPTION_DECRYPTED || e == Message.ENCRYPTION_DECRYPTION_FAILED) {
663						return Message.ENCRYPTION_PGP;
664					} else {
665						return e;
666					}
667				}
668			}
669		}
670		return Message.ENCRYPTION_NONE;
671	}
672
673	public int getNextEncryption() {
674		final AxolotlService axolotlService = getAccount().getAxolotlService();
675		int next = this.getIntAttribute(ATTRIBUTE_NEXT_ENCRYPTION, -1);
676		if (next == -1) {
677			if (Config.X509_VERIFICATION) {
678				if (axolotlService != null && axolotlService.isConversationAxolotlCapable(this)) {
679					return Message.ENCRYPTION_AXOLOTL;
680				} else {
681					return Message.ENCRYPTION_NONE;
682				}
683			}
684			int outgoing = this.getMostRecentlyUsedOutgoingEncryption();
685			if (outgoing == Message.ENCRYPTION_NONE) {
686				next = this.getMostRecentlyUsedIncomingEncryption();
687			} else {
688				next = outgoing;
689			}
690		}
691
692		if (!Config.supportUnencrypted() && next <= 0) {
693			if (Config.supportOmemo()
694					&& (axolotlService != null && axolotlService.isConversationAxolotlCapable(this) || !Config.multipleEncryptionChoices())) {
695				return Message.ENCRYPTION_AXOLOTL;
696			} else if (Config.supportOtr() && mode == MODE_SINGLE) {
697				return Message.ENCRYPTION_OTR;
698			} else if (Config.supportOpenPgp()
699					&& (mode == MODE_SINGLE) || !Config.multipleEncryptionChoices()) {
700				return Message.ENCRYPTION_PGP;
701			}
702		} else if (next == Message.ENCRYPTION_AXOLOTL
703				&& (!Config.supportOmemo() || axolotlService == null || !axolotlService.isConversationAxolotlCapable(this))) {
704			next = Message.ENCRYPTION_NONE;
705		}
706		return next;
707	}
708
709	public void setNextEncryption(int encryption) {
710		this.setAttribute(ATTRIBUTE_NEXT_ENCRYPTION, String.valueOf(encryption));
711	}
712
713	public String getNextMessage() {
714		if (this.nextMessage == null) {
715			return "";
716		} else {
717			return this.nextMessage;
718		}
719	}
720
721	public boolean smpRequested() {
722		return smp().status == Smp.STATUS_CONTACT_REQUESTED;
723	}
724
725	public void setNextMessage(String message) {
726		this.nextMessage = message;
727	}
728
729	public void setSymmetricKey(byte[] key) {
730		this.symmetricKey = key;
731	}
732
733	public byte[] getSymmetricKey() {
734		return this.symmetricKey;
735	}
736
737	public void setBookmark(Bookmark bookmark) {
738		this.bookmark = bookmark;
739		this.bookmark.setConversation(this);
740	}
741
742	public void deregisterWithBookmark() {
743		if (this.bookmark != null) {
744			this.bookmark.setConversation(null);
745		}
746	}
747
748	public Bookmark getBookmark() {
749		return this.bookmark;
750	}
751
752	public boolean hasDuplicateMessage(Message message) {
753		synchronized (this.messages) {
754			for (int i = this.messages.size() - 1; i >= 0; --i) {
755				if (this.messages.get(i).equals(message)) {
756					return true;
757				}
758			}
759		}
760		return false;
761	}
762
763	public Message findSentMessageWithBody(String body) {
764		synchronized (this.messages) {
765			for (int i = this.messages.size() - 1; i >= 0; --i) {
766				Message message = this.messages.get(i);
767				if (message.getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_SEND) {
768					String otherBody;
769					if (message.hasFileOnRemoteHost()) {
770						otherBody = message.getFileParams().url.toString();
771					} else {
772						otherBody = message.body;
773					}
774					if (otherBody != null && otherBody.equals(body)) {
775						return message;
776					}
777				}
778			}
779			return null;
780		}
781	}
782
783	public long getLastMessageTransmitted() {
784		long last_clear = getLastClearHistory();
785		if (last_clear != 0) {
786			return last_clear;
787		}
788		synchronized (this.messages) {
789			for(int i = this.messages.size() - 1; i >= 0; --i) {
790				Message message = this.messages.get(i);
791				if (message.getStatus() == Message.STATUS_RECEIVED || message.isCarbon()) {
792					return message.getTimeSent();
793				}
794			}
795		}
796		return 0;
797	}
798
799	public void setMutedTill(long value) {
800		this.setAttribute(ATTRIBUTE_MUTED_TILL, String.valueOf(value));
801	}
802
803	public boolean isMuted() {
804		return System.currentTimeMillis() < this.getLongAttribute(ATTRIBUTE_MUTED_TILL, 0);
805	}
806
807	public boolean alwaysNotify() {
808		return mode == MODE_SINGLE || getBooleanAttribute(ATTRIBUTE_ALWAYS_NOTIFY, Config.ALWAYS_NOTIFY_BY_DEFAULT || isPnNA());
809	}
810
811	public boolean setAttribute(String key, String value) {
812		synchronized (this.attributes) {
813			try {
814				this.attributes.put(key, value);
815				return true;
816			} catch (JSONException e) {
817				return false;
818			}
819		}
820	}
821
822	public boolean setAttribute(String key, List<Jid> jids) {
823		JSONArray array = new JSONArray();
824		for(Jid jid : jids) {
825			array.put(jid.toBareJid().toString());
826		}
827		synchronized (this.attributes) {
828			try {
829				this.attributes.put(key, array);
830				return true;
831			} catch (JSONException e) {
832				e.printStackTrace();
833				return false;
834			}
835		}
836	}
837
838	public String getAttribute(String key) {
839		synchronized (this.attributes) {
840			try {
841				return this.attributes.getString(key);
842			} catch (JSONException e) {
843				return null;
844			}
845		}
846	}
847
848	public List<Jid> getJidListAttribute(String key) {
849		ArrayList<Jid> list = new ArrayList<>();
850		synchronized (this.attributes) {
851			try {
852				JSONArray array = this.attributes.getJSONArray(key);
853				for (int i = 0; i < array.length(); ++i) {
854					try {
855						list.add(Jid.fromString(array.getString(i)));
856					} catch (InvalidJidException e) {
857						//ignored
858					}
859				}
860			} catch (JSONException e) {
861				//ignored
862			}
863		}
864		return list;
865	}
866
867	public int getIntAttribute(String key, int defaultValue) {
868		String value = this.getAttribute(key);
869		if (value == null) {
870			return defaultValue;
871		} else {
872			try {
873				return Integer.parseInt(value);
874			} catch (NumberFormatException e) {
875				return defaultValue;
876			}
877		}
878	}
879
880	public long getLongAttribute(String key, long defaultValue) {
881		String value = this.getAttribute(key);
882		if (value == null) {
883			return defaultValue;
884		} else {
885			try {
886				return Long.parseLong(value);
887			} catch (NumberFormatException e) {
888				return defaultValue;
889			}
890		}
891	}
892
893	public boolean getBooleanAttribute(String key, boolean defaultValue) {
894		String value = this.getAttribute(key);
895		if (value == null) {
896			return defaultValue;
897		} else {
898			return Boolean.parseBoolean(value);
899		}
900	}
901
902	public void add(Message message) {
903		message.setConversation(this);
904		synchronized (this.messages) {
905			this.messages.add(message);
906		}
907	}
908
909	public void prepend(Message message) {
910		message.setConversation(this);
911		synchronized (this.messages) {
912			this.messages.add(0,message);
913		}
914	}
915
916	public void addAll(int index, List<Message> messages) {
917		synchronized (this.messages) {
918			this.messages.addAll(index, messages);
919		}
920		account.getPgpDecryptionService().addAll(messages);
921	}
922
923	public void sort() {
924		synchronized (this.messages) {
925			Collections.sort(this.messages, new Comparator<Message>() {
926				@Override
927				public int compare(Message left, Message right) {
928					if (left.getTimeSent() < right.getTimeSent()) {
929						return -1;
930					} else if (left.getTimeSent() > right.getTimeSent()) {
931						return 1;
932					} else {
933						return 0;
934					}
935				}
936			});
937			for(Message message : this.messages) {
938				message.untie();
939			}
940		}
941	}
942
943	public int unreadCount() {
944		synchronized (this.messages) {
945			int count = 0;
946			for(int i = this.messages.size() - 1; i >= 0; --i) {
947				if (this.messages.get(i).isRead()) {
948					return count;
949				}
950				++count;
951			}
952			return count;
953		}
954	}
955
956	public class Smp {
957		public static final int STATUS_NONE = 0;
958		public static final int STATUS_CONTACT_REQUESTED = 1;
959		public static final int STATUS_WE_REQUESTED = 2;
960		public static final int STATUS_FAILED = 3;
961		public static final int STATUS_VERIFIED = 4;
962
963		public String secret = null;
964		public String hint = null;
965		public int status = 0;
966	}
967}