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 || !isPnNA())) {
 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 if (isWithStranger()) {
 485			return contactJid.toBareJid().toString();
 486		} else {
 487			return this.getContact().getDisplayName();
 488		}
 489	}
 490
 491	public String getAccountUuid() {
 492		return this.accountUuid;
 493	}
 494
 495	public Account getAccount() {
 496		return this.account;
 497	}
 498
 499	public Contact getContact() {
 500		return this.account.getRoster().getContact(this.contactJid);
 501	}
 502
 503	public void setAccount(final Account account) {
 504		this.account = account;
 505	}
 506
 507	@Override
 508	public Jid getJid() {
 509		return this.contactJid;
 510	}
 511
 512	public int getStatus() {
 513		return this.status;
 514	}
 515
 516	public long getCreated() {
 517		return this.created;
 518	}
 519
 520	public ContentValues getContentValues() {
 521		ContentValues values = new ContentValues();
 522		values.put(UUID, uuid);
 523		values.put(NAME, name);
 524		values.put(CONTACT, contactUuid);
 525		values.put(ACCOUNT, accountUuid);
 526		values.put(CONTACTJID, contactJid.toPreppedString());
 527		values.put(CREATED, created);
 528		values.put(STATUS, status);
 529		values.put(MODE, mode);
 530		values.put(ATTRIBUTES, attributes.toString());
 531		return values;
 532	}
 533
 534	public static Conversation fromCursor(Cursor cursor) {
 535		Jid jid;
 536		try {
 537			jid = Jid.fromString(cursor.getString(cursor.getColumnIndex(CONTACTJID)), true);
 538		} catch (final InvalidJidException e) {
 539			// Borked DB..
 540			jid = null;
 541		}
 542		return new Conversation(cursor.getString(cursor.getColumnIndex(UUID)),
 543				cursor.getString(cursor.getColumnIndex(NAME)),
 544				cursor.getString(cursor.getColumnIndex(CONTACT)),
 545				cursor.getString(cursor.getColumnIndex(ACCOUNT)),
 546				jid,
 547				cursor.getLong(cursor.getColumnIndex(CREATED)),
 548				cursor.getInt(cursor.getColumnIndex(STATUS)),
 549				cursor.getInt(cursor.getColumnIndex(MODE)),
 550				cursor.getString(cursor.getColumnIndex(ATTRIBUTES)));
 551	}
 552
 553	public void setStatus(int status) {
 554		this.status = status;
 555	}
 556
 557	public int getMode() {
 558		return this.mode;
 559	}
 560
 561	public void setMode(int mode) {
 562		this.mode = mode;
 563	}
 564
 565	public SessionImpl startOtrSession(String presence, boolean sendStart) {
 566		if (this.otrSession != null) {
 567			return this.otrSession;
 568		} else {
 569			final SessionID sessionId = new SessionID(this.getJid().toBareJid().toString(),
 570					presence,
 571					"xmpp");
 572			this.otrSession = new SessionImpl(sessionId, getAccount().getOtrService());
 573			try {
 574				if (sendStart) {
 575					this.otrSession.startSession();
 576					return this.otrSession;
 577				}
 578				return this.otrSession;
 579			} catch (OtrException e) {
 580				return null;
 581			}
 582		}
 583
 584	}
 585
 586	public SessionImpl getOtrSession() {
 587		return this.otrSession;
 588	}
 589
 590	public void resetOtrSession() {
 591		this.otrFingerprint = null;
 592		this.otrSession = null;
 593		this.mSmp.hint = null;
 594		this.mSmp.secret = null;
 595		this.mSmp.status = Smp.STATUS_NONE;
 596	}
 597
 598	public Smp smp() {
 599		return mSmp;
 600	}
 601
 602	public boolean startOtrIfNeeded() {
 603		if (this.otrSession != null && this.otrSession.getSessionStatus() != SessionStatus.ENCRYPTED) {
 604			try {
 605				this.otrSession.startSession();
 606				return true;
 607			} catch (OtrException e) {
 608				this.resetOtrSession();
 609				return false;
 610			}
 611		} else {
 612			return true;
 613		}
 614	}
 615
 616	public boolean endOtrIfNeeded() {
 617		if (this.otrSession != null) {
 618			if (this.otrSession.getSessionStatus() == SessionStatus.ENCRYPTED) {
 619				try {
 620					this.otrSession.endSession();
 621					this.resetOtrSession();
 622					return true;
 623				} catch (OtrException e) {
 624					this.resetOtrSession();
 625					return false;
 626				}
 627			} else {
 628				this.resetOtrSession();
 629				return false;
 630			}
 631		} else {
 632			return false;
 633		}
 634	}
 635
 636	public boolean hasValidOtrSession() {
 637		return this.otrSession != null;
 638	}
 639
 640	public synchronized String getOtrFingerprint() {
 641		if (this.otrFingerprint == null) {
 642			try {
 643				if (getOtrSession() == null || getOtrSession().getSessionStatus() != SessionStatus.ENCRYPTED) {
 644					return null;
 645				}
 646				DSAPublicKey remotePubKey = (DSAPublicKey) getOtrSession().getRemotePublicKey();
 647				this.otrFingerprint = getAccount().getOtrService().getFingerprint(remotePubKey).toLowerCase(Locale.US);
 648			} catch (final OtrCryptoException | UnsupportedOperationException ignored) {
 649				return null;
 650			}
 651		}
 652		return this.otrFingerprint;
 653	}
 654
 655	public boolean verifyOtrFingerprint() {
 656		final String fingerprint = getOtrFingerprint();
 657		if (fingerprint != null) {
 658			getContact().addOtrFingerprint(fingerprint);
 659			return true;
 660		} else {
 661			return false;
 662		}
 663	}
 664
 665	public boolean isOtrFingerprintVerified() {
 666		return getContact().getOtrFingerprints().contains(getOtrFingerprint());
 667	}
 668
 669	/**
 670	 * short for is Private and Non-anonymous
 671	 */
 672	private boolean isPnNA() {
 673		return mode == MODE_SINGLE || (getMucOptions().membersOnly() && getMucOptions().nonanonymous());
 674	}
 675
 676	public synchronized MucOptions getMucOptions() {
 677		if (this.mucOptions == null) {
 678			this.mucOptions = new MucOptions(this);
 679		}
 680		return this.mucOptions;
 681	}
 682
 683	public void resetMucOptions() {
 684		this.mucOptions = null;
 685	}
 686
 687	public void setContactJid(final Jid jid) {
 688		this.contactJid = jid;
 689	}
 690
 691	public void setNextCounterpart(Jid jid) {
 692		this.nextCounterpart = jid;
 693	}
 694
 695	public Jid getNextCounterpart() {
 696		return this.nextCounterpart;
 697	}
 698
 699	public int getNextEncryption() {
 700		return fixAvailableEncryption(this.getIntAttribute(ATTRIBUTE_NEXT_ENCRYPTION, getDefaultEncryption()));
 701	}
 702
 703	private int fixAvailableEncryption(int selectedEncryption) {
 704		switch(selectedEncryption) {
 705			case Message.ENCRYPTION_NONE:
 706				return Config.supportUnencrypted() ? selectedEncryption : getDefaultEncryption();
 707			case Message.ENCRYPTION_AXOLOTL:
 708				return Config.supportOmemo() ? selectedEncryption : getDefaultEncryption();
 709			case Message.ENCRYPTION_OTR:
 710				return Config.supportOtr() ? selectedEncryption : getDefaultEncryption();
 711			case Message.ENCRYPTION_PGP:
 712			case Message.ENCRYPTION_DECRYPTED:
 713			case Message.ENCRYPTION_DECRYPTION_FAILED:
 714				return Config.supportOpenPgp() ? Message.ENCRYPTION_PGP : getDefaultEncryption();
 715			default:
 716				return getDefaultEncryption();
 717		}
 718	}
 719
 720	private int getDefaultEncryption() {
 721		AxolotlService axolotlService = account.getAxolotlService();
 722		if (Config.supportUnencrypted()) {
 723			return Message.ENCRYPTION_NONE;
 724		} else if (Config.supportOmemo()
 725				&& (axolotlService != null && axolotlService.isConversationAxolotlCapable(this) || !Config.multipleEncryptionChoices())) {
 726			return Message.ENCRYPTION_AXOLOTL;
 727		} else if (Config.supportOtr() && mode == MODE_SINGLE) {
 728			return Message.ENCRYPTION_OTR;
 729		} else if (Config.supportOpenPgp()) {
 730			return Message.ENCRYPTION_PGP;
 731		} else {
 732			return Message.ENCRYPTION_NONE;
 733		}
 734	}
 735
 736	public void setNextEncryption(int encryption) {
 737		this.setAttribute(ATTRIBUTE_NEXT_ENCRYPTION, String.valueOf(encryption));
 738	}
 739
 740	public String getNextMessage() {
 741		if (this.nextMessage == null) {
 742			return "";
 743		} else {
 744			return this.nextMessage;
 745		}
 746	}
 747
 748	public boolean smpRequested() {
 749		return smp().status == Smp.STATUS_CONTACT_REQUESTED;
 750	}
 751
 752	public void setNextMessage(String message) {
 753		this.nextMessage = message;
 754	}
 755
 756	public void setSymmetricKey(byte[] key) {
 757		this.symmetricKey = key;
 758	}
 759
 760	public byte[] getSymmetricKey() {
 761		return this.symmetricKey;
 762	}
 763
 764	public void setBookmark(Bookmark bookmark) {
 765		this.bookmark = bookmark;
 766		this.bookmark.setConversation(this);
 767	}
 768
 769	public void deregisterWithBookmark() {
 770		if (this.bookmark != null) {
 771			this.bookmark.setConversation(null);
 772		}
 773	}
 774
 775	public Bookmark getBookmark() {
 776		return this.bookmark;
 777	}
 778
 779	public boolean hasDuplicateMessage(Message message) {
 780		synchronized (this.messages) {
 781			for (int i = this.messages.size() - 1; i >= 0; --i) {
 782				if (this.messages.get(i).similar(message)) {
 783					return true;
 784				}
 785			}
 786		}
 787		return false;
 788	}
 789
 790	public Message findSentMessageWithBody(String body) {
 791		synchronized (this.messages) {
 792			for (int i = this.messages.size() - 1; i >= 0; --i) {
 793				Message message = this.messages.get(i);
 794				if (message.getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_SEND) {
 795					String otherBody;
 796					if (message.hasFileOnRemoteHost()) {
 797						otherBody = message.getFileParams().url.toString();
 798					} else {
 799						otherBody = message.body;
 800					}
 801					if (otherBody != null && otherBody.equals(body)) {
 802						return message;
 803					}
 804				}
 805			}
 806			return null;
 807		}
 808	}
 809
 810	public long getLastMessageTransmitted() {
 811		final long last_clear = getLastClearHistory();
 812		long last_received = 0;
 813		synchronized (this.messages) {
 814			for(int i = this.messages.size() - 1; i >= 0; --i) {
 815				Message message = this.messages.get(i);
 816				if (message.getStatus() == Message.STATUS_RECEIVED || message.isCarbon()) {
 817					last_received = message.getTimeSent();
 818					break;
 819				}
 820			}
 821		}
 822		return Math.max(last_clear,last_received);
 823	}
 824
 825	public void setMutedTill(long value) {
 826		this.setAttribute(ATTRIBUTE_MUTED_TILL, String.valueOf(value));
 827	}
 828
 829	public boolean isMuted() {
 830		return System.currentTimeMillis() < this.getLongAttribute(ATTRIBUTE_MUTED_TILL, 0);
 831	}
 832
 833	public boolean alwaysNotify() {
 834		return mode == MODE_SINGLE || getBooleanAttribute(ATTRIBUTE_ALWAYS_NOTIFY, Config.ALWAYS_NOTIFY_BY_DEFAULT || isPnNA());
 835	}
 836
 837	public boolean setAttribute(String key, String value) {
 838		synchronized (this.attributes) {
 839			try {
 840				this.attributes.put(key, value);
 841				return true;
 842			} catch (JSONException e) {
 843				return false;
 844			}
 845		}
 846	}
 847
 848	public boolean setAttribute(String key, List<Jid> jids) {
 849		JSONArray array = new JSONArray();
 850		for(Jid jid : jids) {
 851			array.put(jid.toBareJid().toString());
 852		}
 853		synchronized (this.attributes) {
 854			try {
 855				this.attributes.put(key, array);
 856				return true;
 857			} catch (JSONException e) {
 858				e.printStackTrace();
 859				return false;
 860			}
 861		}
 862	}
 863
 864	public String getAttribute(String key) {
 865		synchronized (this.attributes) {
 866			try {
 867				return this.attributes.getString(key);
 868			} catch (JSONException e) {
 869				return null;
 870			}
 871		}
 872	}
 873
 874	public List<Jid> getJidListAttribute(String key) {
 875		ArrayList<Jid> list = new ArrayList<>();
 876		synchronized (this.attributes) {
 877			try {
 878				JSONArray array = this.attributes.getJSONArray(key);
 879				for (int i = 0; i < array.length(); ++i) {
 880					try {
 881						list.add(Jid.fromString(array.getString(i)));
 882					} catch (InvalidJidException e) {
 883						//ignored
 884					}
 885				}
 886			} catch (JSONException e) {
 887				//ignored
 888			}
 889		}
 890		return list;
 891	}
 892
 893	public int getIntAttribute(String key, int defaultValue) {
 894		String value = this.getAttribute(key);
 895		if (value == null) {
 896			return defaultValue;
 897		} else {
 898			try {
 899				return Integer.parseInt(value);
 900			} catch (NumberFormatException e) {
 901				return defaultValue;
 902			}
 903		}
 904	}
 905
 906	public long getLongAttribute(String key, long defaultValue) {
 907		String value = this.getAttribute(key);
 908		if (value == null) {
 909			return defaultValue;
 910		} else {
 911			try {
 912				return Long.parseLong(value);
 913			} catch (NumberFormatException e) {
 914				return defaultValue;
 915			}
 916		}
 917	}
 918
 919	public boolean getBooleanAttribute(String key, boolean defaultValue) {
 920		String value = this.getAttribute(key);
 921		if (value == null) {
 922			return defaultValue;
 923		} else {
 924			return Boolean.parseBoolean(value);
 925		}
 926	}
 927
 928	public void add(Message message) {
 929		message.setConversation(this);
 930		synchronized (this.messages) {
 931			this.messages.add(message);
 932		}
 933	}
 934
 935	public void prepend(Message message) {
 936		message.setConversation(this);
 937		synchronized (this.messages) {
 938			this.messages.add(0,message);
 939		}
 940	}
 941
 942	public void addAll(int index, List<Message> messages) {
 943		synchronized (this.messages) {
 944			this.messages.addAll(index, messages);
 945		}
 946		account.getPgpDecryptionService().decrypt(messages);
 947	}
 948
 949	public void expireOldMessages(long timestamp) {
 950		synchronized (this.messages) {
 951			for(ListIterator<Message> iterator = this.messages.listIterator(); iterator.hasNext();) {
 952				if (iterator.next().getTimeSent() < timestamp) {
 953					iterator.remove();
 954				}
 955			}
 956			untieMessages();
 957		}
 958	}
 959
 960	public void sort() {
 961		synchronized (this.messages) {
 962			Collections.sort(this.messages, new Comparator<Message>() {
 963				@Override
 964				public int compare(Message left, Message right) {
 965					if (left.getTimeSent() < right.getTimeSent()) {
 966						return -1;
 967					} else if (left.getTimeSent() > right.getTimeSent()) {
 968						return 1;
 969					} else {
 970						return 0;
 971					}
 972				}
 973			});
 974			untieMessages();
 975		}
 976	}
 977
 978	private void untieMessages() {
 979		for(Message message : this.messages) {
 980			message.untie();
 981		}
 982	}
 983
 984	public int unreadCount() {
 985		synchronized (this.messages) {
 986			int count = 0;
 987			for(int i = this.messages.size() - 1; i >= 0; --i) {
 988				if (this.messages.get(i).isRead()) {
 989					return count;
 990				}
 991				++count;
 992			}
 993			return count;
 994		}
 995	}
 996
 997	private int sentMessagesCount() {
 998		int count = 0;
 999		synchronized (this.messages) {
1000			for(Message message : messages) {
1001				if (message.getStatus() != Message.STATUS_RECEIVED) {
1002					++count;
1003				}
1004			}
1005		}
1006		return count;
1007	}
1008
1009	public boolean isWithStranger() {
1010		return mode == MODE_SINGLE
1011				&& !getJid().equals(account.getJid().toDomainJid())
1012				&& !getContact().mutualPresenceSubscription()
1013				&& sentMessagesCount() == 0;
1014	}
1015
1016	public class Smp {
1017		public static final int STATUS_NONE = 0;
1018		public static final int STATUS_CONTACT_REQUESTED = 1;
1019		public static final int STATUS_WE_REQUESTED = 2;
1020		public static final int STATUS_FAILED = 3;
1021		public static final int STATUS_VERIFIED = 4;
1022
1023		public String secret = null;
1024		public String hint = null;
1025		public int status = 0;
1026	}
1027}