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