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