use stanza-id for display markers in group chats

Daniel Gultsch created

Change summary

src/main/java/eu/siacs/conversations/entities/Conversation.java          | 2022 
src/main/java/eu/siacs/conversations/generator/MessageGenerator.java     |   23 
src/main/java/eu/siacs/conversations/parser/MessageParser.java           |   39 
src/main/java/eu/siacs/conversations/services/XmppConnectionService.java |    9 
4 files changed, 1,060 insertions(+), 1,033 deletions(-)

Detailed changes

src/main/java/eu/siacs/conversations/entities/Conversation.java 🔗

@@ -33,1011 +33,1023 @@ import static eu.siacs.conversations.entities.Bookmark.printableValue;
 
 
 public class Conversation extends AbstractEntity implements Blockable, Comparable<Conversation>, Conversational, AvatarService.Avatarable {
-	public static final String TABLENAME = "conversations";
-
-	public static final int STATUS_AVAILABLE = 0;
-	public static final int STATUS_ARCHIVED = 1;
-
-	public static final String NAME = "name";
-	public static final String ACCOUNT = "accountUuid";
-	public static final String CONTACT = "contactUuid";
-	public static final String CONTACTJID = "contactJid";
-	public static final String STATUS = "status";
-	public static final String CREATED = "created";
-	public static final String MODE = "mode";
-	public static final String ATTRIBUTES = "attributes";
-
-	public static final String ATTRIBUTE_MUTED_TILL = "muted_till";
-	public static final String ATTRIBUTE_ALWAYS_NOTIFY = "always_notify";
-	public static final String ATTRIBUTE_PUSH_NODE = "push_node";
-	public static final String ATTRIBUTE_LAST_CLEAR_HISTORY = "last_clear_history";
-	static final String ATTRIBUTE_MUC_PASSWORD = "muc_password";
-	private static final String ATTRIBUTE_NEXT_MESSAGE = "next_message";
-	private static final String ATTRIBUTE_NEXT_MESSAGE_TIMESTAMP = "next_message_timestamp";
-	private static final String ATTRIBUTE_CRYPTO_TARGETS = "crypto_targets";
-	private static final String ATTRIBUTE_NEXT_ENCRYPTION = "next_encryption";
-	private static final String ATTRIBUTE_CORRECTING_MESSAGE = "correcting_message";
-	static final String ATTRIBUTE_MEMBERS_ONLY = "members_only";
-	static final String ATTRIBUTE_MODERATED = "moderated";
-	static final String ATTRIBUTE_NON_ANONYMOUS = "non_anonymous";
-	public static final String ATTRIBUTE_FORMERLY_PRIVATE_NON_ANONYMOUS = "formerly_private_non_anonymous";
-	protected final ArrayList<Message> messages = new ArrayList<>();
-	public AtomicBoolean messagesLoaded = new AtomicBoolean(true);
-	protected Account account = null;
-	private String draftMessage;
-	private String name;
-	private String contactUuid;
-	private String accountUuid;
-	private Jid contactJid;
-	private int status;
-	private long created;
-	private int mode;
-	private JSONObject attributes;
-	private Jid nextCounterpart;
-	private transient MucOptions mucOptions = null;
-	private boolean messagesLeftOnServer = true;
-	private ChatState mOutgoingChatState = Config.DEFAULT_CHAT_STATE;
-	private ChatState mIncomingChatState = Config.DEFAULT_CHAT_STATE;
-	private String mFirstMamReference = null;
-
-	public Conversation(final String name, final Account account, final Jid contactJid,
-	                    final int mode) {
-		this(java.util.UUID.randomUUID().toString(), name, null, account
-						.getUuid(), contactJid, System.currentTimeMillis(),
-				STATUS_AVAILABLE, mode, "");
-		this.account = account;
-	}
-
-	public Conversation(final String uuid, final String name, final String contactUuid,
-	                    final String accountUuid, final Jid contactJid, final long created, final int status,
-	                    final int mode, final String attributes) {
-		this.uuid = uuid;
-		this.name = name;
-		this.contactUuid = contactUuid;
-		this.accountUuid = accountUuid;
-		this.contactJid = contactJid;
-		this.created = created;
-		this.status = status;
-		this.mode = mode;
-		try {
-			this.attributes = new JSONObject(attributes == null ? "" : attributes);
-		} catch (JSONException e) {
-			this.attributes = new JSONObject();
-		}
-	}
-
-	public static Conversation fromCursor(Cursor cursor) {
-		return new Conversation(cursor.getString(cursor.getColumnIndex(UUID)),
-				cursor.getString(cursor.getColumnIndex(NAME)),
-				cursor.getString(cursor.getColumnIndex(CONTACT)),
-				cursor.getString(cursor.getColumnIndex(ACCOUNT)),
-				JidHelper.parseOrFallbackToInvalid(cursor.getString(cursor.getColumnIndex(CONTACTJID))),
-				cursor.getLong(cursor.getColumnIndex(CREATED)),
-				cursor.getInt(cursor.getColumnIndex(STATUS)),
-				cursor.getInt(cursor.getColumnIndex(MODE)),
-				cursor.getString(cursor.getColumnIndex(ATTRIBUTES)));
-	}
-
-	public boolean hasMessagesLeftOnServer() {
-		return messagesLeftOnServer;
-	}
-
-	public void setHasMessagesLeftOnServer(boolean value) {
-		this.messagesLeftOnServer = value;
-	}
-
-	public Message getFirstUnreadMessage() {
-		Message first = null;
-		synchronized (this.messages) {
-			for (int i = messages.size() - 1; i >= 0; --i) {
-				if (messages.get(i).isRead()) {
-					return first;
-				} else {
-					first = messages.get(i);
-				}
-			}
-		}
-		return first;
-	}
-
-	public Message findUnsentMessageWithUuid(String uuid) {
-		synchronized (this.messages) {
-			for (final Message message : this.messages) {
-				final int s = message.getStatus();
-				if ((s == Message.STATUS_UNSEND || s == Message.STATUS_WAITING) && message.getUuid().equals(uuid)) {
-					return message;
-				}
-			}
-		}
-		return null;
-	}
-
-	public void findWaitingMessages(OnMessageFound onMessageFound) {
-		final ArrayList<Message> results = new ArrayList<>();
-		synchronized (this.messages) {
-			for (Message message : this.messages) {
-				if (message.getStatus() == Message.STATUS_WAITING) {
-					results.add(message);
-				}
-			}
-		}
-		for(Message result : results) {
-			onMessageFound.onMessageFound(result);
-		}
-	}
-
-	public void findUnreadMessages(OnMessageFound onMessageFound) {
-		final ArrayList<Message> results = new ArrayList<>();
-		synchronized (this.messages) {
-			for (Message message : this.messages) {
-				if (!message.isRead()) {
-					results.add(message);
-				}
-			}
-		}
-		for(Message result : results) {
-			onMessageFound.onMessageFound(result);
-		}
-	}
-
-	public Message findMessageWithFileAndUuid(final String uuid) {
-		synchronized (this.messages) {
-			for (final Message message : this.messages) {
-				if (message.getUuid().equals(uuid)
-						&& message.getEncryption() != Message.ENCRYPTION_PGP
-						&& (message.isFileOrImage() || message.treatAsDownloadable())) {
-					return message;
-				}
-			}
-		}
-		return null;
-	}
-
-	public boolean markAsDeleted(final List<String> uuids) {
-		boolean deleted = false;
-		final PgpDecryptionService pgpDecryptionService = account.getPgpDecryptionService();
-		synchronized (this.messages) {
-			for(Message message : this.messages) {
-				if (uuids.contains(message.getUuid())) {
-					message.setDeleted(true);
-					deleted = true;
-					if (message.getEncryption() == Message.ENCRYPTION_PGP && pgpDecryptionService != null) {
-						pgpDecryptionService.discard(message);
-					}
-				}
-			}
-		}
-		return deleted;
-	}
-
-	public boolean markAsChanged(final List<DatabaseBackend.FilePathInfo> files) {
-		boolean changed = false;
-		final PgpDecryptionService pgpDecryptionService = account.getPgpDecryptionService();
-		synchronized (this.messages) {
-			for(Message message : this.messages) {
-				for(final DatabaseBackend.FilePathInfo file : files)
-				if (file.uuid.toString().equals(message.getUuid())) {
-					message.setDeleted(file.deleted);
-					changed = true;
-					if (file.deleted && message.getEncryption() == Message.ENCRYPTION_PGP && pgpDecryptionService != null) {
-						pgpDecryptionService.discard(message);
-					}
-				}
-			}
-		}
-		return changed;
-	}
-
-	public void clearMessages() {
-		synchronized (this.messages) {
-			this.messages.clear();
-		}
-	}
-
-	public boolean setIncomingChatState(ChatState state) {
-		if (this.mIncomingChatState == state) {
-			return false;
-		}
-		this.mIncomingChatState = state;
-		return true;
-	}
-
-	public ChatState getIncomingChatState() {
-		return this.mIncomingChatState;
-	}
-
-	public boolean setOutgoingChatState(ChatState state) {
-		if (mode == MODE_SINGLE && !getContact().isSelf() || (isPrivateAndNonAnonymous() && getNextCounterpart() == null)) {
-			if (this.mOutgoingChatState != state) {
-				this.mOutgoingChatState = state;
-				return true;
-			}
-		}
-		return false;
-	}
-
-	public ChatState getOutgoingChatState() {
-		return this.mOutgoingChatState;
-	}
-
-	public void trim() {
-		synchronized (this.messages) {
-			final int size = messages.size();
-			final int maxsize = Config.PAGE_SIZE * Config.MAX_NUM_PAGES;
-			if (size > maxsize) {
-				List<Message> discards = this.messages.subList(0, size - maxsize);
-				final PgpDecryptionService pgpDecryptionService = account.getPgpDecryptionService();
-				if (pgpDecryptionService != null) {
-					pgpDecryptionService.discard(discards);
-				}
-				discards.clear();
-				untieMessages();
-			}
-		}
-	}
-
-	public void findUnsentTextMessages(OnMessageFound onMessageFound) {
-		final ArrayList<Message> results = new ArrayList<>();
-		synchronized (this.messages) {
-			for (Message message : this.messages) {
-				if ((message.getType() == Message.TYPE_TEXT || message.hasFileOnRemoteHost()) && message.getStatus() == Message.STATUS_UNSEND) {
-					results.add(message);
-				}
-			}
-		}
-		for(Message result : results) {
-			onMessageFound.onMessageFound(result);
-		}
-	}
-
-	public Message findSentMessageWithUuidOrRemoteId(String id) {
-		synchronized (this.messages) {
-			for (Message message : this.messages) {
-				if (id.equals(message.getUuid())
-						|| (message.getStatus() >= Message.STATUS_SEND
-						&& id.equals(message.getRemoteMsgId()))) {
-					return message;
-				}
-			}
-		}
-		return null;
-	}
-
-	public Message findMessageWithRemoteIdAndCounterpart(String id, Jid counterpart, boolean received, boolean carbon) {
-		synchronized (this.messages) {
-			for (int i = this.messages.size() - 1; i >= 0; --i) {
-				final Message message = messages.get(i);
-				final Jid mcp = message.getCounterpart();
-				if (mcp == null) {
-					continue;
-				}
-				final boolean counterpartMatch = mode == MODE_SINGLE ?
-					counterpart.asBareJid().equals(mcp.asBareJid()) :
-					counterpart.equals(mcp);
-				if (counterpartMatch && ((message.getStatus() == Message.STATUS_RECEIVED) == received)
-						&& (carbon == message.isCarbon() || received)) {
-					final boolean idMatch = id.equals(message.getRemoteMsgId()) || message.remoteMsgIdMatchInEdit(id);
-					if (idMatch && !message.isFileOrImage() && !message.treatAsDownloadable()) {
-						return message;
-					} else {
-						return null;
-					}
-				}
-			}
-		}
-		return null;
-	}
-
-	public Message findSentMessageWithUuid(String id) {
-		synchronized (this.messages) {
-			for (Message message : this.messages) {
-				if (id.equals(message.getUuid())) {
-					return message;
-				}
-			}
-		}
-		return null;
-	}
-
-	public Message findMessageWithRemoteId(String id, Jid counterpart) {
-		synchronized (this.messages) {
-			for (Message message : this.messages) {
-				if (counterpart.equals(message.getCounterpart())
-						&& (id.equals(message.getRemoteMsgId()) || id.equals(message.getUuid()))) {
-					return message;
-				}
-			}
-		}
-		return null;
-	}
-
-	public boolean hasMessageWithCounterpart(Jid counterpart) {
-		synchronized (this.messages) {
-			for (Message message : this.messages) {
-				if (counterpart.equals(message.getCounterpart())) {
-					return true;
-				}
-			}
-		}
-		return false;
-	}
-
-	public void populateWithMessages(final List<Message> messages) {
-		synchronized (this.messages) {
-			messages.clear();
-			messages.addAll(this.messages);
-		}
-		for (Iterator<Message> iterator = messages.iterator(); iterator.hasNext(); ) {
-			if (iterator.next().wasMergedIntoPrevious()) {
-				iterator.remove();
-			}
-		}
-	}
-
-	@Override
-	public boolean isBlocked() {
-		return getContact().isBlocked();
-	}
-
-	@Override
-	public boolean isDomainBlocked() {
-		return getContact().isDomainBlocked();
-	}
-
-	@Override
-	public Jid getBlockedJid() {
-		return getContact().getBlockedJid();
-	}
-
-	public int countMessages() {
-		synchronized (this.messages) {
-			return this.messages.size();
-		}
-	}
-
-	public String getFirstMamReference() {
-		return this.mFirstMamReference;
-	}
-
-	public void setFirstMamReference(String reference) {
-		this.mFirstMamReference = reference;
-	}
-
-	public void setLastClearHistory(long time, String reference) {
-		if (reference != null) {
-			setAttribute(ATTRIBUTE_LAST_CLEAR_HISTORY, String.valueOf(time) + ":" + reference);
-		} else {
-			setAttribute(ATTRIBUTE_LAST_CLEAR_HISTORY, time);
-		}
-	}
-
-	public MamReference getLastClearHistory() {
-		return MamReference.fromAttribute(getAttribute(ATTRIBUTE_LAST_CLEAR_HISTORY));
-	}
-
-	public List<Jid> getAcceptedCryptoTargets() {
-		if (mode == MODE_SINGLE) {
-			return Collections.singletonList(getJid().asBareJid());
-		} else {
-			return getJidListAttribute(ATTRIBUTE_CRYPTO_TARGETS);
-		}
-	}
-
-	public void setAcceptedCryptoTargets(List<Jid> acceptedTargets) {
-		setAttribute(ATTRIBUTE_CRYPTO_TARGETS, acceptedTargets);
-	}
-
-	public boolean setCorrectingMessage(Message correctingMessage) {
-		setAttribute(ATTRIBUTE_CORRECTING_MESSAGE,correctingMessage == null ? null : correctingMessage.getUuid());
-		return correctingMessage == null && draftMessage != null;
-	}
-
-	public Message getCorrectingMessage() {
-		final String uuid = getAttribute(ATTRIBUTE_CORRECTING_MESSAGE);
-		return uuid == null ? null : findSentMessageWithUuid(uuid);
-	}
-
-	public boolean withSelf() {
-		return getContact().isSelf();
-	}
-
-	@Override
-	public int compareTo(@NonNull Conversation another) {
-		return Long.compare(another.getSortableTime(), getSortableTime());
-	}
-
-	private long getSortableTime() {
-		Draft draft = getDraft();
-		long messageTime = getLatestMessage().getTimeSent();
-		if (draft == null) {
-			return messageTime;
-		} else {
-			return Math.max(messageTime, draft.getTimestamp());
-		}
-	}
-
-	public String getDraftMessage() {
-		return draftMessage;
-	}
-
-	public void setDraftMessage(String draftMessage) {
-		this.draftMessage = draftMessage;
-	}
-
-	public boolean isRead() {
-		return (this.messages.size() == 0) || this.messages.get(this.messages.size() - 1).isRead();
-	}
-
-	public List<Message> markRead(String upToUuid) {
-		final List<Message> unread = new ArrayList<>();
-		synchronized (this.messages) {
-			for (Message message : this.messages) {
-				if (!message.isRead()) {
-					message.markRead();
-					unread.add(message);
-				}
-				if (message.getUuid().equals(upToUuid)) {
-					return unread;
-				}
-			}
-		}
-		return unread;
-	}
-
-	public static Message getLatestMarkableMessage(final List<Message> messages, boolean isPrivateAndNonAnonymousMuc) {
-			for (int i = messages.size() - 1; i >= 0; --i) {
-				final Message message = messages.get(i);
-				if (message.getStatus() <= Message.STATUS_RECEIVED
-						&& (message.markable || isPrivateAndNonAnonymousMuc)
-						&& !message.isPrivateMessage()) {
-					return message;
-				}
-			}
-		return null;
-	}
-
-	public Message getLatestMessage() {
-		synchronized (this.messages) {
-			if (this.messages.size() == 0) {
-				Message message = new Message(this, "", Message.ENCRYPTION_NONE);
-				message.setType(Message.TYPE_STATUS);
-				message.setTime(Math.max(getCreated(), getLastClearHistory().getTimestamp()));
-				return message;
-			} else {
-				return this.messages.get(this.messages.size() - 1);
-			}
-		}
-	}
-
-	public @NonNull CharSequence getName() {
-		if (getMode() == MODE_MULTI) {
-			final String roomName = getMucOptions().getName();
-			final String subject = getMucOptions().getSubject();
-			final Bookmark bookmark = getBookmark();
-			final String bookmarkName = bookmark != null ? bookmark.getBookmarkName() : null;
-			if (printableValue(roomName)) {
-				return roomName;
-			} else if (printableValue(subject)) {
-				return subject;
-			} else if (printableValue(bookmarkName, false)) {
-				return bookmarkName;
-			} else {
-				final String generatedName = getMucOptions().createNameFromParticipants();
-				if (printableValue(generatedName)) {
-					return generatedName;
-				} else {
-					return contactJid.getLocal() != null ? contactJid.getLocal() : contactJid;
-				}
-			}
-		} else if ((QuickConversationsService.isConversations() || !Config.QUICKSY_DOMAIN.equals(contactJid.getDomain())) && isWithStranger()) {
-			return contactJid;
-		} else {
-			return this.getContact().getDisplayName();
-		}
-	}
-
-	public String getAccountUuid() {
-		return this.accountUuid;
-	}
-
-	public Account getAccount() {
-		return this.account;
-	}
-
-	public void setAccount(final Account account) {
-		this.account = account;
-	}
-
-	public Contact getContact() {
-		return this.account.getRoster().getContact(this.contactJid);
-	}
-
-	@Override
-	public Jid getJid() {
-		return this.contactJid;
-	}
-
-	public int getStatus() {
-		return this.status;
-	}
-
-	public void setStatus(int status) {
-		this.status = status;
-	}
-
-	public long getCreated() {
-		return this.created;
-	}
-
-	public ContentValues getContentValues() {
-		ContentValues values = new ContentValues();
-		values.put(UUID, uuid);
-		values.put(NAME, name);
-		values.put(CONTACT, contactUuid);
-		values.put(ACCOUNT, accountUuid);
-		values.put(CONTACTJID, contactJid.toString());
-		values.put(CREATED, created);
-		values.put(STATUS, status);
-		values.put(MODE, mode);
-		synchronized (this.attributes) {
-			values.put(ATTRIBUTES, attributes.toString());
-		}
-		return values;
-	}
-
-	public int getMode() {
-		return this.mode;
-	}
-
-	public void setMode(int mode) {
-		this.mode = mode;
-	}
-
-	/**
-	 * short for is Private and Non-anonymous
-	 */
-	public boolean isSingleOrPrivateAndNonAnonymous() {
-		return mode == MODE_SINGLE || isPrivateAndNonAnonymous();
-	}
-
-	public boolean isPrivateAndNonAnonymous() {
-		return getMucOptions().isPrivateAndNonAnonymous();
-	}
-
-	public synchronized MucOptions getMucOptions() {
-		if (this.mucOptions == null) {
-			this.mucOptions = new MucOptions(this);
-		}
-		return this.mucOptions;
-	}
-
-	public void resetMucOptions() {
-		this.mucOptions = null;
-	}
-
-	public void setContactJid(final Jid jid) {
-		this.contactJid = jid;
-	}
-
-	public Jid getNextCounterpart() {
-		return this.nextCounterpart;
-	}
-
-	public void setNextCounterpart(Jid jid) {
-		this.nextCounterpart = jid;
-	}
-
-	public int getNextEncryption() {
-		if (!Config.supportOmemo() && !Config.supportOpenPgp()) {
-			return Message.ENCRYPTION_NONE;
-		}
-		if (OmemoSetting.isAlways()) {
-			return suitableForOmemoByDefault(this) ? Message.ENCRYPTION_AXOLOTL : Message.ENCRYPTION_NONE;
-		}
-		final int defaultEncryption;
-		if (suitableForOmemoByDefault(this)) {
-			defaultEncryption = OmemoSetting.getEncryption();
-		} else {
-			defaultEncryption = Message.ENCRYPTION_NONE;
-		}
-		int encryption = this.getIntAttribute(ATTRIBUTE_NEXT_ENCRYPTION, defaultEncryption);
-		if (encryption == Message.ENCRYPTION_OTR || encryption < 0) {
-			return defaultEncryption;
-		} else {
-			return encryption;
-		}
-	}
-
-	private static boolean suitableForOmemoByDefault(final Conversation conversation) {
-		if (conversation.getJid().asBareJid().equals(Config.BUG_REPORTS)) {
-			return false;
-		}
-		if (conversation.getContact().isOwnServer()) {
-			return false;
-		}
-		final String contact = conversation.getJid().getDomain();
-		final String account = conversation.getAccount().getServer();
-		if (Config.OMEMO_EXCEPTIONS.CONTACT_DOMAINS.contains(contact) || Config.OMEMO_EXCEPTIONS.ACCOUNT_DOMAINS.contains(account)) {
-			return false;
-		}
-		return conversation.isSingleOrPrivateAndNonAnonymous() || conversation.getBooleanAttribute(ATTRIBUTE_FORMERLY_PRIVATE_NON_ANONYMOUS, false);
-	}
-
-	public boolean setNextEncryption(int encryption) {
-		return this.setAttribute(ATTRIBUTE_NEXT_ENCRYPTION, encryption);
-	}
-
-	public String getNextMessage() {
-		final String nextMessage = getAttribute(ATTRIBUTE_NEXT_MESSAGE);
-		return nextMessage == null ? "" : nextMessage;
-	}
-
-	public @Nullable
-	Draft getDraft() {
-		long timestamp = getLongAttribute(ATTRIBUTE_NEXT_MESSAGE_TIMESTAMP, 0);
-		if (timestamp > getLatestMessage().getTimeSent()) {
-			String message = getAttribute(ATTRIBUTE_NEXT_MESSAGE);
-			if (!TextUtils.isEmpty(message) && timestamp != 0) {
-				return new Draft(message, timestamp);
-			}
-		}
-		return null;
-	}
-
-	public boolean setNextMessage(final String input) {
-		final String message = input == null || input.trim().isEmpty() ? null : input;
-		boolean changed = !getNextMessage().equals(message);
-		this.setAttribute(ATTRIBUTE_NEXT_MESSAGE, message);
-		if (changed) {
-			this.setAttribute(ATTRIBUTE_NEXT_MESSAGE_TIMESTAMP, message == null ? 0 : System.currentTimeMillis());
-		}
-		return changed;
-	}
-
-	public Bookmark getBookmark() {
-		return this.account.getBookmark(this.contactJid);
-	}
-
-	public Message findDuplicateMessage(Message message) {
-		synchronized (this.messages) {
-			for (int i = this.messages.size() - 1; i >= 0; --i) {
-				if (this.messages.get(i).similar(message)) {
-					return this.messages.get(i);
-				}
-			}
-		}
-		return null;
-	}
-
-	public boolean hasDuplicateMessage(Message message) {
-		return findDuplicateMessage(message) != null;
-	}
-
-	public Message findSentMessageWithBody(String body) {
-		synchronized (this.messages) {
-			for (int i = this.messages.size() - 1; i >= 0; --i) {
-				Message message = this.messages.get(i);
-				if (message.getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_SEND) {
-					String otherBody;
-					if (message.hasFileOnRemoteHost()) {
-						otherBody = message.getFileParams().url.toString();
-					} else {
-						otherBody = message.body;
-					}
-					if (otherBody != null && otherBody.equals(body)) {
-						return message;
-					}
-				}
-			}
-			return null;
-		}
-	}
-
-	public Message findRtpSession(final String sessionId, final int s) {
-		synchronized (this.messages) {
-			for (int i = this.messages.size() - 1; i >= 0; --i) {
-				final Message message = this.messages.get(i);
-				if ((message.getStatus() == s) && (message.getType() == Message.TYPE_RTP_SESSION) && sessionId.equals(message.getRemoteMsgId())) {
-					return message;
-				}
-			}
-		}
-		return null;
-	}
-
-	public boolean possibleDuplicate(final String serverMsgId, final String remoteMsgId) {
-		if (serverMsgId == null || remoteMsgId == null) {
-			return false;
-		}
-		synchronized (this.messages) {
-			for(Message message : this.messages) {
-				if (serverMsgId.equals(message.getServerMsgId()) || remoteMsgId.equals(message.getRemoteMsgId())) {
-					return true;
-				}
-			}
-		}
-		return false;
-	}
-
-	public MamReference getLastMessageTransmitted() {
-		final MamReference lastClear = getLastClearHistory();
-		MamReference lastReceived = new MamReference(0);
-		synchronized (this.messages) {
-			for (int i = this.messages.size() - 1; i >= 0; --i) {
-				final Message message = this.messages.get(i);
-				if (message.isPrivateMessage()) {
-					continue; //it's unsafe to use private messages as anchor. They could be coming from user archive
-				}
-				if (message.getStatus() == Message.STATUS_RECEIVED || message.isCarbon() || message.getServerMsgId() != null) {
-					lastReceived = new MamReference(message.getTimeSent(), message.getServerMsgId());
-					break;
-				}
-			}
-		}
-		return MamReference.max(lastClear, lastReceived);
-	}
-
-	public void setMutedTill(long value) {
-		this.setAttribute(ATTRIBUTE_MUTED_TILL, String.valueOf(value));
-	}
-
-	public boolean isMuted() {
-		return System.currentTimeMillis() < this.getLongAttribute(ATTRIBUTE_MUTED_TILL, 0);
-	}
-
-	public boolean alwaysNotify() {
-		return mode == MODE_SINGLE || getBooleanAttribute(ATTRIBUTE_ALWAYS_NOTIFY, Config.ALWAYS_NOTIFY_BY_DEFAULT || isPrivateAndNonAnonymous());
-	}
-
-	public boolean setAttribute(String key, boolean value) {
-		return setAttribute(key, String.valueOf(value));
-	}
-
-	private boolean setAttribute(String key, long value) {
-		return setAttribute(key, Long.toString(value));
-	}
-
-	private boolean setAttribute(String key, int value) {
-		return setAttribute(key, String.valueOf(value));
-	}
-
-	public boolean setAttribute(String key, String value) {
-		synchronized (this.attributes) {
-			try {
-				if (value == null) {
-					if (this.attributes.has(key)) {
-						this.attributes.remove(key);
-						return true;
-					} else {
-						return false;
-					}
-				} else {
-					final String prev = this.attributes.optString(key, null);
-					this.attributes.put(key, value);
-					return !value.equals(prev);
-				}
-			} catch (JSONException e) {
-				throw new AssertionError(e);
-			}
-		}
-	}
-
-	public boolean setAttribute(String key, List<Jid> jids) {
-		JSONArray array = new JSONArray();
-		for (Jid jid : jids) {
-			array.put(jid.asBareJid().toString());
-		}
-		synchronized (this.attributes) {
-			try {
-				this.attributes.put(key, array);
-				return true;
-			} catch (JSONException e) {
-				return false;
-			}
-		}
-	}
-
-	public String getAttribute(String key) {
-		synchronized (this.attributes) {
-		    return this.attributes.optString(key, null);
-		}
-	}
-
-	private List<Jid> getJidListAttribute(String key) {
-		ArrayList<Jid> list = new ArrayList<>();
-		synchronized (this.attributes) {
-			try {
-				JSONArray array = this.attributes.getJSONArray(key);
-				for (int i = 0; i < array.length(); ++i) {
-					try {
-						list.add(Jid.of(array.getString(i)));
-					} catch (IllegalArgumentException e) {
-						//ignored
-					}
-				}
-			} catch (JSONException e) {
-				//ignored
-			}
-		}
-		return list;
-	}
-
-	private int getIntAttribute(String key, int defaultValue) {
-		String value = this.getAttribute(key);
-		if (value == null) {
-			return defaultValue;
-		} else {
-			try {
-				return Integer.parseInt(value);
-			} catch (NumberFormatException e) {
-				return defaultValue;
-			}
-		}
-	}
-
-	public long getLongAttribute(String key, long defaultValue) {
-		String value = this.getAttribute(key);
-		if (value == null) {
-			return defaultValue;
-		} else {
-			try {
-				return Long.parseLong(value);
-			} catch (NumberFormatException e) {
-				return defaultValue;
-			}
-		}
-	}
-
-	public boolean getBooleanAttribute(String key, boolean defaultValue) {
-		String value = this.getAttribute(key);
-		if (value == null) {
-			return defaultValue;
-		} else {
-			return Boolean.parseBoolean(value);
-		}
-	}
-
-	public void add(Message message) {
-		synchronized (this.messages) {
-			this.messages.add(message);
-		}
-	}
-
-	public void prepend(int offset, Message message) {
-		synchronized (this.messages) {
-			this.messages.add(Math.min(offset, this.messages.size()), message);
-		}
-	}
-
-	public void addAll(int index, List<Message> messages) {
-		synchronized (this.messages) {
-			this.messages.addAll(index, messages);
-		}
-		account.getPgpDecryptionService().decrypt(messages);
-	}
-
-	public void expireOldMessages(long timestamp) {
-		synchronized (this.messages) {
-			for (ListIterator<Message> iterator = this.messages.listIterator(); iterator.hasNext(); ) {
-				if (iterator.next().getTimeSent() < timestamp) {
-					iterator.remove();
-				}
-			}
-			untieMessages();
-		}
-	}
-
-	public void sort() {
-		synchronized (this.messages) {
-			Collections.sort(this.messages, (left, right) -> {
-				if (left.getTimeSent() < right.getTimeSent()) {
-					return -1;
-				} else if (left.getTimeSent() > right.getTimeSent()) {
-					return 1;
-				} else {
-					return 0;
-				}
-			});
-			untieMessages();
-		}
-	}
-
-	private void untieMessages() {
-		for (Message message : this.messages) {
-			message.untie();
-		}
-	}
-
-	public int unreadCount() {
-		synchronized (this.messages) {
-			int count = 0;
-			for (int i = this.messages.size() - 1; i >= 0; --i) {
-				if (this.messages.get(i).isRead()) {
-					return count;
-				}
-				++count;
-			}
-			return count;
-		}
-	}
-
-	public int receivedMessagesCount() {
-		int count = 0;
-		synchronized (this.messages) {
-			for (Message message : messages) {
-				if (message.getStatus() == Message.STATUS_RECEIVED) {
-					++count;
-				}
-			}
-		}
-		return count;
-	}
-
-	public int sentMessagesCount() {
-		int count = 0;
-		synchronized (this.messages) {
-			for (Message message : messages) {
-				if (message.getStatus() != Message.STATUS_RECEIVED) {
-					++count;
-				}
-			}
-		}
-		return count;
-	}
-
-	public boolean isWithStranger() {
-		final Contact contact = getContact();
-		return mode == MODE_SINGLE
-				&& !contact.isOwnServer()
-				&& !contact.showInContactList()
-				&& !contact.isSelf()
-				&& !Config.QUICKSY_DOMAIN.equals(contact.getJid().toEscapedString())
-				&& sentMessagesCount() == 0;
-	}
-
-	public int getReceivedMessagesCountSinceUuid(String uuid) {
-		if (uuid == null) {
-			return  0;
-		}
-		int count = 0;
-		synchronized (this.messages) {
-			for (int i = messages.size() - 1; i >= 0; i--) {
-				final Message message = messages.get(i);
-				if (uuid.equals(message.getUuid())) {
-					return count;
-				}
-				if (message.getStatus() <= Message.STATUS_RECEIVED) {
-					++count;
-				}
-			}
-		}
-		return 0;
-	}
-
-	@Override
-	public int getAvatarBackgroundColor() {
-		return UIHelper.getColorForName(getName().toString());
-	}
+    public static final String TABLENAME = "conversations";
+
+    public static final int STATUS_AVAILABLE = 0;
+    public static final int STATUS_ARCHIVED = 1;
+
+    public static final String NAME = "name";
+    public static final String ACCOUNT = "accountUuid";
+    public static final String CONTACT = "contactUuid";
+    public static final String CONTACTJID = "contactJid";
+    public static final String STATUS = "status";
+    public static final String CREATED = "created";
+    public static final String MODE = "mode";
+    public static final String ATTRIBUTES = "attributes";
+
+    public static final String ATTRIBUTE_MUTED_TILL = "muted_till";
+    public static final String ATTRIBUTE_ALWAYS_NOTIFY = "always_notify";
+    public static final String ATTRIBUTE_PUSH_NODE = "push_node";
+    public static final String ATTRIBUTE_LAST_CLEAR_HISTORY = "last_clear_history";
+    public static final String ATTRIBUTE_FORMERLY_PRIVATE_NON_ANONYMOUS = "formerly_private_non_anonymous";
+    static final String ATTRIBUTE_MUC_PASSWORD = "muc_password";
+    static final String ATTRIBUTE_MEMBERS_ONLY = "members_only";
+    static final String ATTRIBUTE_MODERATED = "moderated";
+    static final String ATTRIBUTE_NON_ANONYMOUS = "non_anonymous";
+    private static final String ATTRIBUTE_NEXT_MESSAGE = "next_message";
+    private static final String ATTRIBUTE_NEXT_MESSAGE_TIMESTAMP = "next_message_timestamp";
+    private static final String ATTRIBUTE_CRYPTO_TARGETS = "crypto_targets";
+    private static final String ATTRIBUTE_NEXT_ENCRYPTION = "next_encryption";
+    private static final String ATTRIBUTE_CORRECTING_MESSAGE = "correcting_message";
+    protected final ArrayList<Message> messages = new ArrayList<>();
+    public AtomicBoolean messagesLoaded = new AtomicBoolean(true);
+    protected Account account = null;
+    private String draftMessage;
+    private String name;
+    private String contactUuid;
+    private String accountUuid;
+    private Jid contactJid;
+    private int status;
+    private long created;
+    private int mode;
+    private JSONObject attributes;
+    private Jid nextCounterpart;
+    private transient MucOptions mucOptions = null;
+    private boolean messagesLeftOnServer = true;
+    private ChatState mOutgoingChatState = Config.DEFAULT_CHAT_STATE;
+    private ChatState mIncomingChatState = Config.DEFAULT_CHAT_STATE;
+    private String mFirstMamReference = null;
+
+    public Conversation(final String name, final Account account, final Jid contactJid,
+                        final int mode) {
+        this(java.util.UUID.randomUUID().toString(), name, null, account
+                        .getUuid(), contactJid, System.currentTimeMillis(),
+                STATUS_AVAILABLE, mode, "");
+        this.account = account;
+    }
+
+    public Conversation(final String uuid, final String name, final String contactUuid,
+                        final String accountUuid, final Jid contactJid, final long created, final int status,
+                        final int mode, final String attributes) {
+        this.uuid = uuid;
+        this.name = name;
+        this.contactUuid = contactUuid;
+        this.accountUuid = accountUuid;
+        this.contactJid = contactJid;
+        this.created = created;
+        this.status = status;
+        this.mode = mode;
+        try {
+            this.attributes = new JSONObject(attributes == null ? "" : attributes);
+        } catch (JSONException e) {
+            this.attributes = new JSONObject();
+        }
+    }
+
+    public static Conversation fromCursor(Cursor cursor) {
+        return new Conversation(cursor.getString(cursor.getColumnIndex(UUID)),
+                cursor.getString(cursor.getColumnIndex(NAME)),
+                cursor.getString(cursor.getColumnIndex(CONTACT)),
+                cursor.getString(cursor.getColumnIndex(ACCOUNT)),
+                JidHelper.parseOrFallbackToInvalid(cursor.getString(cursor.getColumnIndex(CONTACTJID))),
+                cursor.getLong(cursor.getColumnIndex(CREATED)),
+                cursor.getInt(cursor.getColumnIndex(STATUS)),
+                cursor.getInt(cursor.getColumnIndex(MODE)),
+                cursor.getString(cursor.getColumnIndex(ATTRIBUTES)));
+    }
+
+    public static Message getLatestMarkableMessage(final List<Message> messages, boolean isPrivateAndNonAnonymousMuc) {
+        for (int i = messages.size() - 1; i >= 0; --i) {
+            final Message message = messages.get(i);
+            if (message.getStatus() <= Message.STATUS_RECEIVED
+                    && (message.markable || isPrivateAndNonAnonymousMuc)
+                    && !message.isPrivateMessage()) {
+                return message;
+            }
+        }
+        return null;
+    }
+
+    private static boolean suitableForOmemoByDefault(final Conversation conversation) {
+        if (conversation.getJid().asBareJid().equals(Config.BUG_REPORTS)) {
+            return false;
+        }
+        if (conversation.getContact().isOwnServer()) {
+            return false;
+        }
+        final String contact = conversation.getJid().getDomain();
+        final String account = conversation.getAccount().getServer();
+        if (Config.OMEMO_EXCEPTIONS.CONTACT_DOMAINS.contains(contact) || Config.OMEMO_EXCEPTIONS.ACCOUNT_DOMAINS.contains(account)) {
+            return false;
+        }
+        return conversation.isSingleOrPrivateAndNonAnonymous() || conversation.getBooleanAttribute(ATTRIBUTE_FORMERLY_PRIVATE_NON_ANONYMOUS, false);
+    }
+
+    public boolean hasMessagesLeftOnServer() {
+        return messagesLeftOnServer;
+    }
+
+    public void setHasMessagesLeftOnServer(boolean value) {
+        this.messagesLeftOnServer = value;
+    }
+
+    public Message getFirstUnreadMessage() {
+        Message first = null;
+        synchronized (this.messages) {
+            for (int i = messages.size() - 1; i >= 0; --i) {
+                if (messages.get(i).isRead()) {
+                    return first;
+                } else {
+                    first = messages.get(i);
+                }
+            }
+        }
+        return first;
+    }
+
+    public Message findUnsentMessageWithUuid(String uuid) {
+        synchronized (this.messages) {
+            for (final Message message : this.messages) {
+                final int s = message.getStatus();
+                if ((s == Message.STATUS_UNSEND || s == Message.STATUS_WAITING) && message.getUuid().equals(uuid)) {
+                    return message;
+                }
+            }
+        }
+        return null;
+    }
+
+    public void findWaitingMessages(OnMessageFound onMessageFound) {
+        final ArrayList<Message> results = new ArrayList<>();
+        synchronized (this.messages) {
+            for (Message message : this.messages) {
+                if (message.getStatus() == Message.STATUS_WAITING) {
+                    results.add(message);
+                }
+            }
+        }
+        for (Message result : results) {
+            onMessageFound.onMessageFound(result);
+        }
+    }
+
+    public void findUnreadMessages(OnMessageFound onMessageFound) {
+        final ArrayList<Message> results = new ArrayList<>();
+        synchronized (this.messages) {
+            for (Message message : this.messages) {
+                if (!message.isRead()) {
+                    results.add(message);
+                }
+            }
+        }
+        for (Message result : results) {
+            onMessageFound.onMessageFound(result);
+        }
+    }
+
+    public Message findMessageWithFileAndUuid(final String uuid) {
+        synchronized (this.messages) {
+            for (final Message message : this.messages) {
+                if (message.getUuid().equals(uuid)
+                        && message.getEncryption() != Message.ENCRYPTION_PGP
+                        && (message.isFileOrImage() || message.treatAsDownloadable())) {
+                    return message;
+                }
+            }
+        }
+        return null;
+    }
+
+    public boolean markAsDeleted(final List<String> uuids) {
+        boolean deleted = false;
+        final PgpDecryptionService pgpDecryptionService = account.getPgpDecryptionService();
+        synchronized (this.messages) {
+            for (Message message : this.messages) {
+                if (uuids.contains(message.getUuid())) {
+                    message.setDeleted(true);
+                    deleted = true;
+                    if (message.getEncryption() == Message.ENCRYPTION_PGP && pgpDecryptionService != null) {
+                        pgpDecryptionService.discard(message);
+                    }
+                }
+            }
+        }
+        return deleted;
+    }
+
+    public boolean markAsChanged(final List<DatabaseBackend.FilePathInfo> files) {
+        boolean changed = false;
+        final PgpDecryptionService pgpDecryptionService = account.getPgpDecryptionService();
+        synchronized (this.messages) {
+            for (Message message : this.messages) {
+                for (final DatabaseBackend.FilePathInfo file : files)
+                    if (file.uuid.toString().equals(message.getUuid())) {
+                        message.setDeleted(file.deleted);
+                        changed = true;
+                        if (file.deleted && message.getEncryption() == Message.ENCRYPTION_PGP && pgpDecryptionService != null) {
+                            pgpDecryptionService.discard(message);
+                        }
+                    }
+            }
+        }
+        return changed;
+    }
+
+    public void clearMessages() {
+        synchronized (this.messages) {
+            this.messages.clear();
+        }
+    }
+
+    public boolean setIncomingChatState(ChatState state) {
+        if (this.mIncomingChatState == state) {
+            return false;
+        }
+        this.mIncomingChatState = state;
+        return true;
+    }
+
+    public ChatState getIncomingChatState() {
+        return this.mIncomingChatState;
+    }
+
+    public boolean setOutgoingChatState(ChatState state) {
+        if (mode == MODE_SINGLE && !getContact().isSelf() || (isPrivateAndNonAnonymous() && getNextCounterpart() == null)) {
+            if (this.mOutgoingChatState != state) {
+                this.mOutgoingChatState = state;
+                return true;
+            }
+        }
+        return false;
+    }
+
+    public ChatState getOutgoingChatState() {
+        return this.mOutgoingChatState;
+    }
+
+    public void trim() {
+        synchronized (this.messages) {
+            final int size = messages.size();
+            final int maxsize = Config.PAGE_SIZE * Config.MAX_NUM_PAGES;
+            if (size > maxsize) {
+                List<Message> discards = this.messages.subList(0, size - maxsize);
+                final PgpDecryptionService pgpDecryptionService = account.getPgpDecryptionService();
+                if (pgpDecryptionService != null) {
+                    pgpDecryptionService.discard(discards);
+                }
+                discards.clear();
+                untieMessages();
+            }
+        }
+    }
+
+    public void findUnsentTextMessages(OnMessageFound onMessageFound) {
+        final ArrayList<Message> results = new ArrayList<>();
+        synchronized (this.messages) {
+            for (Message message : this.messages) {
+                if ((message.getType() == Message.TYPE_TEXT || message.hasFileOnRemoteHost()) && message.getStatus() == Message.STATUS_UNSEND) {
+                    results.add(message);
+                }
+            }
+        }
+        for (Message result : results) {
+            onMessageFound.onMessageFound(result);
+        }
+    }
+
+    public Message findSentMessageWithUuidOrRemoteId(String id) {
+        synchronized (this.messages) {
+            for (Message message : this.messages) {
+                if (id.equals(message.getUuid())
+                        || (message.getStatus() >= Message.STATUS_SEND
+                        && id.equals(message.getRemoteMsgId()))) {
+                    return message;
+                }
+            }
+        }
+        return null;
+    }
+
+    public Message findMessageWithRemoteIdAndCounterpart(String id, Jid counterpart, boolean received, boolean carbon) {
+        synchronized (this.messages) {
+            for (int i = this.messages.size() - 1; i >= 0; --i) {
+                final Message message = messages.get(i);
+                final Jid mcp = message.getCounterpart();
+                if (mcp == null) {
+                    continue;
+                }
+                final boolean counterpartMatch = mode == MODE_SINGLE ?
+                        counterpart.asBareJid().equals(mcp.asBareJid()) :
+                        counterpart.equals(mcp);
+                if (counterpartMatch && ((message.getStatus() == Message.STATUS_RECEIVED) == received)
+                        && (carbon == message.isCarbon() || received)) {
+                    final boolean idMatch = id.equals(message.getRemoteMsgId()) || message.remoteMsgIdMatchInEdit(id);
+                    if (idMatch && !message.isFileOrImage() && !message.treatAsDownloadable()) {
+                        return message;
+                    } else {
+                        return null;
+                    }
+                }
+            }
+        }
+        return null;
+    }
+
+    public Message findSentMessageWithUuid(String id) {
+        synchronized (this.messages) {
+            for (Message message : this.messages) {
+                if (id.equals(message.getUuid())) {
+                    return message;
+                }
+            }
+        }
+        return null;
+    }
+
+    public Message findMessageWithRemoteId(String id, Jid counterpart) {
+        synchronized (this.messages) {
+            for (Message message : this.messages) {
+                if (counterpart.equals(message.getCounterpart())
+                        && (id.equals(message.getRemoteMsgId()) || id.equals(message.getUuid()))) {
+                    return message;
+                }
+            }
+        }
+        return null;
+    }
+
+    public Message findMessageWithServerMsgId(String id) {
+        synchronized (this.messages) {
+            for (Message message : this.messages) {
+                if (id != null && id.equals(message.getServerMsgId())) {
+                    return message;
+                }
+            }
+        }
+        return null;
+    }
+
+    public boolean hasMessageWithCounterpart(Jid counterpart) {
+        synchronized (this.messages) {
+            for (Message message : this.messages) {
+                if (counterpart.equals(message.getCounterpart())) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    public void populateWithMessages(final List<Message> messages) {
+        synchronized (this.messages) {
+            messages.clear();
+            messages.addAll(this.messages);
+        }
+        for (Iterator<Message> iterator = messages.iterator(); iterator.hasNext(); ) {
+            if (iterator.next().wasMergedIntoPrevious()) {
+                iterator.remove();
+            }
+        }
+    }
+
+    @Override
+    public boolean isBlocked() {
+        return getContact().isBlocked();
+    }
+
+    @Override
+    public boolean isDomainBlocked() {
+        return getContact().isDomainBlocked();
+    }
+
+    @Override
+    public Jid getBlockedJid() {
+        return getContact().getBlockedJid();
+    }
+
+    public int countMessages() {
+        synchronized (this.messages) {
+            return this.messages.size();
+        }
+    }
+
+    public String getFirstMamReference() {
+        return this.mFirstMamReference;
+    }
+
+    public void setFirstMamReference(String reference) {
+        this.mFirstMamReference = reference;
+    }
+
+    public void setLastClearHistory(long time, String reference) {
+        if (reference != null) {
+            setAttribute(ATTRIBUTE_LAST_CLEAR_HISTORY, String.valueOf(time) + ":" + reference);
+        } else {
+            setAttribute(ATTRIBUTE_LAST_CLEAR_HISTORY, time);
+        }
+    }
+
+    public MamReference getLastClearHistory() {
+        return MamReference.fromAttribute(getAttribute(ATTRIBUTE_LAST_CLEAR_HISTORY));
+    }
+
+    public List<Jid> getAcceptedCryptoTargets() {
+        if (mode == MODE_SINGLE) {
+            return Collections.singletonList(getJid().asBareJid());
+        } else {
+            return getJidListAttribute(ATTRIBUTE_CRYPTO_TARGETS);
+        }
+    }
+
+    public void setAcceptedCryptoTargets(List<Jid> acceptedTargets) {
+        setAttribute(ATTRIBUTE_CRYPTO_TARGETS, acceptedTargets);
+    }
+
+    public boolean setCorrectingMessage(Message correctingMessage) {
+        setAttribute(ATTRIBUTE_CORRECTING_MESSAGE, correctingMessage == null ? null : correctingMessage.getUuid());
+        return correctingMessage == null && draftMessage != null;
+    }
+
+    public Message getCorrectingMessage() {
+        final String uuid = getAttribute(ATTRIBUTE_CORRECTING_MESSAGE);
+        return uuid == null ? null : findSentMessageWithUuid(uuid);
+    }
+
+    public boolean withSelf() {
+        return getContact().isSelf();
+    }
+
+    @Override
+    public int compareTo(@NonNull Conversation another) {
+        return Long.compare(another.getSortableTime(), getSortableTime());
+    }
+
+    private long getSortableTime() {
+        Draft draft = getDraft();
+        long messageTime = getLatestMessage().getTimeSent();
+        if (draft == null) {
+            return messageTime;
+        } else {
+            return Math.max(messageTime, draft.getTimestamp());
+        }
+    }
+
+    public String getDraftMessage() {
+        return draftMessage;
+    }
+
+    public void setDraftMessage(String draftMessage) {
+        this.draftMessage = draftMessage;
+    }
+
+    public boolean isRead() {
+        return (this.messages.size() == 0) || this.messages.get(this.messages.size() - 1).isRead();
+    }
+
+    public List<Message> markRead(String upToUuid) {
+        final List<Message> unread = new ArrayList<>();
+        synchronized (this.messages) {
+            for (Message message : this.messages) {
+                if (!message.isRead()) {
+                    message.markRead();
+                    unread.add(message);
+                }
+                if (message.getUuid().equals(upToUuid)) {
+                    return unread;
+                }
+            }
+        }
+        return unread;
+    }
+
+    public Message getLatestMessage() {
+        synchronized (this.messages) {
+            if (this.messages.size() == 0) {
+                Message message = new Message(this, "", Message.ENCRYPTION_NONE);
+                message.setType(Message.TYPE_STATUS);
+                message.setTime(Math.max(getCreated(), getLastClearHistory().getTimestamp()));
+                return message;
+            } else {
+                return this.messages.get(this.messages.size() - 1);
+            }
+        }
+    }
+
+    public @NonNull
+    CharSequence getName() {
+        if (getMode() == MODE_MULTI) {
+            final String roomName = getMucOptions().getName();
+            final String subject = getMucOptions().getSubject();
+            final Bookmark bookmark = getBookmark();
+            final String bookmarkName = bookmark != null ? bookmark.getBookmarkName() : null;
+            if (printableValue(roomName)) {
+                return roomName;
+            } else if (printableValue(subject)) {
+                return subject;
+            } else if (printableValue(bookmarkName, false)) {
+                return bookmarkName;
+            } else {
+                final String generatedName = getMucOptions().createNameFromParticipants();
+                if (printableValue(generatedName)) {
+                    return generatedName;
+                } else {
+                    return contactJid.getLocal() != null ? contactJid.getLocal() : contactJid;
+                }
+            }
+        } else if ((QuickConversationsService.isConversations() || !Config.QUICKSY_DOMAIN.equals(contactJid.getDomain())) && isWithStranger()) {
+            return contactJid;
+        } else {
+            return this.getContact().getDisplayName();
+        }
+    }
+
+    public String getAccountUuid() {
+        return this.accountUuid;
+    }
+
+    public Account getAccount() {
+        return this.account;
+    }
+
+    public void setAccount(final Account account) {
+        this.account = account;
+    }
+
+    public Contact getContact() {
+        return this.account.getRoster().getContact(this.contactJid);
+    }
+
+    @Override
+    public Jid getJid() {
+        return this.contactJid;
+    }
+
+    public int getStatus() {
+        return this.status;
+    }
+
+    public void setStatus(int status) {
+        this.status = status;
+    }
+
+    public long getCreated() {
+        return this.created;
+    }
+
+    public ContentValues getContentValues() {
+        ContentValues values = new ContentValues();
+        values.put(UUID, uuid);
+        values.put(NAME, name);
+        values.put(CONTACT, contactUuid);
+        values.put(ACCOUNT, accountUuid);
+        values.put(CONTACTJID, contactJid.toString());
+        values.put(CREATED, created);
+        values.put(STATUS, status);
+        values.put(MODE, mode);
+        synchronized (this.attributes) {
+            values.put(ATTRIBUTES, attributes.toString());
+        }
+        return values;
+    }
+
+    public int getMode() {
+        return this.mode;
+    }
+
+    public void setMode(int mode) {
+        this.mode = mode;
+    }
+
+    /**
+     * short for is Private and Non-anonymous
+     */
+    public boolean isSingleOrPrivateAndNonAnonymous() {
+        return mode == MODE_SINGLE || isPrivateAndNonAnonymous();
+    }
+
+    public boolean isPrivateAndNonAnonymous() {
+        return getMucOptions().isPrivateAndNonAnonymous();
+    }
+
+    public synchronized MucOptions getMucOptions() {
+        if (this.mucOptions == null) {
+            this.mucOptions = new MucOptions(this);
+        }
+        return this.mucOptions;
+    }
+
+    public void resetMucOptions() {
+        this.mucOptions = null;
+    }
+
+    public void setContactJid(final Jid jid) {
+        this.contactJid = jid;
+    }
+
+    public Jid getNextCounterpart() {
+        return this.nextCounterpart;
+    }
+
+    public void setNextCounterpart(Jid jid) {
+        this.nextCounterpart = jid;
+    }
+
+    public int getNextEncryption() {
+        if (!Config.supportOmemo() && !Config.supportOpenPgp()) {
+            return Message.ENCRYPTION_NONE;
+        }
+        if (OmemoSetting.isAlways()) {
+            return suitableForOmemoByDefault(this) ? Message.ENCRYPTION_AXOLOTL : Message.ENCRYPTION_NONE;
+        }
+        final int defaultEncryption;
+        if (suitableForOmemoByDefault(this)) {
+            defaultEncryption = OmemoSetting.getEncryption();
+        } else {
+            defaultEncryption = Message.ENCRYPTION_NONE;
+        }
+        int encryption = this.getIntAttribute(ATTRIBUTE_NEXT_ENCRYPTION, defaultEncryption);
+        if (encryption == Message.ENCRYPTION_OTR || encryption < 0) {
+            return defaultEncryption;
+        } else {
+            return encryption;
+        }
+    }
+
+    public boolean setNextEncryption(int encryption) {
+        return this.setAttribute(ATTRIBUTE_NEXT_ENCRYPTION, encryption);
+    }
+
+    public String getNextMessage() {
+        final String nextMessage = getAttribute(ATTRIBUTE_NEXT_MESSAGE);
+        return nextMessage == null ? "" : nextMessage;
+    }
+
+    public @Nullable
+    Draft getDraft() {
+        long timestamp = getLongAttribute(ATTRIBUTE_NEXT_MESSAGE_TIMESTAMP, 0);
+        if (timestamp > getLatestMessage().getTimeSent()) {
+            String message = getAttribute(ATTRIBUTE_NEXT_MESSAGE);
+            if (!TextUtils.isEmpty(message) && timestamp != 0) {
+                return new Draft(message, timestamp);
+            }
+        }
+        return null;
+    }
+
+    public boolean setNextMessage(final String input) {
+        final String message = input == null || input.trim().isEmpty() ? null : input;
+        boolean changed = !getNextMessage().equals(message);
+        this.setAttribute(ATTRIBUTE_NEXT_MESSAGE, message);
+        if (changed) {
+            this.setAttribute(ATTRIBUTE_NEXT_MESSAGE_TIMESTAMP, message == null ? 0 : System.currentTimeMillis());
+        }
+        return changed;
+    }
+
+    public Bookmark getBookmark() {
+        return this.account.getBookmark(this.contactJid);
+    }
+
+    public Message findDuplicateMessage(Message message) {
+        synchronized (this.messages) {
+            for (int i = this.messages.size() - 1; i >= 0; --i) {
+                if (this.messages.get(i).similar(message)) {
+                    return this.messages.get(i);
+                }
+            }
+        }
+        return null;
+    }
+
+    public boolean hasDuplicateMessage(Message message) {
+        return findDuplicateMessage(message) != null;
+    }
+
+    public Message findSentMessageWithBody(String body) {
+        synchronized (this.messages) {
+            for (int i = this.messages.size() - 1; i >= 0; --i) {
+                Message message = this.messages.get(i);
+                if (message.getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_SEND) {
+                    String otherBody;
+                    if (message.hasFileOnRemoteHost()) {
+                        otherBody = message.getFileParams().url.toString();
+                    } else {
+                        otherBody = message.body;
+                    }
+                    if (otherBody != null && otherBody.equals(body)) {
+                        return message;
+                    }
+                }
+            }
+            return null;
+        }
+    }
+
+    public Message findRtpSession(final String sessionId, final int s) {
+        synchronized (this.messages) {
+            for (int i = this.messages.size() - 1; i >= 0; --i) {
+                final Message message = this.messages.get(i);
+                if ((message.getStatus() == s) && (message.getType() == Message.TYPE_RTP_SESSION) && sessionId.equals(message.getRemoteMsgId())) {
+                    return message;
+                }
+            }
+        }
+        return null;
+    }
+
+    public boolean possibleDuplicate(final String serverMsgId, final String remoteMsgId) {
+        if (serverMsgId == null || remoteMsgId == null) {
+            return false;
+        }
+        synchronized (this.messages) {
+            for (Message message : this.messages) {
+                if (serverMsgId.equals(message.getServerMsgId()) || remoteMsgId.equals(message.getRemoteMsgId())) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    public MamReference getLastMessageTransmitted() {
+        final MamReference lastClear = getLastClearHistory();
+        MamReference lastReceived = new MamReference(0);
+        synchronized (this.messages) {
+            for (int i = this.messages.size() - 1; i >= 0; --i) {
+                final Message message = this.messages.get(i);
+                if (message.isPrivateMessage()) {
+                    continue; //it's unsafe to use private messages as anchor. They could be coming from user archive
+                }
+                if (message.getStatus() == Message.STATUS_RECEIVED || message.isCarbon() || message.getServerMsgId() != null) {
+                    lastReceived = new MamReference(message.getTimeSent(), message.getServerMsgId());
+                    break;
+                }
+            }
+        }
+        return MamReference.max(lastClear, lastReceived);
+    }
+
+    public void setMutedTill(long value) {
+        this.setAttribute(ATTRIBUTE_MUTED_TILL, String.valueOf(value));
+    }
+
+    public boolean isMuted() {
+        return System.currentTimeMillis() < this.getLongAttribute(ATTRIBUTE_MUTED_TILL, 0);
+    }
+
+    public boolean alwaysNotify() {
+        return mode == MODE_SINGLE || getBooleanAttribute(ATTRIBUTE_ALWAYS_NOTIFY, Config.ALWAYS_NOTIFY_BY_DEFAULT || isPrivateAndNonAnonymous());
+    }
+
+    public boolean setAttribute(String key, boolean value) {
+        return setAttribute(key, String.valueOf(value));
+    }
+
+    private boolean setAttribute(String key, long value) {
+        return setAttribute(key, Long.toString(value));
+    }
+
+    private boolean setAttribute(String key, int value) {
+        return setAttribute(key, String.valueOf(value));
+    }
+
+    public boolean setAttribute(String key, String value) {
+        synchronized (this.attributes) {
+            try {
+                if (value == null) {
+                    if (this.attributes.has(key)) {
+                        this.attributes.remove(key);
+                        return true;
+                    } else {
+                        return false;
+                    }
+                } else {
+                    final String prev = this.attributes.optString(key, null);
+                    this.attributes.put(key, value);
+                    return !value.equals(prev);
+                }
+            } catch (JSONException e) {
+                throw new AssertionError(e);
+            }
+        }
+    }
+
+    public boolean setAttribute(String key, List<Jid> jids) {
+        JSONArray array = new JSONArray();
+        for (Jid jid : jids) {
+            array.put(jid.asBareJid().toString());
+        }
+        synchronized (this.attributes) {
+            try {
+                this.attributes.put(key, array);
+                return true;
+            } catch (JSONException e) {
+                return false;
+            }
+        }
+    }
+
+    public String getAttribute(String key) {
+        synchronized (this.attributes) {
+            return this.attributes.optString(key, null);
+        }
+    }
+
+    private List<Jid> getJidListAttribute(String key) {
+        ArrayList<Jid> list = new ArrayList<>();
+        synchronized (this.attributes) {
+            try {
+                JSONArray array = this.attributes.getJSONArray(key);
+                for (int i = 0; i < array.length(); ++i) {
+                    try {
+                        list.add(Jid.of(array.getString(i)));
+                    } catch (IllegalArgumentException e) {
+                        //ignored
+                    }
+                }
+            } catch (JSONException e) {
+                //ignored
+            }
+        }
+        return list;
+    }
+
+    private int getIntAttribute(String key, int defaultValue) {
+        String value = this.getAttribute(key);
+        if (value == null) {
+            return defaultValue;
+        } else {
+            try {
+                return Integer.parseInt(value);
+            } catch (NumberFormatException e) {
+                return defaultValue;
+            }
+        }
+    }
+
+    public long getLongAttribute(String key, long defaultValue) {
+        String value = this.getAttribute(key);
+        if (value == null) {
+            return defaultValue;
+        } else {
+            try {
+                return Long.parseLong(value);
+            } catch (NumberFormatException e) {
+                return defaultValue;
+            }
+        }
+    }
+
+    public boolean getBooleanAttribute(String key, boolean defaultValue) {
+        String value = this.getAttribute(key);
+        if (value == null) {
+            return defaultValue;
+        } else {
+            return Boolean.parseBoolean(value);
+        }
+    }
+
+    public void add(Message message) {
+        synchronized (this.messages) {
+            this.messages.add(message);
+        }
+    }
+
+    public void prepend(int offset, Message message) {
+        synchronized (this.messages) {
+            this.messages.add(Math.min(offset, this.messages.size()), message);
+        }
+    }
+
+    public void addAll(int index, List<Message> messages) {
+        synchronized (this.messages) {
+            this.messages.addAll(index, messages);
+        }
+        account.getPgpDecryptionService().decrypt(messages);
+    }
+
+    public void expireOldMessages(long timestamp) {
+        synchronized (this.messages) {
+            for (ListIterator<Message> iterator = this.messages.listIterator(); iterator.hasNext(); ) {
+                if (iterator.next().getTimeSent() < timestamp) {
+                    iterator.remove();
+                }
+            }
+            untieMessages();
+        }
+    }
+
+    public void sort() {
+        synchronized (this.messages) {
+            Collections.sort(this.messages, (left, right) -> {
+                if (left.getTimeSent() < right.getTimeSent()) {
+                    return -1;
+                } else if (left.getTimeSent() > right.getTimeSent()) {
+                    return 1;
+                } else {
+                    return 0;
+                }
+            });
+            untieMessages();
+        }
+    }
+
+    private void untieMessages() {
+        for (Message message : this.messages) {
+            message.untie();
+        }
+    }
+
+    public int unreadCount() {
+        synchronized (this.messages) {
+            int count = 0;
+            for (int i = this.messages.size() - 1; i >= 0; --i) {
+                if (this.messages.get(i).isRead()) {
+                    return count;
+                }
+                ++count;
+            }
+            return count;
+        }
+    }
+
+    public int receivedMessagesCount() {
+        int count = 0;
+        synchronized (this.messages) {
+            for (Message message : messages) {
+                if (message.getStatus() == Message.STATUS_RECEIVED) {
+                    ++count;
+                }
+            }
+        }
+        return count;
+    }
+
+    public int sentMessagesCount() {
+        int count = 0;
+        synchronized (this.messages) {
+            for (Message message : messages) {
+                if (message.getStatus() != Message.STATUS_RECEIVED) {
+                    ++count;
+                }
+            }
+        }
+        return count;
+    }
+
+    public boolean isWithStranger() {
+        final Contact contact = getContact();
+        return mode == MODE_SINGLE
+                && !contact.isOwnServer()
+                && !contact.showInContactList()
+                && !contact.isSelf()
+                && !Config.QUICKSY_DOMAIN.equals(contact.getJid().toEscapedString())
+                && sentMessagesCount() == 0;
+    }
+
+    public int getReceivedMessagesCountSinceUuid(String uuid) {
+        if (uuid == null) {
+            return 0;
+        }
+        int count = 0;
+        synchronized (this.messages) {
+            for (int i = messages.size() - 1; i >= 0; i--) {
+                final Message message = messages.get(i);
+                if (uuid.equals(message.getUuid())) {
+                    return count;
+                }
+                if (message.getStatus() <= Message.STATUS_RECEIVED) {
+                    ++count;
+                }
+            }
+        }
+        return 0;
+    }
+
+    @Override
+    public int getAvatarBackgroundColor() {
+        return UIHelper.getColorForName(getName().toString());
+    }
 
     public interface OnMessageFound {
-		void onMessageFound(final Message message);
-	}
-
-	public static class Draft {
-		private final String message;
-		private final long timestamp;
-
-		private Draft(String message, long timestamp) {
-			this.message = message;
-			this.timestamp = timestamp;
-		}
-
-		public long getTimestamp() {
-			return timestamp;
-		}
-
-		public String getMessage() {
-			return message;
-		}
-	}
+        void onMessageFound(final Message message);
+    }
+
+    public static class Draft {
+        private final String message;
+        private final long timestamp;
+
+        private Draft(String message, long timestamp) {
+            this.message = message;
+            this.timestamp = timestamp;
+        }
+
+        public long getTimestamp() {
+            return timestamp;
+        }
+
+        public String getMessage() {
+            return message;
+        }
+    }
 }

src/main/java/eu/siacs/conversations/generator/MessageGenerator.java 🔗

@@ -12,6 +12,7 @@ import eu.siacs.conversations.crypto.axolotl.AxolotlService;
 import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage;
 import eu.siacs.conversations.entities.Account;
 import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.Conversational;
 import eu.siacs.conversations.entities.Message;
 import eu.siacs.conversations.http.P1S3UrlStreamHandler;
 import eu.siacs.conversations.services.XmppConnectionService;
@@ -160,15 +161,23 @@ public class MessageGenerator extends AbstractGenerator {
         return packet;
     }
 
-    public MessagePacket confirm(final Account account, final Jid to, final String id, final Jid counterpart, final boolean groupChat) {
-        MessagePacket packet = new MessagePacket();
+    public MessagePacket confirm(final Message message) {
+        final boolean groupChat = message.getConversation().getMode() == Conversational.MODE_MULTI;
+        final Jid to = message.getCounterpart();
+        final MessagePacket packet = new MessagePacket();
         packet.setType(groupChat ? MessagePacket.TYPE_GROUPCHAT : MessagePacket.TYPE_CHAT);
         packet.setTo(groupChat ? to.asBareJid() : to);
-        packet.setFrom(account.getJid());
-        Element displayed = packet.addChild("displayed", "urn:xmpp:chat-markers:0");
-        displayed.setAttribute("id", id);
-        if (groupChat && counterpart != null) {
-            displayed.setAttribute("sender", counterpart.toString());
+        final Element displayed = packet.addChild("displayed", "urn:xmpp:chat-markers:0");
+        if (groupChat) {
+            final String stanzaId = message.getServerMsgId();
+            if (stanzaId != null) {
+                displayed.setAttribute("id", stanzaId);
+            } else {
+                displayed.setAttribute("sender", to.toString());
+                displayed.setAttribute("id", message.getRemoteMsgId());
+            }
+        } else {
+            displayed.setAttribute("id", message.getRemoteMsgId());
         }
         packet.addChild("store", "urn:xmpp:hints");
         return packet;

src/main/java/eu/siacs/conversations/parser/MessageParser.java 🔗

@@ -927,22 +927,29 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
                     activateGracePeriod(account);
                 }
             } else if (isTypeGroupChat) {
-                Conversation conversation = mXmppConnectionService.find(account, counterpart.asBareJid());
-                if (conversation != null && id != null && sender != null) {
-                    Message message = conversation.findMessageWithRemoteId(id, sender);
-                    if (message != null) {
-                        final Jid fallback = conversation.getMucOptions().getTrueCounterpart(counterpart);
-                        final Jid trueJid = getTrueCounterpart((query != null && query.safeToExtractTrueCounterpart()) ? mucUserElement : null, fallback);
-                        final boolean trueJidMatchesAccount = account.getJid().asBareJid().equals(trueJid == null ? null : trueJid.asBareJid());
-                        if (trueJidMatchesAccount || conversation.getMucOptions().isSelf(counterpart)) {
-                            if (!message.isRead() && (query == null || query.isCatchup())) { //checking if message is unread fixes race conditions with reflections
-                                mXmppConnectionService.markRead(conversation);
-                            }
-                        } else if (!counterpart.isBareJid() && trueJid != null) {
-                            final ReadByMarker readByMarker = ReadByMarker.from(counterpart, trueJid);
-                            if (message.addReadByMarker(readByMarker)) {
-                                mXmppConnectionService.updateMessage(message, false);
-                            }
+                final Conversation conversation = mXmppConnectionService.find(account, counterpart.asBareJid());
+                final Message message;
+                if (conversation != null && id != null) {
+                    if (sender != null) {
+                        message = conversation.findMessageWithRemoteId(id, sender);
+                    } else {
+                        message = conversation.findMessageWithServerMsgId(id);
+                    }
+                } else {
+                    message = null;
+                }
+                if (message != null) {
+                    final Jid fallback = conversation.getMucOptions().getTrueCounterpart(counterpart);
+                    final Jid trueJid = getTrueCounterpart((query != null && query.safeToExtractTrueCounterpart()) ? mucUserElement : null, fallback);
+                    final boolean trueJidMatchesAccount = account.getJid().asBareJid().equals(trueJid == null ? null : trueJid.asBareJid());
+                    if (trueJidMatchesAccount || conversation.getMucOptions().isSelf(counterpart)) {
+                        if (!message.isRead() && (query == null || query.isCatchup())) { //checking if message is unread fixes race conditions with reflections
+                            mXmppConnectionService.markRead(conversation);
+                        }
+                    } else if (!counterpart.isBareJid() && trueJid != null) {
+                        final ReadByMarker readByMarker = ReadByMarker.from(counterpart, trueJid);
+                        if (message.addReadByMarker(readByMarker)) {
+                            mXmppConnectionService.updateMessage(message, false);
                         }
                     }
                 }

src/main/java/eu/siacs/conversations/services/XmppConnectionService.java 🔗

@@ -4062,6 +4062,7 @@ public class XmppConnectionService extends Service {
                 }
             };
             mDatabaseWriterExecutor.execute(runnable);
+            updateConversationUi();
             updateUnreadCountBadge();
             return readMessages;
         } else {
@@ -4094,11 +4095,9 @@ public class XmppConnectionService extends Service {
                 && (markable.trusted() || isPrivateAndNonAnonymousMuc)
                 && markable.getRemoteMsgId() != null) {
             Log.d(Config.LOGTAG, conversation.getAccount().getJid().asBareJid() + ": sending read marker to " + markable.getCounterpart().toString());
-            Account account = conversation.getAccount();
-            final Jid to = markable.getCounterpart();
-            final boolean groupChat = conversation.getMode() == Conversation.MODE_MULTI;
-            MessagePacket packet = mMessageGenerator.confirm(account, to, markable.getRemoteMsgId(), markable.getCounterpart(), groupChat);
-            this.sendMessagePacket(conversation.getAccount(), packet);
+            final Account account = conversation.getAccount();
+            final MessagePacket packet = mMessageGenerator.confirm(markable);
+            this.sendMessagePacket(account, packet);
         }
     }