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