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