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