make list selection manager work with app compat

This commit is contained in:
Daniel Gultsch 2019-09-26 23:47:55 +02:00
parent f9e1e856d2
commit 11736ce48c

View file

@ -1,8 +1,5 @@
package eu.siacs.conversations.ui.widget; package eu.siacs.conversations.ui.widget;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import android.os.Handler; import android.os.Handler;
import android.os.Looper; import android.os.Looper;
import android.os.Message; import android.os.Message;
@ -13,199 +10,202 @@ import android.view.Menu;
import android.view.MenuItem; import android.view.MenuItem;
import android.widget.TextView; import android.widget.TextView;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
public class ListSelectionManager { public class ListSelectionManager {
private static final int MESSAGE_SEND_RESET = 1; private static final int MESSAGE_SEND_RESET = 1;
private static final int MESSAGE_RESET = 2; private static final int MESSAGE_RESET = 2;
private static final int MESSAGE_START_SELECTION = 3; 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() {
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;
}
});
@Override static {
public boolean handleMessage(Message msg) { Field editor;
switch (msg.what) { try {
case MESSAGE_SEND_RESET: { editor = TextView.class.getDeclaredField("mEditor");
// Skip one more message queue loop editor.setAccessible(true);
HANDLER.obtainMessage(MESSAGE_RESET, msg.obj).sendToTarget(); } catch (Exception e) {
return true; editor = null;
} }
case MESSAGE_RESET: { FIELD_EDITOR = editor;
final ListSelectionManager listSelectionManager = (ListSelectionManager) msg.obj; Method startSelection = null;
listSelectionManager.futureSelectionIdentifier = null; if (editor != null) {
return true; String[] startSelectionNames = {"startSelectionActionMode", "startSelectionActionModeWithSelection"};
} for (String startSelectionName : startSelectionNames) {
case MESSAGE_START_SELECTION: { try {
final StartSelectionHolder holder = (StartSelectionHolder) msg.obj; startSelection = editor.getType().getDeclaredMethod(startSelectionName);
holder.listSelectionManager.futureSelectionIdentifier = null; startSelection.setAccessible(true);
startSelection(holder.textView, holder.start, holder.end); break;
return true; } catch (Exception e) {
} startSelection = null;
} }
return false; }
} }
}); METHOD_START_SELECTION = startSelection;
SUPPORTED = FIELD_EDITOR != null && METHOD_START_SELECTION != null;
}
private static class StartSelectionHolder { private ActionMode selectionActionMode;
private Object selectionIdentifier;
private TextView selectionTextView;
private Object futureSelectionIdentifier;
private int futureSelectionStart;
private int futureSelectionEnd;
public final ListSelectionManager listSelectionManager; public static boolean isSupported() {
public final TextView textView; return SUPPORTED;
public final int start; }
public final int end;
public StartSelectionHolder(ListSelectionManager listSelectionManager, TextView textView, private static void startSelection(TextView textView, int start, int end) {
int start, int end) { final CharSequence text = textView.getText();
this.listSelectionManager = listSelectionManager; if (SUPPORTED && start >= 0 && end > start && textView.isTextSelectable() && text instanceof Spannable) {
this.textView = textView; final Spannable spannable = (Spannable) text;
this.start = start; start = Math.min(start, spannable.length());
this.end = end; 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) {
}
}
}
private ActionMode selectionActionMode; public void onCreate(TextView textView, ActionMode.Callback additionalCallback) {
private Object selectionIdentifier; final CustomCallback callback = new CustomCallback(textView, additionalCallback);
private TextView selectionTextView; textView.setCustomSelectionActionModeCallback(callback);
}
private Object futureSelectionIdentifier; public void onUpdate(TextView textView, Object identifier) {
private int futureSelectionStart; if (SUPPORTED) {
private int futureSelectionEnd; 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 onCreate(TextView textView, ActionMode.Callback additionalCallback) { public void onBeforeNotifyDataSetChanged() {
final CustomCallback callback = new CustomCallback(textView, additionalCallback); if (SUPPORTED) {
textView.setCustomSelectionActionModeCallback(callback); 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 onUpdate(TextView textView, Object identifier) { public void onAfterNotifyDataSetChanged() {
if (SUPPORTED) { if (SUPPORTED && futureSelectionIdentifier != null) {
CustomCallback callback = (CustomCallback) textView.getCustomSelectionActionModeCallback(); HANDLER.obtainMessage(MESSAGE_SEND_RESET, this).sendToTarget();
callback.identifier = identifier; }
if (futureSelectionIdentifier == identifier) { }
HANDLER.obtainMessage(MESSAGE_START_SELECTION, new StartSelectionHolder(this,
textView, futureSelectionStart, futureSelectionEnd)).sendToTarget();
}
}
}
public void onBeforeNotifyDataSetChanged() { private static class StartSelectionHolder {
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() { final ListSelectionManager listSelectionManager;
if (SUPPORTED && futureSelectionIdentifier != null) { final TextView textView;
HANDLER.obtainMessage(MESSAGE_SEND_RESET, this).sendToTarget(); public final int start;
} public final int end;
}
private class CustomCallback implements ActionMode.Callback { StartSelectionHolder(ListSelectionManager listSelectionManager, TextView textView,
int start, int end) {
this.listSelectionManager = listSelectionManager;
this.textView = textView;
this.start = start;
this.end = end;
}
}
private final TextView textView; private class CustomCallback implements ActionMode.Callback {
private final ActionMode.Callback additionalCallback;
public Object identifier;
public CustomCallback(TextView textView, ActionMode.Callback additionalCallback) { private final TextView textView;
this.textView = textView; private final ActionMode.Callback additionalCallback;
this.additionalCallback = additionalCallback; Object identifier;
}
@Override CustomCallback(TextView textView, ActionMode.Callback additionalCallback) {
public boolean onCreateActionMode(ActionMode mode, Menu menu) { this.textView = textView;
selectionActionMode = mode; this.additionalCallback = additionalCallback;
selectionIdentifier = identifier; }
selectionTextView = textView;
if (additionalCallback != null) {
additionalCallback.onCreateActionMode(mode, menu);
}
return true;
}
@Override @Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) { public boolean onCreateActionMode(ActionMode mode, Menu menu) {
if (additionalCallback != null) { selectionActionMode = mode;
additionalCallback.onPrepareActionMode(mode, menu); selectionIdentifier = identifier;
} selectionTextView = textView;
return true; if (additionalCallback != null) {
} additionalCallback.onCreateActionMode(mode, menu);
}
return true;
}
@Override @Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) { public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
if (additionalCallback != null && additionalCallback.onActionItemClicked(mode, item)) { if (additionalCallback != null) {
return true; additionalCallback.onPrepareActionMode(mode, menu);
} }
return false; return true;
} }
@Override @Override
public void onDestroyActionMode(ActionMode mode) { public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
if (additionalCallback != null) { if (additionalCallback != null && additionalCallback.onActionItemClicked(mode, item)) {
additionalCallback.onDestroyActionMode(mode); return true;
} }
if (selectionActionMode == mode) { return false;
selectionActionMode = null; }
selectionIdentifier = null;
selectionTextView = null;
}
}
}
private static final Field FIELD_EDITOR; @Override
private static final Method METHOD_START_SELECTION; public void onDestroyActionMode(ActionMode mode) {
private static final boolean SUPPORTED; if (additionalCallback != null) {
additionalCallback.onDestroyActionMode(mode);
static { }
Field editor; if (selectionActionMode == mode) {
try { selectionActionMode = null;
editor = TextView.class.getDeclaredField("mEditor"); selectionIdentifier = null;
editor.setAccessible(true); selectionTextView = null;
} 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;
}
public static boolean isSupported() {
return SUPPORTED;
}
public 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) {
}
}
}
} }