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