From b4805ac2c51574cd4486121950c4256af6398a43 Mon Sep 17 00:00:00 2001 From: eta Date: Sun, 4 Oct 2020 14:53:13 +0100 Subject: [PATCH] Remove the ListSelectionManager / message body selection (fixes memory leak!) - When the `viewHolder.messageBody` `TextView` created by a `MessageAdapter` is set to selectable, it leaks an `android.widget.Editor` (because that editor registers a view observer that never gets unregistered). - This memory leak is really quite problematic, as the message adapter is used a lot! - Having the text be selectable is useless anyway, though; there isn't any way to select it (because long pressing just opens the context menu anyway). - It looks like the ListSelectionManager was meant to track selections across multiple messages. However, I'm not sure this feature ever gets used. - Accordingly, this commit removes the entire feature, thus fixing the memory leak (since no `Editor` objects are ever created). - It should also reduce memory usage in general, since we aren't attaching an `Editor` to every single textview we create. - A `TextView` only allocates an `Editor` if you ask it to do certain things, like make the text selectable or register custom selection callbacks. --- .../ui/adapter/MessageAdapter.java | 13 -- .../ui/widget/ListSelectionManager.java | 211 ------------------ 2 files changed, 224 deletions(-) delete mode 100644 src/main/java/eu/siacs/conversations/ui/widget/ListSelectionManager.java 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 02e925f7d..c6287034c 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java @@ -66,7 +66,6 @@ import eu.siacs.conversations.ui.util.MyLinkify; import eu.siacs.conversations.ui.util.ViewUtil; import eu.siacs.conversations.ui.widget.ClickableMovementMethod; import eu.siacs.conversations.ui.widget.CopyTextView; -import eu.siacs.conversations.ui.widget.ListSelectionManager; import eu.siacs.conversations.utils.CryptoHelper; import eu.siacs.conversations.utils.EmojiWrapper; import eu.siacs.conversations.utils.Emoticons; @@ -87,7 +86,6 @@ public class MessageAdapter extends ArrayAdapter implements CopyTextVie private static final int DATE_SEPARATOR = 3; private static final int RTP_SESSION = 4; private final XmppActivity activity; - private final ListSelectionManager listSelectionManager = new ListSelectionManager(); private final AudioPlayer audioPlayer; private List highlightedTerm = null; private DisplayMetrics metrics; @@ -503,9 +501,7 @@ public class MessageAdapter extends ArrayAdapter implements CopyTextVie MyLinkify.addLinks(body, true); viewHolder.messageBody.setAutoLinkMask(0); viewHolder.messageBody.setText(EmojiWrapper.transform(body)); - viewHolder.messageBody.setTextIsSelectable(true); viewHolder.messageBody.setMovementMethod(ClickableMovementMethod.getInstance()); - listSelectionManager.onUpdate(viewHolder.messageBody, message); } else { viewHolder.messageBody.setText(""); viewHolder.messageBody.setTextIsSelectable(false); @@ -676,8 +672,6 @@ public class MessageAdapter extends ArrayAdapter implements CopyTextVie throw new AssertionError("Unknown view type"); } if (viewHolder.messageBody != null) { - listSelectionManager.onCreate(viewHolder.messageBody, - new MessageBodyActionModeCallback(viewHolder.messageBody)); viewHolder.messageBody.setCopyHandler(this); } view.setTag(viewHolder); @@ -875,13 +869,6 @@ public class MessageAdapter extends ArrayAdapter implements CopyTextVie activity.showInstallPgpDialog(); } - @Override - public void notifyDataSetChanged() { - listSelectionManager.onBeforeNotifyDataSetChanged(); - super.notifyDataSetChanged(); - listSelectionManager.onAfterNotifyDataSetChanged(); - } - private String transformText(CharSequence text, int start, int end, boolean forCopy) { SpannableStringBuilder builder = new SpannableStringBuilder(text); Object copySpan = new Object(); diff --git a/src/main/java/eu/siacs/conversations/ui/widget/ListSelectionManager.java b/src/main/java/eu/siacs/conversations/ui/widget/ListSelectionManager.java deleted file mode 100644 index 24f1cac8d..000000000 --- a/src/main/java/eu/siacs/conversations/ui/widget/ListSelectionManager.java +++ /dev/null @@ -1,211 +0,0 @@ -package eu.siacs.conversations.ui.widget; - -import android.os.Handler; -import android.os.Looper; -import android.os.Message; -import android.text.Selection; -import android.text.Spannable; -import android.view.ActionMode; -import android.view.Menu; -import android.view.MenuItem; -import android.widget.TextView; - -import java.lang.reflect.Field; -import java.lang.reflect.Method; - -public class ListSelectionManager { - - private static final int MESSAGE_SEND_RESET = 1; - private static final int MESSAGE_RESET = 2; - private static final int MESSAGE_START_SELECTION = 3; - private static final Field FIELD_EDITOR; - private static final Method METHOD_START_SELECTION; - private static final boolean SUPPORTED; - private static final Handler HANDLER = new Handler(Looper.getMainLooper(), new Handler.Callback() { - - @Override - public boolean handleMessage(Message msg) { - switch (msg.what) { - case MESSAGE_SEND_RESET: { - // Skip one more message queue loop - HANDLER.obtainMessage(MESSAGE_RESET, msg.obj).sendToTarget(); - return true; - } - case MESSAGE_RESET: { - final ListSelectionManager listSelectionManager = (ListSelectionManager) msg.obj; - listSelectionManager.futureSelectionIdentifier = null; - return true; - } - case MESSAGE_START_SELECTION: { - final StartSelectionHolder holder = (StartSelectionHolder) msg.obj; - holder.listSelectionManager.futureSelectionIdentifier = null; - startSelection(holder.textView, holder.start, holder.end); - return true; - } - } - return false; - } - }); - - static { - Field editor; - try { - editor = TextView.class.getDeclaredField("mEditor"); - editor.setAccessible(true); - } catch (Exception e) { - editor = null; - } - FIELD_EDITOR = editor; - Method startSelection = null; - if (editor != null) { - String[] startSelectionNames = {"startSelectionActionMode", "startSelectionActionModeWithSelection"}; - for (String startSelectionName : startSelectionNames) { - try { - startSelection = editor.getType().getDeclaredMethod(startSelectionName); - startSelection.setAccessible(true); - break; - } catch (Exception e) { - startSelection = null; - } - } - } - METHOD_START_SELECTION = startSelection; - SUPPORTED = FIELD_EDITOR != null && METHOD_START_SELECTION != null; - } - - private ActionMode selectionActionMode; - private Object selectionIdentifier; - private TextView selectionTextView; - private Object futureSelectionIdentifier; - private int futureSelectionStart; - private int futureSelectionEnd; - - public static boolean isSupported() { - return SUPPORTED; - } - - private static void startSelection(TextView textView, int start, int end) { - final CharSequence text = textView.getText(); - if (SUPPORTED && start >= 0 && end > start && textView.isTextSelectable() && text instanceof Spannable) { - final Spannable spannable = (Spannable) text; - start = Math.min(start, spannable.length()); - end = Math.min(end, spannable.length()); - Selection.setSelection(spannable, start, end); - try { - final Object editor = FIELD_EDITOR != null ? FIELD_EDITOR.get(textView) : textView; - METHOD_START_SELECTION.invoke(editor); - } catch (Exception e) { - } - } - } - - public void onCreate(TextView textView, ActionMode.Callback additionalCallback) { - final CustomCallback callback = new CustomCallback(textView, additionalCallback); - textView.setCustomSelectionActionModeCallback(callback); - } - - public void onUpdate(TextView textView, Object identifier) { - if (SUPPORTED) { - final ActionMode.Callback callback = textView.getCustomSelectionActionModeCallback(); - if (callback instanceof CustomCallback) { - final CustomCallback customCallback = (CustomCallback) textView.getCustomSelectionActionModeCallback(); - customCallback.identifier = identifier; - if (futureSelectionIdentifier == identifier) { - HANDLER.obtainMessage(MESSAGE_START_SELECTION, new StartSelectionHolder(this, - textView, futureSelectionStart, futureSelectionEnd)).sendToTarget(); - } - } - } - } - - public void onBeforeNotifyDataSetChanged() { - if (SUPPORTED) { - HANDLER.removeMessages(MESSAGE_SEND_RESET); - HANDLER.removeMessages(MESSAGE_RESET); - HANDLER.removeMessages(MESSAGE_START_SELECTION); - if (selectionActionMode != null) { - final CharSequence text = selectionTextView.getText(); - futureSelectionIdentifier = selectionIdentifier; - futureSelectionStart = Selection.getSelectionStart(text); - futureSelectionEnd = Selection.getSelectionEnd(text); - selectionActionMode.finish(); - selectionActionMode = null; - selectionIdentifier = null; - selectionTextView = null; - } - } - } - - public void onAfterNotifyDataSetChanged() { - if (SUPPORTED && futureSelectionIdentifier != null) { - HANDLER.obtainMessage(MESSAGE_SEND_RESET, this).sendToTarget(); - } - } - - private static class StartSelectionHolder { - - final ListSelectionManager listSelectionManager; - final TextView textView; - public final int start; - public final int end; - - StartSelectionHolder(ListSelectionManager listSelectionManager, TextView textView, - int start, int end) { - this.listSelectionManager = listSelectionManager; - this.textView = textView; - this.start = start; - this.end = end; - } - } - - private class CustomCallback implements ActionMode.Callback { - - private final TextView textView; - private final ActionMode.Callback additionalCallback; - Object identifier; - - CustomCallback(TextView textView, ActionMode.Callback additionalCallback) { - this.textView = textView; - this.additionalCallback = additionalCallback; - } - - @Override - public boolean onCreateActionMode(ActionMode mode, Menu menu) { - selectionActionMode = mode; - selectionIdentifier = identifier; - selectionTextView = textView; - if (additionalCallback != null) { - additionalCallback.onCreateActionMode(mode, menu); - } - return true; - } - - @Override - public boolean onPrepareActionMode(ActionMode mode, Menu menu) { - if (additionalCallback != null) { - additionalCallback.onPrepareActionMode(mode, menu); - } - return true; - } - - @Override - public boolean onActionItemClicked(ActionMode mode, MenuItem item) { - if (additionalCallback != null && additionalCallback.onActionItemClicked(mode, item)) { - return true; - } - return false; - } - - @Override - public void onDestroyActionMode(ActionMode mode) { - if (additionalCallback != null) { - additionalCallback.onDestroyActionMode(mode); - } - if (selectionActionMode == mode) { - selectionActionMode = null; - selectionIdentifier = null; - selectionTextView = null; - } - } - } -} \ No newline at end of file