Conversation.java

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