Conversation.java

  1package eu.siacs.conversations.entities;
  2
  3import android.content.ContentValues;
  4import android.database.Cursor;
  5import android.support.annotation.NonNull;
  6import android.support.annotation.Nullable;
  7import android.text.TextUtils;
  8
  9import org.json.JSONArray;
 10import org.json.JSONException;
 11import org.json.JSONObject;
 12
 13import java.util.ArrayList;
 14import java.util.Collections;
 15import java.util.Comparator;
 16import java.util.Iterator;
 17import java.util.List;
 18import java.util.ListIterator;
 19import java.util.Locale;
 20import java.util.concurrent.atomic.AtomicBoolean;
 21
 22import eu.siacs.conversations.Config;
 23import eu.siacs.conversations.crypto.PgpDecryptionService;
 24import eu.siacs.conversations.crypto.axolotl.AxolotlService;
 25import eu.siacs.conversations.xmpp.chatstate.ChatState;
 26import eu.siacs.conversations.xmpp.mam.MamReference;
 27import rocks.xmpp.addr.Jid;
 28
 29import static eu.siacs.conversations.entities.Bookmark.printableValue;
 30
 31
 32public class Conversation extends AbstractEntity implements Blockable, Comparable<Conversation> {
 33	public static final String TABLENAME = "conversations";
 34
 35	public static final int STATUS_AVAILABLE = 0;
 36	public static final int STATUS_ARCHIVED = 1;
 37
 38	public static final int MODE_MULTI = 1;
 39	public static final int MODE_SINGLE = 0;
 40
 41	public static final String NAME = "name";
 42	public static final String ACCOUNT = "accountUuid";
 43	public static final String CONTACT = "contactUuid";
 44	public static final String CONTACTJID = "contactJid";
 45	public static final String STATUS = "status";
 46	public static final String CREATED = "created";
 47	public static final String MODE = "mode";
 48	public static final String ATTRIBUTES = "attributes";
 49
 50	public static final String ATTRIBUTE_MUTED_TILL = "muted_till";
 51	public static final String ATTRIBUTE_ALWAYS_NOTIFY = "always_notify";
 52	public static final String ATTRIBUTE_LAST_CLEAR_HISTORY = "last_clear_history";
 53	static final String ATTRIBUTE_MUC_PASSWORD = "muc_password";
 54	private static final String ATTRIBUTE_NEXT_MESSAGE = "next_message";
 55	private static final String ATTRIBUTE_NEXT_MESSAGE_TIMESTAMP = "next_message_timestamp";
 56	private static final String ATTRIBUTE_CRYPTO_TARGETS = "crypto_targets";
 57	private static final String ATTRIBUTE_NEXT_ENCRYPTION = "next_encryption";
 58	protected final ArrayList<Message> messages = new ArrayList<>();
 59	public AtomicBoolean messagesLoaded = new AtomicBoolean(true);
 60	protected Account account = null;
 61	private String draftMessage;
 62	private String name;
 63	private String contactUuid;
 64	private String accountUuid;
 65	private Jid contactJid;
 66	private int status;
 67	private long created;
 68	private int mode;
 69	private JSONObject attributes = new JSONObject();
 70	private Jid nextCounterpart;
 71	private transient MucOptions mucOptions = null;
 72	private boolean messagesLeftOnServer = true;
 73	private ChatState mOutgoingChatState = Config.DEFAULT_CHATSTATE;
 74	private ChatState mIncomingChatState = Config.DEFAULT_CHATSTATE;
 75	private String mFirstMamReference = null;
 76	private Message correctingMessage;
 77
 78	public Conversation(final String name, final Account account, final Jid contactJid,
 79	                    final int mode) {
 80		this(java.util.UUID.randomUUID().toString(), name, null, account
 81						.getUuid(), contactJid, System.currentTimeMillis(),
 82				STATUS_AVAILABLE, mode, "");
 83		this.account = account;
 84	}
 85
 86	public Conversation(final String uuid, final String name, final String contactUuid,
 87	                    final String accountUuid, final Jid contactJid, final long created, final int status,
 88	                    final int mode, final String attributes) {
 89		this.uuid = uuid;
 90		this.name = name;
 91		this.contactUuid = contactUuid;
 92		this.accountUuid = accountUuid;
 93		this.contactJid = contactJid;
 94		this.created = created;
 95		this.status = status;
 96		this.mode = mode;
 97		try {
 98			this.attributes = new JSONObject(attributes == null ? "" : attributes);
 99		} catch (JSONException e) {
100			this.attributes = new JSONObject();
101		}
102	}
103
104	public static Conversation fromCursor(Cursor cursor) {
105		Jid jid;
106		try {
107			jid = Jid.of(cursor.getString(cursor.getColumnIndex(CONTACTJID)));
108		} catch (final IllegalArgumentException e) {
109			// Borked DB..
110			jid = null;
111		}
112		return new Conversation(cursor.getString(cursor.getColumnIndex(UUID)),
113				cursor.getString(cursor.getColumnIndex(NAME)),
114				cursor.getString(cursor.getColumnIndex(CONTACT)),
115				cursor.getString(cursor.getColumnIndex(ACCOUNT)),
116				jid,
117				cursor.getLong(cursor.getColumnIndex(CREATED)),
118				cursor.getInt(cursor.getColumnIndex(STATUS)),
119				cursor.getInt(cursor.getColumnIndex(MODE)),
120				cursor.getString(cursor.getColumnIndex(ATTRIBUTES)));
121	}
122
123	public boolean hasMessagesLeftOnServer() {
124		return messagesLeftOnServer;
125	}
126
127	public void setHasMessagesLeftOnServer(boolean value) {
128		this.messagesLeftOnServer = value;
129	}
130
131	public Message getFirstUnreadMessage() {
132		Message first = null;
133		synchronized (this.messages) {
134			for (int i = messages.size() - 1; i >= 0; --i) {
135				if (messages.get(i).isRead()) {
136					return first;
137				} else {
138					first = messages.get(i);
139				}
140			}
141		}
142		return first;
143	}
144
145	public Message findUnsentMessageWithUuid(String uuid) {
146		synchronized (this.messages) {
147			for (final Message message : this.messages) {
148				final int s = message.getStatus();
149				if ((s == Message.STATUS_UNSEND || s == Message.STATUS_WAITING) && message.getUuid().equals(uuid)) {
150					return message;
151				}
152			}
153		}
154		return null;
155	}
156
157	public void findWaitingMessages(OnMessageFound onMessageFound) {
158		synchronized (this.messages) {
159			for (Message message : this.messages) {
160				if (message.getStatus() == Message.STATUS_WAITING) {
161					onMessageFound.onMessageFound(message);
162				}
163			}
164		}
165	}
166
167	public void findUnreadMessages(OnMessageFound onMessageFound) {
168		synchronized (this.messages) {
169			for (Message message : this.messages) {
170				if (!message.isRead()) {
171					onMessageFound.onMessageFound(message);
172				}
173			}
174		}
175	}
176
177	public void findMessagesWithFiles(final OnMessageFound onMessageFound) {
178		synchronized (this.messages) {
179			for (final Message message : this.messages) {
180				if ((message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE)
181						&& message.getEncryption() != Message.ENCRYPTION_PGP) {
182					onMessageFound.onMessageFound(message);
183				}
184			}
185		}
186	}
187
188	public Message findMessageWithFileAndUuid(final String uuid) {
189		synchronized (this.messages) {
190			for (final Message message : this.messages) {
191				if (message.getUuid().equals(uuid)
192						&& message.getEncryption() != Message.ENCRYPTION_PGP
193						&& (message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE || message.treatAsDownloadable())) {
194					return message;
195				}
196			}
197		}
198		return null;
199	}
200
201	public void clearMessages() {
202		synchronized (this.messages) {
203			this.messages.clear();
204		}
205	}
206
207	public boolean setIncomingChatState(ChatState state) {
208		if (this.mIncomingChatState == state) {
209			return false;
210		}
211		this.mIncomingChatState = state;
212		return true;
213	}
214
215	public ChatState getIncomingChatState() {
216		return this.mIncomingChatState;
217	}
218
219	public boolean setOutgoingChatState(ChatState state) {
220		if (mode == MODE_SINGLE && !getContact().isSelf() || (isPrivateAndNonAnonymous() && getNextCounterpart() == null)) {
221			if (this.mOutgoingChatState != state) {
222				this.mOutgoingChatState = state;
223				return true;
224			}
225		}
226		return false;
227	}
228
229	public ChatState getOutgoingChatState() {
230		return this.mOutgoingChatState;
231	}
232
233	public void trim() {
234		synchronized (this.messages) {
235			final int size = messages.size();
236			final int maxsize = Config.PAGE_SIZE * Config.MAX_NUM_PAGES;
237			if (size > maxsize) {
238				List<Message> discards = this.messages.subList(0, size - maxsize);
239				final PgpDecryptionService pgpDecryptionService = account.getPgpDecryptionService();
240				if (pgpDecryptionService != null) {
241					pgpDecryptionService.discard(discards);
242				}
243				discards.clear();
244				untieMessages();
245			}
246		}
247	}
248
249	public void findUnsentMessagesWithEncryption(int encryptionType, OnMessageFound onMessageFound) {
250		synchronized (this.messages) {
251			for (Message message : this.messages) {
252				if ((message.getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_WAITING)
253						&& (message.getEncryption() == encryptionType)) {
254					onMessageFound.onMessageFound(message);
255				}
256			}
257		}
258	}
259
260	public void findUnsentTextMessages(OnMessageFound onMessageFound) {
261		synchronized (this.messages) {
262			for (Message message : this.messages) {
263				if (message.getType() != Message.TYPE_IMAGE
264						&& message.getStatus() == Message.STATUS_UNSEND) {
265					onMessageFound.onMessageFound(message);
266				}
267			}
268		}
269	}
270
271	public Message findSentMessageWithUuidOrRemoteId(String id) {
272		synchronized (this.messages) {
273			for (Message message : this.messages) {
274				if (id.equals(message.getUuid())
275						|| (message.getStatus() >= Message.STATUS_SEND
276						&& id.equals(message.getRemoteMsgId()))) {
277					return message;
278				}
279			}
280		}
281		return null;
282	}
283
284	public Message findMessageWithRemoteIdAndCounterpart(String id, Jid counterpart, boolean received, boolean carbon) {
285		synchronized (this.messages) {
286			for (int i = this.messages.size() - 1; i >= 0; --i) {
287				Message message = messages.get(i);
288				if (counterpart.equals(message.getCounterpart())
289						&& ((message.getStatus() == Message.STATUS_RECEIVED) == received)
290						&& (carbon == message.isCarbon() || received)) {
291					if (id.equals(message.getRemoteMsgId()) && !message.isFileOrImage() && !message.treatAsDownloadable()) {
292						return message;
293					} else {
294						return null;
295					}
296				}
297			}
298		}
299		return null;
300	}
301
302	public Message findSentMessageWithUuid(String id) {
303		synchronized (this.messages) {
304			for (Message message : this.messages) {
305				if (id.equals(message.getUuid())) {
306					return message;
307				}
308			}
309		}
310		return null;
311	}
312
313	public Message findMessageWithRemoteId(String id, Jid counterpart) {
314		synchronized (this.messages) {
315			for (Message message : this.messages) {
316				if (counterpart.equals(message.getCounterpart())
317						&& (id.equals(message.getRemoteMsgId()) || id.equals(message.getUuid()))) {
318					return message;
319				}
320			}
321		}
322		return null;
323	}
324
325	public boolean hasMessageWithCounterpart(Jid counterpart) {
326		synchronized (this.messages) {
327			for (Message message : this.messages) {
328				if (counterpart.equals(message.getCounterpart())) {
329					return true;
330				}
331			}
332		}
333		return false;
334	}
335
336	public void populateWithMessages(final List<Message> messages) {
337		synchronized (this.messages) {
338			messages.clear();
339			messages.addAll(this.messages);
340		}
341		for (Iterator<Message> iterator = messages.iterator(); iterator.hasNext(); ) {
342			if (iterator.next().wasMergedIntoPrevious()) {
343				iterator.remove();
344			}
345		}
346	}
347
348	@Override
349	public boolean isBlocked() {
350		return getContact().isBlocked();
351	}
352
353	@Override
354	public boolean isDomainBlocked() {
355		return getContact().isDomainBlocked();
356	}
357
358	@Override
359	public Jid getBlockedJid() {
360		return getContact().getBlockedJid();
361	}
362
363	public int countMessages() {
364		synchronized (this.messages) {
365			return this.messages.size();
366		}
367	}
368
369	public String getFirstMamReference() {
370		return this.mFirstMamReference;
371	}
372
373	public void setFirstMamReference(String reference) {
374		this.mFirstMamReference = reference;
375	}
376
377	public void setLastClearHistory(long time, String reference) {
378		if (reference != null) {
379			setAttribute(ATTRIBUTE_LAST_CLEAR_HISTORY, String.valueOf(time) + ":" + reference);
380		} else {
381			setAttribute(ATTRIBUTE_LAST_CLEAR_HISTORY, time);
382		}
383	}
384
385	public MamReference getLastClearHistory() {
386		return MamReference.fromAttribute(getAttribute(ATTRIBUTE_LAST_CLEAR_HISTORY));
387	}
388
389	public List<Jid> getAcceptedCryptoTargets() {
390		if (mode == MODE_SINGLE) {
391			return Collections.singletonList(getJid().asBareJid());
392		} else {
393			return getJidListAttribute(ATTRIBUTE_CRYPTO_TARGETS);
394		}
395	}
396
397	public void setAcceptedCryptoTargets(List<Jid> acceptedTargets) {
398		setAttribute(ATTRIBUTE_CRYPTO_TARGETS, acceptedTargets);
399	}
400
401	public boolean setCorrectingMessage(Message correctingMessage) {
402		this.correctingMessage = correctingMessage;
403		return correctingMessage == null && draftMessage != null;
404	}
405
406	public Message getCorrectingMessage() {
407		return this.correctingMessage;
408	}
409
410	public boolean withSelf() {
411		return getContact().isSelf();
412	}
413
414	@Override
415	public int compareTo(@NonNull Conversation another) {
416		return Long.compare(another.getSortableTime(), getSortableTime());
417	}
418
419	private long getSortableTime() {
420		Draft draft = getDraft();
421		long messageTime = getLatestMessage().getTimeSent();
422		if (draft == null) {
423			return messageTime;
424		} else {
425			return Math.max(messageTime, draft.getTimestamp());
426		}
427	}
428
429	public String getDraftMessage() {
430		return draftMessage;
431	}
432
433	public void setDraftMessage(String draftMessage) {
434		this.draftMessage = draftMessage;
435	}
436
437	public boolean isRead() {
438		return (this.messages.size() == 0) || this.messages.get(this.messages.size() - 1).isRead();
439	}
440
441	public List<Message> markRead() {
442		final List<Message> unread = new ArrayList<>();
443		synchronized (this.messages) {
444			for (Message message : this.messages) {
445				if (!message.isRead()) {
446					message.markRead();
447					unread.add(message);
448				}
449			}
450		}
451		return unread;
452	}
453
454	public Message getLatestMarkableMessage(boolean isPrivateAndNonAnonymousMuc) {
455		synchronized (this.messages) {
456			for (int i = this.messages.size() - 1; i >= 0; --i) {
457				final Message message = this.messages.get(i);
458				if (message.getStatus() <= Message.STATUS_RECEIVED
459						&& (message.markable || isPrivateAndNonAnonymousMuc)
460						&& message.getType() != Message.TYPE_PRIVATE) {
461					return message.isRead() ? null : message;
462				}
463			}
464		}
465		return null;
466	}
467
468	public Message getLatestMessage() {
469		synchronized (this.messages) {
470			if (this.messages.size() == 0) {
471				Message message = new Message(this, "", Message.ENCRYPTION_NONE);
472				message.setType(Message.TYPE_STATUS);
473				message.setTime(Math.max(getCreated(), getLastClearHistory().getTimestamp()));
474				return message;
475			} else {
476				return this.messages.get(this.messages.size() - 1);
477			}
478		}
479	}
480
481	public CharSequence getName() {
482		if (getMode() == MODE_MULTI) {
483			final String subject = getMucOptions().getSubject();
484			final Bookmark bookmark = getBookmark();
485			final String bookmarkName = bookmark != null ? bookmark.getBookmarkName() : null;
486			if (printableValue(subject)) {
487				return subject;
488			} else if (printableValue(bookmarkName, false)) {
489				return bookmarkName;
490			} else {
491				final String generatedName = getMucOptions().createNameFromParticipants();
492				if (printableValue(generatedName)) {
493					return generatedName;
494				} else {
495					return getJid().getLocal();
496				}
497			}
498		} else if (isWithStranger()) {
499			return contactJid;
500		} else {
501			return this.getContact().getDisplayName();
502		}
503	}
504
505	public String getAccountUuid() {
506		return this.accountUuid;
507	}
508
509	public Account getAccount() {
510		return this.account;
511	}
512
513	public void setAccount(final Account account) {
514		this.account = account;
515	}
516
517	public Contact getContact() {
518		return this.account.getRoster().getContact(this.contactJid);
519	}
520
521	@Override
522	public Jid getJid() {
523		return this.contactJid;
524	}
525
526	public int getStatus() {
527		return this.status;
528	}
529
530	public void setStatus(int status) {
531		this.status = status;
532	}
533
534	public long getCreated() {
535		return this.created;
536	}
537
538	public ContentValues getContentValues() {
539		ContentValues values = new ContentValues();
540		values.put(UUID, uuid);
541		values.put(NAME, name);
542		values.put(CONTACT, contactUuid);
543		values.put(ACCOUNT, accountUuid);
544		values.put(CONTACTJID, contactJid.toString());
545		values.put(CREATED, created);
546		values.put(STATUS, status);
547		values.put(MODE, mode);
548		values.put(ATTRIBUTES, attributes.toString());
549		return values;
550	}
551
552	public int getMode() {
553		return this.mode;
554	}
555
556	public void setMode(int mode) {
557		this.mode = mode;
558	}
559
560	/**
561	 * short for is Private and Non-anonymous
562	 */
563	public boolean isSingleOrPrivateAndNonAnonymous() {
564		return mode == MODE_SINGLE || isPrivateAndNonAnonymous();
565	}
566
567	public boolean isPrivateAndNonAnonymous() {
568		return getMucOptions().isPrivateAndNonAnonymous();
569	}
570
571	public synchronized MucOptions getMucOptions() {
572		if (this.mucOptions == null) {
573			this.mucOptions = new MucOptions(this);
574		}
575		return this.mucOptions;
576	}
577
578	public void resetMucOptions() {
579		this.mucOptions = null;
580	}
581
582	public void setContactJid(final Jid jid) {
583		this.contactJid = jid;
584	}
585
586	public Jid getNextCounterpart() {
587		return this.nextCounterpart;
588	}
589
590	public void setNextCounterpart(Jid jid) {
591		this.nextCounterpart = jid;
592	}
593
594	public int getNextEncryption() {
595		final int defaultEncryption;
596		if (!Config.supportOmemo() && !Config.supportOpenPgp()) {
597			return Message.ENCRYPTION_NONE;
598		}
599		if (contactJid.asBareJid().equals(Config.BUG_REPORTS)) {
600			defaultEncryption = Message.ENCRYPTION_NONE;
601		} else if (suitableForOmemoByDefault(this)) {
602			defaultEncryption = Message.ENCRYPTION_AXOLOTL;
603		} else {
604			defaultEncryption = Message.ENCRYPTION_NONE;
605		}
606		int encryption = this.getIntAttribute(ATTRIBUTE_NEXT_ENCRYPTION, defaultEncryption);
607		if (encryption == Message.ENCRYPTION_OTR || encryption < 0) {
608			return defaultEncryption;
609		} else {
610			return encryption;
611		}
612	}
613
614	private static boolean suitableForOmemoByDefault(final Conversation conversation) {
615		final String contact = conversation.getJid().getDomain();
616		final String account = conversation.getAccount().getServer();
617		if (Config.OMEMO_EXCEPTIONS.CONTACT_DOMAINS.contains(contact) || Config.OMEMO_EXCEPTIONS.ACCOUNT_DOMAINS.contains(account)) {
618			return false;
619		}
620		final AxolotlService axolotlService = conversation.getAccount().getAxolotlService();
621		return axolotlService != null && axolotlService.isConversationAxolotlCapable(conversation);
622	}
623
624	public void setNextEncryption(int encryption) {
625		this.setAttribute(ATTRIBUTE_NEXT_ENCRYPTION, String.valueOf(encryption));
626	}
627
628	public String getNextMessage() {
629		final String nextMessage = getAttribute(ATTRIBUTE_NEXT_MESSAGE);
630		return nextMessage == null ? "" : nextMessage;
631	}
632
633	public @Nullable
634	Draft getDraft() {
635		long timestamp = getLongAttribute(ATTRIBUTE_NEXT_MESSAGE_TIMESTAMP, 0);
636		if (timestamp > getLatestMessage().getTimeSent()) {
637			String message = getAttribute(ATTRIBUTE_NEXT_MESSAGE);
638			if (!TextUtils.isEmpty(message) && timestamp != 0) {
639				return new Draft(message, timestamp);
640			}
641		}
642		return null;
643	}
644
645	public boolean setNextMessage(String message) {
646		boolean changed = !getNextMessage().equals(message);
647		this.setAttribute(ATTRIBUTE_NEXT_MESSAGE, message);
648		if (changed) {
649			this.setAttribute(ATTRIBUTE_NEXT_MESSAGE_TIMESTAMP, TextUtils.isEmpty(message) ? 0 : System.currentTimeMillis());
650		}
651		return changed;
652	}
653
654	public Bookmark getBookmark() {
655		return this.account.getBookmark(this.contactJid);
656	}
657
658	public Message findDuplicateMessage(Message message) {
659		synchronized (this.messages) {
660			for (int i = this.messages.size() - 1; i >= 0; --i) {
661				if (this.messages.get(i).similar(message)) {
662					return this.messages.get(i);
663				}
664			}
665		}
666		return null;
667	}
668
669	public boolean hasDuplicateMessage(Message message) {
670		return findDuplicateMessage(message) != null;
671	}
672
673	public Message findSentMessageWithBody(String body) {
674		synchronized (this.messages) {
675			for (int i = this.messages.size() - 1; i >= 0; --i) {
676				Message message = this.messages.get(i);
677				if (message.getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_SEND) {
678					String otherBody;
679					if (message.hasFileOnRemoteHost()) {
680						otherBody = message.getFileParams().url.toString();
681					} else {
682						otherBody = message.body;
683					}
684					if (otherBody != null && otherBody.equals(body)) {
685						return message;
686					}
687				}
688			}
689			return null;
690		}
691	}
692
693	public MamReference getLastMessageTransmitted() {
694		final MamReference lastClear = getLastClearHistory();
695		MamReference lastReceived = new MamReference(0);
696		synchronized (this.messages) {
697			for (int i = this.messages.size() - 1; i >= 0; --i) {
698				final Message message = this.messages.get(i);
699				if (message.getType() == Message.TYPE_PRIVATE) {
700					continue; //it's unsafe to use private messages as anchor. They could be coming from user archive
701				}
702				if (message.getStatus() == Message.STATUS_RECEIVED || message.isCarbon() || message.getServerMsgId() != null) {
703					lastReceived = new MamReference(message.getTimeSent(), message.getServerMsgId());
704					break;
705				}
706			}
707		}
708		return MamReference.max(lastClear, lastReceived);
709	}
710
711	public void setMutedTill(long value) {
712		this.setAttribute(ATTRIBUTE_MUTED_TILL, String.valueOf(value));
713	}
714
715	public boolean isMuted() {
716		return System.currentTimeMillis() < this.getLongAttribute(ATTRIBUTE_MUTED_TILL, 0);
717	}
718
719	public boolean alwaysNotify() {
720		return mode == MODE_SINGLE || getBooleanAttribute(ATTRIBUTE_ALWAYS_NOTIFY, Config.ALWAYS_NOTIFY_BY_DEFAULT || isPrivateAndNonAnonymous());
721	}
722
723	private boolean setAttribute(String key, long value) {
724		return setAttribute(key, Long.toString(value));
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 interface OnMessageFound {
914		void onMessageFound(final Message message);
915	}
916
917	public static class Draft {
918		private final String message;
919		private final long timestamp;
920
921		private Draft(String message, long timestamp) {
922			this.message = message;
923			this.timestamp = timestamp;
924		}
925
926		public long getTimestamp() {
927			return timestamp;
928		}
929
930		public String getMessage() {
931			return message;
932		}
933	}
934}