Conversation.java

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