Conversation.java

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