Conversation.java

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