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