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