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