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