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 Message findMessageWithRemoteId(String id, Jid counterpart) {
 293		synchronized (this.messages) {
 294			for(Message message : this.messages) {
 295				if (counterpart.equals(message.getCounterpart())
 296						&& (id.equals(message.getRemoteMsgId()) || id.equals(message.getUuid()))) {
 297					return message;
 298				}
 299			}
 300		}
 301		return null;
 302	}
 303
 304	public boolean hasMessageWithCounterpart(Jid counterpart) {
 305		synchronized (this.messages) {
 306			for(Message message : this.messages) {
 307				if (counterpart.equals(message.getCounterpart())) {
 308					return true;
 309				}
 310			}
 311		}
 312		return false;
 313	}
 314
 315	public void populateWithMessages(final List<Message> messages) {
 316		synchronized (this.messages) {
 317			messages.clear();
 318			messages.addAll(this.messages);
 319		}
 320		for(Iterator<Message> iterator = messages.iterator(); iterator.hasNext();) {
 321			if (iterator.next().wasMergedIntoPrevious()) {
 322				iterator.remove();
 323			}
 324		}
 325	}
 326
 327	@Override
 328	public boolean isBlocked() {
 329		return getContact().isBlocked();
 330	}
 331
 332	@Override
 333	public boolean isDomainBlocked() {
 334		return getContact().isDomainBlocked();
 335	}
 336
 337	@Override
 338	public Jid getBlockedJid() {
 339		return getContact().getBlockedJid();
 340	}
 341
 342	public String getLastReceivedOtrMessageId() {
 343		return this.mLastReceivedOtrMessageId;
 344	}
 345
 346	public void setLastReceivedOtrMessageId(String id) {
 347		this.mLastReceivedOtrMessageId = id;
 348	}
 349
 350	public int countMessages() {
 351		synchronized (this.messages) {
 352			return this.messages.size();
 353		}
 354	}
 355
 356	public void setFirstMamReference(String reference) {
 357		this.mFirstMamReference = reference;
 358	}
 359
 360	public String getFirstMamReference() {
 361		return this.mFirstMamReference;
 362	}
 363
 364	public void setLastClearHistory(long time,String reference) {
 365		if (reference != null) {
 366			setAttribute(ATTRIBUTE_LAST_CLEAR_HISTORY, String.valueOf(time) + ":" + reference);
 367		} else {
 368			setAttribute(ATTRIBUTE_LAST_CLEAR_HISTORY, String.valueOf(time));
 369		}
 370	}
 371
 372	public MamReference getLastClearHistory() {
 373		return MamReference.fromAttribute(getAttribute(ATTRIBUTE_LAST_CLEAR_HISTORY));
 374	}
 375
 376	public List<Jid> getAcceptedCryptoTargets() {
 377		if (mode == MODE_SINGLE) {
 378			return Collections.singletonList(getJid().toBareJid());
 379		} else {
 380			return getJidListAttribute(ATTRIBUTE_CRYPTO_TARGETS);
 381		}
 382	}
 383
 384	public void setAcceptedCryptoTargets(List<Jid> acceptedTargets) {
 385		setAttribute(ATTRIBUTE_CRYPTO_TARGETS, acceptedTargets);
 386	}
 387
 388	public boolean setCorrectingMessage(Message correctingMessage) {
 389		this.correctingMessage = correctingMessage;
 390		return correctingMessage == null && draftMessage != null;
 391	}
 392
 393	public Message getCorrectingMessage() {
 394		return this.correctingMessage;
 395	}
 396
 397	public boolean withSelf() {
 398		return getContact().isSelf();
 399	}
 400
 401	@Override
 402	public int compareTo(@NonNull Conversation another) {
 403		final Message left = getLatestMessage();
 404		final Message right = another.getLatestMessage();
 405		if (left.getTimeSent() > right.getTimeSent()) {
 406			return -1;
 407		} else if (left.getTimeSent() < right.getTimeSent()) {
 408			return 1;
 409		} else {
 410			return 0;
 411		}
 412	}
 413
 414	public void setDraftMessage(String draftMessage) {
 415		this.draftMessage = draftMessage;
 416	}
 417
 418	public String getDraftMessage() {
 419		return draftMessage;
 420	}
 421
 422	public interface OnMessageFound {
 423		void onMessageFound(final Message message);
 424	}
 425
 426	public Conversation(final String name, final Account account, final Jid contactJid,
 427			final int mode) {
 428		this(java.util.UUID.randomUUID().toString(), name, null, account
 429				.getUuid(), contactJid, System.currentTimeMillis(),
 430				STATUS_AVAILABLE, mode, "");
 431		this.account = account;
 432	}
 433
 434	public Conversation(final String uuid, final String name, final String contactUuid,
 435			final String accountUuid, final Jid contactJid, final long created, final int status,
 436			final int mode, final String attributes) {
 437		this.uuid = uuid;
 438		this.name = name;
 439		this.contactUuid = contactUuid;
 440		this.accountUuid = accountUuid;
 441		this.contactJid = contactJid;
 442		this.created = created;
 443		this.status = status;
 444		this.mode = mode;
 445		try {
 446			this.attributes = new JSONObject(attributes == null ? "" : attributes);
 447		} catch (JSONException e) {
 448			this.attributes = new JSONObject();
 449		}
 450	}
 451
 452	public boolean isRead() {
 453		return (this.messages.size() == 0) || this.messages.get(this.messages.size() - 1).isRead();
 454	}
 455
 456	public List<Message> markRead() {
 457		final List<Message> unread = new ArrayList<>();
 458		synchronized (this.messages) {
 459			for(Message message : this.messages) {
 460				if (!message.isRead()) {
 461					message.markRead();
 462					unread.add(message);
 463				}
 464			}
 465		}
 466		return unread;
 467	}
 468
 469	public Message getLatestMarkableMessage() {
 470		synchronized (this.messages) {
 471			for (int i = this.messages.size() - 1; i >= 0; --i) {
 472				final Message message = this.messages.get(i);
 473				if (message.getStatus() <= Message.STATUS_RECEIVED && message.markable) {
 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			if (getMucOptions().getSubject() != null) {
 497				return getMucOptions().getSubject();
 498			} else if (bookmark != null
 499					&& bookmark.getBookmarkName() != null
 500					&& !bookmark.getBookmarkName().trim().isEmpty()) {
 501				return bookmark.getBookmarkName().trim();
 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	private boolean isPnNA() {
 699		return mode == MODE_SINGLE || (getMucOptions().membersOnly() && getMucOptions().nonanonymous());
 700	}
 701
 702	public synchronized MucOptions getMucOptions() {
 703		if (this.mucOptions == null) {
 704			this.mucOptions = new MucOptions(this);
 705		}
 706		return this.mucOptions;
 707	}
 708
 709	public void resetMucOptions() {
 710		this.mucOptions = null;
 711	}
 712
 713	public void setContactJid(final Jid jid) {
 714		this.contactJid = jid;
 715	}
 716
 717	public void setNextCounterpart(Jid jid) {
 718		this.nextCounterpart = jid;
 719	}
 720
 721	public Jid getNextCounterpart() {
 722		return this.nextCounterpart;
 723	}
 724
 725	public int getNextEncryption() {
 726		return fixAvailableEncryption(this.getIntAttribute(ATTRIBUTE_NEXT_ENCRYPTION, getDefaultEncryption()));
 727	}
 728
 729	private int fixAvailableEncryption(int selectedEncryption) {
 730		switch(selectedEncryption) {
 731			case Message.ENCRYPTION_NONE:
 732				return Config.supportUnencrypted() ? selectedEncryption : getDefaultEncryption();
 733			case Message.ENCRYPTION_AXOLOTL:
 734				return Config.supportOmemo() ? selectedEncryption : getDefaultEncryption();
 735			case Message.ENCRYPTION_OTR:
 736				return Config.supportOtr() ? selectedEncryption : getDefaultEncryption();
 737			case Message.ENCRYPTION_PGP:
 738			case Message.ENCRYPTION_DECRYPTED:
 739			case Message.ENCRYPTION_DECRYPTION_FAILED:
 740				return Config.supportOpenPgp() ? Message.ENCRYPTION_PGP : getDefaultEncryption();
 741			default:
 742				return getDefaultEncryption();
 743		}
 744	}
 745
 746	private int getDefaultEncryption() {
 747		AxolotlService axolotlService = account.getAxolotlService();
 748		if (Config.supportUnencrypted()) {
 749			return Message.ENCRYPTION_NONE;
 750		} else if (Config.supportOmemo()
 751				&& (axolotlService != null && axolotlService.isConversationAxolotlCapable(this) || !Config.multipleEncryptionChoices())) {
 752			return Message.ENCRYPTION_AXOLOTL;
 753		} else if (Config.supportOtr() && mode == MODE_SINGLE) {
 754			return Message.ENCRYPTION_OTR;
 755		} else if (Config.supportOpenPgp()) {
 756			return Message.ENCRYPTION_PGP;
 757		} else {
 758			return Message.ENCRYPTION_NONE;
 759		}
 760	}
 761
 762	public void setNextEncryption(int encryption) {
 763		this.setAttribute(ATTRIBUTE_NEXT_ENCRYPTION, String.valueOf(encryption));
 764	}
 765
 766	public String getNextMessage() {
 767		final String nextMessage = getAttribute(ATTRIBUTE_NEXT_MESSAGE);
 768		return nextMessage == null ? "" : nextMessage;
 769	}
 770
 771	public boolean smpRequested() {
 772		return smp().status == Smp.STATUS_CONTACT_REQUESTED;
 773	}
 774
 775	public boolean setNextMessage(String message) {
 776		boolean changed = !getNextMessage().equals(message);
 777		this.setAttribute(ATTRIBUTE_NEXT_MESSAGE, message);
 778		return changed;
 779	}
 780
 781	public void setSymmetricKey(byte[] key) {
 782		this.symmetricKey = key;
 783	}
 784
 785	public byte[] getSymmetricKey() {
 786		return this.symmetricKey;
 787	}
 788
 789	public void setBookmark(Bookmark bookmark) {
 790		this.bookmark = bookmark;
 791		this.bookmark.setConversation(this);
 792	}
 793
 794	public void deregisterWithBookmark() {
 795		if (this.bookmark != null) {
 796			this.bookmark.setConversation(null);
 797		}
 798		this.bookmark = null;
 799	}
 800
 801	public Bookmark getBookmark() {
 802		return this.bookmark;
 803	}
 804
 805	public boolean hasDuplicateMessage(Message message) {
 806		synchronized (this.messages) {
 807			for (int i = this.messages.size() - 1; i >= 0; --i) {
 808				if (this.messages.get(i).similar(message)) {
 809					return true;
 810				}
 811			}
 812		}
 813		return false;
 814	}
 815
 816	public Message findSentMessageWithBody(String body) {
 817		synchronized (this.messages) {
 818			for (int i = this.messages.size() - 1; i >= 0; --i) {
 819				Message message = this.messages.get(i);
 820				if (message.getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_SEND) {
 821					String otherBody;
 822					if (message.hasFileOnRemoteHost()) {
 823						otherBody = message.getFileParams().url.toString();
 824					} else {
 825						otherBody = message.body;
 826					}
 827					if (otherBody != null && otherBody.equals(body)) {
 828						return message;
 829					}
 830				}
 831			}
 832			return null;
 833		}
 834	}
 835
 836	public MamReference getLastMessageTransmitted() {
 837		final MamReference lastClear = getLastClearHistory();
 838		MamReference lastReceived = new MamReference(0);
 839		synchronized (this.messages) {
 840			for(int i = this.messages.size() - 1; i >= 0; --i) {
 841				Message message = this.messages.get(i);
 842				if (message.getStatus() == Message.STATUS_RECEIVED || message.isCarbon() || message.getServerMsgId() != null) {
 843					lastReceived = new MamReference(message.getTimeSent(),message.getServerMsgId());
 844					break;
 845				}
 846			}
 847		}
 848		return MamReference.max(lastClear,lastReceived);
 849	}
 850
 851	public void setMutedTill(long value) {
 852		this.setAttribute(ATTRIBUTE_MUTED_TILL, String.valueOf(value));
 853	}
 854
 855	public boolean isMuted() {
 856		return System.currentTimeMillis() < this.getLongAttribute(ATTRIBUTE_MUTED_TILL, 0);
 857	}
 858
 859	public boolean alwaysNotify() {
 860		return mode == MODE_SINGLE || getBooleanAttribute(ATTRIBUTE_ALWAYS_NOTIFY, Config.ALWAYS_NOTIFY_BY_DEFAULT || isPnNA());
 861	}
 862
 863	public boolean setAttribute(String key, String value) {
 864		synchronized (this.attributes) {
 865			try {
 866				this.attributes.put(key, value == null ? "" : value);
 867				return true;
 868			} catch (JSONException e) {
 869				return false;
 870			}
 871		}
 872	}
 873
 874	public boolean setAttribute(String key, List<Jid> jids) {
 875		JSONArray array = new JSONArray();
 876		for(Jid jid : jids) {
 877			array.put(jid.toBareJid().toString());
 878		}
 879		synchronized (this.attributes) {
 880			try {
 881				this.attributes.put(key, array);
 882				return true;
 883			} catch (JSONException e) {
 884				e.printStackTrace();
 885				return false;
 886			}
 887		}
 888	}
 889
 890	public String getAttribute(String key) {
 891		synchronized (this.attributes) {
 892			try {
 893				return this.attributes.getString(key);
 894			} catch (JSONException e) {
 895				return null;
 896			}
 897		}
 898	}
 899
 900	private List<Jid> getJidListAttribute(String key) {
 901		ArrayList<Jid> list = new ArrayList<>();
 902		synchronized (this.attributes) {
 903			try {
 904				JSONArray array = this.attributes.getJSONArray(key);
 905				for (int i = 0; i < array.length(); ++i) {
 906					try {
 907						list.add(Jid.fromString(array.getString(i)));
 908					} catch (InvalidJidException e) {
 909						//ignored
 910					}
 911				}
 912			} catch (JSONException e) {
 913				//ignored
 914			}
 915		}
 916		return list;
 917	}
 918
 919	private int getIntAttribute(String key, int defaultValue) {
 920		String value = this.getAttribute(key);
 921		if (value == null) {
 922			return defaultValue;
 923		} else {
 924			try {
 925				return Integer.parseInt(value);
 926			} catch (NumberFormatException e) {
 927				return defaultValue;
 928			}
 929		}
 930	}
 931
 932	public long getLongAttribute(String key, long defaultValue) {
 933		String value = this.getAttribute(key);
 934		if (value == null) {
 935			return defaultValue;
 936		} else {
 937			try {
 938				return Long.parseLong(value);
 939			} catch (NumberFormatException e) {
 940				return defaultValue;
 941			}
 942		}
 943	}
 944
 945	private boolean getBooleanAttribute(String key, boolean defaultValue) {
 946		String value = this.getAttribute(key);
 947		if (value == null) {
 948			return defaultValue;
 949		} else {
 950			return Boolean.parseBoolean(value);
 951		}
 952	}
 953
 954	public void add(Message message) {
 955		synchronized (this.messages) {
 956			this.messages.add(message);
 957		}
 958	}
 959
 960	public void prepend(Message message) {
 961		synchronized (this.messages) {
 962			this.messages.add(0,message);
 963		}
 964	}
 965
 966	public void addAll(int index, List<Message> messages) {
 967		synchronized (this.messages) {
 968			this.messages.addAll(index, messages);
 969		}
 970		account.getPgpDecryptionService().decrypt(messages);
 971	}
 972
 973	public void expireOldMessages(long timestamp) {
 974		synchronized (this.messages) {
 975			for(ListIterator<Message> iterator = this.messages.listIterator(); iterator.hasNext();) {
 976				if (iterator.next().getTimeSent() < timestamp) {
 977					iterator.remove();
 978				}
 979			}
 980			untieMessages();
 981		}
 982	}
 983
 984	public void sort() {
 985		synchronized (this.messages) {
 986			Collections.sort(this.messages, new Comparator<Message>() {
 987				@Override
 988				public int compare(Message left, Message right) {
 989					if (left.getTimeSent() < right.getTimeSent()) {
 990						return -1;
 991					} else if (left.getTimeSent() > right.getTimeSent()) {
 992						return 1;
 993					} else {
 994						return 0;
 995					}
 996				}
 997			});
 998			untieMessages();
 999		}
1000	}
1001
1002	private void untieMessages() {
1003		for(Message message : this.messages) {
1004			message.untie();
1005		}
1006	}
1007
1008	public int unreadCount() {
1009		synchronized (this.messages) {
1010			int count = 0;
1011			for(int i = this.messages.size() - 1; i >= 0; --i) {
1012				if (this.messages.get(i).isRead()) {
1013					return count;
1014				}
1015				++count;
1016			}
1017			return count;
1018		}
1019	}
1020
1021	public int receivedMessagesCount() {
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	private int sentMessagesCount() {
1034		int count = 0;
1035		synchronized (this.messages) {
1036			for(Message message : messages) {
1037				if (message.getStatus() != Message.STATUS_RECEIVED) {
1038					++count;
1039				}
1040			}
1041		}
1042		return count;
1043	}
1044
1045	public boolean isWithStranger() {
1046		return mode == MODE_SINGLE
1047				&& !getJid().equals(account.getJid().toDomainJid())
1048				&& !getContact().showInRoster()
1049				&& sentMessagesCount() == 0;
1050	}
1051
1052	public class Smp {
1053		public static final int STATUS_NONE = 0;
1054		public static final int STATUS_CONTACT_REQUESTED = 1;
1055		public static final int STATUS_WE_REQUESTED = 2;
1056		public static final int STATUS_FAILED = 3;
1057		public static final int STATUS_VERIFIED = 4;
1058
1059		public String secret = null;
1060		public String hint = null;
1061		public int status = 0;
1062	}
1063}