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