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