Remove the ListSelectionManager / message body selection (fixes memory leak!)

eta created

- 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.

Change summary

src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java      |  13 
src/main/java/eu/siacs/conversations/ui/widget/ListSelectionManager.java | 211 
2 files changed, 224 deletions(-)

Detailed changes

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<Message> 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<String> highlightedTerm = null;
     private DisplayMetrics metrics;
@@ -503,9 +501,7 @@ public class MessageAdapter extends ArrayAdapter<Message> 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<Message> 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<Message> 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();

src/main/java/eu/siacs/conversations/ui/widget/ListSelectionManager.java 🔗

@@ -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;
-            }
-        }
-    }
-}