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