ListSelectionManager.java

  1package eu.siacs.conversations.ui.widget;
  2
  3import android.os.Handler;
  4import android.os.Looper;
  5import android.os.Message;
  6import android.text.Selection;
  7import android.text.Spannable;
  8import android.view.ActionMode;
  9import android.view.Menu;
 10import android.view.MenuItem;
 11import android.widget.TextView;
 12
 13import java.lang.reflect.Field;
 14import java.lang.reflect.Method;
 15
 16public class ListSelectionManager {
 17
 18    private static final int MESSAGE_SEND_RESET = 1;
 19    private static final int MESSAGE_RESET = 2;
 20    private static final int MESSAGE_START_SELECTION = 3;
 21    private static final Field FIELD_EDITOR;
 22    private static final Method METHOD_START_SELECTION;
 23    private static final boolean SUPPORTED;
 24    private static final Handler HANDLER = new Handler(Looper.getMainLooper(), new Handler.Callback() {
 25
 26        @Override
 27        public boolean handleMessage(Message msg) {
 28            switch (msg.what) {
 29                case MESSAGE_SEND_RESET: {
 30                    // Skip one more message queue loop
 31                    HANDLER.obtainMessage(MESSAGE_RESET, msg.obj).sendToTarget();
 32                    return true;
 33                }
 34                case MESSAGE_RESET: {
 35                    final ListSelectionManager listSelectionManager = (ListSelectionManager) msg.obj;
 36                    listSelectionManager.futureSelectionIdentifier = null;
 37                    return true;
 38                }
 39                case MESSAGE_START_SELECTION: {
 40                    final StartSelectionHolder holder = (StartSelectionHolder) msg.obj;
 41                    holder.listSelectionManager.futureSelectionIdentifier = null;
 42                    startSelection(holder.textView, holder.start, holder.end);
 43                    return true;
 44                }
 45            }
 46            return false;
 47        }
 48    });
 49
 50    static {
 51        Field editor;
 52        try {
 53            editor = TextView.class.getDeclaredField("mEditor");
 54            editor.setAccessible(true);
 55        } catch (Exception e) {
 56            editor = null;
 57        }
 58        FIELD_EDITOR = editor;
 59        Method startSelection = null;
 60        if (editor != null) {
 61            String[] startSelectionNames = {"startSelectionActionMode", "startSelectionActionModeWithSelection"};
 62            for (String startSelectionName : startSelectionNames) {
 63                try {
 64                    startSelection = editor.getType().getDeclaredMethod(startSelectionName);
 65                    startSelection.setAccessible(true);
 66                    break;
 67                } catch (Exception e) {
 68                    startSelection = null;
 69                }
 70            }
 71        }
 72        METHOD_START_SELECTION = startSelection;
 73        SUPPORTED = FIELD_EDITOR != null && METHOD_START_SELECTION != null;
 74    }
 75
 76    private ActionMode selectionActionMode;
 77    private Object selectionIdentifier;
 78    private TextView selectionTextView;
 79    private Object futureSelectionIdentifier;
 80    private int futureSelectionStart;
 81    private int futureSelectionEnd;
 82
 83    public static boolean isSupported() {
 84        return SUPPORTED;
 85    }
 86
 87    private static void startSelection(TextView textView, int start, int end) {
 88        final CharSequence text = textView.getText();
 89        if (SUPPORTED && start >= 0 && end > start && textView.isTextSelectable() && text instanceof Spannable) {
 90            final Spannable spannable = (Spannable) text;
 91            start = Math.min(start, spannable.length());
 92            end = Math.min(end, spannable.length());
 93            Selection.setSelection(spannable, start, end);
 94            try {
 95                final Object editor = FIELD_EDITOR != null ? FIELD_EDITOR.get(textView) : textView;
 96                METHOD_START_SELECTION.invoke(editor);
 97            } catch (Exception e) {
 98            }
 99        }
100    }
101
102    public void onCreate(TextView textView, ActionMode.Callback additionalCallback) {
103        final CustomCallback callback = new CustomCallback(textView, additionalCallback);
104        textView.setCustomSelectionActionModeCallback(callback);
105    }
106
107    public void onUpdate(TextView textView, Object identifier) {
108        if (SUPPORTED) {
109            final ActionMode.Callback callback = textView.getCustomSelectionActionModeCallback();
110            if (callback instanceof CustomCallback) {
111                final CustomCallback customCallback = (CustomCallback) textView.getCustomSelectionActionModeCallback();
112                customCallback.identifier = identifier;
113                if (futureSelectionIdentifier == identifier) {
114                    HANDLER.obtainMessage(MESSAGE_START_SELECTION, new StartSelectionHolder(this,
115                            textView, futureSelectionStart, futureSelectionEnd)).sendToTarget();
116                }
117            }
118        }
119    }
120
121    public void onBeforeNotifyDataSetChanged() {
122        if (SUPPORTED) {
123            HANDLER.removeMessages(MESSAGE_SEND_RESET);
124            HANDLER.removeMessages(MESSAGE_RESET);
125            HANDLER.removeMessages(MESSAGE_START_SELECTION);
126            if (selectionActionMode != null) {
127                final CharSequence text = selectionTextView.getText();
128                futureSelectionIdentifier = selectionIdentifier;
129                futureSelectionStart = Selection.getSelectionStart(text);
130                futureSelectionEnd = Selection.getSelectionEnd(text);
131                selectionActionMode.finish();
132                selectionActionMode = null;
133                selectionIdentifier = null;
134                selectionTextView = null;
135            }
136        }
137    }
138
139    public void onAfterNotifyDataSetChanged() {
140        if (SUPPORTED && futureSelectionIdentifier != null) {
141            HANDLER.obtainMessage(MESSAGE_SEND_RESET, this).sendToTarget();
142        }
143    }
144
145    private static class StartSelectionHolder {
146
147        final ListSelectionManager listSelectionManager;
148        final TextView textView;
149        public final int start;
150        public final int end;
151
152        StartSelectionHolder(ListSelectionManager listSelectionManager, TextView textView,
153                                    int start, int end) {
154            this.listSelectionManager = listSelectionManager;
155            this.textView = textView;
156            this.start = start;
157            this.end = end;
158        }
159    }
160
161    private class CustomCallback implements ActionMode.Callback {
162
163        private final TextView textView;
164        private final ActionMode.Callback additionalCallback;
165        Object identifier;
166
167        CustomCallback(TextView textView, ActionMode.Callback additionalCallback) {
168            this.textView = textView;
169            this.additionalCallback = additionalCallback;
170        }
171
172        @Override
173        public boolean onCreateActionMode(ActionMode mode, Menu menu) {
174            selectionActionMode = mode;
175            selectionIdentifier = identifier;
176            selectionTextView = textView;
177            if (additionalCallback != null) {
178                additionalCallback.onCreateActionMode(mode, menu);
179            }
180            return true;
181        }
182
183        @Override
184        public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
185            if (additionalCallback != null) {
186                additionalCallback.onPrepareActionMode(mode, menu);
187            }
188            return true;
189        }
190
191        @Override
192        public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
193            if (additionalCallback != null && additionalCallback.onActionItemClicked(mode, item)) {
194                return true;
195            }
196            return false;
197        }
198
199        @Override
200        public void onDestroyActionMode(ActionMode mode) {
201            if (additionalCallback != null) {
202                additionalCallback.onDestroyActionMode(mode);
203            }
204            if (selectionActionMode == mode) {
205                selectionActionMode = null;
206                selectionIdentifier = null;
207                selectionTextView = null;
208            }
209        }
210    }
211}