mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
Implement repeat filtering logic in Android Embedder (flutter/engine#17509)
This commit is contained in:
parent
e894999885
commit
a03f69c53a
@ -37,10 +37,46 @@ class InputConnectionAdaptor extends BaseInputConnection {
|
||||
private int mBatchCount;
|
||||
private InputMethodManager mImm;
|
||||
private final Layout mLayout;
|
||||
|
||||
// Used to determine if Samsung-specific hacks should be applied.
|
||||
private final boolean isSamsung;
|
||||
|
||||
private boolean mRepeatCheckNeeded = false;
|
||||
private TextEditingValue mLastSentTextEditngValue;
|
||||
// Data class used to get and store the last-sent values via updateEditingState to
|
||||
// the framework. These are then compared against to prevent redundant messages
|
||||
// with the same data before any valid operations were made to the contents.
|
||||
private class TextEditingValue {
|
||||
public int selectionStart;
|
||||
public int selectionEnd;
|
||||
public int composingStart;
|
||||
public int composingEnd;
|
||||
public String text;
|
||||
|
||||
public TextEditingValue(Editable editable) {
|
||||
selectionStart = Selection.getSelectionStart(editable);
|
||||
selectionEnd = Selection.getSelectionEnd(editable);
|
||||
composingStart = BaseInputConnection.getComposingSpanStart(editable);
|
||||
composingEnd = BaseInputConnection.getComposingSpanEnd(editable);
|
||||
text = editable.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (o == this) {
|
||||
return true;
|
||||
}
|
||||
if (!(o instanceof TextEditingValue)) {
|
||||
return false;
|
||||
}
|
||||
TextEditingValue value = (TextEditingValue) o;
|
||||
return selectionStart == value.selectionStart
|
||||
&& selectionEnd == value.selectionEnd
|
||||
&& composingStart == value.composingStart
|
||||
&& composingEnd == value.composingEnd
|
||||
&& text.equals(value.text);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
public InputConnectionAdaptor(
|
||||
View view,
|
||||
@ -76,15 +112,42 @@ class InputConnectionAdaptor extends BaseInputConnection {
|
||||
// If the IME is in the middle of a batch edit, then wait until it completes.
|
||||
if (mBatchCount > 0) return;
|
||||
|
||||
int selectionStart = Selection.getSelectionStart(mEditable);
|
||||
int selectionEnd = Selection.getSelectionEnd(mEditable);
|
||||
int composingStart = BaseInputConnection.getComposingSpanStart(mEditable);
|
||||
int composingEnd = BaseInputConnection.getComposingSpanEnd(mEditable);
|
||||
TextEditingValue currentValue = new TextEditingValue(mEditable);
|
||||
|
||||
mImm.updateSelection(mFlutterView, selectionStart, selectionEnd, composingStart, composingEnd);
|
||||
// Return if this data has already been sent and no meaningful changes have
|
||||
// occurred to mark this as dirty. This prevents duplicate remote updates of
|
||||
// the same data, which can break formatters that change the length of the
|
||||
// contents.
|
||||
if (mRepeatCheckNeeded && currentValue.equals(mLastSentTextEditngValue)) {
|
||||
return;
|
||||
}
|
||||
|
||||
mImm.updateSelection(
|
||||
mFlutterView,
|
||||
currentValue.selectionStart,
|
||||
currentValue.selectionEnd,
|
||||
currentValue.composingStart,
|
||||
currentValue.composingEnd);
|
||||
|
||||
textInputChannel.updateEditingState(
|
||||
mClient, mEditable.toString(), selectionStart, selectionEnd, composingStart, composingEnd);
|
||||
mClient,
|
||||
currentValue.text,
|
||||
currentValue.selectionStart,
|
||||
currentValue.selectionEnd,
|
||||
currentValue.composingStart,
|
||||
currentValue.composingEnd);
|
||||
|
||||
mRepeatCheckNeeded = true;
|
||||
mLastSentTextEditngValue = currentValue;
|
||||
}
|
||||
|
||||
// This should be called whenever a change could have been made to
|
||||
// the value of mEditable, which will make any call of updateEditingState()
|
||||
// ineligible for repeat checking as we do not want to skip sending real changes
|
||||
// to the framework.
|
||||
public void markDirty() {
|
||||
// Disable updateEditngState's repeat-update check
|
||||
mRepeatCheckNeeded = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -109,7 +172,7 @@ class InputConnectionAdaptor extends BaseInputConnection {
|
||||
@Override
|
||||
public boolean commitText(CharSequence text, int newCursorPosition) {
|
||||
boolean result = super.commitText(text, newCursorPosition);
|
||||
updateEditingState();
|
||||
markDirty();
|
||||
return result;
|
||||
}
|
||||
|
||||
@ -118,14 +181,21 @@ class InputConnectionAdaptor extends BaseInputConnection {
|
||||
if (Selection.getSelectionStart(mEditable) == -1) return true;
|
||||
|
||||
boolean result = super.deleteSurroundingText(beforeLength, afterLength);
|
||||
updateEditingState();
|
||||
markDirty();
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean deleteSurroundingTextInCodePoints(int beforeLength, int afterLength) {
|
||||
boolean result = super.deleteSurroundingTextInCodePoints(beforeLength, afterLength);
|
||||
markDirty();
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean setComposingRegion(int start, int end) {
|
||||
boolean result = super.setComposingRegion(start, end);
|
||||
updateEditingState();
|
||||
markDirty();
|
||||
return result;
|
||||
}
|
||||
|
||||
@ -137,7 +207,7 @@ class InputConnectionAdaptor extends BaseInputConnection {
|
||||
} else {
|
||||
result = super.setComposingText(text, newCursorPosition);
|
||||
}
|
||||
updateEditingState();
|
||||
markDirty();
|
||||
return result;
|
||||
}
|
||||
|
||||
@ -159,7 +229,7 @@ class InputConnectionAdaptor extends BaseInputConnection {
|
||||
}
|
||||
}
|
||||
|
||||
updateEditingState();
|
||||
markDirty();
|
||||
return result;
|
||||
}
|
||||
|
||||
@ -173,6 +243,13 @@ class InputConnectionAdaptor extends BaseInputConnection {
|
||||
return extractedText;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean clearMetaKeyStates(int states) {
|
||||
boolean result = super.clearMetaKeyStates(states);
|
||||
markDirty();
|
||||
return result;
|
||||
}
|
||||
|
||||
// Detect if the keyboard is a Samsung keyboard, where we apply Samsung-specific hacks to
|
||||
// fix critical bugs that make the keyboard otherwise unusable. See finishComposingText() for
|
||||
// more details.
|
||||
@ -197,7 +274,7 @@ class InputConnectionAdaptor extends BaseInputConnection {
|
||||
@Override
|
||||
public boolean setSelection(int start, int end) {
|
||||
boolean result = super.setSelection(start, end);
|
||||
updateEditingState();
|
||||
markDirty();
|
||||
return result;
|
||||
}
|
||||
|
||||
@ -219,6 +296,7 @@ class InputConnectionAdaptor extends BaseInputConnection {
|
||||
|
||||
@Override
|
||||
public boolean sendKeyEvent(KeyEvent event) {
|
||||
markDirty();
|
||||
if (event.getAction() == KeyEvent.ACTION_DOWN) {
|
||||
if (event.getKeyCode() == KeyEvent.KEYCODE_DEL) {
|
||||
int selStart = clampIndexToEditable(Selection.getSelectionStart(mEditable), mEditable);
|
||||
@ -344,6 +422,7 @@ class InputConnectionAdaptor extends BaseInputConnection {
|
||||
|
||||
@Override
|
||||
public boolean performContextMenuAction(int id) {
|
||||
markDirty();
|
||||
if (id == android.R.id.selectAll) {
|
||||
setSelection(0, mEditable.length());
|
||||
return true;
|
||||
@ -397,6 +476,7 @@ class InputConnectionAdaptor extends BaseInputConnection {
|
||||
|
||||
@Override
|
||||
public boolean performEditorAction(int actionCode) {
|
||||
markDirty();
|
||||
switch (actionCode) {
|
||||
case EditorInfo.IME_ACTION_NONE:
|
||||
textInputChannel.newline(mClient);
|
||||
|
||||
@ -322,6 +322,10 @@ public class TextInputPlugin {
|
||||
}
|
||||
// Always apply state to selection which handles updating the selection if needed.
|
||||
applyStateToSelection(state);
|
||||
InputConnection connection = getLastInputConnection();
|
||||
if (connection != null && connection instanceof InputConnectionAdaptor) {
|
||||
((InputConnectionAdaptor) connection).markDirty();
|
||||
}
|
||||
// Use updateSelection to update imm on selection if it is not neccessary to restart.
|
||||
if (!restartAlwaysRequired && !mRestartInputPending) {
|
||||
mImm.updateSelection(
|
||||
|
||||
@ -270,6 +270,49 @@ public class InputConnectionAdaptorTest {
|
||||
assertEquals(extractedText.selectionEnd, selStart);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void inputConnectionAdaptor_RepeatFilter() throws NullPointerException {
|
||||
View testView = new View(RuntimeEnvironment.application);
|
||||
FlutterJNI mockFlutterJni = mock(FlutterJNI.class);
|
||||
DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJni, mock(AssetManager.class)));
|
||||
int inputTargetId = 0;
|
||||
TestTextInputChannel textInputChannel = new TestTextInputChannel(dartExecutor);
|
||||
Editable mEditable = Editable.Factory.getInstance().newEditable("");
|
||||
Editable spyEditable = spy(mEditable);
|
||||
EditorInfo outAttrs = new EditorInfo();
|
||||
outAttrs.inputType = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_MULTI_LINE;
|
||||
|
||||
InputConnectionAdaptor inputConnectionAdaptor =
|
||||
new InputConnectionAdaptor(
|
||||
testView, inputTargetId, textInputChannel, spyEditable, outAttrs);
|
||||
|
||||
inputConnectionAdaptor.beginBatchEdit();
|
||||
assertEquals(textInputChannel.updateEditingStateInvocations, 0);
|
||||
inputConnectionAdaptor.setComposingText("I do not fear computers. I fear the lack of them.", 1);
|
||||
assertEquals(textInputChannel.text, null);
|
||||
assertEquals(textInputChannel.updateEditingStateInvocations, 0);
|
||||
inputConnectionAdaptor.endBatchEdit();
|
||||
assertEquals(textInputChannel.updateEditingStateInvocations, 1);
|
||||
assertEquals(textInputChannel.text, "I do not fear computers. I fear the lack of them.");
|
||||
|
||||
inputConnectionAdaptor.beginBatchEdit();
|
||||
assertEquals(textInputChannel.updateEditingStateInvocations, 1);
|
||||
inputConnectionAdaptor.endBatchEdit();
|
||||
assertEquals(textInputChannel.updateEditingStateInvocations, 1);
|
||||
|
||||
inputConnectionAdaptor.beginBatchEdit();
|
||||
assertEquals(textInputChannel.text, "I do not fear computers. I fear the lack of them.");
|
||||
assertEquals(textInputChannel.updateEditingStateInvocations, 1);
|
||||
inputConnectionAdaptor.setSelection(3, 4);
|
||||
assertEquals(textInputChannel.updateEditingStateInvocations, 1);
|
||||
assertEquals(textInputChannel.selectionStart, 49);
|
||||
assertEquals(textInputChannel.selectionEnd, 49);
|
||||
inputConnectionAdaptor.endBatchEdit();
|
||||
assertEquals(textInputChannel.updateEditingStateInvocations, 2);
|
||||
assertEquals(textInputChannel.selectionStart, 3);
|
||||
assertEquals(textInputChannel.selectionEnd, 4);
|
||||
}
|
||||
|
||||
private static final String SAMPLE_TEXT =
|
||||
"Lorem ipsum dolor sit amet," + "\nconsectetur adipiscing elit.";
|
||||
|
||||
@ -285,4 +328,35 @@ public class InputConnectionAdaptorTest {
|
||||
TextInputChannel textInputChannel = mock(TextInputChannel.class);
|
||||
return new InputConnectionAdaptor(testView, client, textInputChannel, editable, null);
|
||||
}
|
||||
|
||||
private class TestTextInputChannel extends TextInputChannel {
|
||||
public TestTextInputChannel(DartExecutor dartExecutor) {
|
||||
super(dartExecutor);
|
||||
}
|
||||
|
||||
public int inputClientId;
|
||||
public String text;
|
||||
public int selectionStart;
|
||||
public int selectionEnd;
|
||||
public int composingStart;
|
||||
public int composingEnd;
|
||||
public int updateEditingStateInvocations = 0;
|
||||
|
||||
@Override
|
||||
public void updateEditingState(
|
||||
int inputClientId,
|
||||
String text,
|
||||
int selectionStart,
|
||||
int selectionEnd,
|
||||
int composingStart,
|
||||
int composingEnd) {
|
||||
this.inputClientId = inputClientId;
|
||||
this.text = text;
|
||||
this.selectionStart = selectionStart;
|
||||
this.selectionEnd = selectionEnd;
|
||||
this.composingStart = composingStart;
|
||||
this.composingEnd = composingEnd;
|
||||
updateEditingStateInvocations++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user