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