Skip to content

Commit 3da13fc

Browse files
authored
Make android more lenient when it comes to out-of-order key event responses (flutter#23604)
Relaxes enforcement of key events being handled in order, to match similar code in the Linux and Windows implementations.
1 parent 1474d08 commit 3da13fc

File tree

4 files changed

+147
-31
lines changed

4 files changed

+147
-31
lines changed

shell/platform/android/io/flutter/embedding/android/AndroidKeyProcessor.java

Lines changed: 21 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import io.flutter.plugin.editing.TextInputPlugin;
1515
import java.util.ArrayDeque;
1616
import java.util.Deque;
17+
import java.util.Iterator;
1718

1819
/**
1920
* A class to process key events from Android, passing them to the framework as messages using
@@ -93,11 +94,11 @@ public boolean onKeyEvent(@NonNull KeyEvent keyEvent) {
9394
// case the theory is wrong.
9495
return false;
9596
}
96-
if (eventResponder.isHeadEvent(keyEvent)) {
97-
// If the keyEvent is at the head of the queue of pending events we've seen,
98-
// and has the same id, then we know that this is a re-dispatched keyEvent, and
99-
// we shouldn't respond to it, but we should remove it from tracking now.
100-
eventResponder.removeHeadEvent();
97+
if (isPendingEvent(keyEvent)) {
98+
// If the keyEvent is in the queue of pending events we've seen, and has
99+
// the same id, then we know that this is a re-dispatched keyEvent, and we
100+
// shouldn't respond to it, but we should remove it from tracking now.
101+
eventResponder.removePendingEvent(keyEvent);
101102
return false;
102103
}
103104

@@ -122,8 +123,8 @@ public boolean onKeyEvent(@NonNull KeyEvent keyEvent) {
122123
* @param event the event to check for being the current event.
123124
* @return
124125
*/
125-
public boolean isCurrentEvent(@NonNull KeyEvent event) {
126-
return eventResponder.isHeadEvent(event);
126+
public boolean isPendingEvent(@NonNull KeyEvent event) {
127+
return eventResponder.findPendingEvent(event) != null;
127128
}
128129

129130
/**
@@ -199,27 +200,19 @@ public EventResponder(@NonNull View view, @NonNull TextInputPlugin textInputPlug
199200
}
200201

201202
/** Removes the first pending event from the cache of pending events. */
202-
private KeyEvent removeHeadEvent() {
203-
return pendingEvents.removeFirst();
203+
private void removePendingEvent(KeyEvent event) {
204+
pendingEvents.remove(event);
204205
}
205206

206-
private KeyEvent checkIsHeadEvent(KeyEvent event) {
207-
if (pendingEvents.size() == 0) {
208-
throw new AssertionError(
209-
"Event response received when no events are in the queue. Received event " + event);
210-
}
211-
if (pendingEvents.getFirst() != event) {
212-
throw new AssertionError(
213-
"Event response received out of order. Should have seen event "
214-
+ pendingEvents.getFirst()
215-
+ " first. Instead, received "
216-
+ event);
207+
private KeyEvent findPendingEvent(KeyEvent event) {
208+
Iterator<KeyEvent> iter = pendingEvents.iterator();
209+
while (iter.hasNext()) {
210+
KeyEvent item = iter.next();
211+
if (item == event) {
212+
return item;
213+
}
217214
}
218-
return pendingEvents.getFirst();
219-
}
220-
221-
private boolean isHeadEvent(KeyEvent event) {
222-
return pendingEvents.size() > 0 && pendingEvents.getFirst() == event;
215+
return null;
223216
}
224217

225218
/**
@@ -229,7 +222,7 @@ private boolean isHeadEvent(KeyEvent event) {
229222
*/
230223
@Override
231224
public void onKeyEventHandled(KeyEvent event) {
232-
removeHeadEvent();
225+
removePendingEvent(event);
233226
}
234227

235228
/**
@@ -240,7 +233,7 @@ public void onKeyEventHandled(KeyEvent event) {
240233
*/
241234
@Override
242235
public void onKeyEventNotHandled(KeyEvent event) {
243-
redispatchKeyEvent(checkIsHeadEvent(event));
236+
redispatchKeyEvent(findPendingEvent(event));
244237
}
245238

246239
/** Adds an Android key event to the event responder to wait for a response. */
@@ -269,7 +262,7 @@ private void redispatchKeyEvent(KeyEvent event) {
269262
&& textInputPlugin.getLastInputConnection() != null
270263
&& textInputPlugin.getLastInputConnection().sendKeyEvent(event)) {
271264
// The event was handled, so we can remove it from the queue.
272-
removeHeadEvent();
265+
removePendingEvent(event);
273266
return;
274267
}
275268

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,7 @@ public boolean sendKeyEvent(KeyEvent event) {
299299
// already know about (i.e. when events arrive here from a soft keyboard and
300300
// not a hardware keyboard), to avoid a loop.
301301
if (keyProcessor != null
302-
&& !keyProcessor.isCurrentEvent(event)
302+
&& !keyProcessor.isPendingEvent(event)
303303
&& keyProcessor.onKeyEvent(event)) {
304304
return true;
305305
}

shell/platform/android/test/io/flutter/embedding/android/AndroidKeyProcessorTest.java

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,54 @@ public void destroyTest() {
7474
.setEventResponseHandler(isNull(KeyEventChannel.EventResponseHandler.class));
7575
}
7676

77+
public void removesPendingEventsWhenKeyDownHandled() {
78+
FlutterEngine flutterEngine = mockFlutterEngine();
79+
KeyEventChannel fakeKeyEventChannel = flutterEngine.getKeyEventChannel();
80+
View fakeView = mock(View.class);
81+
View fakeRootView = mock(View.class);
82+
when(fakeView.getRootView())
83+
.then(
84+
new Answer<View>() {
85+
@Override
86+
public View answer(InvocationOnMock invocation) throws Throwable {
87+
return fakeRootView;
88+
}
89+
});
90+
91+
ArgumentCaptor<KeyEventChannel.EventResponseHandler> handlerCaptor =
92+
ArgumentCaptor.forClass(KeyEventChannel.EventResponseHandler.class);
93+
verify(fakeKeyEventChannel).setEventResponseHandler(handlerCaptor.capture());
94+
AndroidKeyProcessor processor =
95+
new AndroidKeyProcessor(fakeView, fakeKeyEventChannel, mock(TextInputPlugin.class));
96+
ArgumentCaptor<KeyEventChannel.FlutterKeyEvent> eventCaptor =
97+
ArgumentCaptor.forClass(KeyEventChannel.FlutterKeyEvent.class);
98+
FakeKeyEvent fakeKeyEvent = new FakeKeyEvent(KeyEvent.ACTION_DOWN, 65);
99+
100+
boolean result = processor.onKeyEvent(fakeKeyEvent);
101+
assertEquals(true, processor.isPendingEvent(fakeKeyEvent));
102+
assertEquals(true, result);
103+
104+
// Capture the FlutterKeyEvent so we can find out its event ID to use when
105+
// faking our response.
106+
verify(fakeKeyEventChannel, times(1)).keyDown(eventCaptor.capture());
107+
boolean[] dispatchResult = {true};
108+
when(fakeView.dispatchKeyEvent(any(KeyEvent.class)))
109+
.then(
110+
new Answer<Boolean>() {
111+
@Override
112+
public Boolean answer(InvocationOnMock invocation) throws Throwable {
113+
KeyEvent event = (KeyEvent) invocation.getArguments()[0];
114+
assertEquals(fakeKeyEvent, event);
115+
dispatchResult[0] = processor.onKeyEvent(event);
116+
return dispatchResult[0];
117+
}
118+
});
119+
120+
// Fake a response from the framework.
121+
handlerCaptor.getValue().onKeyEventHandled(eventCaptor.getValue().event);
122+
assertEquals(false, processor.isPendingEvent(fakeKeyEvent));
123+
}
124+
77125
public void synthesizesEventsWhenKeyDownNotHandled() {
78126
FlutterEngine flutterEngine = mockFlutterEngine();
79127
KeyEventChannel fakeKeyEventChannel = flutterEngine.getKeyEventChannel();
@@ -98,6 +146,7 @@ public View answer(InvocationOnMock invocation) throws Throwable {
98146
FakeKeyEvent fakeKeyEvent = new FakeKeyEvent(KeyEvent.ACTION_DOWN, 65);
99147

100148
boolean result = processor.onKeyEvent(fakeKeyEvent);
149+
assertEquals(true, processor.isPendingEvent(fakeKeyEvent));
101150
assertEquals(true, result);
102151

103152
// Capture the FlutterKeyEvent so we can find out its event ID to use when
@@ -118,6 +167,7 @@ public Boolean answer(InvocationOnMock invocation) throws Throwable {
118167

119168
// Fake a response from the framework.
120169
handlerCaptor.getValue().onKeyEventNotHandled(eventCaptor.getValue().event);
170+
assertEquals(true, processor.isPendingEvent(fakeKeyEvent));
121171
verify(fakeView, times(1)).dispatchKeyEvent(fakeKeyEvent);
122172
assertEquals(false, dispatchResult[0]);
123173
verify(fakeKeyEventChannel, times(0)).keyUp(any(KeyEventChannel.FlutterKeyEvent.class));
@@ -148,6 +198,7 @@ public View answer(InvocationOnMock invocation) throws Throwable {
148198
FakeKeyEvent fakeKeyEvent = new FakeKeyEvent(KeyEvent.ACTION_UP, 65);
149199

150200
boolean result = processor.onKeyEvent(fakeKeyEvent);
201+
assertEquals(true, processor.isPendingEvent(fakeKeyEvent));
151202
assertEquals(true, result);
152203

153204
// Capture the FlutterKeyEvent so we can find out its event ID to use when
@@ -168,12 +219,84 @@ public Boolean answer(InvocationOnMock invocation) throws Throwable {
168219

169220
// Fake a response from the framework.
170221
handlerCaptor.getValue().onKeyEventNotHandled(eventCaptor.getValue().event);
222+
assertEquals(true, processor.isPendingEvent(fakeKeyEvent));
171223
verify(fakeView, times(1)).dispatchKeyEvent(fakeKeyEvent);
172224
assertEquals(false, dispatchResult[0]);
173225
verify(fakeKeyEventChannel, times(0)).keyUp(any(KeyEventChannel.FlutterKeyEvent.class));
174226
verify(fakeRootView, times(1)).dispatchKeyEvent(fakeKeyEvent);
175227
}
176228

229+
public void respondsCorrectlyWhenEventsAreReturnedOutOfOrder() {
230+
FlutterEngine flutterEngine = mockFlutterEngine();
231+
KeyEventChannel fakeKeyEventChannel = flutterEngine.getKeyEventChannel();
232+
View fakeView = mock(View.class);
233+
View fakeRootView = mock(View.class);
234+
when(fakeView.getRootView())
235+
.then(
236+
new Answer<View>() {
237+
@Override
238+
public View answer(InvocationOnMock invocation) throws Throwable {
239+
return fakeRootView;
240+
}
241+
});
242+
243+
ArgumentCaptor<KeyEventChannel.EventResponseHandler> handlerCaptor =
244+
ArgumentCaptor.forClass(KeyEventChannel.EventResponseHandler.class);
245+
verify(fakeKeyEventChannel).setEventResponseHandler(handlerCaptor.capture());
246+
AndroidKeyProcessor processor =
247+
new AndroidKeyProcessor(fakeView, fakeKeyEventChannel, mock(TextInputPlugin.class));
248+
ArgumentCaptor<KeyEventChannel.FlutterKeyEvent> event1Captor =
249+
ArgumentCaptor.forClass(KeyEventChannel.FlutterKeyEvent.class);
250+
ArgumentCaptor<KeyEventChannel.FlutterKeyEvent> event2Captor =
251+
ArgumentCaptor.forClass(KeyEventChannel.FlutterKeyEvent.class);
252+
FakeKeyEvent fakeKeyEvent1 = new FakeKeyEvent(KeyEvent.ACTION_DOWN, 65);
253+
FakeKeyEvent fakeKeyEvent2 = new FakeKeyEvent(KeyEvent.ACTION_DOWN, 20);
254+
255+
boolean result1 = processor.onKeyEvent(fakeKeyEvent1);
256+
boolean result2 = processor.onKeyEvent(fakeKeyEvent2);
257+
assertEquals(true, processor.isPendingEvent(fakeKeyEvent1));
258+
assertEquals(true, processor.isPendingEvent(fakeKeyEvent2));
259+
assertEquals(true, result1);
260+
assertEquals(true, result2);
261+
262+
// Capture the FlutterKeyEvent so we can find out its event ID to use when
263+
// faking our response.
264+
verify(fakeKeyEventChannel, times(1)).keyDown(event1Captor.capture());
265+
verify(fakeKeyEventChannel, times(1)).keyDown(event2Captor.capture());
266+
boolean[] dispatchResult = {true, true};
267+
when(fakeView.dispatchKeyEvent(any(KeyEvent.class)))
268+
.then(
269+
new Answer<Boolean>() {
270+
@Override
271+
public Boolean answer(InvocationOnMock invocation) throws Throwable {
272+
KeyEvent event = (KeyEvent) invocation.getArguments()[0];
273+
assertEquals(true, fakeKeyEvent1 == event || fakeKeyEvent2 == event);
274+
if (fakeKeyEvent1 == event) {
275+
dispatchResult[0] = processor.onKeyEvent(fakeKeyEvent1);
276+
return dispatchResult[0];
277+
} else {
278+
dispatchResult[1] = processor.onKeyEvent(fakeKeyEvent2);
279+
return dispatchResult[1];
280+
}
281+
}
282+
});
283+
284+
assertEquals(true, processor.isPendingEvent(fakeKeyEvent1));
285+
assertEquals(true, processor.isPendingEvent(fakeKeyEvent2));
286+
287+
// Fake a "handled" response from the framework, but do it in reverse order.
288+
handlerCaptor.getValue().onKeyEventNotHandled(event2Captor.getValue().event);
289+
handlerCaptor.getValue().onKeyEventNotHandled(event1Captor.getValue().event);
290+
291+
verify(fakeView, times(1)).dispatchKeyEvent(fakeKeyEvent1);
292+
verify(fakeView, times(1)).dispatchKeyEvent(fakeKeyEvent2);
293+
assertEquals(false, dispatchResult[0]);
294+
assertEquals(false, dispatchResult[1]);
295+
verify(fakeKeyEventChannel, times(0)).keyUp(any(KeyEventChannel.FlutterKeyEvent.class));
296+
verify(fakeRootView, times(1)).dispatchKeyEvent(fakeKeyEvent1);
297+
verify(fakeRootView, times(1)).dispatchKeyEvent(fakeKeyEvent2);
298+
}
299+
177300
@NonNull
178301
private FlutterEngine mockFlutterEngine() {
179302
// Mock FlutterEngine and all of its required direct calls.

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1033,7 +1033,7 @@ public void testCursorAnchorInfo() {
10331033
public void testSendKeyEvent_sendSoftKeyEvents() {
10341034
ListenableEditingState editable = sampleEditable(5, 5);
10351035
AndroidKeyProcessor mockKeyProcessor = mock(AndroidKeyProcessor.class);
1036-
when(mockKeyProcessor.isCurrentEvent(any())).thenReturn(true);
1036+
when(mockKeyProcessor.isPendingEvent(any())).thenReturn(true);
10371037
InputConnectionAdaptor adaptor = sampleInputConnectionAdaptor(editable, mockKeyProcessor);
10381038

10391039
KeyEvent shiftKeyDown = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_SHIFT_LEFT);
@@ -1047,7 +1047,7 @@ public void testSendKeyEvent_sendSoftKeyEvents() {
10471047
public void testSendKeyEvent_sendHardwareKeyEvents() {
10481048
ListenableEditingState editable = sampleEditable(5, 5);
10491049
AndroidKeyProcessor mockKeyProcessor = mock(AndroidKeyProcessor.class);
1050-
when(mockKeyProcessor.isCurrentEvent(any())).thenReturn(false);
1050+
when(mockKeyProcessor.isPendingEvent(any())).thenReturn(false);
10511051
when(mockKeyProcessor.onKeyEvent(any())).thenReturn(true);
10521052
InputConnectionAdaptor adaptor = sampleInputConnectionAdaptor(editable, mockKeyProcessor);
10531053

0 commit comments

Comments
 (0)