Conversation.java

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