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