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