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