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		AxolotlService axolotlService = account.getAxolotlService();
597		if (contactJid.asBareJid().equals(Config.BUG_REPORTS)) {
598			defaultEncryption = Message.ENCRYPTION_NONE;
599		} else if (axolotlService != null && axolotlService.isConversationAxolotlCapable(this)) {
600			defaultEncryption = Message.ENCRYPTION_AXOLOTL;
601		} else {
602			defaultEncryption = Message.ENCRYPTION_NONE;
603		}
604		int encryption = this.getIntAttribute(ATTRIBUTE_NEXT_ENCRYPTION, defaultEncryption);
605		if (encryption == Message.ENCRYPTION_OTR || encryption < 0) {
606			return defaultEncryption;
607		} else {
608			return encryption;
609		}
610	}
611
612	public void setNextEncryption(int encryption) {
613		this.setAttribute(ATTRIBUTE_NEXT_ENCRYPTION, String.valueOf(encryption));
614	}
615
616	public String getNextMessage() {
617		final String nextMessage = getAttribute(ATTRIBUTE_NEXT_MESSAGE);
618		return nextMessage == null ? "" : nextMessage;
619	}
620
621	public @Nullable
622	Draft getDraft() {
623		long timestamp = getLongAttribute(ATTRIBUTE_NEXT_MESSAGE_TIMESTAMP, 0);
624		if (timestamp > getLatestMessage().getTimeSent()) {
625			String message = getAttribute(ATTRIBUTE_NEXT_MESSAGE);
626			if (!TextUtils.isEmpty(message) && timestamp != 0) {
627				return new Draft(message, timestamp);
628			}
629		}
630		return null;
631	}
632
633	public boolean setNextMessage(String message) {
634		boolean changed = !getNextMessage().equals(message);
635		this.setAttribute(ATTRIBUTE_NEXT_MESSAGE, message);
636		if (changed) {
637			this.setAttribute(ATTRIBUTE_NEXT_MESSAGE_TIMESTAMP, TextUtils.isEmpty(message) ? 0 : System.currentTimeMillis());
638		}
639		return changed;
640	}
641
642	public Bookmark getBookmark() {
643		return this.account.getBookmark(this.contactJid);
644	}
645
646	public Message findDuplicateMessage(Message message) {
647		synchronized (this.messages) {
648			for (int i = this.messages.size() - 1; i >= 0; --i) {
649				if (this.messages.get(i).similar(message)) {
650					return this.messages.get(i);
651				}
652			}
653		}
654		return null;
655	}
656
657	public boolean hasDuplicateMessage(Message message) {
658		return findDuplicateMessage(message) != null;
659	}
660
661	public Message findSentMessageWithBody(String body) {
662		synchronized (this.messages) {
663			for (int i = this.messages.size() - 1; i >= 0; --i) {
664				Message message = this.messages.get(i);
665				if (message.getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_SEND) {
666					String otherBody;
667					if (message.hasFileOnRemoteHost()) {
668						otherBody = message.getFileParams().url.toString();
669					} else {
670						otherBody = message.body;
671					}
672					if (otherBody != null && otherBody.equals(body)) {
673						return message;
674					}
675				}
676			}
677			return null;
678		}
679	}
680
681	public MamReference getLastMessageTransmitted() {
682		final MamReference lastClear = getLastClearHistory();
683		MamReference lastReceived = new MamReference(0);
684		synchronized (this.messages) {
685			for (int i = this.messages.size() - 1; i >= 0; --i) {
686				final Message message = this.messages.get(i);
687				if (message.getType() == Message.TYPE_PRIVATE) {
688					continue; //it's unsafe to use private messages as anchor. They could be coming from user archive
689				}
690				if (message.getStatus() == Message.STATUS_RECEIVED || message.isCarbon() || message.getServerMsgId() != null) {
691					lastReceived = new MamReference(message.getTimeSent(), message.getServerMsgId());
692					break;
693				}
694			}
695		}
696		return MamReference.max(lastClear, lastReceived);
697	}
698
699	public void setMutedTill(long value) {
700		this.setAttribute(ATTRIBUTE_MUTED_TILL, String.valueOf(value));
701	}
702
703	public boolean isMuted() {
704		return System.currentTimeMillis() < this.getLongAttribute(ATTRIBUTE_MUTED_TILL, 0);
705	}
706
707	public boolean alwaysNotify() {
708		return mode == MODE_SINGLE || getBooleanAttribute(ATTRIBUTE_ALWAYS_NOTIFY, Config.ALWAYS_NOTIFY_BY_DEFAULT || isPrivateAndNonAnonymous());
709	}
710
711	private boolean setAttribute(String key, long value) {
712		return setAttribute(key, Long.toString(value));
713	}
714
715	public boolean setAttribute(String key, String value) {
716		synchronized (this.attributes) {
717			try {
718				this.attributes.put(key, value == null ? "" : value);
719				return true;
720			} catch (JSONException e) {
721				return false;
722			}
723		}
724	}
725
726	public boolean setAttribute(String key, List<Jid> jids) {
727		JSONArray array = new JSONArray();
728		for (Jid jid : jids) {
729			array.put(jid.asBareJid().toString());
730		}
731		synchronized (this.attributes) {
732			try {
733				this.attributes.put(key, array);
734				return true;
735			} catch (JSONException e) {
736				e.printStackTrace();
737				return false;
738			}
739		}
740	}
741
742	public String getAttribute(String key) {
743		synchronized (this.attributes) {
744			try {
745				return this.attributes.getString(key);
746			} catch (JSONException e) {
747				return null;
748			}
749		}
750	}
751
752	private List<Jid> getJidListAttribute(String key) {
753		ArrayList<Jid> list = new ArrayList<>();
754		synchronized (this.attributes) {
755			try {
756				JSONArray array = this.attributes.getJSONArray(key);
757				for (int i = 0; i < array.length(); ++i) {
758					try {
759						list.add(Jid.of(array.getString(i)));
760					} catch (IllegalArgumentException e) {
761						//ignored
762					}
763				}
764			} catch (JSONException e) {
765				//ignored
766			}
767		}
768		return list;
769	}
770
771	private int getIntAttribute(String key, int defaultValue) {
772		String value = this.getAttribute(key);
773		if (value == null) {
774			return defaultValue;
775		} else {
776			try {
777				return Integer.parseInt(value);
778			} catch (NumberFormatException e) {
779				return defaultValue;
780			}
781		}
782	}
783
784	public long getLongAttribute(String key, long defaultValue) {
785		String value = this.getAttribute(key);
786		if (value == null) {
787			return defaultValue;
788		} else {
789			try {
790				return Long.parseLong(value);
791			} catch (NumberFormatException e) {
792				return defaultValue;
793			}
794		}
795	}
796
797	private boolean getBooleanAttribute(String key, boolean defaultValue) {
798		String value = this.getAttribute(key);
799		if (value == null) {
800			return defaultValue;
801		} else {
802			return Boolean.parseBoolean(value);
803		}
804	}
805
806	public void add(Message message) {
807		synchronized (this.messages) {
808			this.messages.add(message);
809		}
810	}
811
812	public void prepend(int offset, Message message) {
813		synchronized (this.messages) {
814			this.messages.add(Math.min(offset, this.messages.size()), message);
815		}
816	}
817
818	public void addAll(int index, List<Message> messages) {
819		synchronized (this.messages) {
820			this.messages.addAll(index, messages);
821		}
822		account.getPgpDecryptionService().decrypt(messages);
823	}
824
825	public void expireOldMessages(long timestamp) {
826		synchronized (this.messages) {
827			for (ListIterator<Message> iterator = this.messages.listIterator(); iterator.hasNext(); ) {
828				if (iterator.next().getTimeSent() < timestamp) {
829					iterator.remove();
830				}
831			}
832			untieMessages();
833		}
834	}
835
836	public void sort() {
837		synchronized (this.messages) {
838			Collections.sort(this.messages, (left, right) -> {
839				if (left.getTimeSent() < right.getTimeSent()) {
840					return -1;
841				} else if (left.getTimeSent() > right.getTimeSent()) {
842					return 1;
843				} else {
844					return 0;
845				}
846			});
847			untieMessages();
848		}
849	}
850
851	private void untieMessages() {
852		for (Message message : this.messages) {
853			message.untie();
854		}
855	}
856
857	public int unreadCount() {
858		synchronized (this.messages) {
859			int count = 0;
860			for (int i = this.messages.size() - 1; i >= 0; --i) {
861				if (this.messages.get(i).isRead()) {
862					return count;
863				}
864				++count;
865			}
866			return count;
867		}
868	}
869
870	public int receivedMessagesCount() {
871		int count = 0;
872		synchronized (this.messages) {
873			for (Message message : messages) {
874				if (message.getStatus() == Message.STATUS_RECEIVED) {
875					++count;
876				}
877			}
878		}
879		return count;
880	}
881
882	private int sentMessagesCount() {
883		int count = 0;
884		synchronized (this.messages) {
885			for (Message message : messages) {
886				if (message.getStatus() != Message.STATUS_RECEIVED) {
887					++count;
888				}
889			}
890		}
891		return count;
892	}
893
894	public boolean isWithStranger() {
895		return mode == MODE_SINGLE
896				&& !getJid().equals(Jid.ofDomain(account.getJid().getDomain()))
897				&& !getContact().showInRoster()
898				&& sentMessagesCount() == 0;
899	}
900
901	public interface OnMessageFound {
902		void onMessageFound(final Message message);
903	}
904
905	public static class Draft {
906		private final String message;
907		private final long timestamp;
908
909		private Draft(String message, long timestamp) {
910			this.message = message;
911			this.timestamp = timestamp;
912		}
913
914		public long getTimestamp() {
915			return timestamp;
916		}
917
918		public String getMessage() {
919			return message;
920		}
921	}
922}