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

Commit 83ae776

Browse files
committed
Fix race condition in key event handling on Android.
1 parent 020efea commit 83ae776

File tree

7 files changed

+128
-62
lines changed

7 files changed

+128
-62
lines changed

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

Lines changed: 56 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import androidx.annotation.Nullable;
1212
import io.flutter.Log;
1313
import io.flutter.embedding.engine.systemchannels.KeyEventChannel;
14+
import io.flutter.embedding.engine.systemchannels.KeyEventChannel.FlutterKeyEvent;
1415
import io.flutter.plugin.editing.TextInputPlugin;
1516
import java.util.AbstractMap.SimpleImmutableEntry;
1617
import java.util.ArrayDeque;
@@ -33,7 +34,6 @@
3334
*/
3435
public class AndroidKeyProcessor {
3536
private static final String TAG = "AndroidKeyProcessor";
36-
private static long eventIdSerial = 0;
3737

3838
@NonNull private final KeyEventChannel keyEventChannel;
3939
@NonNull private final TextInputPlugin textInputPlugin;
@@ -50,8 +50,8 @@ public class AndroidKeyProcessor {
5050
* <p>It is possible that that in the middle of the async round trip, the focus chain could
5151
* change, and instead of the native widget that was "next" when the event was fired getting the
5252
* 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.
53+
* shouldn't be a huge problem, as this is an unlikely occurrence to happen without user input,
54+
* and it may actually be desired behavior, but it is possible.
5555
*
5656
* @param view takes the activity to use for re-dispatching of events that were not handled by the
5757
* framework.
@@ -88,31 +88,49 @@ public void destroy() {
8888
* @return true if the key event should not be propagated to other Android components. Delayed
8989
* synthesis events will return false, so that other components may handle them.
9090
*/
91-
public boolean onKeyEvent(@NonNull KeyEvent keyEvent) {
92-
int action = keyEvent.getAction();
93-
if (action != KeyEvent.ACTION_DOWN && action != KeyEvent.ACTION_UP) {
91+
public boolean onKeyEvent(@NonNull KeyEvent event) {
92+
int action = event.getAction();
93+
if (action != event.ACTION_DOWN && action != event.ACTION_UP) {
9494
// There is theoretically a KeyEvent.ACTION_MULTIPLE, but theoretically
9595
// that isn't sent by Android anymore, so this is just for protection in
9696
// case the theory is wrong.
9797
return false;
9898
}
99-
if (eventResponder.dispatchingKeyEvent) {
100-
// Don't handle it if it is from our own delayed event dispatch.
99+
long eventId = FlutterKeyEvent.computeEventId(event);
100+
if (eventResponder.isHeadEvent(eventId)) {
101+
// If the event is at the head of the queue of pending events we've seen,
102+
// and has the same id, then we know that this is a re-dispatched event, and
103+
// we shouldn't respond to it, but we should remove it from tracking now.
104+
eventResponder.removePendingEvent(eventId);
101105
return false;
102106
}
103107

104-
Character complexCharacter = applyCombiningCharacterToBaseCharacter(keyEvent.getUnicodeChar());
108+
Character complexCharacter = applyCombiningCharacterToBaseCharacter(event.getUnicodeChar());
105109
KeyEventChannel.FlutterKeyEvent flutterEvent =
106-
new KeyEventChannel.FlutterKeyEvent(keyEvent, complexCharacter, eventIdSerial++);
110+
new KeyEventChannel.FlutterKeyEvent(event, complexCharacter);
111+
112+
eventResponder.addEvent(flutterEvent.eventId, event);
107113
if (action == KeyEvent.ACTION_DOWN) {
108114
keyEventChannel.keyDown(flutterEvent);
109115
} else {
110116
keyEventChannel.keyUp(flutterEvent);
111117
}
112-
eventResponder.addEvent(flutterEvent.eventId, keyEvent);
113118
return true;
114119
}
115120

121+
/**
122+
* Returns whether or not the given event is currently being processed by this key processor. This
123+
* is used to determine if a new key event sent to the {@link InputConnectionAdaptor} originates
124+
* from a hardware key event, or a soft keyboard editing event.
125+
*
126+
* @param event the event to check for being the current event.
127+
* @return
128+
*/
129+
public boolean isCurrentEvent(@NonNull KeyEvent event) {
130+
long id = FlutterKeyEvent.computeEventId(event);
131+
return eventResponder.isHeadEvent(id);
132+
}
133+
116134
/**
117135
* Applies the given Unicode character in {@code newCharacterCodePoint} to a previously entered
118136
* Unicode combining character and returns the combination of these characters if a combination
@@ -179,7 +197,6 @@ private static class EventResponder implements KeyEventChannel.EventResponseHand
179197
final Deque<Entry<Long, KeyEvent>> pendingEvents = new ArrayDeque<Entry<Long, KeyEvent>>();
180198
@NonNull private final View view;
181199
@NonNull private final TextInputPlugin textInputPlugin;
182-
boolean dispatchingKeyEvent = false;
183200

184201
public EventResponder(@NonNull View view, @NonNull TextInputPlugin textInputPlugin) {
185202
this.view = view;
@@ -202,6 +219,25 @@ private KeyEvent removePendingEvent(long id) {
202219
return pendingEvents.removeFirst().getValue();
203220
}
204221

222+
private KeyEvent findPendingEvent(long id) {
223+
if (pendingEvents.size() == 0) {
224+
throw new AssertionError(
225+
"Event response received when no events are in the queue. Received id " + id);
226+
}
227+
if (pendingEvents.getFirst().getKey() != id) {
228+
throw new AssertionError(
229+
"Event response received out of order. Should have seen event "
230+
+ pendingEvents.getFirst().getKey()
231+
+ " first. Instead, received "
232+
+ id);
233+
}
234+
return pendingEvents.getFirst().getValue();
235+
}
236+
237+
private boolean isHeadEvent(long id) {
238+
return pendingEvents.size() > 0 && pendingEvents.getFirst().getKey() == id;
239+
}
240+
205241
/**
206242
* Called whenever the framework responds that a given key event was handled by the framework.
207243
*
@@ -222,18 +258,11 @@ public void onKeyEventHandled(long id) {
222258
*/
223259
@Override
224260
public void onKeyEventNotHandled(long id) {
225-
dispatchKeyEvent(removePendingEvent(id));
261+
dispatchKeyEvent(findPendingEvent(id), id);
226262
}
227263

228264
/** Adds an Android key event with an id to the event responder to wait for a response. */
229265
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-
}
237266
pendingEvents.addLast(new SimpleImmutableEntry<Long, KeyEvent>(id, event));
238267
if (pendingEvents.size() > MAX_PENDING_EVENTS) {
239268
Log.e(
@@ -250,27 +279,21 @@ public void addEvent(long id, @NonNull KeyEvent event) {
250279
*
251280
* @param event the event to be dispatched to the activity.
252281
*/
253-
public void dispatchKeyEvent(KeyEvent event) {
282+
public void dispatchKeyEvent(KeyEvent event, long id) {
254283
// If the textInputPlugin is still valid and accepting text, then we'll try
255284
// and send the key event to it, assuming that if the event can be sent,
256285
// 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-
}
286+
if (textInputPlugin.getInputMethodManager().isAcceptingText()
287+
&& textInputPlugin.getLastInputConnection() != null
288+
&& textInputPlugin.getLastInputConnection().sendKeyEvent(event)) {
289+
// The event was handled, so we can remove it from the queue.
290+
removePendingEvent(id);
291+
return;
265292
}
266293

267294
// Since the framework didn't handle it, dispatch the event again.
268295
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;
272296
view.getRootView().dispatchKeyEvent(event);
273-
dispatchingKeyEvent = false;
274297
}
275298
}
276299
}

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

shell/platform/android/io/flutter/embedding/engine/systemchannels/KeyEventChannel.java

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -225,16 +225,29 @@ public static class FlutterKeyEvent {
225225
* The unique id for this Flutter key event.
226226
*
227227
* <p>This id is used to identify pending events when results are received from the framework.
228-
* This ID does not come from Android.
228+
* This ID does not come from Android, but instead is derived from attributes of this event that
229+
* can be recognized if the event is re-dispatched.
229230
*/
230231
public final long eventId;
231232

232-
public FlutterKeyEvent(@NonNull KeyEvent androidKeyEvent, long eventId) {
233-
this(androidKeyEvent, null, eventId);
233+
/**
234+
* Creates a unique id for a Flutter key event from an Android KeyEvent.
235+
*
236+
* <p>This id is used to identify pending key events when results are received from the
237+
* framework.
238+
*/
239+
public static long computeEventId(@NonNull KeyEvent event) {
240+
return (event.getEventTime() & 0xffffffff)
241+
| ((long) (event.getAction() == KeyEvent.ACTION_DOWN ? 0x1 : 0x0)) << 32
242+
| ((long) (event.getScanCode() & 0xffff)) << 33;
243+
}
244+
245+
public FlutterKeyEvent(@NonNull KeyEvent androidKeyEvent) {
246+
this(androidKeyEvent, null);
234247
}
235248

236249
public FlutterKeyEvent(
237-
@NonNull KeyEvent androidKeyEvent, @Nullable Character complexCharacter, long eventId) {
250+
@NonNull KeyEvent androidKeyEvent, @Nullable Character complexCharacter) {
238251
this(
239252
androidKeyEvent.getDeviceId(),
240253
androidKeyEvent.getFlags(),
@@ -246,7 +259,7 @@ public FlutterKeyEvent(
246259
androidKeyEvent.getMetaState(),
247260
androidKeyEvent.getSource(),
248261
androidKeyEvent.getRepeatCount(),
249-
eventId);
262+
computeEventId(androidKeyEvent));
250263
}
251264

252265
public FlutterKeyEvent(

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

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -285,13 +285,22 @@ private static int clampIndexToEditable(int index, Editable editable) {
285285
return clamped;
286286
}
287287

288+
// This function is called both when hardware key events occur and aren't
289+
// handled by the framework, as well as when soft keyboard editing events
290+
// occur, and need a chance to be handled by the framework.
288291
@Override
289292
public boolean sendKeyEvent(KeyEvent event) {
290-
// Give the key processor a chance to process this event. It will send it
291-
// to the framework to be handled and return true. If the framework ends up
292-
// not handling it, the processor will re-send the event, this time
293-
// returning false so that it can be processed here.
294-
if (keyProcessor != null && keyProcessor.onKeyEvent(event)) {
293+
// This gives the key processor a chance to process this event if it came
294+
// from a soft keyboard. It will send it to the framework to be handled and
295+
// return true. If the framework ends up not handling it, the processor will
296+
// re-send the event to this function. Only do this if the event is not the
297+
// current event, since that indicates that the key processor sent it to us,
298+
// and we only want to call the key processor for events that it doesn't
299+
// already know about (i.e. when events arrive here from a soft keyboard and
300+
// not a hardware keyboard), to avoid a loop.
301+
if (keyProcessor != null
302+
&& !keyProcessor.isCurrentEvent(event)
303+
&& keyProcessor.onKeyEvent(event)) {
295304
return true;
296305
}
297306

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

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
import android.util.AttributeSet;
2222
import android.util.SparseArray;
2323
import android.view.DisplayCutout;
24-
import android.view.KeyEvent;
2524
import android.view.MotionEvent;
2625
import android.view.PointerIcon;
2726
import android.view.Surface;
@@ -268,12 +267,6 @@ public DartExecutor getDartExecutor() {
268267
return dartExecutor;
269268
}
270269

271-
@Override
272-
public boolean dispatchKeyEventPreIme(KeyEvent event) {
273-
return (isAttached() && androidKeyProcessor.onKeyEvent(event))
274-
|| super.dispatchKeyEventPreIme(event);
275-
}
276-
277270
public FlutterNativeView getFlutterNativeView() {
278271
return mNativeView;
279272
}

shell/platform/android/test/io/flutter/embedding/engine/systemchannels/KeyEventChannelTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ public void onKeyEventNotHandled(@NonNull long id) {
6262

6363
KeyEvent event = new FakeKeyEvent(KeyEvent.ACTION_DOWN, 65);
6464
KeyEventChannel.FlutterKeyEvent flutterKeyEvent =
65-
new KeyEventChannel.FlutterKeyEvent(event, null, 10);
65+
new KeyEventChannel.FlutterKeyEvent(event, null);
6666
keyEventChannel.keyDown(flutterKeyEvent);
6767
ArgumentCaptor<ByteBuffer> byteBufferArgumentCaptor = ArgumentCaptor.forClass(ByteBuffer.class);
6868
ArgumentCaptor<BinaryMessenger.BinaryReply> replyArgumentCaptor =
@@ -103,7 +103,7 @@ public void onKeyEventNotHandled(long id) {
103103

104104
KeyEvent event = new FakeKeyEvent(KeyEvent.ACTION_UP, 65);
105105
KeyEventChannel.FlutterKeyEvent flutterKeyEvent =
106-
new KeyEventChannel.FlutterKeyEvent(event, null, 10);
106+
new KeyEventChannel.FlutterKeyEvent(event, null);
107107
keyEventChannel.keyUp(flutterKeyEvent);
108108
ArgumentCaptor<ByteBuffer> byteBufferArgumentCaptor = ArgumentCaptor.forClass(ByteBuffer.class);
109109
ArgumentCaptor<BinaryMessenger.BinaryReply> replyArgumentCaptor =

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

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import static org.mockito.Mockito.anyString;
1010
import static org.mockito.Mockito.eq;
1111
import static org.mockito.Mockito.mock;
12+
import static org.mockito.Mockito.never;
1213
import static org.mockito.Mockito.spy;
1314
import static org.mockito.Mockito.times;
1415
import static org.mockito.Mockito.verify;
@@ -1028,19 +1029,47 @@ public void testCursorAnchorInfo() {
10281029
assertNull(testImm.lastCursorAnchorInfo);
10291030
}
10301031

1032+
@Test
1033+
public void testSendKeyEvent_sendSoftKeyEvents() {
1034+
ListenableEditingState editable = sampleEditable(5, 5);
1035+
AndroidKeyProcessor mockKeyProcessor = mock(AndroidKeyProcessor.class);
1036+
when(mockKeyProcessor.isCurrentEvent(any())).thenReturn(true);
1037+
InputConnectionAdaptor adaptor = sampleInputConnectionAdaptor(editable, mockKeyProcessor);
1038+
1039+
KeyEvent shiftKeyDown = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_SHIFT_LEFT);
1040+
1041+
boolean didConsume = adaptor.sendKeyEvent(shiftKeyDown);
1042+
assertFalse(didConsume);
1043+
verify(mockKeyProcessor, never()).onKeyEvent(shiftKeyDown);
1044+
}
1045+
1046+
@Test
1047+
public void testSendKeyEvent_sendHardwareKeyEvents() {
1048+
ListenableEditingState editable = sampleEditable(5, 5);
1049+
AndroidKeyProcessor mockKeyProcessor = mock(AndroidKeyProcessor.class);
1050+
when(mockKeyProcessor.isCurrentEvent(any())).thenReturn(false);
1051+
when(mockKeyProcessor.onKeyEvent(any())).thenReturn(true);
1052+
InputConnectionAdaptor adaptor = sampleInputConnectionAdaptor(editable, mockKeyProcessor);
1053+
1054+
KeyEvent shiftKeyDown = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_SHIFT_LEFT);
1055+
1056+
boolean didConsume = adaptor.sendKeyEvent(shiftKeyDown);
1057+
assertTrue(didConsume);
1058+
verify(mockKeyProcessor, times(1)).onKeyEvent(shiftKeyDown);
1059+
}
1060+
10311061
@Test
10321062
public void testSendKeyEvent_delKeyNotConsumed() {
1033-
int selStart = 29;
1034-
ListenableEditingState editable = sampleEditable(selStart, selStart, SAMPLE_RTL_TEXT);
1063+
ListenableEditingState editable = sampleEditable(5, 5);
10351064
InputConnectionAdaptor adaptor = sampleInputConnectionAdaptor(editable);
10361065

10371066
KeyEvent downKeyDown = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL);
10381067

1039-
for (int i = 0; i < 10; i++) {
1068+
for (int i = 0; i < 4; i++) {
10401069
boolean didConsume = adaptor.sendKeyEvent(downKeyDown);
10411070
assertFalse(didConsume);
10421071
}
1043-
assertEquals(29, Selection.getSelectionStart(editable));
1072+
assertEquals(5, Selection.getSelectionStart(editable));
10441073
}
10451074

10461075
@Test
@@ -1097,11 +1126,15 @@ private static ListenableEditingState sampleEditable(int selStart, int selEnd, S
10971126

10981127
private static InputConnectionAdaptor sampleInputConnectionAdaptor(
10991128
ListenableEditingState editable) {
1129+
return sampleInputConnectionAdaptor(editable, mock(AndroidKeyProcessor.class));
1130+
}
1131+
1132+
private static InputConnectionAdaptor sampleInputConnectionAdaptor(
1133+
ListenableEditingState editable, AndroidKeyProcessor mockKeyProcessor) {
11001134
View testView = new View(RuntimeEnvironment.application);
11011135
int client = 0;
11021136
TextInputChannel textInputChannel = mock(TextInputChannel.class);
11031137
FlutterJNI mockFlutterJNI = mock(FlutterJNI.class);
1104-
AndroidKeyProcessor mockKeyProcessor = mock(AndroidKeyProcessor.class);
11051138
when(mockFlutterJNI.nativeFlutterTextUtilsIsEmoji(anyInt()))
11061139
.thenAnswer((invocation) -> Emoji.isEmoji((int) invocation.getArguments()[0]));
11071140
when(mockFlutterJNI.nativeFlutterTextUtilsIsEmojiModifier(anyInt()))

0 commit comments

Comments
 (0)