Skip to content

Commit

Permalink
Implement repeat filtering logic in Android Embedder (flutter#17509)
Browse files Browse the repository at this point in the history
  • Loading branch information
GaryQian authored Apr 8, 2020
1 parent e7e4633 commit 3ddd1ef
Show file tree
Hide file tree
Showing 3 changed files with 171 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -76,15 +112,42 @@ private void updateEditingState() {
// 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);

// 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, selectionStart, selectionEnd, composingStart, composingEnd);
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
Expand All @@ -109,7 +172,7 @@ public boolean endBatchEdit() {
@Override
public boolean commitText(CharSequence text, int newCursorPosition) {
boolean result = super.commitText(text, newCursorPosition);
updateEditingState();
markDirty();
return result;
}

Expand All @@ -118,14 +181,21 @@ public boolean deleteSurroundingText(int beforeLength, int afterLength) {
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;
}

Expand All @@ -137,7 +207,7 @@ public boolean setComposingText(CharSequence text, int newCursorPosition) {
} else {
result = super.setComposingText(text, newCursorPosition);
}
updateEditingState();
markDirty();
return result;
}

Expand All @@ -159,7 +229,7 @@ public boolean finishComposingText() {
}
}

updateEditingState();
markDirty();
return result;
}

Expand All @@ -173,6 +243,13 @@ public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) {
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.
Expand All @@ -197,7 +274,7 @@ private boolean isSamsung() {
@Override
public boolean setSelection(int start, int end) {
boolean result = super.setSelection(start, end);
updateEditingState();
markDirty();
return result;
}

Expand All @@ -219,6 +296,7 @@ private static int clampIndexToEditable(int index, Editable editable) {

@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);
Expand Down Expand Up @@ -344,6 +422,7 @@ public boolean sendKeyEvent(KeyEvent event) {

@Override
public boolean performContextMenuAction(int id) {
markDirty();
if (id == android.R.id.selectAll) {
setSelection(0, mEditable.length());
return true;
Expand Down Expand Up @@ -397,6 +476,7 @@ public boolean performContextMenuAction(int id) {

@Override
public boolean performEditorAction(int actionCode) {
markDirty();
switch (actionCode) {
case EditorInfo.IME_ACTION_NONE:
textInputChannel.newline(mClient);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,10 @@ void setTextInputEditingState(View view, TextInputChannel.TextEditState state) {
}
// 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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,49 @@ public void testMethod_getExtractedText() {
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.";

Expand All @@ -285,4 +328,35 @@ private static InputConnectionAdaptor sampleInputConnectionAdaptor(Editable edit
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++;
}
}
}

0 comments on commit 3ddd1ef

Please sign in to comment.