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