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