diff --git a/src/main/java/eu/siacs/conversations/entities/Conversation.java b/src/main/java/eu/siacs/conversations/entities/Conversation.java index 53d2d74b3..94efb8fcf 100644 --- a/src/main/java/eu/siacs/conversations/entities/Conversation.java +++ b/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, 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 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 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 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 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 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 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 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 messages) { - synchronized (this.messages) { - messages.clear(); - messages.addAll(this.messages); - } - for (Iterator 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 getAcceptedCryptoTargets() { - if (mode == MODE_SINGLE) { - return Collections.singletonList(getJid().asBareJid()); - } else { - return getJidListAttribute(ATTRIBUTE_CRYPTO_TARGETS); - } - } - - public void setAcceptedCryptoTargets(List 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 markRead(String upToUuid) { - final List 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 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 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 getJidListAttribute(String key) { - ArrayList 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 messages) { - synchronized (this.messages) { - this.messages.addAll(index, messages); - } - account.getPgpDecryptionService().decrypt(messages); - } - - public void expireOldMessages(long timestamp) { - synchronized (this.messages) { - for (ListIterator 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 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 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 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 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 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 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 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 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 messages) { + synchronized (this.messages) { + messages.clear(); + messages.addAll(this.messages); + } + for (Iterator 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 getAcceptedCryptoTargets() { + if (mode == MODE_SINGLE) { + return Collections.singletonList(getJid().asBareJid()); + } else { + return getJidListAttribute(ATTRIBUTE_CRYPTO_TARGETS); + } + } + + public void setAcceptedCryptoTargets(List 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 markRead(String upToUuid) { + final List 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 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 getJidListAttribute(String key) { + ArrayList 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 messages) { + synchronized (this.messages) { + this.messages.addAll(index, messages); + } + account.getPgpDecryptionService().decrypt(messages); + } + + public void expireOldMessages(long timestamp) { + synchronized (this.messages) { + for (ListIterator 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); - } + void onMessageFound(final Message message); + } - public static class Draft { - private final String message; - private final long timestamp; + public static class Draft { + private final String message; + private final long timestamp; - private Draft(String message, long timestamp) { - this.message = message; - this.timestamp = timestamp; - } + private Draft(String message, long timestamp) { + this.message = message; + this.timestamp = timestamp; + } - public long getTimestamp() { - return timestamp; - } + public long getTimestamp() { + return timestamp; + } - public String getMessage() { - return message; - } - } + public String getMessage() { + return message; + } + } } diff --git a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java index a0cb0ca1f..6969e4a5e 100644 --- a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java +++ b/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; diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java index 9cb6e7bbd..8d11499d2 100644 --- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java +++ b/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); } } } diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index b9204d9d5..4b1c5c2ff 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/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); } }