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
 474						&& message.markable
 475						&& message.getType() != Message.TYPE_PRIVATE) {
 476					return message.isRead() ? null : message;
 477				}
 478			}
 479		}
 480		return null;
 481	}
 482
 483	public Message getLatestMessage() {
 484		synchronized (this.messages) {
 485			if (this.messages.size() == 0) {
 486				Message message = new Message(this, "", Message.ENCRYPTION_NONE);
 487				message.setType(Message.TYPE_STATUS);
 488				message.setTime(Math.max(getCreated(), getLastClearHistory().getTimestamp()));
 489				return message;
 490			} else {
 491				return this.messages.get(this.messages.size() - 1);
 492			}
 493		}
 494	}
 495
 496	public String getName() {
 497		if (getMode() == MODE_MULTI) {
 498			final String subject = getMucOptions().getSubject();
 499			final String bookmarkName = bookmark != null ? bookmark.getBookmarkName() : null;
 500			if (subject != null && !subject.trim().isEmpty()) {
 501				return subject;
 502			} else if (bookmarkName != null && !bookmarkName.trim().isEmpty()) {
 503				return bookmarkName;
 504			} else {
 505				String generatedName = getMucOptions().createNameFromParticipants();
 506				if (generatedName != null) {
 507					return generatedName;
 508				} else {
 509					return getJid().getUnescapedLocalpart();
 510				}
 511			}
 512		} else if (isWithStranger()) {
 513			return contactJid.toBareJid().toString();
 514		} else {
 515			return this.getContact().getDisplayName();
 516		}
 517	}
 518
 519	public String getAccountUuid() {
 520		return this.accountUuid;
 521	}
 522
 523	public Account getAccount() {
 524		return this.account;
 525	}
 526
 527	public Contact getContact() {
 528		return this.account.getRoster().getContact(this.contactJid);
 529	}
 530
 531	public void setAccount(final Account account) {
 532		this.account = account;
 533	}
 534
 535	@Override
 536	public Jid getJid() {
 537		return this.contactJid;
 538	}
 539
 540	public int getStatus() {
 541		return this.status;
 542	}
 543
 544	public long getCreated() {
 545		return this.created;
 546	}
 547
 548	public ContentValues getContentValues() {
 549		ContentValues values = new ContentValues();
 550		values.put(UUID, uuid);
 551		values.put(NAME, name);
 552		values.put(CONTACT, contactUuid);
 553		values.put(ACCOUNT, accountUuid);
 554		values.put(CONTACTJID, contactJid.toPreppedString());
 555		values.put(CREATED, created);
 556		values.put(STATUS, status);
 557		values.put(MODE, mode);
 558		values.put(ATTRIBUTES, attributes.toString());
 559		return values;
 560	}
 561
 562	public static Conversation fromCursor(Cursor cursor) {
 563		Jid jid;
 564		try {
 565			jid = Jid.fromString(cursor.getString(cursor.getColumnIndex(CONTACTJID)), true);
 566		} catch (final InvalidJidException e) {
 567			// Borked DB..
 568			jid = null;
 569		}
 570		return new Conversation(cursor.getString(cursor.getColumnIndex(UUID)),
 571				cursor.getString(cursor.getColumnIndex(NAME)),
 572				cursor.getString(cursor.getColumnIndex(CONTACT)),
 573				cursor.getString(cursor.getColumnIndex(ACCOUNT)),
 574				jid,
 575				cursor.getLong(cursor.getColumnIndex(CREATED)),
 576				cursor.getInt(cursor.getColumnIndex(STATUS)),
 577				cursor.getInt(cursor.getColumnIndex(MODE)),
 578				cursor.getString(cursor.getColumnIndex(ATTRIBUTES)));
 579	}
 580
 581	public void setStatus(int status) {
 582		this.status = status;
 583	}
 584
 585	public int getMode() {
 586		return this.mode;
 587	}
 588
 589	public void setMode(int mode) {
 590		this.mode = mode;
 591	}
 592
 593	public SessionImpl startOtrSession(String presence, boolean sendStart) {
 594		if (this.otrSession != null) {
 595			return this.otrSession;
 596		} else {
 597			final SessionID sessionId = new SessionID(this.getJid().toBareJid().toString(),
 598					presence,
 599					"xmpp");
 600			this.otrSession = new SessionImpl(sessionId, getAccount().getOtrService());
 601			try {
 602				if (sendStart) {
 603					this.otrSession.startSession();
 604					return this.otrSession;
 605				}
 606				return this.otrSession;
 607			} catch (OtrException e) {
 608				return null;
 609			}
 610		}
 611
 612	}
 613
 614	public SessionImpl getOtrSession() {
 615		return this.otrSession;
 616	}
 617
 618	public void resetOtrSession() {
 619		this.otrFingerprint = null;
 620		this.otrSession = null;
 621		this.mSmp.hint = null;
 622		this.mSmp.secret = null;
 623		this.mSmp.status = Smp.STATUS_NONE;
 624	}
 625
 626	public Smp smp() {
 627		return mSmp;
 628	}
 629
 630	public boolean startOtrIfNeeded() {
 631		if (this.otrSession != null && this.otrSession.getSessionStatus() != SessionStatus.ENCRYPTED) {
 632			try {
 633				this.otrSession.startSession();
 634				return true;
 635			} catch (OtrException e) {
 636				this.resetOtrSession();
 637				return false;
 638			}
 639		} else {
 640			return true;
 641		}
 642	}
 643
 644	public boolean endOtrIfNeeded() {
 645		if (this.otrSession != null) {
 646			if (this.otrSession.getSessionStatus() == SessionStatus.ENCRYPTED) {
 647				try {
 648					this.otrSession.endSession();
 649					this.resetOtrSession();
 650					return true;
 651				} catch (OtrException e) {
 652					this.resetOtrSession();
 653					return false;
 654				}
 655			} else {
 656				this.resetOtrSession();
 657				return false;
 658			}
 659		} else {
 660			return false;
 661		}
 662	}
 663
 664	public boolean hasValidOtrSession() {
 665		return this.otrSession != null;
 666	}
 667
 668	public synchronized String getOtrFingerprint() {
 669		if (this.otrFingerprint == null) {
 670			try {
 671				if (getOtrSession() == null || getOtrSession().getSessionStatus() != SessionStatus.ENCRYPTED) {
 672					return null;
 673				}
 674				DSAPublicKey remotePubKey = (DSAPublicKey) getOtrSession().getRemotePublicKey();
 675				this.otrFingerprint = getAccount().getOtrService().getFingerprint(remotePubKey).toLowerCase(Locale.US);
 676			} catch (final OtrCryptoException | UnsupportedOperationException ignored) {
 677				return null;
 678			}
 679		}
 680		return this.otrFingerprint;
 681	}
 682
 683	public boolean verifyOtrFingerprint() {
 684		final String fingerprint = getOtrFingerprint();
 685		if (fingerprint != null) {
 686			getContact().addOtrFingerprint(fingerprint);
 687			return true;
 688		} else {
 689			return false;
 690		}
 691	}
 692
 693	public boolean isOtrFingerprintVerified() {
 694		return getContact().getOtrFingerprints().contains(getOtrFingerprint());
 695	}
 696
 697	/**
 698	 * short for is Private and Non-anonymous
 699	 */
 700	private boolean isPnNA() {
 701		return mode == MODE_SINGLE || (getMucOptions().membersOnly() && getMucOptions().nonanonymous());
 702	}
 703
 704	public synchronized MucOptions getMucOptions() {
 705		if (this.mucOptions == null) {
 706			this.mucOptions = new MucOptions(this);
 707		}
 708		return this.mucOptions;
 709	}
 710
 711	public void resetMucOptions() {
 712		this.mucOptions = null;
 713	}
 714
 715	public void setContactJid(final Jid jid) {
 716		this.contactJid = jid;
 717	}
 718
 719	public void setNextCounterpart(Jid jid) {
 720		this.nextCounterpart = jid;
 721	}
 722
 723	public Jid getNextCounterpart() {
 724		return this.nextCounterpart;
 725	}
 726
 727	public int getNextEncryption() {
 728		return fixAvailableEncryption(this.getIntAttribute(ATTRIBUTE_NEXT_ENCRYPTION, getDefaultEncryption()));
 729	}
 730
 731	private int fixAvailableEncryption(int selectedEncryption) {
 732		switch(selectedEncryption) {
 733			case Message.ENCRYPTION_NONE:
 734				return Config.supportUnencrypted() ? selectedEncryption : getDefaultEncryption();
 735			case Message.ENCRYPTION_AXOLOTL:
 736				return Config.supportOmemo() ? selectedEncryption : getDefaultEncryption();
 737			case Message.ENCRYPTION_OTR:
 738				return Config.supportOtr() ? selectedEncryption : getDefaultEncryption();
 739			case Message.ENCRYPTION_PGP:
 740			case Message.ENCRYPTION_DECRYPTED:
 741			case Message.ENCRYPTION_DECRYPTION_FAILED:
 742				return Config.supportOpenPgp() ? Message.ENCRYPTION_PGP : getDefaultEncryption();
 743			default:
 744				return getDefaultEncryption();
 745		}
 746	}
 747
 748	private int getDefaultEncryption() {
 749		AxolotlService axolotlService = account.getAxolotlService();
 750		if (Config.supportUnencrypted()) {
 751			return Message.ENCRYPTION_NONE;
 752		} else if (Config.supportOmemo()
 753				&& (axolotlService != null && axolotlService.isConversationAxolotlCapable(this) || !Config.multipleEncryptionChoices())) {
 754			return Message.ENCRYPTION_AXOLOTL;
 755		} else if (Config.supportOtr() && mode == MODE_SINGLE) {
 756			return Message.ENCRYPTION_OTR;
 757		} else if (Config.supportOpenPgp()) {
 758			return Message.ENCRYPTION_PGP;
 759		} else {
 760			return Message.ENCRYPTION_NONE;
 761		}
 762	}
 763
 764	public void setNextEncryption(int encryption) {
 765		this.setAttribute(ATTRIBUTE_NEXT_ENCRYPTION, String.valueOf(encryption));
 766	}
 767
 768	public String getNextMessage() {
 769		final String nextMessage = getAttribute(ATTRIBUTE_NEXT_MESSAGE);
 770		return nextMessage == null ? "" : nextMessage;
 771	}
 772
 773	public boolean smpRequested() {
 774		return smp().status == Smp.STATUS_CONTACT_REQUESTED;
 775	}
 776
 777	public boolean setNextMessage(String message) {
 778		boolean changed = !getNextMessage().equals(message);
 779		this.setAttribute(ATTRIBUTE_NEXT_MESSAGE, message);
 780		return changed;
 781	}
 782
 783	public void setSymmetricKey(byte[] key) {
 784		this.symmetricKey = key;
 785	}
 786
 787	public byte[] getSymmetricKey() {
 788		return this.symmetricKey;
 789	}
 790
 791	public void setBookmark(Bookmark bookmark) {
 792		this.bookmark = bookmark;
 793		this.bookmark.setConversation(this);
 794	}
 795
 796	public void deregisterWithBookmark() {
 797		if (this.bookmark != null) {
 798			this.bookmark.setConversation(null);
 799		}
 800		this.bookmark = null;
 801	}
 802
 803	public Bookmark getBookmark() {
 804		return this.bookmark;
 805	}
 806
 807	public Message findDuplicateMessage(Message message) {
 808		synchronized (this.messages) {
 809			for (int i = this.messages.size() - 1; i >= 0; --i) {
 810				if (this.messages.get(i).similar(message)) {
 811					return this.messages.get(i);
 812				}
 813			}
 814		}
 815		return null;
 816	}
 817
 818	public boolean hasDuplicateMessage(Message message) {
 819		return findDuplicateMessage(message) != null;
 820	}
 821
 822	public Message findSentMessageWithBody(String body) {
 823		synchronized (this.messages) {
 824			for (int i = this.messages.size() - 1; i >= 0; --i) {
 825				Message message = this.messages.get(i);
 826				if (message.getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_SEND) {
 827					String otherBody;
 828					if (message.hasFileOnRemoteHost()) {
 829						otherBody = message.getFileParams().url.toString();
 830					} else {
 831						otherBody = message.body;
 832					}
 833					if (otherBody != null && otherBody.equals(body)) {
 834						return message;
 835					}
 836				}
 837			}
 838			return null;
 839		}
 840	}
 841
 842	public MamReference getLastMessageTransmitted() {
 843		final MamReference lastClear = getLastClearHistory();
 844		MamReference lastReceived = new MamReference(0);
 845		synchronized (this.messages) {
 846			for(int i = this.messages.size() - 1; i >= 0; --i) {
 847				final Message message = this.messages.get(i);
 848				if (message.getType() == Message.TYPE_PRIVATE) {
 849					continue; //it's unsafe to use private messages as anchor. They could be coming from user archive
 850				}
 851				if (message.getStatus() == Message.STATUS_RECEIVED || message.isCarbon() || message.getServerMsgId() != null) {
 852					lastReceived = new MamReference(message.getTimeSent(),message.getServerMsgId());
 853					break;
 854				}
 855			}
 856		}
 857		return MamReference.max(lastClear,lastReceived);
 858	}
 859
 860	public void setMutedTill(long value) {
 861		this.setAttribute(ATTRIBUTE_MUTED_TILL, String.valueOf(value));
 862	}
 863
 864	public boolean isMuted() {
 865		return System.currentTimeMillis() < this.getLongAttribute(ATTRIBUTE_MUTED_TILL, 0);
 866	}
 867
 868	public boolean alwaysNotify() {
 869		return mode == MODE_SINGLE || getBooleanAttribute(ATTRIBUTE_ALWAYS_NOTIFY, Config.ALWAYS_NOTIFY_BY_DEFAULT || isPnNA());
 870	}
 871
 872	public boolean setAttribute(String key, String value) {
 873		synchronized (this.attributes) {
 874			try {
 875				this.attributes.put(key, value == null ? "" : value);
 876				return true;
 877			} catch (JSONException e) {
 878				return false;
 879			}
 880		}
 881	}
 882
 883	public boolean setAttribute(String key, List<Jid> jids) {
 884		JSONArray array = new JSONArray();
 885		for(Jid jid : jids) {
 886			array.put(jid.toBareJid().toString());
 887		}
 888		synchronized (this.attributes) {
 889			try {
 890				this.attributes.put(key, array);
 891				return true;
 892			} catch (JSONException e) {
 893				e.printStackTrace();
 894				return false;
 895			}
 896		}
 897	}
 898
 899	public String getAttribute(String key) {
 900		synchronized (this.attributes) {
 901			try {
 902				return this.attributes.getString(key);
 903			} catch (JSONException e) {
 904				return null;
 905			}
 906		}
 907	}
 908
 909	private List<Jid> getJidListAttribute(String key) {
 910		ArrayList<Jid> list = new ArrayList<>();
 911		synchronized (this.attributes) {
 912			try {
 913				JSONArray array = this.attributes.getJSONArray(key);
 914				for (int i = 0; i < array.length(); ++i) {
 915					try {
 916						list.add(Jid.fromString(array.getString(i)));
 917					} catch (InvalidJidException e) {
 918						//ignored
 919					}
 920				}
 921			} catch (JSONException e) {
 922				//ignored
 923			}
 924		}
 925		return list;
 926	}
 927
 928	private int getIntAttribute(String key, int defaultValue) {
 929		String value = this.getAttribute(key);
 930		if (value == null) {
 931			return defaultValue;
 932		} else {
 933			try {
 934				return Integer.parseInt(value);
 935			} catch (NumberFormatException e) {
 936				return defaultValue;
 937			}
 938		}
 939	}
 940
 941	public long getLongAttribute(String key, long defaultValue) {
 942		String value = this.getAttribute(key);
 943		if (value == null) {
 944			return defaultValue;
 945		} else {
 946			try {
 947				return Long.parseLong(value);
 948			} catch (NumberFormatException e) {
 949				return defaultValue;
 950			}
 951		}
 952	}
 953
 954	private boolean getBooleanAttribute(String key, boolean defaultValue) {
 955		String value = this.getAttribute(key);
 956		if (value == null) {
 957			return defaultValue;
 958		} else {
 959			return Boolean.parseBoolean(value);
 960		}
 961	}
 962
 963	public void add(Message message) {
 964		synchronized (this.messages) {
 965			this.messages.add(message);
 966		}
 967	}
 968
 969	public void prepend(Message message) {
 970		synchronized (this.messages) {
 971			this.messages.add(0,message);
 972		}
 973	}
 974
 975	public void addAll(int index, List<Message> messages) {
 976		synchronized (this.messages) {
 977			this.messages.addAll(index, messages);
 978		}
 979		account.getPgpDecryptionService().decrypt(messages);
 980	}
 981
 982	public void expireOldMessages(long timestamp) {
 983		synchronized (this.messages) {
 984			for(ListIterator<Message> iterator = this.messages.listIterator(); iterator.hasNext();) {
 985				if (iterator.next().getTimeSent() < timestamp) {
 986					iterator.remove();
 987				}
 988			}
 989			untieMessages();
 990		}
 991	}
 992
 993	public void sort() {
 994		synchronized (this.messages) {
 995			Collections.sort(this.messages, new Comparator<Message>() {
 996				@Override
 997				public int compare(Message left, Message right) {
 998					if (left.getTimeSent() < right.getTimeSent()) {
 999						return -1;
1000					} else if (left.getTimeSent() > right.getTimeSent()) {
1001						return 1;
1002					} else {
1003						return 0;
1004					}
1005				}
1006			});
1007			untieMessages();
1008		}
1009	}
1010
1011	private void untieMessages() {
1012		for(Message message : this.messages) {
1013			message.untie();
1014		}
1015	}
1016
1017	public int unreadCount() {
1018		synchronized (this.messages) {
1019			int count = 0;
1020			for(int i = this.messages.size() - 1; i >= 0; --i) {
1021				if (this.messages.get(i).isRead()) {
1022					return count;
1023				}
1024				++count;
1025			}
1026			return count;
1027		}
1028	}
1029
1030	public int receivedMessagesCount() {
1031		int count = 0;
1032		synchronized (this.messages) {
1033			for(Message message : messages) {
1034				if (message.getStatus() == Message.STATUS_RECEIVED) {
1035					++count;
1036				}
1037			}
1038		}
1039		return count;
1040	}
1041
1042	private int sentMessagesCount() {
1043		int count = 0;
1044		synchronized (this.messages) {
1045			for(Message message : messages) {
1046				if (message.getStatus() != Message.STATUS_RECEIVED) {
1047					++count;
1048				}
1049			}
1050		}
1051		return count;
1052	}
1053
1054	public boolean isWithStranger() {
1055		return mode == MODE_SINGLE
1056				&& !getJid().equals(account.getJid().toDomainJid())
1057				&& !getContact().showInRoster()
1058				&& sentMessagesCount() == 0;
1059	}
1060
1061	public class Smp {
1062		public static final int STATUS_NONE = 0;
1063		public static final int STATUS_CONTACT_REQUESTED = 1;
1064		public static final int STATUS_WE_REQUESTED = 2;
1065		public static final int STATUS_FAILED = 3;
1066		public static final int STATUS_VERIFIED = 4;
1067
1068		public String secret = null;
1069		public String hint = null;
1070		public int status = 0;
1071	}
1072}