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