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		long last_clear = getLastClearHistory();
807		if (last_clear != 0) {
808			return last_clear;
809		}
810		synchronized (this.messages) {
811			for(int i = this.messages.size() - 1; i >= 0; --i) {
812				Message message = this.messages.get(i);
813				if (message.getStatus() == Message.STATUS_RECEIVED || message.isCarbon()) {
814					return message.getTimeSent();
815				}
816			}
817		}
818		return 0;
819	}
820
821	public void setMutedTill(long value) {
822		this.setAttribute(ATTRIBUTE_MUTED_TILL, String.valueOf(value));
823	}
824
825	public boolean isMuted() {
826		return System.currentTimeMillis() < this.getLongAttribute(ATTRIBUTE_MUTED_TILL, 0);
827	}
828
829	public boolean alwaysNotify() {
830		return mode == MODE_SINGLE || getBooleanAttribute(ATTRIBUTE_ALWAYS_NOTIFY, Config.ALWAYS_NOTIFY_BY_DEFAULT || isPnNA());
831	}
832
833	public boolean setAttribute(String key, String value) {
834		synchronized (this.attributes) {
835			try {
836				this.attributes.put(key, value);
837				return true;
838			} catch (JSONException e) {
839				return false;
840			}
841		}
842	}
843
844	public boolean setAttribute(String key, List<Jid> jids) {
845		JSONArray array = new JSONArray();
846		for(Jid jid : jids) {
847			array.put(jid.toBareJid().toString());
848		}
849		synchronized (this.attributes) {
850			try {
851				this.attributes.put(key, array);
852				return true;
853			} catch (JSONException e) {
854				e.printStackTrace();
855				return false;
856			}
857		}
858	}
859
860	public String getAttribute(String key) {
861		synchronized (this.attributes) {
862			try {
863				return this.attributes.getString(key);
864			} catch (JSONException e) {
865				return null;
866			}
867		}
868	}
869
870	public List<Jid> getJidListAttribute(String key) {
871		ArrayList<Jid> list = new ArrayList<>();
872		synchronized (this.attributes) {
873			try {
874				JSONArray array = this.attributes.getJSONArray(key);
875				for (int i = 0; i < array.length(); ++i) {
876					try {
877						list.add(Jid.fromString(array.getString(i)));
878					} catch (InvalidJidException e) {
879						//ignored
880					}
881				}
882			} catch (JSONException e) {
883				//ignored
884			}
885		}
886		return list;
887	}
888
889	public int getIntAttribute(String key, int defaultValue) {
890		String value = this.getAttribute(key);
891		if (value == null) {
892			return defaultValue;
893		} else {
894			try {
895				return Integer.parseInt(value);
896			} catch (NumberFormatException e) {
897				return defaultValue;
898			}
899		}
900	}
901
902	public long getLongAttribute(String key, long defaultValue) {
903		String value = this.getAttribute(key);
904		if (value == null) {
905			return defaultValue;
906		} else {
907			try {
908				return Long.parseLong(value);
909			} catch (NumberFormatException e) {
910				return defaultValue;
911			}
912		}
913	}
914
915	public boolean getBooleanAttribute(String key, boolean defaultValue) {
916		String value = this.getAttribute(key);
917		if (value == null) {
918			return defaultValue;
919		} else {
920			return Boolean.parseBoolean(value);
921		}
922	}
923
924	public void add(Message message) {
925		message.setConversation(this);
926		synchronized (this.messages) {
927			this.messages.add(message);
928		}
929	}
930
931	public void prepend(Message message) {
932		message.setConversation(this);
933		synchronized (this.messages) {
934			this.messages.add(0,message);
935		}
936	}
937
938	public void addAll(int index, List<Message> messages) {
939		synchronized (this.messages) {
940			this.messages.addAll(index, messages);
941		}
942		account.getPgpDecryptionService().decrypt(messages);
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}