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