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