Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit 78b567f

Browse files
authored
Reland: "Fix race condition in key event handling on Android (#22658)" (#22834)
1 parent 24d289e commit 78b567f

File tree

8 files changed

+183
-250
lines changed

8 files changed

+183
-250
lines changed

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

Lines changed: 57 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,8 @@
1212
import io.flutter.Log;
1313
import io.flutter.embedding.engine.systemchannels.KeyEventChannel;
1414
import io.flutter.plugin.editing.TextInputPlugin;
15-
import java.util.AbstractMap.SimpleImmutableEntry;
1615
import java.util.ArrayDeque;
1716
import java.util.Deque;
18-
import java.util.Map.Entry;
1917

2018
/**
2119
* A class to process key events from Android, passing them to the framework as messages using
@@ -33,7 +31,6 @@
3331
*/
3432
public class AndroidKeyProcessor {
3533
private static final String TAG = "AndroidKeyProcessor";
36-
private static long eventIdSerial = 0;
3734

3835
@NonNull private final KeyEventChannel keyEventChannel;
3936
@NonNull private final TextInputPlugin textInputPlugin;
@@ -50,8 +47,8 @@ public class AndroidKeyProcessor {
5047
* <p>It is possible that that in the middle of the async round trip, the focus chain could
5148
* change, and instead of the native widget that was "next" when the event was fired getting the
5249
* event, it may be the next widget when the event is synthesized that gets it. In practice, this
53-
* shouldn't be a huge problem, as this is an unlikely occurance to happen without user input, and
54-
* it may actually be desired behavior, but it is possible.
50+
* shouldn't be a huge problem, as this is an unlikely occurrence to happen without user input,
51+
* and it may actually be desired behavior, but it is possible.
5552
*
5653
* @param view takes the activity to use for re-dispatching of events that were not handled by the
5754
* framework.
@@ -96,23 +93,39 @@ public boolean onKeyEvent(@NonNull KeyEvent keyEvent) {
9693
// case the theory is wrong.
9794
return false;
9895
}
99-
if (eventResponder.dispatchingKeyEvent) {
100-
// Don't handle it if it is from our own delayed event dispatch.
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();
101101
return false;
102102
}
103103

104104
Character complexCharacter = applyCombiningCharacterToBaseCharacter(keyEvent.getUnicodeChar());
105105
KeyEventChannel.FlutterKeyEvent flutterEvent =
106-
new KeyEventChannel.FlutterKeyEvent(keyEvent, complexCharacter, eventIdSerial++);
106+
new KeyEventChannel.FlutterKeyEvent(keyEvent, complexCharacter);
107+
108+
eventResponder.addEvent(keyEvent);
107109
if (action == KeyEvent.ACTION_DOWN) {
108110
keyEventChannel.keyDown(flutterEvent);
109111
} else {
110112
keyEventChannel.keyUp(flutterEvent);
111113
}
112-
eventResponder.addEvent(flutterEvent.eventId, keyEvent);
113114
return true;
114115
}
115116

117+
/**
118+
* Returns whether or not the given event is currently being processed by this key processor. This
119+
* is used to determine if a new key event sent to the {@link InputConnectionAdaptor} originates
120+
* from a hardware key event, or a soft keyboard editing event.
121+
*
122+
* @param event the event to check for being the current event.
123+
* @return
124+
*/
125+
public boolean isCurrentEvent(@NonNull KeyEvent event) {
126+
return eventResponder.isHeadEvent(event);
127+
}
128+
116129
/**
117130
* Applies the given Unicode character in {@code newCharacterCodePoint} to a previously entered
118131
* Unicode combining character and returns the combination of these characters if a combination
@@ -176,65 +189,63 @@ private static class EventResponder implements KeyEventChannel.EventResponseHand
176189
// The maximum number of pending events that are held before starting to
177190
// complain.
178191
private static final long MAX_PENDING_EVENTS = 1000;
179-
final Deque<Entry<Long, KeyEvent>> pendingEvents = new ArrayDeque<Entry<Long, KeyEvent>>();
192+
final Deque<KeyEvent> pendingEvents = new ArrayDeque<KeyEvent>();
180193
@NonNull private final View view;
181194
@NonNull private final TextInputPlugin textInputPlugin;
182-
boolean dispatchingKeyEvent = false;
183195

184196
public EventResponder(@NonNull View view, @NonNull TextInputPlugin textInputPlugin) {
185197
this.view = view;
186198
this.textInputPlugin = textInputPlugin;
187199
}
188200

189-
/**
190-
* Removes the pending event with the given id from the cache of pending events.
191-
*
192-
* @param id the id of the event to be removed.
193-
*/
194-
private KeyEvent removePendingEvent(long id) {
195-
if (pendingEvents.getFirst().getKey() != id) {
201+
/** Removes the first pending event from the cache of pending events. */
202+
private KeyEvent removeHeadEvent() {
203+
return pendingEvents.removeFirst();
204+
}
205+
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) {
196212
throw new AssertionError(
197213
"Event response received out of order. Should have seen event "
198-
+ pendingEvents.getFirst().getKey()
214+
+ pendingEvents.getFirst()
199215
+ " first. Instead, received "
200-
+ id);
216+
+ event);
201217
}
202-
return pendingEvents.removeFirst().getValue();
218+
return pendingEvents.getFirst();
219+
}
220+
221+
private boolean isHeadEvent(KeyEvent event) {
222+
return pendingEvents.size() > 0 && pendingEvents.getFirst() == event;
203223
}
204224

205225
/**
206226
* Called whenever the framework responds that a given key event was handled by the framework.
207227
*
208-
* @param id the event id of the event to be marked as being handled by the framework. Must not
209-
* be null.
228+
* @param event the event to be marked as being handled by the framework. Must not be null.
210229
*/
211230
@Override
212-
public void onKeyEventHandled(long id) {
213-
removePendingEvent(id);
231+
public void onKeyEventHandled(KeyEvent event) {
232+
removeHeadEvent();
214233
}
215234

216235
/**
217236
* Called whenever the framework responds that a given key event wasn't handled by the
218237
* framework.
219238
*
220-
* @param id the event id of the event to be marked as not being handled by the framework. Must
221-
* not be null.
239+
* @param event the event to be marked as not being handled by the framework. Must not be null.
222240
*/
223241
@Override
224-
public void onKeyEventNotHandled(long id) {
225-
dispatchKeyEvent(removePendingEvent(id));
242+
public void onKeyEventNotHandled(KeyEvent event) {
243+
redispatchKeyEvent(checkIsHeadEvent(event));
226244
}
227245

228-
/** Adds an Android key event with an id to the event responder to wait for a response. */
229-
public void addEvent(long id, @NonNull KeyEvent event) {
230-
if (pendingEvents.size() > 0 && pendingEvents.getFirst().getKey() >= id) {
231-
throw new AssertionError(
232-
"New events must have ids greater than the most recent pending event. New id "
233-
+ id
234-
+ " is less than or equal to the last event id of "
235-
+ pendingEvents.getFirst().getKey());
236-
}
237-
pendingEvents.addLast(new SimpleImmutableEntry<Long, KeyEvent>(id, event));
246+
/** Adds an Android key event to the event responder to wait for a response. */
247+
public void addEvent(@NonNull KeyEvent event) {
248+
pendingEvents.addLast(event);
238249
if (pendingEvents.size() > MAX_PENDING_EVENTS) {
239250
Log.e(
240251
TAG,
@@ -250,27 +261,21 @@ public void addEvent(long id, @NonNull KeyEvent event) {
250261
*
251262
* @param event the event to be dispatched to the activity.
252263
*/
253-
public void dispatchKeyEvent(KeyEvent event) {
264+
private void redispatchKeyEvent(KeyEvent event) {
254265
// If the textInputPlugin is still valid and accepting text, then we'll try
255266
// and send the key event to it, assuming that if the event can be sent,
256267
// that it has been handled.
257-
if (textInputPlugin.getLastInputConnection() != null
258-
&& textInputPlugin.getInputMethodManager().isAcceptingText()) {
259-
dispatchingKeyEvent = true;
260-
boolean handled = textInputPlugin.getLastInputConnection().sendKeyEvent(event);
261-
dispatchingKeyEvent = false;
262-
if (handled) {
263-
return;
264-
}
268+
if (textInputPlugin.getInputMethodManager().isAcceptingText()
269+
&& textInputPlugin.getLastInputConnection() != null
270+
&& textInputPlugin.getLastInputConnection().sendKeyEvent(event)) {
271+
// The event was handled, so we can remove it from the queue.
272+
removeHeadEvent();
273+
return;
265274
}
266275

267276
// Since the framework didn't handle it, dispatch the event again.
268277
if (view != null) {
269-
// Turn on dispatchingKeyEvent so that we don't dispatch to ourselves and
270-
// send it to the framework again.
271-
dispatchingKeyEvent = true;
272278
view.getRootView().dispatchKeyEvent(event);
273-
dispatchingKeyEvent = false;
274279
}
275280
}
276281
}

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

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -739,11 +739,6 @@ public boolean dispatchKeyEvent(KeyEvent event) {
739739
} else if (event.getAction() == KeyEvent.ACTION_UP) {
740740
// Stop tracking the event.
741741
getKeyDispatcherState().handleUpEvent(event);
742-
if (!event.isTracking() || event.isCanceled()) {
743-
// Don't send the event to the key processor if it was canceled, or no
744-
// longer being tracked.
745-
return super.dispatchKeyEvent(event);
746-
}
747742
}
748743
// If the key processor doesn't handle it, then send it on to the
749744
// superclass. The key processor will typically handle all events except

0 commit comments

Comments
 (0)