Conversation.java

  1package eu.siacs.conversations.entities;
  2
  3import android.content.ContentValues;
  4import android.database.Cursor;
  5import android.support.annotation.NonNull;
  6import android.support.annotation.Nullable;
  7import android.text.TextUtils;
  8
  9import org.json.JSONArray;
 10import org.json.JSONException;
 11import org.json.JSONObject;
 12
 13import java.util.ArrayList;
 14import java.util.Collections;
 15import java.util.Comparator;
 16import java.util.Iterator;
 17import java.util.List;
 18import java.util.ListIterator;
 19import java.util.Locale;
 20import java.util.concurrent.atomic.AtomicBoolean;
 21
 22import eu.siacs.conversations.Config;
 23import eu.siacs.conversations.crypto.OmemoSetting;
 24import eu.siacs.conversations.crypto.PgpDecryptionService;
 25import eu.siacs.conversations.crypto.axolotl.AxolotlService;
 26import eu.siacs.conversations.xmpp.chatstate.ChatState;
 27import eu.siacs.conversations.xmpp.mam.MamReference;
 28import rocks.xmpp.addr.Jid;
 29
 30import static eu.siacs.conversations.entities.Bookmark.printableValue;
 31
 32
 33public class Conversation extends AbstractEntity implements Blockable, Comparable<Conversation> {
 34	public static final String TABLENAME = "conversations";
 35
 36	public static final int STATUS_AVAILABLE = 0;
 37	public static final int STATUS_ARCHIVED = 1;
 38
 39	public static final int MODE_MULTI = 1;
 40	public static final int MODE_SINGLE = 0;
 41
 42	public static final String NAME = "name";
 43	public static final String ACCOUNT = "accountUuid";
 44	public static final String CONTACT = "contactUuid";
 45	public static final String CONTACTJID = "contactJid";
 46	public static final String STATUS = "status";
 47	public static final String CREATED = "created";
 48	public static final String MODE = "mode";
 49	public static final String ATTRIBUTES = "attributes";
 50
 51	public static final String ATTRIBUTE_MUTED_TILL = "muted_till";
 52	public static final String ATTRIBUTE_ALWAYS_NOTIFY = "always_notify";
 53	public static final String ATTRIBUTE_LAST_CLEAR_HISTORY = "last_clear_history";
 54	static final String ATTRIBUTE_MUC_PASSWORD = "muc_password";
 55	private static final String ATTRIBUTE_NEXT_MESSAGE = "next_message";
 56	private static final String ATTRIBUTE_NEXT_MESSAGE_TIMESTAMP = "next_message_timestamp";
 57	private static final String ATTRIBUTE_CRYPTO_TARGETS = "crypto_targets";
 58	private static final String ATTRIBUTE_NEXT_ENCRYPTION = "next_encryption";
 59	public static final String ATTRIBUTE_ALLOW_PM = "allow_pm";
 60	public static final String ATTRIBUTE_MEMBERS_ONLY = "members_only";
 61	public static final String ATTRIBUTE_MODERATED = "moderated";
 62	public static final String ATTRIBUTE_NON_ANONYMOUS = "non_anonymous";
 63	protected final ArrayList<Message> messages = new ArrayList<>();
 64	public AtomicBoolean messagesLoaded = new AtomicBoolean(true);
 65	protected Account account = null;
 66	private String draftMessage;
 67	private String name;
 68	private String contactUuid;
 69	private String accountUuid;
 70	private Jid contactJid;
 71	private int status;
 72	private long created;
 73	private int mode;
 74	private JSONObject attributes = new JSONObject();
 75	private Jid nextCounterpart;
 76	private transient MucOptions mucOptions = null;
 77	private boolean messagesLeftOnServer = true;
 78	private ChatState mOutgoingChatState = Config.DEFAULT_CHATSTATE;
 79	private ChatState mIncomingChatState = Config.DEFAULT_CHATSTATE;
 80	private String mFirstMamReference = null;
 81	private Message correctingMessage;
 82
 83	public Conversation(final String name, final Account account, final Jid contactJid,
 84	                    final int mode) {
 85		this(java.util.UUID.randomUUID().toString(), name, null, account
 86						.getUuid(), contactJid, System.currentTimeMillis(),
 87				STATUS_AVAILABLE, mode, "");
 88		this.account = account;
 89	}
 90
 91	public Conversation(final String uuid, final String name, final String contactUuid,
 92	                    final String accountUuid, final Jid contactJid, final long created, final int status,
 93	                    final int mode, final String attributes) {
 94		this.uuid = uuid;
 95		this.name = name;
 96		this.contactUuid = contactUuid;
 97		this.accountUuid = accountUuid;
 98		this.contactJid = contactJid;
 99		this.created = created;
100		this.status = status;
101		this.mode = mode;
102		try {
103			this.attributes = new JSONObject(attributes == null ? "" : attributes);
104		} catch (JSONException e) {
105			this.attributes = new JSONObject();
106		}
107	}
108
109	public static Conversation fromCursor(Cursor cursor) {
110		Jid jid;
111		try {
112			jid = Jid.of(cursor.getString(cursor.getColumnIndex(CONTACTJID)));
113		} catch (final IllegalArgumentException e) {
114			// Borked DB..
115			jid = null;
116		}
117		return new Conversation(cursor.getString(cursor.getColumnIndex(UUID)),
118				cursor.getString(cursor.getColumnIndex(NAME)),
119				cursor.getString(cursor.getColumnIndex(CONTACT)),
120				cursor.getString(cursor.getColumnIndex(ACCOUNT)),
121				jid,
122				cursor.getLong(cursor.getColumnIndex(CREATED)),
123				cursor.getInt(cursor.getColumnIndex(STATUS)),
124				cursor.getInt(cursor.getColumnIndex(MODE)),
125				cursor.getString(cursor.getColumnIndex(ATTRIBUTES)));
126	}
127
128	public boolean hasMessagesLeftOnServer() {
129		return messagesLeftOnServer;
130	}
131
132	public void setHasMessagesLeftOnServer(boolean value) {
133		this.messagesLeftOnServer = value;
134	}
135
136	public Message getFirstUnreadMessage() {
137		Message first = null;
138		synchronized (this.messages) {
139			for (int i = messages.size() - 1; i >= 0; --i) {
140				if (messages.get(i).isRead()) {
141					return first;
142				} else {
143					first = messages.get(i);
144				}
145			}
146		}
147		return first;
148	}
149
150	public Message findUnsentMessageWithUuid(String uuid) {
151		synchronized (this.messages) {
152			for (final Message message : this.messages) {
153				final int s = message.getStatus();
154				if ((s == Message.STATUS_UNSEND || s == Message.STATUS_WAITING) && message.getUuid().equals(uuid)) {
155					return message;
156				}
157			}
158		}
159		return null;
160	}
161
162	public void findWaitingMessages(OnMessageFound onMessageFound) {
163		synchronized (this.messages) {
164			for (Message message : this.messages) {
165				if (message.getStatus() == Message.STATUS_WAITING) {
166					onMessageFound.onMessageFound(message);
167				}
168			}
169		}
170	}
171
172	public void findUnreadMessages(OnMessageFound onMessageFound) {
173		synchronized (this.messages) {
174			for (Message message : this.messages) {
175				if (!message.isRead()) {
176					onMessageFound.onMessageFound(message);
177				}
178			}
179		}
180	}
181
182	public void findMessagesWithFiles(final OnMessageFound onMessageFound) {
183		synchronized (this.messages) {
184			for (final Message message : this.messages) {
185				if ((message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE)
186						&& message.getEncryption() != Message.ENCRYPTION_PGP) {
187					onMessageFound.onMessageFound(message);
188				}
189			}
190		}
191	}
192
193	public Message findMessageWithFileAndUuid(final String uuid) {
194		synchronized (this.messages) {
195			for (final Message message : this.messages) {
196				if (message.getUuid().equals(uuid)
197						&& message.getEncryption() != Message.ENCRYPTION_PGP
198						&& (message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE || message.treatAsDownloadable())) {
199					return message;
200				}
201			}
202		}
203		return null;
204	}
205
206	public void clearMessages() {
207		synchronized (this.messages) {
208			this.messages.clear();
209		}
210	}
211
212	public boolean setIncomingChatState(ChatState state) {
213		if (this.mIncomingChatState == state) {
214			return false;
215		}
216		this.mIncomingChatState = state;
217		return true;
218	}
219
220	public ChatState getIncomingChatState() {
221		return this.mIncomingChatState;
222	}
223
224	public boolean setOutgoingChatState(ChatState state) {
225		if (mode == MODE_SINGLE && !getContact().isSelf() || (isPrivateAndNonAnonymous() && getNextCounterpart() == null)) {
226			if (this.mOutgoingChatState != state) {
227				this.mOutgoingChatState = state;
228				return true;
229			}
230		}
231		return false;
232	}
233
234	public ChatState getOutgoingChatState() {
235		return this.mOutgoingChatState;
236	}
237
238	public void trim() {
239		synchronized (this.messages) {
240			final int size = messages.size();
241			final int maxsize = Config.PAGE_SIZE * Config.MAX_NUM_PAGES;
242			if (size > maxsize) {
243				List<Message> discards = this.messages.subList(0, size - maxsize);
244				final PgpDecryptionService pgpDecryptionService = account.getPgpDecryptionService();
245				if (pgpDecryptionService != null) {
246					pgpDecryptionService.discard(discards);
247				}
248				discards.clear();
249				untieMessages();
250			}
251		}
252	}
253
254	public void findUnsentMessagesWithEncryption(int encryptionType, OnMessageFound onMessageFound) {
255		synchronized (this.messages) {
256			for (Message message : this.messages) {
257				if ((message.getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_WAITING)
258						&& (message.getEncryption() == encryptionType)) {
259					onMessageFound.onMessageFound(message);
260				}
261			}
262		}
263	}
264
265	public void findUnsentTextMessages(OnMessageFound onMessageFound) {
266		synchronized (this.messages) {
267			for (Message message : this.messages) {
268				if (message.getType() != Message.TYPE_IMAGE
269						&& message.getStatus() == Message.STATUS_UNSEND) {
270					onMessageFound.onMessageFound(message);
271				}
272			}
273		}
274	}
275
276	public Message findSentMessageWithUuidOrRemoteId(String id) {
277		synchronized (this.messages) {
278			for (Message message : this.messages) {
279				if (id.equals(message.getUuid())
280						|| (message.getStatus() >= Message.STATUS_SEND
281						&& id.equals(message.getRemoteMsgId()))) {
282					return message;
283				}
284			}
285		}
286		return null;
287	}
288
289	public Message findMessageWithRemoteIdAndCounterpart(String id, Jid counterpart, boolean received, boolean carbon) {
290		synchronized (this.messages) {
291			for (int i = this.messages.size() - 1; i >= 0; --i) {
292				Message message = messages.get(i);
293				if (counterpart.equals(message.getCounterpart())
294						&& ((message.getStatus() == Message.STATUS_RECEIVED) == received)
295						&& (carbon == message.isCarbon() || received)) {
296					if (id.equals(message.getRemoteMsgId()) && !message.isFileOrImage() && !message.treatAsDownloadable()) {
297						return message;
298					} else {
299						return null;
300					}
301				}
302			}
303		}
304		return null;
305	}
306
307	public Message findSentMessageWithUuid(String id) {
308		synchronized (this.messages) {
309			for (Message message : this.messages) {
310				if (id.equals(message.getUuid())) {
311					return message;
312				}
313			}
314		}
315		return null;
316	}
317
318	public Message findMessageWithRemoteId(String id, Jid counterpart) {
319		synchronized (this.messages) {
320			for (Message message : this.messages) {
321				if (counterpart.equals(message.getCounterpart())
322						&& (id.equals(message.getRemoteMsgId()) || id.equals(message.getUuid()))) {
323					return message;
324				}
325			}
326		}
327		return null;
328	}
329
330	public boolean hasMessageWithCounterpart(Jid counterpart) {
331		synchronized (this.messages) {
332			for (Message message : this.messages) {
333				if (counterpart.equals(message.getCounterpart())) {
334					return true;
335				}
336			}
337		}
338		return false;
339	}
340
341	public void populateWithMessages(final List<Message> messages) {
342		synchronized (this.messages) {
343			messages.clear();
344			messages.addAll(this.messages);
345		}
346		for (Iterator<Message> iterator = messages.iterator(); iterator.hasNext(); ) {
347			if (iterator.next().wasMergedIntoPrevious()) {
348				iterator.remove();
349			}
350		}
351	}
352
353	@Override
354	public boolean isBlocked() {
355		return getContact().isBlocked();
356	}
357
358	@Override
359	public boolean isDomainBlocked() {
360		return getContact().isDomainBlocked();
361	}
362
363	@Override
364	public Jid getBlockedJid() {
365		return getContact().getBlockedJid();
366	}
367
368	public int countMessages() {
369		synchronized (this.messages) {
370			return this.messages.size();
371		}
372	}
373
374	public String getFirstMamReference() {
375		return this.mFirstMamReference;
376	}
377
378	public void setFirstMamReference(String reference) {
379		this.mFirstMamReference = reference;
380	}
381
382	public void setLastClearHistory(long time, String reference) {
383		if (reference != null) {
384			setAttribute(ATTRIBUTE_LAST_CLEAR_HISTORY, String.valueOf(time) + ":" + reference);
385		} else {
386			setAttribute(ATTRIBUTE_LAST_CLEAR_HISTORY, time);
387		}
388	}
389
390	public MamReference getLastClearHistory() {
391		return MamReference.fromAttribute(getAttribute(ATTRIBUTE_LAST_CLEAR_HISTORY));
392	}
393
394	public List<Jid> getAcceptedCryptoTargets() {
395		if (mode == MODE_SINGLE) {
396			return Collections.singletonList(getJid().asBareJid());
397		} else {
398			return getJidListAttribute(ATTRIBUTE_CRYPTO_TARGETS);
399		}
400	}
401
402	public void setAcceptedCryptoTargets(List<Jid> acceptedTargets) {
403		setAttribute(ATTRIBUTE_CRYPTO_TARGETS, acceptedTargets);
404	}
405
406	public boolean setCorrectingMessage(Message correctingMessage) {
407		this.correctingMessage = correctingMessage;
408		return correctingMessage == null && draftMessage != null;
409	}
410
411	public Message getCorrectingMessage() {
412		return this.correctingMessage;
413	}
414
415	public boolean withSelf() {
416		return getContact().isSelf();
417	}
418
419	@Override
420	public int compareTo(@NonNull Conversation another) {
421		return Long.compare(another.getSortableTime(), getSortableTime());
422	}
423
424	private long getSortableTime() {
425		Draft draft = getDraft();
426		long messageTime = getLatestMessage().getTimeSent();
427		if (draft == null) {
428			return messageTime;
429		} else {
430			return Math.max(messageTime, draft.getTimestamp());
431		}
432	}
433
434	public String getDraftMessage() {
435		return draftMessage;
436	}
437
438	public void setDraftMessage(String draftMessage) {
439		this.draftMessage = draftMessage;
440	}
441
442	public boolean isRead() {
443		return (this.messages.size() == 0) || this.messages.get(this.messages.size() - 1).isRead();
444	}
445
446	public List<Message> markRead(String upToUuid) {
447		final List<Message> unread = new ArrayList<>();
448		synchronized (this.messages) {
449			for (Message message : this.messages) {
450				if (!message.isRead()) {
451					message.markRead();
452					unread.add(message);
453				}
454				if (message.getUuid().equals(upToUuid)) {
455					return unread;
456				}
457			}
458		}
459		return unread;
460	}
461
462	public static Message getLatestMarkableMessage(final List<Message> messages, boolean isPrivateAndNonAnonymousMuc) {
463			for (int i = messages.size() - 1; i >= 0; --i) {
464				final Message message = messages.get(i);
465				if (message.getStatus() <= Message.STATUS_RECEIVED
466						&& (message.markable || isPrivateAndNonAnonymousMuc)
467						&& message.getType() != Message.TYPE_PRIVATE) {
468					return message;
469				}
470			}
471		return null;
472	}
473
474	public Message getLatestMessage() {
475		synchronized (this.messages) {
476			if (this.messages.size() == 0) {
477				Message message = new Message(this, "", Message.ENCRYPTION_NONE);
478				message.setType(Message.TYPE_STATUS);
479				message.setTime(Math.max(getCreated(), getLastClearHistory().getTimestamp()));
480				return message;
481			} else {
482				return this.messages.get(this.messages.size() - 1);
483			}
484		}
485	}
486
487	public @NonNull CharSequence getName() {
488		if (getMode() == MODE_MULTI) {
489			final String subject = getMucOptions().getSubject();
490			final Bookmark bookmark = getBookmark();
491			final String bookmarkName = bookmark != null ? bookmark.getBookmarkName() : null;
492			if (printableValue(subject)) {
493				return subject;
494			} else if (printableValue(bookmarkName, false)) {
495				return bookmarkName;
496			} else {
497				final String generatedName = getMucOptions().createNameFromParticipants();
498				if (printableValue(generatedName)) {
499					return generatedName;
500				} else {
501					return contactJid.getLocal() != null ? contactJid.getLocal() : contactJid;
502				}
503			}
504		} else if (isWithStranger()) {
505			return contactJid;
506		} else {
507			return this.getContact().getDisplayName();
508		}
509	}
510
511	public String getAccountUuid() {
512		return this.accountUuid;
513	}
514
515	public Account getAccount() {
516		return this.account;
517	}
518
519	public void setAccount(final Account account) {
520		this.account = account;
521	}
522
523	public Contact getContact() {
524		return this.account.getRoster().getContact(this.contactJid);
525	}
526
527	@Override
528	public Jid getJid() {
529		return this.contactJid;
530	}
531
532	public int getStatus() {
533		return this.status;
534	}
535
536	public void setStatus(int status) {
537		this.status = status;
538	}
539
540	public long getCreated() {
541		return this.created;
542	}
543
544	public ContentValues getContentValues() {
545		ContentValues values = new ContentValues();
546		values.put(UUID, uuid);
547		values.put(NAME, name);
548		values.put(CONTACT, contactUuid);
549		values.put(ACCOUNT, accountUuid);
550		values.put(CONTACTJID, contactJid.toString());
551		values.put(CREATED, created);
552		values.put(STATUS, status);
553		values.put(MODE, mode);
554		values.put(ATTRIBUTES, attributes.toString());
555		return values;
556	}
557
558	public int getMode() {
559		return this.mode;
560	}
561
562	public void setMode(int mode) {
563		this.mode = mode;
564	}
565
566	/**
567	 * short for is Private and Non-anonymous
568	 */
569	public boolean isSingleOrPrivateAndNonAnonymous() {
570		return mode == MODE_SINGLE || isPrivateAndNonAnonymous();
571	}
572
573	public boolean isPrivateAndNonAnonymous() {
574		return getMucOptions().isPrivateAndNonAnonymous();
575	}
576
577	public synchronized MucOptions getMucOptions() {
578		if (this.mucOptions == null) {
579			this.mucOptions = new MucOptions(this);
580		}
581		return this.mucOptions;
582	}
583
584	public void resetMucOptions() {
585		this.mucOptions = null;
586	}
587
588	public void setContactJid(final Jid jid) {
589		this.contactJid = jid;
590	}
591
592	public Jid getNextCounterpart() {
593		return this.nextCounterpart;
594	}
595
596	public void setNextCounterpart(Jid jid) {
597		this.nextCounterpart = jid;
598	}
599
600	public int getNextEncryption() {
601		if (!Config.supportOmemo() && !Config.supportOpenPgp()) {
602			return Message.ENCRYPTION_NONE;
603		}
604		if (OmemoSetting.isAlways()) {
605			return suitableForOmemoByDefault(this) ? Message.ENCRYPTION_AXOLOTL : Message.ENCRYPTION_NONE;
606		}
607		final int defaultEncryption;
608		if (suitableForOmemoByDefault(this)) {
609			defaultEncryption = OmemoSetting.getEncryption();
610		} else {
611			defaultEncryption = Message.ENCRYPTION_NONE;
612		}
613		int encryption = this.getIntAttribute(ATTRIBUTE_NEXT_ENCRYPTION, defaultEncryption);
614		if (encryption == Message.ENCRYPTION_OTR || encryption < 0) {
615			return defaultEncryption;
616		} else {
617			return encryption;
618		}
619	}
620
621	private static boolean suitableForOmemoByDefault(final Conversation conversation) {
622		if (conversation.getJid().asBareJid().equals(Config.BUG_REPORTS)) {
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				&& !contactJid.equals(Jid.ofDomain(account.getJid().getDomain()))
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}