From da385845c2d4284e34ba9a747d6c8da49a77c081 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 28 Nov 2024 10:37:17 +0100 Subject: [PATCH] do not merge message bubbles --- .../conversations/entities/Conversation.java | 249 +++++++----- .../siacs/conversations/entities/Message.java | 290 ++++++-------- .../services/XmppConnectionService.java | 22 +- .../ui/ConversationFragment.java | 128 +++--- .../ui/adapter/MessageAdapter.java | 61 ++- .../conversations/ui/util/ShareUtil.java | 129 +++--- .../conversations/utils/MessageUtils.java | 26 +- .../conversations/utils/StylingHelper.java | 378 +++++++++--------- 8 files changed, 647 insertions(+), 636 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/entities/Conversation.java b/src/main/java/eu/siacs/conversations/entities/Conversation.java index bf8bdd7fb1d729cb73070b02f85f5a788cebdb58..0fec5bcb3d64ffee9cb0250a13635e0c84b6bf27 100644 --- a/src/main/java/eu/siacs/conversations/entities/Conversation.java +++ b/src/main/java/eu/siacs/conversations/entities/Conversation.java @@ -1,27 +1,16 @@ package eu.siacs.conversations.entities; +import static eu.siacs.conversations.entities.Bookmark.printableValue; + import android.content.ContentValues; import android.database.Cursor; import android.text.TextUtils; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; - +import com.google.common.base.Strings; import com.google.common.collect.ComparisonChain; import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.Iterator; -import java.util.List; -import java.util.ListIterator; -import java.util.concurrent.atomic.AtomicBoolean; - import eu.siacs.conversations.Config; import eu.siacs.conversations.crypto.OmemoSetting; import eu.siacs.conversations.crypto.PgpDecryptionService; @@ -34,11 +23,17 @@ import eu.siacs.conversations.utils.UIHelper; import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.chatstate.ChatState; import eu.siacs.conversations.xmpp.mam.MamReference; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.ListIterator; +import java.util.concurrent.atomic.AtomicBoolean; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; -import static eu.siacs.conversations.entities.Bookmark.printableValue; - - -public class Conversation extends AbstractEntity implements Blockable, Comparable, Conversational, AvatarService.Avatarable { +public class Conversation extends AbstractEntity + implements Blockable, Comparable, Conversational, AvatarService.Avatarable { public static final String TABLENAME = "conversations"; public static final int STATUS_AVAILABLE = 0; @@ -56,7 +51,8 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl public static final String ATTRIBUTE_MUTED_TILL = "muted_till"; public static final String ATTRIBUTE_ALWAYS_NOTIFY = "always_notify"; public static final String ATTRIBUTE_LAST_CLEAR_HISTORY = "last_clear_history"; - public static final String ATTRIBUTE_FORMERLY_PRIVATE_NON_ANONYMOUS = "formerly_private_non_anonymous"; + public static final String ATTRIBUTE_FORMERLY_PRIVATE_NON_ANONYMOUS = + "formerly_private_non_anonymous"; public static final String ATTRIBUTE_PINNED_ON_TOP = "pinned_on_top"; static final String ATTRIBUTE_MUC_PASSWORD = "muc_password"; static final String ATTRIBUTE_MEMBERS_ONLY = "members_only"; @@ -78,7 +74,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl private int status; private final long created; private int mode; - private JSONObject attributes; + private final JSONObject attributes; private Jid nextCounterpart; private transient MucOptions mucOptions = null; private boolean messagesLeftOnServer = true; @@ -87,17 +83,31 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl private String mFirstMamReference = null; private String displayState = 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, ""); + 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) { + 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; @@ -106,26 +116,37 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl this.created = created; this.status = status; this.mode = mode; - try { - this.attributes = new JSONObject(attributes == null ? "" : attributes); - } catch (JSONException e) { - this.attributes = new JSONObject(); + this.attributes = parseAttributes(attributes); + } + + private static JSONObject parseAttributes(final String attributes) { + if (Strings.isNullOrEmpty(attributes)) { + return new JSONObject(); + } else { + try { + return new JSONObject(attributes); + } catch (final JSONException e) { + return 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 Conversation fromCursor(final Cursor cursor) { + return new Conversation( + cursor.getString(cursor.getColumnIndexOrThrow(UUID)), + cursor.getString(cursor.getColumnIndexOrThrow(NAME)), + cursor.getString(cursor.getColumnIndexOrThrow(CONTACT)), + cursor.getString(cursor.getColumnIndexOrThrow(ACCOUNT)), + JidHelper.parseOrFallbackToInvalid( + cursor.getString(cursor.getColumnIndexOrThrow(CONTACTJID))), + cursor.getLong(cursor.getColumnIndexOrThrow(CREATED)), + cursor.getInt(cursor.getColumnIndexOrThrow(STATUS)), + cursor.getInt(cursor.getColumnIndexOrThrow(MODE)), + cursor.getString(cursor.getColumnIndexOrThrow(ATTRIBUTES))); } - public static Message getLatestMarkableMessage(final List messages, boolean isPrivateAndNonAnonymousMuc) { + 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 @@ -146,10 +167,13 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl } final String contact = conversation.getJid().getDomain().toEscapedString(); final String account = conversation.getAccount().getServer(); - if (Config.OMEMO_EXCEPTIONS.matchesContactDomain(contact) || Config.OMEMO_EXCEPTIONS.ACCOUNT_DOMAINS.contains(account)) { + if (Config.OMEMO_EXCEPTIONS.matchesContactDomain(contact) + || Config.OMEMO_EXCEPTIONS.ACCOUNT_DOMAINS.contains(account)) { return false; } - return conversation.isSingleOrPrivateAndNonAnonymous() || conversation.getBooleanAttribute(ATTRIBUTE_FORMERLY_PRIVATE_NON_ANONYMOUS, false); + return conversation.isSingleOrPrivateAndNonAnonymous() + || conversation.getBooleanAttribute( + ATTRIBUTE_FORMERLY_PRIVATE_NON_ANONYMOUS, false); } public boolean hasMessagesLeftOnServer() { @@ -193,7 +217,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl public int countFailedDeliveries() { int count = 0; synchronized (this.messages) { - for(final Message message : this.messages) { + for (final Message message : this.messages) { if (message.getStatus() == Message.STATUS_SEND_FAILED) { ++count; } @@ -216,12 +240,12 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl return null; } - 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)) { + if ((s == Message.STATUS_UNSEND || s == Message.STATUS_WAITING) + && message.getUuid().equals(uuid)) { return message; } } @@ -262,10 +286,16 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl synchronized (this.messages) { for (final Message message : this.messages) { final Transferable transferable = message.getTransferable(); - final boolean unInitiatedButKnownSize = MessageUtils.unInitiatedButKnownSize(message); + final boolean unInitiatedButKnownSize = + MessageUtils.unInitiatedButKnownSize(message); if (message.getUuid().equals(uuid) && message.getEncryption() != Message.ENCRYPTION_PGP - && (message.isFileOrImage() || message.treatAsDownloadable() || unInitiatedButKnownSize || (transferable != null && transferable.getStatus() != Transferable.STATUS_UPLOADING))) { + && (message.isFileOrImage() + || message.treatAsDownloadable() + || unInitiatedButKnownSize + || (transferable != null + && transferable.getStatus() + != Transferable.STATUS_UPLOADING))) { return message; } } @@ -292,7 +322,8 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl if (uuids.contains(message.getUuid())) { message.setDeleted(true); deleted = true; - if (message.getEncryption() == Message.ENCRYPTION_PGP && pgpDecryptionService != null) { + if (message.getEncryption() == Message.ENCRYPTION_PGP + && pgpDecryptionService != null) { pgpDecryptionService.discard(message); } } @@ -310,7 +341,9 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl if (file.uuid.toString().equals(message.getUuid())) { message.setDeleted(file.deleted); changed = true; - if (file.deleted && message.getEncryption() == Message.ENCRYPTION_PGP && pgpDecryptionService != null) { + if (file.deleted + && message.getEncryption() == Message.ENCRYPTION_PGP + && pgpDecryptionService != null) { pgpDecryptionService.discard(message); } } @@ -338,7 +371,8 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl } public boolean setOutgoingChatState(ChatState state) { - if (mode == MODE_SINGLE && !getContact().isSelf() || (isPrivateAndNonAnonymous() && getNextCounterpart() == null)) { + if (mode == MODE_SINGLE && !getContact().isSelf() + || (isPrivateAndNonAnonymous() && getNextCounterpart() == null)) { if (this.mOutgoingChatState != state) { this.mOutgoingChatState = state; return true; @@ -371,7 +405,8 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl 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) { + if ((message.getType() == Message.TYPE_TEXT || message.hasFileOnRemoteHost()) + && message.getStatus() == Message.STATUS_UNSEND) { results.add(message); } } @@ -386,7 +421,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl for (Message message : this.messages) { if (id.equals(message.getUuid()) || (message.getStatus() >= Message.STATUS_SEND - && id.equals(message.getRemoteMsgId()))) { + && id.equals(message.getRemoteMsgId()))) { return message; } } @@ -405,7 +440,8 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl return null; } - public Message findMessageWithRemoteIdAndCounterpart(String id, Jid counterpart, boolean received, boolean carbon) { + 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); @@ -413,9 +449,12 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl if (mcp == null) { continue; } - if (mcp.equals(counterpart) && ((message.getStatus() == Message.STATUS_RECEIVED) == received) + if (mcp.equals(counterpart) + && ((message.getStatus() == Message.STATUS_RECEIVED) == received) && (carbon == message.isCarbon() || received)) { - if (id.equals(message.getRemoteMsgId()) && !message.isFileOrImage() && !message.treatAsDownloadable()) { + if (id.equals(message.getRemoteMsgId()) + && !message.isFileOrImage() + && !message.treatAsDownloadable()) { return message; } else { return null; @@ -452,7 +491,8 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl public Message findReceivedWithRemoteId(final String id) { synchronized (this.messages) { for (final Message message : this.messages) { - if (message.getStatus() == Message.STATUS_RECEIVED && id.equals(message.getRemoteMsgId())) { + if (message.getStatus() == Message.STATUS_RECEIVED + && id.equals(message.getRemoteMsgId())) { return message; } } @@ -487,11 +527,6 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl messages.clear(); messages.addAll(this.messages); } - for (Iterator iterator = messages.iterator(); iterator.hasNext(); ) { - if (iterator.next().wasMergedIntoPrevious()) { - iterator.remove(); - } - } } @Override @@ -548,7 +583,9 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl } public boolean setCorrectingMessage(Message correctingMessage) { - setAttribute(ATTRIBUTE_CORRECTING_MESSAGE, correctingMessage == null ? null : correctingMessage.getUuid()); + setAttribute( + ATTRIBUTE_CORRECTING_MESSAGE, + correctingMessage == null ? null : correctingMessage.getUuid()); return correctingMessage == null && draftMessage != null; } @@ -564,7 +601,9 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl @Override public int compareTo(@NonNull Conversation another) { return ComparisonChain.start() - .compareFalseFirst(another.getBooleanAttribute(ATTRIBUTE_PINNED_ON_TOP, false), getBooleanAttribute(ATTRIBUTE_PINNED_ON_TOP, false)) + .compareFalseFirst( + another.getBooleanAttribute(ATTRIBUTE_PINNED_ON_TOP, false), + getBooleanAttribute(ATTRIBUTE_PINNED_ON_TOP, false)) .compare(another.getSortableTime(), getSortableTime()) .result(); } @@ -589,7 +628,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl public boolean isRead() { synchronized (this.messages) { - for(final Message message : Lists.reverse(this.messages)) { + for (final Message message : Lists.reverse(this.messages)) { if (message.isRead() && message.getType() == Message.TYPE_RTP_SESSION) { continue; } @@ -628,8 +667,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl } } - public @NonNull - CharSequence getName() { + public @NonNull CharSequence getName() { if (getMode() == MODE_MULTI) { final String roomName = getMucOptions().getName(); final String subject = getMucOptions().getSubject(); @@ -649,7 +687,9 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl return contactJid.getLocal() != null ? contactJid.getLocal() : contactJid; } } - } else if ((QuickConversationsService.isConversations() || !Config.QUICKSY_DOMAIN.equals(contactJid.getDomain())) && isWithStranger()) { + } else if ((QuickConversationsService.isConversations() + || !Config.QUICKSY_DOMAIN.equals(contactJid.getDomain())) + && isWithStranger()) { return contactJid; } else { return this.getContact().getDisplayName(); @@ -713,9 +753,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl this.mode = mode; } - /** - * short for is Private and Non-anonymous - */ + /** short for is Private and Non-anonymous */ public boolean isSingleOrPrivateAndNonAnonymous() { return mode == MODE_SINGLE || isPrivateAndNonAnonymous(); } @@ -752,7 +790,9 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl return Message.ENCRYPTION_NONE; } if (OmemoSetting.isAlways()) { - return suitableForOmemoByDefault(this) ? Message.ENCRYPTION_AXOLOTL : Message.ENCRYPTION_NONE; + return suitableForOmemoByDefault(this) + ? Message.ENCRYPTION_AXOLOTL + : Message.ENCRYPTION_NONE; } final int defaultEncryption; if (suitableForOmemoByDefault(this)) { @@ -777,8 +817,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl return nextMessage == null ? "" : nextMessage; } - public @Nullable - Draft getDraft() { + public @Nullable Draft getDraft() { long timestamp = getLongAttribute(ATTRIBUTE_NEXT_MESSAGE_TIMESTAMP, 0); if (timestamp > getLatestMessage().getTimeSent()) { String message = getAttribute(ATTRIBUTE_NEXT_MESSAGE); @@ -794,7 +833,9 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl boolean changed = !getNextMessage().equals(message); this.setAttribute(ATTRIBUTE_NEXT_MESSAGE, message); if (changed) { - this.setAttribute(ATTRIBUTE_NEXT_MESSAGE_TIMESTAMP, message == null ? 0 : System.currentTimeMillis()); + this.setAttribute( + ATTRIBUTE_NEXT_MESSAGE_TIMESTAMP, + message == null ? 0 : System.currentTimeMillis()); } return changed; } @@ -822,7 +863,8 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl 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) { + if (message.getStatus() == Message.STATUS_UNSEND + || message.getStatus() == Message.STATUS_SEND) { String otherBody; if (message.hasFileOnRemoteHost()) { otherBody = message.getFileParams().url; @@ -842,7 +884,9 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl 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())) { + if ((message.getStatus() == s) + && (message.getType() == Message.TYPE_RTP_SESSION) + && sessionId.equals(message.getRemoteMsgId())) { return message; } } @@ -856,7 +900,8 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl } synchronized (this.messages) { for (Message message : this.messages) { - if (serverMsgId.equals(message.getServerMsgId()) || remoteMsgId.equals(message.getRemoteMsgId())) { + if (serverMsgId.equals(message.getServerMsgId()) + || remoteMsgId.equals(message.getRemoteMsgId())) { return true; } } @@ -871,10 +916,14 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl 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 + 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()); + if (message.getStatus() == Message.STATUS_RECEIVED + || message.isCarbon() + || message.getServerMsgId() != null) { + lastReceived = + new MamReference(message.getTimeSent(), message.getServerMsgId()); break; } } @@ -891,7 +940,10 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl } public boolean alwaysNotify() { - return mode == MODE_SINGLE || getBooleanAttribute(ATTRIBUTE_ALWAYS_NOTIFY, Config.ALWAYS_NOTIFY_BY_DEFAULT || isPrivateAndNonAnonymous()); + return mode == MODE_SINGLE + || getBooleanAttribute( + ATTRIBUTE_ALWAYS_NOTIFY, + Config.ALWAYS_NOTIFY_BY_DEFAULT || isPrivateAndNonAnonymous()); } public boolean setAttribute(String key, boolean value) { @@ -957,11 +1009,11 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl try { list.add(Jid.of(array.getString(i))); } catch (IllegalArgumentException e) { - //ignored + // ignored } } } catch (JSONException e) { - //ignored + // ignored } } return list; @@ -1023,7 +1075,8 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl public void expireOldMessages(long timestamp) { synchronized (this.messages) { - for (ListIterator iterator = this.messages.listIterator(); iterator.hasNext(); ) { + for (ListIterator iterator = this.messages.listIterator(); + iterator.hasNext(); ) { if (iterator.next().getTimeSent() < timestamp) { iterator.remove(); } @@ -1034,15 +1087,17 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl 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; - } - }); + 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(); } } @@ -1056,7 +1111,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl public int unreadCount() { synchronized (this.messages) { int count = 0; - for(final Message message : Lists.reverse(this.messages)) { + for (final Message message : Lists.reverse(this.messages)) { if (message.isRead()) { if (message.getType() == Message.TYPE_RTP_SESSION) { continue; diff --git a/src/main/java/eu/siacs/conversations/entities/Message.java b/src/main/java/eu/siacs/conversations/entities/Message.java index 6fb1ec2bd1e5c470635a8e3989d3e91ace474967..b29c1fac5bc05b60472c94768264d0a77fcc075e 100644 --- a/src/main/java/eu/siacs/conversations/entities/Message.java +++ b/src/main/java/eu/siacs/conversations/entities/Message.java @@ -3,26 +3,11 @@ package eu.siacs.conversations.entities; import android.content.ContentValues; import android.database.Cursor; import android.graphics.Color; -import android.text.SpannableStringBuilder; import android.util.Log; - import com.google.common.base.Strings; import com.google.common.collect.Collections2; import com.google.common.collect.ImmutableSet; import com.google.common.primitives.Longs; - -import org.json.JSONException; - -import java.lang.ref.WeakReference; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.Iterator; -import java.util.List; -import java.util.Set; -import java.util.concurrent.CopyOnWriteArraySet; - import eu.siacs.conversations.Config; import eu.siacs.conversations.crypto.axolotl.AxolotlService; import eu.siacs.conversations.crypto.axolotl.FingerprintStatus; @@ -36,6 +21,15 @@ import eu.siacs.conversations.utils.MessageUtils; import eu.siacs.conversations.utils.MimeUtils; import eu.siacs.conversations.utils.UIHelper; import eu.siacs.conversations.xmpp.Jid; +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArraySet; +import org.json.JSONException; public class Message extends AbstractEntity implements AvatarService.Avatarable { @@ -94,7 +88,6 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable public static final String ERROR_MESSAGE_CANCELLED = "eu.siacs.conversations.cancelled"; - public boolean markable = false; protected String conversationUuid; protected Jid counterpart; @@ -140,7 +133,9 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable } public Message(Conversational conversation, String body, int encryption, int status) { - this(conversation, java.util.UUID.randomUUID().toString(), + this( + conversation, + java.util.UUID.randomUUID().toString(), conversation.getUuid(), conversation.getJid() == null ? null : conversation.getJid().asBareJid(), null, @@ -167,7 +162,9 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable } public Message(Conversation conversation, int status, int type, final String remoteMsgId) { - this(conversation, java.util.UUID.randomUUID().toString(), + this( + conversation, + java.util.UUID.randomUUID().toString(), conversation.getUuid(), conversation.getJid() == null ? null : conversation.getJid().asBareJid(), null, @@ -193,13 +190,32 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable Collections.emptyList()); } - protected Message(final Conversational conversation, final String uuid, final String conversationUUid, final Jid counterpart, - final Jid trueCounterpart, final String body, final long timeSent, - final int encryption, final int status, final int type, final boolean carbon, - final String remoteMsgId, final String relativeFilePath, - final String serverMsgId, final String fingerprint, final boolean read, - final String edited, final boolean oob, final String errorMessage, final Set readByMarkers, - final boolean markable, final boolean deleted, final String bodyLanguage, final String occupantId, final Collection reactions) { + protected Message( + final Conversational conversation, + final String uuid, + final String conversationUUid, + final Jid counterpart, + final Jid trueCounterpart, + final String body, + final long timeSent, + final int encryption, + final int status, + final int type, + final boolean carbon, + final String remoteMsgId, + final String relativeFilePath, + final String serverMsgId, + final String fingerprint, + final boolean read, + final String edited, + final boolean oob, + final String errorMessage, + final Set readByMarkers, + final boolean markable, + final boolean deleted, + final String bodyLanguage, + final String occupantId, + final Collection reactions) { this.conversation = conversation; this.uuid = uuid; this.conversationUuid = conversationUUid; @@ -228,7 +244,8 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable } public static Message fromCursor(final Cursor cursor, final Conversation conversation) { - return new Message(conversation, + return new Message( + conversation, cursor.getString(cursor.getColumnIndexOrThrow(UUID)), cursor.getString(cursor.getColumnIndexOrThrow(CONVERSATION)), fromString(cursor.getString(cursor.getColumnIndexOrThrow(COUNTERPART))), @@ -247,14 +264,13 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable cursor.getString(cursor.getColumnIndexOrThrow(EDITED)), cursor.getInt(cursor.getColumnIndexOrThrow(OOB)) > 0, cursor.getString(cursor.getColumnIndexOrThrow(ERROR_MESSAGE)), - ReadByMarker.fromJsonString(cursor.getString(cursor.getColumnIndexOrThrow(READ_BY_MARKERS))), + ReadByMarker.fromJsonString( + cursor.getString(cursor.getColumnIndexOrThrow(READ_BY_MARKERS))), cursor.getInt(cursor.getColumnIndexOrThrow(MARKABLE)) > 0, cursor.getInt(cursor.getColumnIndexOrThrow(DELETED)) > 0, cursor.getString(cursor.getColumnIndexOrThrow(BODY_LANGUAGE)), cursor.getString(cursor.getColumnIndexOrThrow(OCCUPANT_ID)), - Reaction.fromString(cursor.getString(cursor.getColumnIndexOrThrow(REACTIONS))) - - ); + Reaction.fromString(cursor.getString(cursor.getColumnIndexOrThrow(REACTIONS)))); } private static Jid fromString(String value) { @@ -298,7 +314,11 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable } else { values.put(TRUE_COUNTERPART, trueCounterpart.toString()); } - values.put(BODY, body.length() > Config.MAX_STORAGE_MESSAGE_CHARS ? body.substring(0, Config.MAX_STORAGE_MESSAGE_CHARS) : body); + values.put( + BODY, + body.length() > Config.MAX_STORAGE_MESSAGE_CHARS + ? body.substring(0, Config.MAX_STORAGE_MESSAGE_CHARS) + : body); values.put(TIME_SENT, timeSent); values.put(ENCRYPTION, encryption); values.put(STATUS, status); @@ -348,7 +368,9 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable if (this.trueCounterpart == null) { return null; } else { - return this.conversation.getAccount().getRoster() + return this.conversation + .getAccount() + .getRoster() .getContactFromContactList(this.trueCounterpart); } } @@ -375,7 +397,8 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable public boolean sameMucUser(Message otherMessage) { final MucOptions.User thisUser = this.user == null ? null : this.user.get(); - final MucOptions.User otherUser = otherMessage.user == null ? null : otherMessage.user.get(); + final MucOptions.User otherUser = + otherMessage.user == null ? null : otherMessage.user.get(); return thisUser != null && thisUser == otherUser; } @@ -384,8 +407,9 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable } public boolean setErrorMessage(String message) { - boolean changed = (message != null && !message.equals(errorMessage)) - || (message == null && errorMessage != null); + boolean changed = + (message != null && !message.equals(errorMessage)) + || (message == null && errorMessage != null); this.errorMessage = message; return changed; } @@ -533,7 +557,8 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable Iterator iterator = this.readByMarkers.iterator(); while (iterator.hasNext()) { ReadByMarker marker = iterator.next(); - if (marker.getRealJid() == null && readByMarker.getFullJid().equals(marker.getFullJid())) { + if (marker.getRealJid() == null + && readByMarker.getFullJid().equals(marker.getFullJid())) { iterator.remove(); } } @@ -557,7 +582,8 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable boolean similar(Message message) { if (!isPrivateMessage() && this.serverMsgId != null && message.getServerMsgId() != null) { - return this.serverMsgId.equals(message.getServerMsgId()) || Edit.wasPreviouslyEditedServerMsgId(edits, message.getServerMsgId()); + return this.serverMsgId.equals(message.getServerMsgId()) + || Edit.wasPreviouslyEditedServerMsgId(edits, message.getServerMsgId()); } else if (Edit.wasPreviouslyEditedServerMsgId(edits, message.getServerMsgId())) { return true; } else if (this.body == null || this.counterpart == null) { @@ -573,32 +599,37 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable } final boolean matchingCounterpart = this.counterpart.equals(message.getCounterpart()); if (message.getRemoteMsgId() != null) { - final boolean hasUuid = CryptoHelper.UUID_PATTERN.matcher(message.getRemoteMsgId()).matches(); - if (hasUuid && matchingCounterpart && Edit.wasPreviouslyEditedRemoteMsgId(edits, message.getRemoteMsgId())) { + final boolean hasUuid = + CryptoHelper.UUID_PATTERN.matcher(message.getRemoteMsgId()).matches(); + if (hasUuid + && matchingCounterpart + && Edit.wasPreviouslyEditedRemoteMsgId(edits, message.getRemoteMsgId())) { return true; } - return (message.getRemoteMsgId().equals(this.remoteMsgId) || message.getRemoteMsgId().equals(this.uuid)) + return (message.getRemoteMsgId().equals(this.remoteMsgId) + || message.getRemoteMsgId().equals(this.uuid)) && matchingCounterpart - && (body.equals(otherBody) || (message.getEncryption() == Message.ENCRYPTION_PGP && hasUuid)); + && (body.equals(otherBody) + || (message.getEncryption() == Message.ENCRYPTION_PGP && hasUuid)); } else { return this.remoteMsgId == null && matchingCounterpart && body.equals(otherBody) - && Math.abs(this.getTimeSent() - message.getTimeSent()) < Config.MESSAGE_MERGE_WINDOW * 1000; + && Math.abs(this.getTimeSent() - message.getTimeSent()) + < Config.MESSAGE_MERGE_WINDOW * 1000; } } } public Message next() { - if (this.conversation instanceof Conversation) { - final Conversation conversation = (Conversation) this.conversation; - synchronized (conversation.messages) { + if (this.conversation instanceof Conversation c) { + synchronized (c.messages) { if (this.mNextMessage == null) { - int index = conversation.messages.indexOf(this); - if (index < 0 || index >= conversation.messages.size() - 1) { + int index = c.messages.indexOf(this); + if (index < 0 || index >= c.messages.size() - 1) { this.mNextMessage = null; } else { - this.mNextMessage = conversation.messages.get(index + 1); + this.mNextMessage = c.messages.get(index + 1); } } return this.mNextMessage; @@ -609,15 +640,14 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable } public Message prev() { - if (this.conversation instanceof Conversation) { - final Conversation conversation = (Conversation) this.conversation; - synchronized (conversation.messages) { + if (this.conversation instanceof Conversation c) { + synchronized (c.messages) { if (this.mPreviousMessage == null) { - int index = conversation.messages.indexOf(this); - if (index <= 0 || index > conversation.messages.size()) { + int index = c.messages.indexOf(this); + if (index <= 0 || index > c.messages.size()) { this.mPreviousMessage = null; } else { - this.mPreviousMessage = conversation.messages.get(index - 1); + this.mPreviousMessage = c.messages.get(index - 1); } } } @@ -642,56 +672,6 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable return status != STATUS_RECEIVED && !isCarbon() && type != Message.TYPE_RTP_SESSION; } - public boolean mergeable(final Message message) { - return message != null && - (message.getType() == Message.TYPE_TEXT && - this.getTransferable() == null && - message.getTransferable() == null && - message.getEncryption() != Message.ENCRYPTION_PGP && - message.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED && - this.getType() == message.getType() && - this.isReactionsEmpty() && - message.isReactionsEmpty() && - isStatusMergeable(this.getStatus(), message.getStatus()) && - isEncryptionMergeable(this.getEncryption(),message.getEncryption()) && - this.getCounterpart() != null && - this.getCounterpart().equals(message.getCounterpart()) && - this.edited() == message.edited() && - (message.getTimeSent() - this.getTimeSent()) <= (Config.MESSAGE_MERGE_WINDOW * 1000) && - this.getBody().length() + message.getBody().length() <= Config.MAX_DISPLAY_MESSAGE_CHARS && - !message.isGeoUri() && - !this.isGeoUri() && - !message.isOOb() && - !this.isOOb() && - !message.treatAsDownloadable() && - !this.treatAsDownloadable() && - !message.hasMeCommand() && - !this.hasMeCommand() && - !this.bodyIsOnlyEmojis() && - !message.bodyIsOnlyEmojis() && - ((this.axolotlFingerprint == null && message.axolotlFingerprint == null) || this.axolotlFingerprint.equals(message.getFingerprint())) && - UIHelper.sameDay(message.getTimeSent(), this.getTimeSent()) && - this.getReadByMarkers().equals(message.getReadByMarkers()) && - !this.conversation.getJid().asBareJid().equals(Config.BUG_REPORTS) - ); - } - - private static boolean isStatusMergeable(int a, int b) { - return a == b || ( - (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_UNSEND) - || (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_SEND) - || (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_WAITING) - || (a == Message.STATUS_SEND && b == Message.STATUS_UNSEND) - || (a == Message.STATUS_SEND && b == Message.STATUS_WAITING) - ); - } - - private static boolean isEncryptionMergeable(final int a, final int b) { - return a == b - && Arrays.asList(ENCRYPTION_NONE, ENCRYPTION_DECRYPTED, ENCRYPTION_AXOLOTL) - .contains(a); - } - public void setCounterparts(List counterparts) { this.counterparts = counterparts; } @@ -702,7 +682,9 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable @Override public int getAvatarBackgroundColor() { - if (type == Message.TYPE_STATUS && getCounterparts() != null && getCounterparts().size() > 1) { + if (type == Message.TYPE_STATUS + && getCounterparts() != null + && getCounterparts().size() > 1) { return Color.TRANSPARENT; } else { return UIHelper.getColorForName(UIHelper.getMessageDisplayName(this)); @@ -730,10 +712,6 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable return this.reactions; } - public boolean isReactionsEmpty() { - return this.reactions.isEmpty(); - } - public Reaction.Aggregated getAggregatedReactions() { return Reaction.aggregated(this.reactions); } @@ -742,75 +720,28 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable this.reactions = reactions; } - public static class MergeSeparator { - } - - public SpannableStringBuilder getMergedBody() { - SpannableStringBuilder body = new SpannableStringBuilder(MessageUtils.filterLtrRtl(this.body).trim()); - Message current = this; - while (current.mergeable(current.next())) { - current = current.next(); - if (current == null) { - break; - } - body.append("\n\n"); - body.setSpan(new MergeSeparator(), body.length() - 2, body.length(), - SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE); - body.append(MessageUtils.filterLtrRtl(current.getBody()).trim()); - } - return body; - } - public boolean hasMeCommand() { return this.body.trim().startsWith(ME_COMMAND); } - public int getMergedStatus() { - int status = this.status; - Message current = this; - while (current.mergeable(current.next())) { - current = current.next(); - if (current == null) { - break; - } - status = current.status; - } - return status; - } - - public long getMergedTimeSent() { - long time = this.timeSent; - Message current = this; - while (current.mergeable(current.next())) { - current = current.next(); - if (current == null) { - break; - } - time = current.timeSent; - } - return time; - } - - public boolean wasMergedIntoPrevious() { - Message prev = this.prev(); - return prev != null && prev.mergeable(this); - } - public boolean trusted() { Contact contact = this.getContact(); - return status > STATUS_RECEIVED || (contact != null && (contact.showInContactList() || contact.isSelf())); + return status > STATUS_RECEIVED + || (contact != null && (contact.showInContactList() || contact.isSelf())); } public boolean fixCounterpart() { final Presences presences = conversation.getContact().getPresences(); if (counterpart != null && presences.has(Strings.nullToEmpty(counterpart.getResource()))) { return true; - } else if (presences.size() >= 1) { - counterpart = PresenceSelector.getNextCounterpart(getContact(), presences.toResourceArray()[0]); - return true; - } else { + } else if (presences.isEmpty()) { counterpart = null; return false; + } else { + counterpart = + PresenceSelector.getNextCounterpart( + getContact(), presences.toResourceArray()[0]); + return true; } } @@ -930,7 +861,6 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable return type == TYPE_FILE || type == TYPE_IMAGE || type == TYPE_PRIVATE_FILE; } - public boolean isTypeText() { return type == TYPE_TEXT || type == TYPE_PRIVATE; } @@ -965,7 +895,10 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable public boolean isTrusted() { final AxolotlService axolotlService = conversation.getAccount().getAxolotlService(); - final FingerprintStatus s = axolotlService != null ? axolotlService.getFingerprintTrust(axolotlFingerprint) : null; + final FingerprintStatus s = + axolotlService != null + ? axolotlService.getFingerprintTrust(axolotlFingerprint) + : null; return s != null && s.isTrusted(); } @@ -980,17 +913,18 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable } private int getNextEncryption() { - if (this.conversation instanceof Conversation) { - Conversation conversation = (Conversation) this.conversation; + if (this.conversation instanceof Conversation c) { for (Message iterator = this.next(); iterator != null; iterator = iterator.next()) { if (iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED) { continue; } return iterator.getEncryption(); } - return conversation.getNextEncryption(); + return c.getNextEncryption(); } else { - throw new AssertionError("This should never be called since isInValidSession should be disabled for stubs"); + throw new AssertionError( + "This should never be called since isInValidSession should be disabled for" + + " stubs"); } } @@ -998,9 +932,10 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable int pastEncryption = getCleanedEncryption(this.getPreviousEncryption()); int futureEncryption = getCleanedEncryption(this.getNextEncryption()); - boolean inUnencryptedSession = pastEncryption == ENCRYPTION_NONE - || futureEncryption == ENCRYPTION_NONE - || pastEncryption != futureEncryption; + boolean inUnencryptedSession = + pastEncryption == ENCRYPTION_NONE + || futureEncryption == ENCRYPTION_NONE + || pastEncryption != futureEncryption; return inUnencryptedSession || getCleanedEncryption(this.getEncryption()) == pastEncryption; } @@ -1009,7 +944,8 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable if (encryption == ENCRYPTION_DECRYPTED || encryption == ENCRYPTION_DECRYPTION_FAILED) { return ENCRYPTION_PGP; } - if (encryption == ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE || encryption == ENCRYPTION_AXOLOTL_FAILED) { + if (encryption == ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE + || encryption == ENCRYPTION_AXOLOTL_FAILED) { return ENCRYPTION_AXOLOTL; } return encryption; @@ -1047,7 +983,11 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable return configurePrivateMessage(conversation, message, counterpart, false); } - private static boolean configurePrivateMessage(final Conversation conversation, final Message message, final Jid counterpart, final boolean isFile) { + private static boolean configurePrivateMessage( + final Conversation conversation, + final Message message, + final Jid counterpart, + final boolean isFile) { if (counterpart == null) { return false; } diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 1d324232514a83f026b47ef50c0ec158eb463871..17a9a3ce9da5059469df6ed01747c2e9edbe037c 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -5918,23 +5918,11 @@ public class XmppConnectionService extends Service { } public void resendFailedMessages(final Message message) { - final Collection messages = new ArrayList<>(); - Message current = message; - while (current.getStatus() == Message.STATUS_SEND_FAILED) { - messages.add(current); - if (current.mergeable(current.next())) { - current = current.next(); - } else { - break; - } - } - for (final Message msg : messages) { - msg.setTime(System.currentTimeMillis()); - markMessage(msg, Message.STATUS_WAITING); - this.resendMessage(msg, false); - } - if (message.getConversation() instanceof Conversation) { - ((Conversation) message.getConversation()).sort(); + message.setTime(System.currentTimeMillis()); + markMessage(message, Message.STATUS_WAITING); + this.resendMessage(message, false); + if (message.getConversation() instanceof Conversation c) { + c.sort(); } updateConversationUi(); } diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index 9149862e6f057f9142ef93b3335db07d8f846732..c3b0bcd9d9253374f98fe671e3e73074fd93fd19 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -31,6 +31,7 @@ import android.os.SystemClock; import android.preference.PreferenceManager; import android.provider.MediaStore; import android.text.Editable; +import android.text.SpannableStringBuilder; import android.text.TextUtils; import android.util.Log; import android.view.ContextMenu; @@ -55,18 +56,15 @@ import android.widget.ListView; import android.widget.PopupMenu; import android.widget.TextView.OnEditorActionListener; import android.widget.Toast; - import androidx.annotation.IdRes; import androidx.annotation.NonNull; import androidx.annotation.StringRes; import androidx.core.view.inputmethod.InputConnectionCompat; import androidx.core.view.inputmethod.InputContentInfoCompat; import androidx.databinding.DataBindingUtil; - import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.common.base.Optional; import com.google.common.collect.ImmutableList; - import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.crypto.axolotl.AxolotlService; @@ -128,9 +126,6 @@ import eu.siacs.conversations.xmpp.jingle.JingleFileTransferConnection; import eu.siacs.conversations.xmpp.jingle.Media; import eu.siacs.conversations.xmpp.jingle.OngoingRtpSession; import eu.siacs.conversations.xmpp.jingle.RtpCapability; -import eu.siacs.conversations.xmpp.jingle.RtpEndUserState; - - import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -329,7 +324,10 @@ public class ConversationFragment extends XmppFragment } catch (IllegalStateException e) { Log.d( Config.LOGTAG, - "caught illegal state exception while updating status messages"); + "caught illegal state" + + " exception while" + + " updating status" + + " messages"); } messageListAdapter .notifyDataSetChanged(); @@ -692,14 +690,6 @@ public class ConversationFragment extends XmppFragment for (int i = 0; i < messages.size(); ++i) { if (uuid.equals(messages.get(i).getUuid())) { return i; - } else { - Message next = messages.get(i); - while (next != null && next.wasMergedIntoPrevious()) { - if (uuid.equals(next.getUuid())) { - return i; - } - next = next.next(); - } } } return -1; @@ -1045,13 +1035,15 @@ public class ConversationFragment extends XmppFragment } else if (attachment.getType() == Attachment.Type.IMAGE) { Log.d( Config.LOGTAG, - "ConversationsActivity.commitAttachments() - attaching image to conversations. CHOOSE_IMAGE"); + "ConversationsActivity.commitAttachments() - attaching image to" + + " conversations. CHOOSE_IMAGE"); attachImageToConversation( conversation, attachment.getUri(), attachment.getMime()); } else { Log.d( Config.LOGTAG, - "ConversationsActivity.commitAttachments() - attaching file to conversations. CHOOSE_FILE/RECORD_VOICE/RECORD_VIDEO"); + "ConversationsActivity.commitAttachments() - attaching file to" + + " conversations. CHOOSE_FILE/RECORD_VOICE/RECORD_VIDEO"); attachFileToConversation( conversation, attachment.getUri(), attachment.getMime()); } @@ -1277,13 +1269,9 @@ public class ConversationFragment extends XmppFragment } } - private void populateContextMenu(ContextMenu menu) { + private void populateContextMenu(final ContextMenu menu) { final Message m = this.selectedMessage; final Transferable t = m.getTransferable(); - Message relevantForCorrection = m; - while (relevantForCorrection.mergeable(relevantForCorrection.next())) { - relevantForCorrection = relevantForCorrection.next(); - } if (m.getType() != Message.TYPE_STATUS && m.getType() != Message.TYPE_RTP_SESSION) { if (m.getEncryption() == Message.ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE @@ -1329,7 +1317,8 @@ public class ConversationFragment extends XmppFragment && m.getErrorMessage() != null && !Message.ERROR_MESSAGE_CANCELLED.equals(m.getErrorMessage()); final Conversational conversational = m.getConversation(); - if (m.getStatus() == Message.STATUS_RECEIVED && conversational instanceof Conversation c) { + if (m.getStatus() == Message.STATUS_RECEIVED + && conversational instanceof Conversation c) { final XmppConnection connection = c.getAccount().getXmppConnection(); if (c.isWithStranger() && m.getServerMsgId() != null @@ -1357,7 +1346,8 @@ public class ConversationFragment extends XmppFragment && t == null) { copyMessage.setVisible(true); quoteMessage.setVisible(!showError && !MessageUtils.prepareQuote(m).isEmpty()); - final String scheme = ShareUtil.getLinkScheme(m.getMergedBody()); + final String scheme = + ShareUtil.getLinkScheme(new SpannableStringBuilder(m.getBody())); if ("xmpp".equals(scheme)) { copyLink.setTitle(R.string.copy_jabber_id); copyLink.setVisible(true); @@ -1369,9 +1359,9 @@ public class ConversationFragment extends XmppFragment retryDecryption.setVisible(true); } if (!showError - && relevantForCorrection.getType() == Message.TYPE_TEXT + && m.getType() == Message.TYPE_TEXT && !m.isGeoUri() - && relevantForCorrection.isLastCorrectableMessage() + && m.isLastCorrectableMessage() && m.getConversation() instanceof Conversation) { correctMessage.setVisible(true); } @@ -1672,7 +1662,11 @@ public class ConversationFragment extends XmppFragment } private void triggerRtpSession(final Account account, final Jid with, final String action) { - CallIntegrationConnectionService.placeCall(activity.xmppConnectionService, account,with,RtpSessionActivity.actionToMedia(action)); + CallIntegrationConnectionService.placeCall( + activity.xmppConnectionService, + account, + with, + RtpSessionActivity.actionToMedia(action)); } private void handleAttachmentSelection(MenuItem item) { @@ -1946,7 +1940,8 @@ public class ConversationFragment extends XmppFragment @SuppressLint("InflateParams") protected void clearHistoryDialog(final Conversation conversation) { - final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireActivity()); + final MaterialAlertDialogBuilder builder = + new MaterialAlertDialogBuilder(requireActivity()); builder.setTitle(R.string.clear_conversation_history); final View dialogView = requireActivity().getLayoutInflater().inflate(R.layout.dialog_clear_history, null); @@ -1970,7 +1965,8 @@ public class ConversationFragment extends XmppFragment } protected void muteConversationDialog(final Conversation conversation) { - final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireActivity()); + final MaterialAlertDialogBuilder builder = + new MaterialAlertDialogBuilder(requireActivity()); builder.setTitle(R.string.disable_notifications); final int[] durations = getResources().getIntArray(R.array.mute_options_durations); final CharSequence[] labels = new CharSequence[durations.length]; @@ -2002,7 +1998,9 @@ public class ConversationFragment extends XmppFragment private boolean hasPermissions(int requestCode, List permissions) { final List missingPermissions = new ArrayList<>(); for (String permission : permissions) { - if ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU || Config.ONLY_INTERNAL_STORAGE) && permission.equals(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { + if ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU + || Config.ONLY_INTERNAL_STORAGE) + && permission.equals(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { continue; } if (activity.checkSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) { @@ -2012,9 +2010,7 @@ public class ConversationFragment extends XmppFragment if (missingPermissions.size() == 0) { return true; } else { - requestPermissions( - missingPermissions.toArray(new String[0]), - requestCode); + requestPermissions(missingPermissions.toArray(new String[0]), requestCode); return false; } } @@ -2119,9 +2115,6 @@ public class ConversationFragment extends XmppFragment } } if (message != null) { - while (message.next() != null && message.next().wasMergedIntoPrevious()) { - message = message.next(); - } return message.getUuid(); } } @@ -2140,12 +2133,15 @@ public class ConversationFragment extends XmppFragment } private void addReaction(final Message message) { - activity.addReaction(message, reactions -> { - if (activity.xmppConnectionService.sendReactions(message, reactions)) { - return; - } - Toast.makeText(activity, R.string.could_not_add_reaction, Toast.LENGTH_LONG).show(); - }); + activity.addReaction( + message, + reactions -> { + if (activity.xmppConnectionService.sendReactions(message, reactions)) { + return; + } + Toast.makeText(activity, R.string.could_not_add_reaction, Toast.LENGTH_LONG) + .show(); + }); } private void reportMessage(final Message message) { @@ -2153,7 +2149,8 @@ public class ConversationFragment extends XmppFragment } private void showErrorMessage(final Message message) { - final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireActivity()); + final MaterialAlertDialogBuilder builder = + new MaterialAlertDialogBuilder(requireActivity()); builder.setTitle(R.string.error_message); final String errorMessage = message.getErrorMessage(); final String[] errorMessageParts = @@ -2180,7 +2177,8 @@ public class ConversationFragment extends XmppFragment } private void deleteFile(final Message message) { - final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireActivity()); + final MaterialAlertDialogBuilder builder = + new MaterialAlertDialogBuilder(requireActivity()); builder.setNegativeButton(R.string.cancel, null); builder.setTitle(R.string.delete_file_dialog); builder.setMessage(R.string.delete_file_dialog_msg); @@ -2278,10 +2276,7 @@ public class ConversationFragment extends XmppFragment updateEditablity(); } - private void correctMessage(Message message) { - while (message.mergeable(message.next())) { - message = message.next(); - } + private void correctMessage(final Message message) { this.conversation.setCorrectingMessage(message); final Editable editable = binding.textinput.getText(); this.conversation.setDraftMessage(editable.toString()); @@ -2395,7 +2390,8 @@ public class ConversationFragment extends XmppFragment final String uuid = pendingConversationsUuid.pop(); Log.d( Config.LOGTAG, - "ConversationFragment.onStart() - activity was bound but no conversation loaded. uuid=" + "ConversationFragment.onStart() - activity was bound but no conversation" + + " loaded. uuid=" + uuid); if (uuid != null) { findAndReInitByUuidOrArchive(uuid); @@ -2710,7 +2706,10 @@ public class ConversationFragment extends XmppFragment R.string.enable, this.mEnableAccountListener); } else if (account.getStatus() == Account.State.LOGGED_OUT) { - showSnackbar(R.string.this_account_is_logged_out,R.string.log_in,this.mEnableAccountListener); + showSnackbar( + R.string.this_account_is_logged_out, + R.string.log_in, + this.mEnableAccountListener); } else if (conversation.isBlocked()) { showSnackbar(R.string.contact_blocked, R.string.unblock, this.mUnblockClickListener); } else if (contact != null @@ -2772,7 +2771,8 @@ public class ConversationFragment extends XmppFragment showSnackbar(R.string.conference_kicked, R.string.join, joinMuc); break; case TECHNICAL_PROBLEMS: - showSnackbar(R.string.conference_technical_problems, R.string.try_again, joinMuc); + showSnackbar( + R.string.conference_technical_problems, R.string.try_again, joinMuc); break; case UNKNOWN: showSnackbar(R.string.conference_unknown_error, R.string.try_again, joinMuc); @@ -2942,12 +2942,14 @@ public class ConversationFragment extends XmppFragment status = Presence.Status.OFFLINE; } this.binding.textSendButton.setTag(action); - this.binding.textSendButton.setIconResource(SendButtonTool.getSendButtonImageResource(action)); - this.binding.textSendButton.setIconTint(ColorStateList.valueOf(SendButtonTool.getSendButtonColor(this.binding.textSendButton, status))); + this.binding.textSendButton.setIconResource( + SendButtonTool.getSendButtonImageResource(action)); + this.binding.textSendButton.setIconTint( + ColorStateList.valueOf( + SendButtonTool.getSendButtonColor(this.binding.textSendButton, status))); // TODO send button color final Activity activity = getActivity(); - if (activity != null) { - } + if (activity != null) {} } protected void updateStatusMessages() { @@ -3010,7 +3012,7 @@ public class ConversationFragment extends XmppFragment if (!ReadByMarker.contains(marker, addedMarkers)) { addedMarkers.add( marker); // may be put outside this condition. set should do - // dedup anyway + // dedup anyway MucOptions.User user = mucOptions.findUser(marker); if (user != null && !users.contains(user)) { shownMarkers.add(user); @@ -3269,8 +3271,10 @@ public class ConversationFragment extends XmppFragment }); } - public void showNoPGPKeyDialog(final boolean plural, final DialogInterface.OnClickListener listener) { - final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireActivity()); + public void showNoPGPKeyDialog( + final boolean plural, final DialogInterface.OnClickListener listener) { + final MaterialAlertDialogBuilder builder = + new MaterialAlertDialogBuilder(requireActivity()); if (plural) { builder.setTitle(getString(R.string.no_pgp_keys)); builder.setMessage(getText(R.string.contacts_have_no_pgp_keys)); @@ -3439,7 +3443,13 @@ public class ConversationFragment extends XmppFragment try { getActivity() .startIntentSenderForResult( - pendingIntent.getIntentSender(), requestCode, null, 0, 0, 0, Compatibility.pgpStartIntentSenderOptions()); + pendingIntent.getIntentSender(), + requestCode, + null, + 0, + 0, + 0, + Compatibility.pgpStartIntentSenderOptions()); } catch (final SendIntentException ignored) { } } diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java index 759960dcbbf3eda1902bc76912f6fbff15be66df..e26977c5c291e7f95c1ca0906ba8fcbc6be0c4fe 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java @@ -15,18 +15,15 @@ import android.text.style.ForegroundColorSpan; import android.text.style.RelativeSizeSpan; import android.text.style.StyleSpan; import android.util.DisplayMetrics; -import android.util.Log; import android.view.View; import android.view.ViewGroup; import android.view.WindowManager; import android.widget.ArrayAdapter; -import android.widget.Button; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.RelativeLayout; import android.widget.TextView; import android.widget.Toast; - import androidx.annotation.AttrRes; import androidx.annotation.ColorInt; import androidx.annotation.DrawableRes; @@ -35,10 +32,6 @@ import androidx.annotation.Nullable; import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; import androidx.core.widget.ImageViewCompat; -import androidx.databinding.DataBindingUtil; -import androidx.emoji2.emojipicker.EmojiViewItem; -import androidx.emoji2.emojipicker.RecentEmojiProvider; - import com.google.android.material.button.MaterialButton; import com.google.android.material.chip.ChipGroup; import com.google.android.material.color.MaterialColors; @@ -47,14 +40,10 @@ import com.google.common.base.Joiner; import com.google.common.base.Strings; import com.google.common.collect.Collections2; import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableSet; -import com.google.common.collect.Lists; - import eu.siacs.conversations.AppSettings; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.crypto.axolotl.FingerprintStatus; -import eu.siacs.conversations.databinding.DialogAddReactionBinding; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Conversational; @@ -89,14 +78,11 @@ import eu.siacs.conversations.utils.TimeFrameUtils; import eu.siacs.conversations.utils.UIHelper; import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.mam.MamReference; -import kotlin.coroutines.Continuation; - import java.net.URI; import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.Locale; -import java.util.function.Consumer; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -192,7 +178,7 @@ public class MessageAdapter extends ArrayAdapter { final Message message, final int type, final BubbleColor bubbleColor) { - final int mergedStatus = message.getMergedStatus(); + final int mergedStatus = message.getStatus(); final boolean error; if (viewHolder.indicatorReceived != null) { viewHolder.indicatorReceived.setVisibility(View.GONE); @@ -285,7 +271,7 @@ public class MessageAdapter extends ArrayAdapter { } final String formattedTime = - UIHelper.readableTimeDifferenceFull(getContext(), message.getMergedTimeSent()); + UIHelper.readableTimeDifferenceFull(getContext(), message.getTimeSent()); final String bodyLanguage = message.getBodyLanguage(); final ImmutableList.Builder timeInfoBuilder = new ImmutableList.Builder<>(); if (message.getStatus() <= Message.STATUS_RECEIVED) { @@ -328,8 +314,8 @@ public class MessageAdapter extends ArrayAdapter { case Message.STATUS_WAITING -> R.drawable.ic_more_horiz_24dp; case Message.STATUS_UNSEND -> transferable == null ? null : R.drawable.ic_upload_24dp; case Message.STATUS_SEND -> R.drawable.ic_done_24dp; - case Message.STATUS_SEND_RECEIVED, Message.STATUS_SEND_DISPLAYED -> R.drawable - .ic_done_all_24dp; + case Message.STATUS_SEND_RECEIVED, Message.STATUS_SEND_DISPLAYED -> + R.drawable.ic_done_all_24dp; case Message.STATUS_SEND_FAILED -> { final String errorMessage = message.getErrorMessage(); if (Message.ERROR_MESSAGE_CANCELLED.equals(errorMessage)) { @@ -486,21 +472,17 @@ public class MessageAdapter extends ArrayAdapter { if (message.getBody() != null) { final String nick = UIHelper.getMessageDisplayName(message); - SpannableStringBuilder body = message.getMergedBody(); - boolean hasMeCommand = message.hasMeCommand(); - if (hasMeCommand) { - body = body.replace(0, Message.ME_COMMAND.length(), nick + " "); - } - if (body.length() > Config.MAX_DISPLAY_MESSAGE_CHARS) { - body = new SpannableStringBuilder(body, 0, Config.MAX_DISPLAY_MESSAGE_CHARS); - body.append("\u2026"); + final boolean hasMeCommand = message.hasMeCommand(); + final var rawBody = message.getBody(); + final SpannableStringBuilder body; + if (rawBody.length() > Config.MAX_DISPLAY_MESSAGE_CHARS) { + body = new SpannableStringBuilder(rawBody, 0, Config.MAX_DISPLAY_MESSAGE_CHARS); + body.append("…"); + } else { + body = new SpannableStringBuilder(rawBody); } - Message.MergeSeparator[] mergeSeparators = - body.getSpans(0, body.length(), Message.MergeSeparator.class); - for (Message.MergeSeparator mergeSeparator : mergeSeparators) { - int start = body.getSpanStart(mergeSeparator); - int end = body.getSpanEnd(mergeSeparator); - body.setSpan(new DividerSpan(true), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + if (hasMeCommand) { + body.replace(0, Message.ME_COMMAND.length(), String.format("%s ", nick)); } boolean startsWithQuote = handleTextQuotes(viewHolder.messageBody, body, bubbleColor); if (!message.isPrivateMessage()) { @@ -1211,12 +1193,15 @@ public class MessageAdapter extends ArrayAdapter { final View view, final BubbleColor bubbleColor) { final @AttrRes int colorAttributeResId = switch (bubbleColor) { - case SURFACE -> Activities.isNightMode(view.getContext()) - ? com.google.android.material.R.attr.colorSurfaceContainerHigh - : com.google.android.material.R.attr.colorSurfaceContainerLow; - case SURFACE_HIGH -> Activities.isNightMode(view.getContext()) - ? com.google.android.material.R.attr.colorSurfaceContainerHighest - : com.google.android.material.R.attr.colorSurfaceContainerHigh; + case SURFACE -> + Activities.isNightMode(view.getContext()) + ? com.google.android.material.R.attr.colorSurfaceContainerHigh + : com.google.android.material.R.attr.colorSurfaceContainerLow; + case SURFACE_HIGH -> + Activities.isNightMode(view.getContext()) + ? com.google.android.material.R.attr + .colorSurfaceContainerHighest + : com.google.android.material.R.attr.colorSurfaceContainerHigh; case PRIMARY -> com.google.android.material.R.attr.colorPrimaryContainer; case SECONDARY -> com.google.android.material.R.attr.colorSecondaryContainer; case TERTIARY -> com.google.android.material.R.attr.colorTertiaryContainer; diff --git a/src/main/java/eu/siacs/conversations/ui/util/ShareUtil.java b/src/main/java/eu/siacs/conversations/ui/util/ShareUtil.java index 007575307c7b02903e2dc485dd14e585a18c26be..99b63fe2746b8aa9995f22647e7a3f7af2a26230 100644 --- a/src/main/java/eu/siacs/conversations/ui/util/ShareUtil.java +++ b/src/main/java/eu/siacs/conversations/ui/util/ShareUtil.java @@ -34,7 +34,6 @@ import android.content.Intent; import android.net.Uri; import android.text.SpannableStringBuilder; import android.widget.Toast; - import eu.siacs.conversations.R; import eu.siacs.conversations.entities.DownloadableFile; import eu.siacs.conversations.entities.Message; @@ -46,66 +45,82 @@ import eu.siacs.conversations.xmpp.Jid; public class ShareUtil { - public static void share(XmppActivity activity, Message message) { - Intent shareIntent = new Intent(); - shareIntent.setAction(Intent.ACTION_SEND); - if (message.isGeoUri()) { - shareIntent.putExtra(Intent.EXTRA_TEXT, message.getBody()); - shareIntent.setType("text/plain"); - } else if (!message.isFileOrImage()) { - shareIntent.putExtra(Intent.EXTRA_TEXT, message.getMergedBody().toString()); - shareIntent.setType("text/plain"); - shareIntent.putExtra(ConversationsActivity.EXTRA_AS_QUOTE, message.getStatus() == Message.STATUS_RECEIVED); - } else { - final DownloadableFile file = activity.xmppConnectionService.getFileBackend().getFile(message); - try { - shareIntent.putExtra(Intent.EXTRA_STREAM, FileBackend.getUriForFile(activity, file)); - } catch (SecurityException e) { - Toast.makeText(activity, activity.getString(R.string.no_permission_to_access_x, file.getAbsolutePath()), Toast.LENGTH_SHORT).show(); - return; - } - shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - String mime = message.getMimeType(); - if (mime == null) { - mime = "*/*"; - } - shareIntent.setType(mime); - } - try { - activity.startActivity(Intent.createChooser(shareIntent, activity.getText(R.string.share_with))); - } catch (ActivityNotFoundException e) { - //This should happen only on faulty androids because normally chooser is always available - Toast.makeText(activity, R.string.no_application_found_to_open_file, Toast.LENGTH_SHORT).show(); - } - } + public static void share(XmppActivity activity, Message message) { + Intent shareIntent = new Intent(); + shareIntent.setAction(Intent.ACTION_SEND); + if (message.isGeoUri()) { + shareIntent.putExtra(Intent.EXTRA_TEXT, message.getBody()); + shareIntent.setType("text/plain"); + } else if (!message.isFileOrImage()) { + shareIntent.putExtra(Intent.EXTRA_TEXT, message.getBody()); + shareIntent.setType("text/plain"); + shareIntent.putExtra( + ConversationsActivity.EXTRA_AS_QUOTE, + message.getStatus() == Message.STATUS_RECEIVED); + } else { + final DownloadableFile file = + activity.xmppConnectionService.getFileBackend().getFile(message); + try { + shareIntent.putExtra( + Intent.EXTRA_STREAM, FileBackend.getUriForFile(activity, file)); + } catch (SecurityException e) { + Toast.makeText( + activity, + activity.getString( + R.string.no_permission_to_access_x, file.getAbsolutePath()), + Toast.LENGTH_SHORT) + .show(); + return; + } + shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + String mime = message.getMimeType(); + if (mime == null) { + mime = "*/*"; + } + shareIntent.setType(mime); + } + try { + activity.startActivity( + Intent.createChooser(shareIntent, activity.getText(R.string.share_with))); + } catch (ActivityNotFoundException e) { + // This should happen only on faulty androids because normally chooser is always + // available + Toast.makeText(activity, R.string.no_application_found_to_open_file, Toast.LENGTH_SHORT) + .show(); + } + } - public static void copyToClipboard(XmppActivity activity, Message message) { - if (activity.copyTextToClipboard(message.getMergedBody().toString(), R.string.message)) { - Toast.makeText(activity, R.string.message_copied_to_clipboard, Toast.LENGTH_SHORT).show(); - } - } + public static void copyToClipboard(XmppActivity activity, Message message) { + if (activity.copyTextToClipboard(message.getBody(), R.string.message)) { + Toast.makeText(activity, R.string.message_copied_to_clipboard, Toast.LENGTH_SHORT) + .show(); + } + } - public static void copyUrlToClipboard(XmppActivity activity, Message message) { - final String url; - final int resId; - if (message.isGeoUri()) { - resId = R.string.location; - url = message.getBody(); - } else if (message.hasFileOnRemoteHost()) { - resId = R.string.file_url; - url = message.getFileParams().url; - } else { - final Message.FileParams fileParams = message.getFileParams(); - url = (fileParams != null && fileParams.url != null) ? fileParams.url : message.getBody().trim(); - resId = R.string.file_url; - } - if (activity.copyTextToClipboard(url, resId)) { - Toast.makeText(activity, R.string.url_copied_to_clipboard, Toast.LENGTH_SHORT).show(); - } - } + public static void copyUrlToClipboard(XmppActivity activity, Message message) { + final String url; + final int resId; + if (message.isGeoUri()) { + resId = R.string.location; + url = message.getBody(); + } else if (message.hasFileOnRemoteHost()) { + resId = R.string.file_url; + url = message.getFileParams().url; + } else { + final Message.FileParams fileParams = message.getFileParams(); + url = + (fileParams != null && fileParams.url != null) + ? fileParams.url + : message.getBody().trim(); + resId = R.string.file_url; + } + if (activity.copyTextToClipboard(url, resId)) { + Toast.makeText(activity, R.string.url_copied_to_clipboard, Toast.LENGTH_SHORT).show(); + } + } public static void copyLinkToClipboard(final XmppActivity activity, final Message message) { - final SpannableStringBuilder body = message.getMergedBody(); + final SpannableStringBuilder body = new SpannableStringBuilder(message.getBody()); for (final String url : MyLinkify.extractLinks(body)) { final Uri uri = Uri.parse(url); if ("xmpp".equals(uri.getScheme())) { diff --git a/src/main/java/eu/siacs/conversations/utils/MessageUtils.java b/src/main/java/eu/siacs/conversations/utils/MessageUtils.java index 1ac9d2c7dcae0354cb6f8efcf125ec3a54320bd2..3b8b6c0d9d388cc4b067cf6028e21a95b4c4ccf7 100644 --- a/src/main/java/eu/siacs/conversations/utils/MessageUtils.java +++ b/src/main/java/eu/siacs/conversations/utils/MessageUtils.java @@ -30,16 +30,14 @@ package eu.siacs.conversations.utils; import com.google.common.base.Strings; - -import java.net.URI; -import java.net.URISyntaxException; -import java.util.regex.Pattern; - import eu.siacs.conversations.entities.Conversational; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.http.AesGcmURL; import eu.siacs.conversations.http.URL; import eu.siacs.conversations.ui.util.QuoteHelper; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.regex.Pattern; public class MessageUtils { @@ -47,7 +45,7 @@ public class MessageUtils { public static final String EMPTY_STRING = ""; - public static String prepareQuote(Message message) { + public static String prepareQuote(final Message message) { final StringBuilder builder = new StringBuilder(); final String body; if (message.hasMeCommand()) { @@ -63,7 +61,7 @@ public class MessageUtils { } body = nick + " " + message.getBody().substring(Message.ME_COMMAND.length()); } else { - body = message.getMergedBody().toString(); + body = message.getBody(); } for (String line : body.split("\n")) { if (!(line.length() <= 0) && QuoteHelper.isNestedTooDeeply(line)) { @@ -100,8 +98,12 @@ public class MessageUtils { final String protocol = uri.getScheme(); final boolean encrypted = ref != null && AesGcmURL.IV_KEY.matcher(ref).matches(); final boolean followedByDataUri = lines.length == 2 && lines[1].startsWith("data:"); - final boolean validAesGcm = AesGcmURL.PROTOCOL_NAME.equalsIgnoreCase(protocol) && encrypted && (lines.length == 1 || followedByDataUri); - final boolean validProtocol = "http".equalsIgnoreCase(protocol) || "https".equalsIgnoreCase(protocol); + final boolean validAesGcm = + AesGcmURL.PROTOCOL_NAME.equalsIgnoreCase(protocol) + && encrypted + && (lines.length == 1 || followedByDataUri); + final boolean validProtocol = + "http".equalsIgnoreCase(protocol) || "https".equalsIgnoreCase(protocol); final boolean validOob = validProtocol && (oob || encrypted) && lines.length == 1; return validAesGcm || validOob; } @@ -111,6 +113,10 @@ public class MessageUtils { } public static boolean unInitiatedButKnownSize(Message message) { - return message.getType() == Message.TYPE_TEXT && message.getTransferable() == null && message.isOOb() && message.getFileParams().size != null && message.getFileParams().url != null; + return message.getType() == Message.TYPE_TEXT + && message.getTransferable() == null + && message.isOOb() + && message.getFileParams().size != null + && message.getFileParams().url != null; } } diff --git a/src/main/java/eu/siacs/conversations/utils/StylingHelper.java b/src/main/java/eu/siacs/conversations/utils/StylingHelper.java index fc669c87d9e017c50e5985753fe8792912ee07f0..5e5bafd3e9d50ca7d4ea85a9045264aa523e117a 100644 --- a/src/main/java/eu/siacs/conversations/utils/StylingHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/StylingHelper.java @@ -29,7 +29,6 @@ package eu.siacs.conversations.utils; -import android.content.Context; import android.graphics.Color; import android.graphics.Typeface; import android.text.Editable; @@ -45,138 +44,149 @@ import android.text.style.StyleSpan; import android.text.style.TypefaceSpan; import android.widget.EditText; import android.widget.TextView; - import androidx.annotation.ColorInt; -import androidx.core.content.ContextCompat; - import com.google.android.material.color.MaterialColors; - +import eu.siacs.conversations.ui.text.QuoteSpan; import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import eu.siacs.conversations.R; -import eu.siacs.conversations.entities.Message; -import eu.siacs.conversations.ui.text.QuoteSpan; - public class StylingHelper { - private static final List> SPAN_CLASSES = Arrays.asList( - StyleSpan.class, - StrikethroughSpan.class, - TypefaceSpan.class, - ForegroundColorSpan.class - ); - - public static void clear(final Editable editable) { - final int end = editable.length() - 1; - for (Class clazz : SPAN_CLASSES) { - for (ParcelableSpan span : editable.getSpans(0, end, clazz)) { - editable.removeSpan(span); - } - } - } - - public static void format(final Editable editable, int start, int end, @ColorInt int textColor) { - for (ImStyleParser.Style style : ImStyleParser.parse(editable, start, end)) { - final int keywordLength = style.getKeyword().length(); - editable.setSpan(createSpanForStyle(style), style.getStart() + keywordLength, style.getEnd() - keywordLength + 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - makeKeywordOpaque(editable, style.getStart(), style.getStart() + keywordLength, textColor); - makeKeywordOpaque(editable, style.getEnd() - keywordLength + 1, style.getEnd() + 1, textColor); - } - } - - public static void format(final Editable editable, @ColorInt int textColor) { - int end = 0; - Message.MergeSeparator[] spans = editable.getSpans(0, editable.length() - 1, Message.MergeSeparator.class); - for (Message.MergeSeparator span : spans) { - format(editable, end, editable.getSpanStart(span), textColor); - end = editable.getSpanEnd(span); - } - format(editable, end, editable.length() - 1, textColor); - } - - public static void highlight(final TextView view, final Editable editable, final List needles) { - for (final String needle : needles) { - if (!FtsUtils.isKeyword(needle)) { - highlight(view, editable, needle); - } - } - } - - public static List filterHighlightedWords(List terms) { - List words = new ArrayList<>(); - for (String term : terms) { - if (!FtsUtils.isKeyword(term)) { - StringBuilder builder = new StringBuilder(); - for (int codepoint, i = 0; i < term.length(); i += Character.charCount(codepoint)) { - codepoint = term.codePointAt(i); - if (Character.isLetterOrDigit(codepoint)) { - builder.append(Character.toChars(codepoint)); - } else if (builder.length() > 0) { - words.add(builder.toString()); - builder.delete(0, builder.length()); - } - } - if (builder.length() > 0) { - words.add(builder.toString()); - } - } - } - return words; - } - - private static void highlight(final TextView view, final Editable editable, final String needle) { - final int length = needle.length(); - String string = editable.toString(); - int start = indexOfIgnoreCase(string, needle, 0); - while (start != -1) { - int end = start + length; - editable.setSpan(new BackgroundColorSpan(MaterialColors.getColor(view, com.google.android.material.R.attr.colorPrimaryFixedDim)), start, end, SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE); - editable.setSpan(new ForegroundColorSpan(MaterialColors.getColor(view, com.google.android.material.R.attr.colorOnPrimaryFixed)), start, end, SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE); - start = indexOfIgnoreCase(string, needle, start + length); - } - - } - - static CharSequence subSequence(CharSequence charSequence, int start, int end) { - if (start == 0 && charSequence.length() + 1 == end) { - return charSequence; - } - if (charSequence instanceof Spannable spannable) { - Spannable sub = (Spannable) spannable.subSequence(start, end); - for (Class clazz : SPAN_CLASSES) { - ParcelableSpan[] spannables = spannable.getSpans(start, end, clazz); - for (ParcelableSpan parcelableSpan : spannables) { - int beginSpan = spannable.getSpanStart(parcelableSpan); - int endSpan = spannable.getSpanEnd(parcelableSpan); - if (beginSpan >= start && endSpan <= end) { - continue; - } - sub.setSpan(clone(parcelableSpan), Math.max(beginSpan - start, 0), Math.min(sub.length() - 1, endSpan), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - } - } - return sub; - } else { - return charSequence.subSequence(start, end); - } - } - - private static ParcelableSpan clone(ParcelableSpan span) { - if (span instanceof ForegroundColorSpan) { - return new ForegroundColorSpan(((ForegroundColorSpan) span).getForegroundColor()); - } else if (span instanceof TypefaceSpan) { - return new TypefaceSpan(((TypefaceSpan) span).getFamily()); - } else if (span instanceof StyleSpan) { - return new StyleSpan(((StyleSpan) span).getStyle()); - } else if (span instanceof StrikethroughSpan) { - return new StrikethroughSpan(); - } else { - throw new AssertionError("Unknown Span"); - } - } - - private static ParcelableSpan createSpanForStyle(final ImStyleParser.Style style) { + private static final List> SPAN_CLASSES = + Arrays.asList( + StyleSpan.class, + StrikethroughSpan.class, + TypefaceSpan.class, + ForegroundColorSpan.class); + + public static void clear(final Editable editable) { + final int end = editable.length() - 1; + for (Class clazz : SPAN_CLASSES) { + for (ParcelableSpan span : editable.getSpans(0, end, clazz)) { + editable.removeSpan(span); + } + } + } + + public static void format( + final Editable editable, int start, int end, @ColorInt int textColor) { + for (ImStyleParser.Style style : ImStyleParser.parse(editable, start, end)) { + final int keywordLength = style.getKeyword().length(); + editable.setSpan( + createSpanForStyle(style), + style.getStart() + keywordLength, + style.getEnd() - keywordLength + 1, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + makeKeywordOpaque( + editable, style.getStart(), style.getStart() + keywordLength, textColor); + makeKeywordOpaque( + editable, style.getEnd() - keywordLength + 1, style.getEnd() + 1, textColor); + } + } + + public static void format(final Editable editable, @ColorInt final int textColor) { + format(editable, 0, editable.length() - 1, textColor); + } + + public static void highlight( + final TextView view, final Editable editable, final List needles) { + for (final String needle : needles) { + if (!FtsUtils.isKeyword(needle)) { + highlight(view, editable, needle); + } + } + } + + public static List filterHighlightedWords(List terms) { + List words = new ArrayList<>(); + for (String term : terms) { + if (!FtsUtils.isKeyword(term)) { + StringBuilder builder = new StringBuilder(); + for (int codepoint, i = 0; i < term.length(); i += Character.charCount(codepoint)) { + codepoint = term.codePointAt(i); + if (Character.isLetterOrDigit(codepoint)) { + builder.append(Character.toChars(codepoint)); + } else if (builder.length() > 0) { + words.add(builder.toString()); + builder.delete(0, builder.length()); + } + } + if (builder.length() > 0) { + words.add(builder.toString()); + } + } + } + return words; + } + + private static void highlight( + final TextView view, final Editable editable, final String needle) { + final int length = needle.length(); + String string = editable.toString(); + int start = indexOfIgnoreCase(string, needle, 0); + while (start != -1) { + int end = start + length; + editable.setSpan( + new BackgroundColorSpan( + MaterialColors.getColor( + view, com.google.android.material.R.attr.colorPrimaryFixedDim)), + start, + end, + SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE); + editable.setSpan( + new ForegroundColorSpan( + MaterialColors.getColor( + view, com.google.android.material.R.attr.colorOnPrimaryFixed)), + start, + end, + SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE); + start = indexOfIgnoreCase(string, needle, start + length); + } + } + + static CharSequence subSequence(CharSequence charSequence, int start, int end) { + if (start == 0 && charSequence.length() + 1 == end) { + return charSequence; + } + if (charSequence instanceof Spannable spannable) { + Spannable sub = (Spannable) spannable.subSequence(start, end); + for (Class clazz : SPAN_CLASSES) { + ParcelableSpan[] spannables = spannable.getSpans(start, end, clazz); + for (ParcelableSpan parcelableSpan : spannables) { + int beginSpan = spannable.getSpanStart(parcelableSpan); + int endSpan = spannable.getSpanEnd(parcelableSpan); + if (beginSpan >= start && endSpan <= end) { + continue; + } + sub.setSpan( + clone(parcelableSpan), + Math.max(beginSpan - start, 0), + Math.min(sub.length() - 1, endSpan), + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + } + return sub; + } else { + return charSequence.subSequence(start, end); + } + } + + private static ParcelableSpan clone(ParcelableSpan span) { + if (span instanceof ForegroundColorSpan) { + return new ForegroundColorSpan(((ForegroundColorSpan) span).getForegroundColor()); + } else if (span instanceof TypefaceSpan) { + return new TypefaceSpan(((TypefaceSpan) span).getFamily()); + } else if (span instanceof StyleSpan) { + return new StyleSpan(((StyleSpan) span).getStyle()); + } else if (span instanceof StrikethroughSpan) { + return new StrikethroughSpan(); + } else { + throw new AssertionError("Unknown Span"); + } + } + + private static ParcelableSpan createSpanForStyle(final ImStyleParser.Style style) { return switch (style.getKeyword()) { case "*" -> new StyleSpan(Typeface.BOLD); case "_" -> new StyleSpan(Typeface.ITALIC); @@ -184,62 +194,64 @@ public class StylingHelper { case "`", "```" -> new TypefaceSpan("monospace"); default -> throw new AssertionError("Unknown Style"); }; - } - - private static void makeKeywordOpaque(final Editable editable, int start, int end, @ColorInt int fallbackTextColor) { - QuoteSpan[] quoteSpans = editable.getSpans(start, end, QuoteSpan.class); - @ColorInt int textColor = quoteSpans.length > 0 ? quoteSpans[0].getColor() : fallbackTextColor; - @ColorInt int keywordColor = transformColor(textColor); - editable.setSpan(new ForegroundColorSpan(keywordColor), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - } - - private static - @ColorInt - int transformColor(@ColorInt int c) { - return Color.argb(Math.round(Color.alpha(c) * 0.45f), Color.red(c), Color.green(c), Color.blue(c)); - } - - private static int indexOfIgnoreCase(final String haystack, final String needle, final int start) { - if (haystack == null || needle == null) { - return -1; - } - final int endLimit = haystack.length() - needle.length() + 1; - if (start > endLimit) { - return -1; - } - if (needle.length() == 0) { - return start; - } - for (int i = start; i < endLimit; i++) { - if (haystack.regionMatches(true, i, needle, 0, needle.length())) { - return i; - } - } - return -1; - } - - public static class MessageEditorStyler implements TextWatcher { - - private final EditText mEditText; - - public MessageEditorStyler(EditText editText) { - this.mEditText = editText; - } - - @Override - public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) { - - } - - @Override - public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) { - - } - - @Override - public void afterTextChanged(Editable editable) { - clear(editable); - format(editable, mEditText.getCurrentTextColor()); - } - } + } + + private static void makeKeywordOpaque( + final Editable editable, int start, int end, @ColorInt int fallbackTextColor) { + QuoteSpan[] quoteSpans = editable.getSpans(start, end, QuoteSpan.class); + @ColorInt + int textColor = quoteSpans.length > 0 ? quoteSpans[0].getColor() : fallbackTextColor; + @ColorInt int keywordColor = transformColor(textColor); + editable.setSpan( + new ForegroundColorSpan(keywordColor), + start, + end, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + + private static @ColorInt int transformColor(@ColorInt int c) { + return Color.argb( + Math.round(Color.alpha(c) * 0.45f), Color.red(c), Color.green(c), Color.blue(c)); + } + + private static int indexOfIgnoreCase( + final String haystack, final String needle, final int start) { + if (haystack == null || needle == null) { + return -1; + } + final int endLimit = haystack.length() - needle.length() + 1; + if (start > endLimit) { + return -1; + } + if (needle.length() == 0) { + return start; + } + for (int i = start; i < endLimit; i++) { + if (haystack.regionMatches(true, i, needle, 0, needle.length())) { + return i; + } + } + return -1; + } + + public static class MessageEditorStyler implements TextWatcher { + + private final EditText mEditText; + + public MessageEditorStyler(EditText editText) { + this.mEditText = editText; + } + + @Override + public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {} + + @Override + public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {} + + @Override + public void afterTextChanged(Editable editable) { + clear(editable); + format(editable, mEditText.getCurrentTextColor()); + } + } }