Conversation.java

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