Skip to content

Commit 3ddd1ef

Browse files
authored
Implement repeat filtering logic in Android Embedder (flutter#17509)
1 parent e7e4633 commit 3ddd1ef

File tree

3 files changed

+171
-13
lines changed

3 files changed

+171
-13
lines changed

shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java

Lines changed: 93 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,46 @@ class InputConnectionAdaptor extends BaseInputConnection {
3737
private int mBatchCount;
3838
private InputMethodManager mImm;
3939
private final Layout mLayout;
40-
4140
// Used to determine if Samsung-specific hacks should be applied.
4241
private final boolean isSamsung;
4342

43+
private boolean mRepeatCheckNeeded = false;
44+
private TextEditingValue mLastSentTextEditngValue;
45+
// Data class used to get and store the last-sent values via updateEditingState to
46+
// the framework. These are then compared against to prevent redundant messages
47+
// with the same data before any valid operations were made to the contents.
48+
private class TextEditingValue {
49+
public int selectionStart;
50+
public int selectionEnd;
51+
public int composingStart;
52+
public int composingEnd;
53+
public String text;
54+
55+
public TextEditingValue(Editable editable) {
56+
selectionStart = Selection.getSelectionStart(editable);
57+
selectionEnd = Selection.getSelectionEnd(editable);
58+
composingStart = BaseInputConnection.getComposingSpanStart(editable);
59+
composingEnd = BaseInputConnection.getComposingSpanEnd(editable);
60+
text = editable.toString();
61+
}
62+
63+
@Override
64+
public boolean equals(Object o) {
65+
if (o == this) {
66+
return true;
67+
}
68+
if (!(o instanceof TextEditingValue)) {
69+
return false;
70+
}
71+
TextEditingValue value = (TextEditingValue) o;
72+
return selectionStart == value.selectionStart
73+
&& selectionEnd == value.selectionEnd
74+
&& composingStart == value.composingStart
75+
&& composingEnd == value.composingEnd
76+
&& text.equals(value.text);
77+
}
78+
}
79+
4480
@SuppressWarnings("deprecation")
4581
public InputConnectionAdaptor(
4682
View view,
@@ -76,15 +112,42 @@ private void updateEditingState() {
76112
// If the IME is in the middle of a batch edit, then wait until it completes.
77113
if (mBatchCount > 0) return;
78114

79-
int selectionStart = Selection.getSelectionStart(mEditable);
80-
int selectionEnd = Selection.getSelectionEnd(mEditable);
81-
int composingStart = BaseInputConnection.getComposingSpanStart(mEditable);
82-
int composingEnd = BaseInputConnection.getComposingSpanEnd(mEditable);
115+
TextEditingValue currentValue = new TextEditingValue(mEditable);
116+
117+
// Return if this data has already been sent and no meaningful changes have
118+
// occurred to mark this as dirty. This prevents duplicate remote updates of
119+
// the same data, which can break formatters that change the length of the
120+
// contents.
121+
if (mRepeatCheckNeeded && currentValue.equals(mLastSentTextEditngValue)) {
122+
return;
123+
}
83124

84-
mImm.updateSelection(mFlutterView, selectionStart, selectionEnd, composingStart, composingEnd);
125+
mImm.updateSelection(
126+
mFlutterView,
127+
currentValue.selectionStart,
128+
currentValue.selectionEnd,
129+
currentValue.composingStart,
130+
currentValue.composingEnd);
85131

86132
textInputChannel.updateEditingState(
87-
mClient, mEditable.toString(), selectionStart, selectionEnd, composingStart, composingEnd);
133+
mClient,
134+
currentValue.text,
135+
currentValue.selectionStart,
136+
currentValue.selectionEnd,
137+
currentValue.composingStart,
138+
currentValue.composingEnd);
139+
140+
mRepeatCheckNeeded = true;
141+
mLastSentTextEditngValue = currentValue;
142+
}
143+
144+
// This should be called whenever a change could have been made to
145+
// the value of mEditable, which will make any call of updateEditingState()
146+
// ineligible for repeat checking as we do not want to skip sending real changes
147+
// to the framework.
148+
public void markDirty() {
149+
// Disable updateEditngState's repeat-update check
150+
mRepeatCheckNeeded = false;
88151
}
89152

90153
@Override
@@ -109,7 +172,7 @@ public boolean endBatchEdit() {
109172
@Override
110173
public boolean commitText(CharSequence text, int newCursorPosition) {
111174
boolean result = super.commitText(text, newCursorPosition);
112-
updateEditingState();
175+
markDirty();
113176
return result;
114177
}
115178

@@ -118,14 +181,21 @@ public boolean deleteSurroundingText(int beforeLength, int afterLength) {
118181
if (Selection.getSelectionStart(mEditable) == -1) return true;
119182

120183
boolean result = super.deleteSurroundingText(beforeLength, afterLength);
121-
updateEditingState();
184+
markDirty();
185+
return result;
186+
}
187+
188+
@Override
189+
public boolean deleteSurroundingTextInCodePoints(int beforeLength, int afterLength) {
190+
boolean result = super.deleteSurroundingTextInCodePoints(beforeLength, afterLength);
191+
markDirty();
122192
return result;
123193
}
124194

125195
@Override
126196
public boolean setComposingRegion(int start, int end) {
127197
boolean result = super.setComposingRegion(start, end);
128-
updateEditingState();
198+
markDirty();
129199
return result;
130200
}
131201

@@ -137,7 +207,7 @@ public boolean setComposingText(CharSequence text, int newCursorPosition) {
137207
} else {
138208
result = super.setComposingText(text, newCursorPosition);
139209
}
140-
updateEditingState();
210+
markDirty();
141211
return result;
142212
}
143213

@@ -159,7 +229,7 @@ public boolean finishComposingText() {
159229
}
160230
}
161231

162-
updateEditingState();
232+
markDirty();
163233
return result;
164234
}
165235

