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 boolean setCorrectingMessage(Message correctingMessage) {
360		boolean corrected = this.correctingMessage != null && correctingMessage == null;
361		this.correctingMessage = correctingMessage;
362		return corrected;
363	}
364
365	public Message getCorrectingMessage() {
366		return this.correctingMessage;
367	}
368
369	public boolean withSelf() {
370		return getContact().isSelf();
371	}
372
373	@Override
374	public int compareTo(Conversation another) {
375		final Message left = getLatestMessage();
376		final Message right = another.getLatestMessage();
377		if (left.getTimeSent() > right.getTimeSent()) {
378			return -1;
379		} else if (left.getTimeSent() < right.getTimeSent()) {
380			return 1;
381		} else {
382			return 0;
383		}
384	}
385
386	public interface OnMessageFound {
387		void onMessageFound(final Message message);
388	}
389
390	public Conversation(final String name, final Account account, final Jid contactJid,
391			final int mode) {
392		this(java.util.UUID.randomUUID().toString(), name, null, account
393				.getUuid(), contactJid, System.currentTimeMillis(),
394				STATUS_AVAILABLE, mode, "");
395		this.account = account;
396	}
397
398	public Conversation(final String uuid, final String name, final String contactUuid,
399			final String accountUuid, final Jid contactJid, final long created, final int status,
400			final int mode, final String attributes) {
401		this.uuid = uuid;
402		this.name = name;
403		this.contactUuid = contactUuid;
404		this.accountUuid = accountUuid;
405		this.contactJid = contactJid;
406		this.created = created;
407		this.status = status;
408		this.mode = mode;
409		try {
410			this.attributes = new JSONObject(attributes == null ? "" : attributes);
411		} catch (JSONException e) {
412			this.attributes = new JSONObject();
413		}
414	}
415
416	public boolean isRead() {
417		return (this.messages.size() == 0) || this.messages.get(this.messages.size() - 1).isRead();
418	}
419
420	public List<Message> markRead() {
421		final List<Message> unread = new ArrayList<>();
422		synchronized (this.messages) {
423			for(Message message : this.messages) {
424				if (!message.isRead()) {
425					message.markRead();
426					unread.add(message);
427				}
428			}
429		}
430		return unread;
431	}
432
433	public Message getLatestMarkableMessage() {
434		for (int i = this.messages.size() - 1; i >= 0; --i) {
435			if (this.messages.get(i).getStatus() <= Message.STATUS_RECEIVED
436					&& this.messages.get(i).markable) {
437				if (this.messages.get(i).isRead()) {
438					return null;
439				} else {
440					return this.messages.get(i);
441				}
442					}
443		}
444		return null;
445	}
446
447	public Message getLatestMessage() {
448		if (this.messages.size() == 0) {
449			Message message = new Message(this, "", Message.ENCRYPTION_NONE);
450			message.setTime(getCreated());
451			return message;
452		} else {
453			Message message = this.messages.get(this.messages.size() - 1);
454			message.setConversation(this);
455			return message;
456		}
457	}
458
459	public String getName() {
460		if (getMode() == MODE_MULTI) {
461			if (getMucOptions().getSubject() != null) {
462				return getMucOptions().getSubject();
463			} else if (bookmark != null
464					&& bookmark.getBookmarkName() != null
465					&& !bookmark.getBookmarkName().trim().isEmpty()) {
466				return bookmark.getBookmarkName().trim();
467			} else {
468				String generatedName = getMucOptions().createNameFromParticipants();
469				if (generatedName != null) {
470					return generatedName;
471				} else {
472					return getJid().getUnescapedLocalpart();
473				}
474			}
475		} else {
476			return this.getContact().getDisplayName();
477		}
478	}
479
480	public String getAccountUuid() {
481		return this.accountUuid;
482	}
483
484	public Account getAccount() {
485		return this.account;
486	}
487
488	public Contact getContact() {
489		return this.account.getRoster().getContact(this.contactJid);
490	}
491
492	public void setAccount(final Account account) {
493		this.account = account;
494	}
495
496	@Override
497	public Jid getJid() {
498		return this.contactJid;
499	}
500
501	public int getStatus() {
502		return this.status;
503	}
504
505	public long getCreated() {
506		return this.created;
507	}
508
509	public ContentValues getContentValues() {
510		ContentValues values = new ContentValues();
511		values.put(UUID, uuid);
512		values.put(NAME, name);
513		values.put(CONTACT, contactUuid);
514		values.put(ACCOUNT, accountUuid);
515		values.put(CONTACTJID, contactJid.toPreppedString());
516		values.put(CREATED, created);
517		values.put(STATUS, status);
518		values.put(MODE, mode);
519		values.put(ATTRIBUTES, attributes.toString());
520		return values;
521	}
522
523	public static Conversation fromCursor(Cursor cursor) {
524		Jid jid;
525		try {
526			jid = Jid.fromString(cursor.getString(cursor.getColumnIndex(CONTACTJID)), true);
527		} catch (final InvalidJidException e) {
528			// Borked DB..
529			jid = null;
530		}
531		return new Conversation(cursor.getString(cursor.getColumnIndex(UUID)),
532				cursor.getString(cursor.getColumnIndex(NAME)),
533				cursor.getString(cursor.getColumnIndex(CONTACT)),
534				cursor.getString(cursor.getColumnIndex(ACCOUNT)),
535				jid,
536				cursor.getLong(cursor.getColumnIndex(CREATED)),
537				cursor.getInt(cursor.getColumnIndex(STATUS)),
538				cursor.getInt(cursor.getColumnIndex(MODE)),
539				cursor.getString(cursor.getColumnIndex(ATTRIBUTES)));
540	}
541
542	public void setStatus(int status) {
543		this.status = status;
544	}
545
546	public int getMode() {
547		return this.mode;
548	}
549
550	public void setMode(int mode) {
551		this.mode = mode;
552	}
553
554	public SessionImpl startOtrSession(String presence, boolean sendStart) {
555		if (this.otrSession != null) {
556			return this.otrSession;
557		} else {
558			final SessionID sessionId = new SessionID(this.getJid().toBareJid().toString(),
559					presence,
560					"xmpp");
561			this.otrSession = new SessionImpl(sessionId, getAccount().getOtrService());
562			try {
563				if (sendStart) {
564					this.otrSession.startSession();
565					return this.otrSession;
566				}
567				return this.otrSession;
568			} catch (OtrException e) {
569				return null;
570			}
571		}
572
573	}
574
575	public SessionImpl getOtrSession() {
576		return this.otrSession;
577	}
578
579	public void resetOtrSession() {
580		this.otrFingerprint = null;
581		this.otrSession = null;
582		this.mSmp.hint = null;
583		this.mSmp.secret = null;
584		this.mSmp.status = Smp.STATUS_NONE;
585	}
586
587	public Smp smp() {
588		return mSmp;
589	}
590
591	public boolean startOtrIfNeeded() {
592		if (this.otrSession != null && this.otrSession.getSessionStatus() != SessionStatus.ENCRYPTED) {
593			try {
594				this.otrSession.startSession();
595				return true;
596			} catch (OtrException e) {
597				this.resetOtrSession();
598				return false;
599			}
600		} else {
601			return true;
602		}
603	}
604
605	public boolean endOtrIfNeeded() {
606		if (this.otrSession != null) {
607			if (this.otrSession.getSessionStatus() == SessionStatus.ENCRYPTED) {
608				try {
609					this.otrSession.endSession();
610					this.resetOtrSession();
611					return true;
612				} catch (OtrException e) {
613					this.resetOtrSession();
614					return false;
615				}
616			} else {
617				this.resetOtrSession();
618				return false;
619			}
620		} else {
621			return false;
622		}
623	}
624
625	public boolean hasValidOtrSession() {
626		return this.otrSession != null;
627	}
628
629	public synchronized String getOtrFingerprint() {
630		if (this.otrFingerprint == null) {
631			try {
632				if (getOtrSession() == null || getOtrSession().getSessionStatus() != SessionStatus.ENCRYPTED) {
633					return null;
634				}
635				DSAPublicKey remotePubKey = (DSAPublicKey) getOtrSession().getRemotePublicKey();
636				this.otrFingerprint = getAccount().getOtrService().getFingerprint(remotePubKey).toLowerCase(Locale.US);
637			} catch (final OtrCryptoException | UnsupportedOperationException ignored) {
638				return null;
639			}
640		}
641		return this.otrFingerprint;
642	}
643
644	public boolean verifyOtrFingerprint() {
645		final String fingerprint = getOtrFingerprint();
646		if (fingerprint != null) {
647			getContact().addOtrFingerprint(fingerprint);
648			return true;
649		} else {
650			return false;
651		}
652	}
653
654	public boolean isOtrFingerprintVerified() {
655		return getContact().getOtrFingerprints().contains(getOtrFingerprint());
656	}
657
658	/**
659	 * short for is Private and Non-anonymous
660	 */
661	private boolean isPnNA() {
662		return mode == MODE_SINGLE || (getMucOptions().membersOnly() && getMucOptions().nonanonymous());
663	}
664
665	public synchronized MucOptions getMucOptions() {
666		if (this.mucOptions == null) {
667			this.mucOptions = new MucOptions(this);
668		}
669		return this.mucOptions;
670	}
671
672	public void resetMucOptions() {
673		this.mucOptions = null;
674	}
675
676	public void setContactJid(final Jid jid) {
677		this.contactJid = jid;
678	}
679
680	public void setNextCounterpart(Jid jid) {
681		this.nextCounterpart = jid;
682	}
683
684	public Jid getNextCounterpart() {
685		return this.nextCounterpart;
686	}
687
688	public int getNextEncryption() {
689		return fixAvailableEncryption(this.getIntAttribute(ATTRIBUTE_NEXT_ENCRYPTION, getDefaultEncryption()));
690	}
691
692	private int fixAvailableEncryption(int selectedEncryption) {
693		switch(selectedEncryption) {
694			case Message.ENCRYPTION_NONE:
695				return Config.supportUnencrypted() ? selectedEncryption : getDefaultEncryption();
696			case Message.ENCRYPTION_AXOLOTL:
697				return Config.supportOmemo() ? selectedEncryption : getDefaultEncryption();
698			case Message.ENCRYPTION_OTR:
699				return Config.supportOtr() ? selectedEncryption : getDefaultEncryption();
700			case Message.ENCRYPTION_PGP:
701			case Message.ENCRYPTION_DECRYPTED:
702			case Message.ENCRYPTION_DECRYPTION_FAILED:
703				return Config.supportOpenPgp() ? Message.ENCRYPTION_PGP : getDefaultEncryption();
704			default:
705				return getDefaultEncryption();
706		}
707	}
708
709	private int getDefaultEncryption() {
710		AxolotlService axolotlService = account.getAxolotlService();
711		if (Config.supportUnencrypted()) {
712			return Message.ENCRYPTION_NONE;
713		} else if (Config.supportOmemo()
714				&& (axolotlService != null && axolotlService.isConversationAxolotlCapable(this) || !Config.multipleEncryptionChoices())) {
715			return Message.ENCRYPTION_AXOLOTL;
716		} else if (Config.supportOtr() && mode == MODE_SINGLE) {
717			return Message.ENCRYPTION_OTR;
718		} else if (Config.supportOpenPgp()) {
719			return Message.ENCRYPTION_PGP;
720		} else {
721			return Message.ENCRYPTION_NONE;
722		}
723	}
724
725	public void setNextEncryption(int encryption) {
726		this.setAttribute(ATTRIBUTE_NEXT_ENCRYPTION, String.valueOf(encryption));
727	}
728
729	public String getNextMessage() {
730		if (this.nextMessage == null) {
731			return "";
732		} else {
733			return this.nextMessage;
734		}
735	}
736
737	public boolean smpRequested() {
738		return smp().status == Smp.STATUS_CONTACT_REQUESTED;
739	}
740
741	public void setNextMessage(String message) {
742		this.nextMessage = message;
743	}
744
745	public void setSymmetricKey(byte[] key) {
746		this.symmetricKey = key;
747	}
748
749	public byte[] getSymmetricKey() {
750		return this.symmetricKey;
751	}
752
753	public void setBookmark(Bookmark bookmark) {
754		this.bookmark = bookmark;
755		this.bookmark.setConversation(this);
756	}
757
758	public void deregisterWithBookmark() {
759		if (this.bookmark != null) {
760			this.bookmark.setConversation(null);
761		}
762	}
763
764	public Bookmark getBookmark() {
765		return this.bookmark;
766	}
767
768	public boolean hasDuplicateMessage(Message message) {
769		synchronized (this.messages) {
770			for (int i = this.messages.size() - 1; i >= 0; --i) {
771				if (this.messages.get(i).similar(message)) {
772					return true;
773				}
774			}
775		}
776		return false;
777	}
778
779	public Message findSentMessageWithBody(String body) {
780		synchronized (this.messages) {
781			for (int i = this.messages.size() - 1; i >= 0; --i) {
782				Message message = this.messages.get(i);
783				if (message.getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_SEND) {
784					String otherBody;
785					if (message.hasFileOnRemoteHost()) {
786						otherBody = message.getFileParams().url.toString();
787					} else {
788						otherBody = message.body;
789					}
790					if (otherBody != null && otherBody.equals(body)) {
791						return message;
792					}
793				}
794			}
795			return null;
796		}
797	}
798
799	public long getLastMessageTransmitted() {
800		final long last_clear = getLastClearHistory();
801		long last_received = 0;
802		synchronized (this.messages) {
803			for(int i = this.messages.size() - 1; i >= 0; --i) {
804				Message message = this.messages.get(i);
805				if (message.getStatus() == Message.STATUS_RECEIVED || message.isCarbon()) {
806					last_received = message.getTimeSent();
807					break;
808				}
809			}
810		}
811		return Math.max(last_clear,last_received);
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().decrypt(messages);
936	}
937
938	public void expireOldMessages(long timestamp) {
939		synchronized (this.messages) {
940			for(ListIterator<Message> iterator = this.messages.listIterator(); iterator.hasNext();) {
941				if (iterator.next().getTimeSent() < timestamp) {
942					iterator.remove();
943				}
944			}
945			untieMessages();
946		}
947	}
948
949	public void sort() {
950		synchronized (this.messages) {
951			Collections.sort(this.messages, new Comparator<Message>() {
952				@Override
953				public int compare(Message left, Message right) {
954					if (left.getTimeSent() < right.getTimeSent()) {
955						return -1;
956					} else if (left.getTimeSent() > right.getTimeSent()) {
957						return 1;
958					} else {
959						return 0;
960					}
961				}
962			});
963			untieMessages();
964		}
965	}
966
967	private void untieMessages() {
968		for(Message message : this.messages) {
969			message.untie();
970		}
971	}
972
973	public int unreadCount() {
974		synchronized (this.messages) {
975			int count = 0;
976			for(int i = this.messages.size() - 1; i >= 0; --i) {
977				if (this.messages.get(i).isRead()) {
978					return count;
979				}
980				++count;
981			}
982			return count;
983		}
984	}
985
986	public class Smp {
987		public static final int STATUS_NONE = 0;
988		public static final int STATUS_CONTACT_REQUESTED = 1;
989		public static final int STATUS_WE_REQUESTED = 2;
990		public static final int STATUS_FAILED = 3;
991		public static final int STATUS_VERIFIED = 4;
992
993		public String secret = null;
994		public String hint = null;
995		public int status = 0;
996	}
997}