Conversation.java

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