Conversation.java

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