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}