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