Conversation.java

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