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