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 CharSequence 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;
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 (contactJid.asBareJid().equals(Config.BUG_REPORTS)) {
604			defaultEncryption = Message.ENCRYPTION_NONE;
605		} else if (axolotlService != null && axolotlService.isConversationAxolotlCapable(this)) {
606			defaultEncryption = Message.ENCRYPTION_AXOLOTL;
607		} else {
608			defaultEncryption =  Message.ENCRYPTION_NONE;
609		}
610		int encryption = this.getIntAttribute(ATTRIBUTE_NEXT_ENCRYPTION, defaultEncryption);
611		if (encryption == Message.ENCRYPTION_OTR) {
612			return defaultEncryption;
613		} else {
614			return encryption;
615		}
616	}
617
618	public void setNextEncryption(int encryption) {
619		this.setAttribute(ATTRIBUTE_NEXT_ENCRYPTION, String.valueOf(encryption));
620	}
621
622	public String getNextMessage() {
623		final String nextMessage = getAttribute(ATTRIBUTE_NEXT_MESSAGE);
624		return nextMessage == null ? "" : nextMessage;
625	}
626
627	public boolean setNextMessage(String message) {
628		boolean changed = !getNextMessage().equals(message);
629		this.setAttribute(ATTRIBUTE_NEXT_MESSAGE, message);
630		return changed;
631	}
632
633	public Bookmark getBookmark() {
634		return this.account.getBookmark(this.contactJid);
635	}
636
637	public Message findDuplicateMessage(Message message) {
638		synchronized (this.messages) {
639			for (int i = this.messages.size() - 1; i >= 0; --i) {
640				if (this.messages.get(i).similar(message)) {
641					return this.messages.get(i);
642				}
643			}
644		}
645		return null;
646	}
647
648	public boolean hasDuplicateMessage(Message message) {
649		return findDuplicateMessage(message) != null;
650	}
651
652	public Message findSentMessageWithBody(String body) {
653		synchronized (this.messages) {
654			for (int i = this.messages.size() - 1; i >= 0; --i) {
655				Message message = this.messages.get(i);
656				if (message.getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_SEND) {
657					String otherBody;
658					if (message.hasFileOnRemoteHost()) {
659						otherBody = message.getFileParams().url.toString();
660					} else {
661						otherBody = message.body;
662					}
663					if (otherBody != null && otherBody.equals(body)) {
664						return message;
665					}
666				}
667			}
668			return null;
669		}
670	}
671
672	public MamReference getLastMessageTransmitted() {
673		final MamReference lastClear = getLastClearHistory();
674		MamReference lastReceived = new MamReference(0);
675		synchronized (this.messages) {
676			for(int i = this.messages.size() - 1; i >= 0; --i) {
677				final Message message = this.messages.get(i);
678				if (message.getType() == Message.TYPE_PRIVATE) {
679					continue; //it's unsafe to use private messages as anchor. They could be coming from user archive
680				}
681				if (message.getStatus() == Message.STATUS_RECEIVED || message.isCarbon() || message.getServerMsgId() != null) {
682					lastReceived = new MamReference(message.getTimeSent(),message.getServerMsgId());
683					break;
684				}
685			}
686		}
687		return MamReference.max(lastClear,lastReceived);
688	}
689
690	public void setMutedTill(long value) {
691		this.setAttribute(ATTRIBUTE_MUTED_TILL, String.valueOf(value));
692	}
693
694	public boolean isMuted() {
695		return System.currentTimeMillis() < this.getLongAttribute(ATTRIBUTE_MUTED_TILL, 0);
696	}
697
698	public boolean alwaysNotify() {
699		return mode == MODE_SINGLE || getBooleanAttribute(ATTRIBUTE_ALWAYS_NOTIFY, Config.ALWAYS_NOTIFY_BY_DEFAULT || isPrivateAndNonAnonymous());
700	}
701
702	public boolean setAttribute(String key, String value) {
703		synchronized (this.attributes) {
704			try {
705				this.attributes.put(key, value == null ? "" : value);
706				return true;
707			} catch (JSONException e) {
708				return false;
709			}
710		}
711	}
712
713	public boolean setAttribute(String key, List<Jid> jids) {
714		JSONArray array = new JSONArray();
715		for(Jid jid : jids) {
716			array.put(jid.asBareJid().toString());
717		}
718		synchronized (this.attributes) {
719			try {
720				this.attributes.put(key, array);
721				return true;
722			} catch (JSONException e) {
723				e.printStackTrace();
724				return false;
725			}
726		}
727	}
728
729	public String getAttribute(String key) {
730		synchronized (this.attributes) {
731			try {
732				return this.attributes.getString(key);
733			} catch (JSONException e) {
734				return null;
735			}
736		}
737	}
738
739	private List<Jid> getJidListAttribute(String key) {
740		ArrayList<Jid> list = new ArrayList<>();
741		synchronized (this.attributes) {
742			try {
743				JSONArray array = this.attributes.getJSONArray(key);
744				for (int i = 0; i < array.length(); ++i) {
745					try {
746						list.add(Jid.of(array.getString(i)));
747					} catch (IllegalArgumentException e) {
748						//ignored
749					}
750				}
751			} catch (JSONException e) {
752				//ignored
753			}
754		}
755		return list;
756	}
757
758	private int getIntAttribute(String key, int defaultValue) {
759		String value = this.getAttribute(key);
760		if (value == null) {
761			return defaultValue;
762		} else {
763			try {
764				return Integer.parseInt(value);
765			} catch (NumberFormatException e) {
766				return defaultValue;
767			}
768		}
769	}
770
771	public long getLongAttribute(String key, long defaultValue) {
772		String value = this.getAttribute(key);
773		if (value == null) {
774			return defaultValue;
775		} else {
776			try {
777				return Long.parseLong(value);
778			} catch (NumberFormatException e) {
779				return defaultValue;
780			}
781		}
782	}
783
784	private boolean getBooleanAttribute(String key, boolean defaultValue) {
785		String value = this.getAttribute(key);
786		if (value == null) {
787			return defaultValue;
788		} else {
789			return Boolean.parseBoolean(value);
790		}
791	}
792
793	public void add(Message message) {
794		synchronized (this.messages) {
795			this.messages.add(message);
796		}
797	}
798
799	public void prepend(int offset, Message message) {
800		synchronized (this.messages) {
801			this.messages.add(Math.min(offset,this.messages.size()),message);
802		}
803	}
804
805	public void addAll(int index, List<Message> messages) {
806		synchronized (this.messages) {
807			this.messages.addAll(index, messages);
808		}
809		account.getPgpDecryptionService().decrypt(messages);
810	}
811
812	public void expireOldMessages(long timestamp) {
813		synchronized (this.messages) {
814			for(ListIterator<Message> iterator = this.messages.listIterator(); iterator.hasNext();) {
815				if (iterator.next().getTimeSent() < timestamp) {
816					iterator.remove();
817				}
818			}
819			untieMessages();
820		}
821	}
822
823	public void sort() {
824		synchronized (this.messages) {
825			Collections.sort(this.messages, (left, right) -> {
826				if (left.getTimeSent() < right.getTimeSent()) {
827					return -1;
828				} else if (left.getTimeSent() > right.getTimeSent()) {
829					return 1;
830				} else {
831					return 0;
832				}
833			});
834			untieMessages();
835		}
836	}
837
838	private void untieMessages() {
839		for(Message message : this.messages) {
840			message.untie();
841		}
842	}
843
844	public int unreadCount() {
845		synchronized (this.messages) {
846			int count = 0;
847			for(int i = this.messages.size() - 1; i >= 0; --i) {
848				if (this.messages.get(i).isRead()) {
849					return count;
850				}
851				++count;
852			}
853			return count;
854		}
855	}
856
857	public int receivedMessagesCount() {
858		int count = 0;
859		synchronized (this.messages) {
860			for(Message message : messages) {
861				if (message.getStatus() == Message.STATUS_RECEIVED) {
862					++count;
863				}
864			}
865		}
866		return count;
867	}
868
869	private int sentMessagesCount() {
870		int count = 0;
871		synchronized (this.messages) {
872			for(Message message : messages) {
873				if (message.getStatus() != Message.STATUS_RECEIVED) {
874					++count;
875				}
876			}
877		}
878		return count;
879	}
880
881	public boolean isWithStranger() {
882		return mode == MODE_SINGLE
883				&& !getJid().equals(Jid.ofDomain(account.getJid().getDomain()))
884				&& !getContact().showInRoster()
885				&& sentMessagesCount() == 0;
886	}
887}