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