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