@@ -173,6 +243,13 @@ public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) {
173243
return extractedText;
174244
}
175245

246+
@Override
247+
public boolean clearMetaKeyStates(int states) {
248+
boolean result = super.clearMetaKeyStates(states);
249+
markDirty();
250+
return result;
251+
}
252+
176253
// Detect if the keyboard is a Samsung keyboard, where we apply Samsung-specific hacks to
177254
// fix critical bugs that make the keyboard otherwise unusable. See finishComposingText() for
178255
// more details.
@@ -197,7 +274,7 @@ private boolean isSamsung() {
197274
@Override
198275
public boolean setSelection(int start, int end) {
199276
boolean result = super.setSelection(start, end);
200-
updateEditingState();
277+
markDirty();
201278
return result;
202279
}
203280

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

220297
@Override
221298
public boolean sendKeyEvent(KeyEvent event) {
299+
markDirty();
222300
if (event.getAction() == KeyEvent.ACTION_DOWN) {
223301
if (event.getKeyCode() == KeyEvent.KEYCODE_DEL) {
224302
int selStart = clampIndexToEditable(Selection.getSelectionStart(mEditable), mEditable);
@@ -344,6 +422,7 @@ public boolean sendKeyEvent(KeyEvent event) {
344422

345423
@Override
346424
public boolean performContextMenuAction(int id) {
425+
markDirty();
347426
if (id == android.R.id.selectAll) {
348427
setSelection(0, mEditable.length());
349428
return true;
@@ -397,6 +476,7 @@ public boolean performContextMenuAction(int id) {
397476

398477
@Override
399478
public boolean performEditorAction(int actionCode) {
479+
markDirty();
400480
switch (actionCode) {
401481
case EditorInfo.IME_ACTION_NONE:
402482
textInputChannel.newline(mClient);

shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,10 @@ void setTextInputEditingState(View view, TextInputChannel.TextEditState state) {
322322
}
323323
// Always apply state to selection which handles updating the selection if needed.
324324
applyStateToSelection(state);
325+
InputConnection connection = getLastInputConnection();
326+
if (connection != null && connection instanceof InputConnectionAdaptor) {
327+
((InputConnectionAdaptor) connection).markDirty();
328+
}
325329
// Use updateSelection to update imm on selection if it is not neccessary to restart.
326330
if (!restartAlwaysRequired && !mRestartInputPending) {
327331
mImm.updateSelection(

shell/platform/android/test/io/flutter/plugin/editing/InputConnectionAdaptorTest.java

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,49 @@ public void testMethod_getExtractedText() {
270270
assertEquals(extractedText.selectionEnd, selStart);
271271
}
272272

273+
@Test
274+
public void inputConnectionAdaptor_RepeatFilter() throws NullPointerException {
275+
View testView = new View(RuntimeEnvironment.application);
276+
FlutterJNI mockFlutterJni = mock(FlutterJNI.class);
277+
DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJni, mock(AssetManager.class)));
278+
int inputTargetId = 0;
279+
TestTextInputChannel textInputChannel = new TestTextInputChannel(dartExecutor);
280+
Editable mEditable = Editable.Factory.getInstance().newEditable("");
281+
Editable spyEditable = spy(mEditable);
282+
EditorInfo outAttrs = new EditorInfo();
283+
outAttrs.inputType = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_MULTI_LINE;
284+
285+
InputConnectionAdaptor inputConnectionAdaptor =
286+
new InputConnectionAdaptor(
287+
testView, inputTargetId, textInputChannel, spyEditable, outAttrs);
288+
289+
inputConnectionAdaptor.beginBatchEdit();
290+
assertEquals(textInputChannel.updateEditingStateInvocations, 0);
291+
inputConnectionAdaptor.setComposingText("I do not fear computers. I fear the lack of them.", 1);
292+
assertEquals(textInputChannel.text, null);
293+
assertEquals(textInputChannel.updateEditingStateInvocations, 0);
294+
inputConnectionAdaptor.endBatchEdit();
295+
assertEquals(textInputChannel.updateEditingStateInvocations, 1);
296+
assertEquals(textInputChannel.text, "I do not fear computers. I fear the lack of them.");
297+
298+
inputConnectionAdaptor.beginBatchEdit();
299+
assertEquals(textInputChannel.updateEditingStateInvocations, 1);
300+
inputConnectionAdaptor.endBatchEdit();
301+
assertEquals(textInputChannel.updateEditingStateInvocations, 1);
302+
303+
inputConnectionAdaptor.beginBatchEdit();
304+
assertEquals(textInputChannel.text, "I do not fear computers. I fear the lack of them.");
305+
assertEquals(textInputChannel.updateEditingStateInvocations, 1);
306+
inputConnectionAdaptor.setSelection(3, 4);
307+
assertEquals(textInputChannel.updateEditingStateInvocations, 1);
308+
assertEquals(textInputChannel.selectionStart, 49);
309+
assertEquals(textInputChannel.selectionEnd, 49);
310+
inputConnectionAdaptor.endBatchEdit();
311+
assertEquals(textInputChannel.updateEditingStateInvocations, 2);
312+
assertEquals(textInputChannel.selectionStart, 3);
313+
assertEquals(textInputChannel.selectionEnd, 4);
314+
}
315+
273316
private static final String SAMPLE_TEXT =
274317
"Lorem ipsum dolor sit amet," + "\nconsectetur adipiscing elit.";
275318

@@ -285,4 +328,35 @@ private static InputConnectionAdaptor sampleInputConnectionAdaptor(Editable edit
285328
TextInputChannel textInputChannel = mock(TextInputChannel.class);
286329
return new InputConnectionAdaptor(testView, client, textInputChannel, editable, null);
287330
}
331+
332+
private class TestTextInputChannel extends TextInputChannel {
333+
public TestTextInputChannel(DartExecutor dartExecutor) {
334+
super(dartExecutor);
335+
}
336+
337+
public int inputClientId;
338+
public String text;
339+
public int selectionStart;
340+
public int selectionEnd;
341+
public int composingStart;
342+
public int composingEnd;
343+
public int updateEditingStateInvocations = 0;
344+
345+
@Override
346+
public void updateEditingState(
347+
int inputClientId,
348+
String text,
349+
int selectionStart,
350+
int selectionEnd,
351+
int composingStart,
352+
int composingEnd) {
353+
this.inputClientId = inputClientId;
354+
this.text = text;
355+
this.selectionStart = selectionStart;
356+
this.selectionEnd = selectionEnd;
357+
this.composingStart = composingStart;
358+
this.composingEnd = composingEnd;
359+
updateEditingStateInvocations++;
360+
}
361+
}
288362
}

0 commit comments

Comments
 (0)