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

Commit 3cd70f2

Browse files
authored
Reland: Use dispatchKeyEventPreIme, and handle keys sent to InputConnection.sendKeyEvent on Android (#21979)
This re-lands #21163, which was reverted in #21513 Now that flutter/flutter#67359 has landed, this change will no longer cause spaces (and other shortcuts) to be ignored in text fields if there is no action associated with the intent, even if there is a shortcut key mapping to an intent. Here's the original PR description: This switches from using dispatchKeyEvent to using dispatchKeyEventPreIme so that keys can be intercepted before they reach the IME and be handled by the framework. It also now intercepts key events sent to InputConnection.sendKeyEvent, as some IMEs do (e.g. the Hacker's Keyboard), and sends the to Flutter before sending them to the IME (which it now only does if they are not handled by the framework). This fixes the problem where pressing TAB on a hardware keyboard sends the tab to both the text field and to the focus traversal system. Note that we still can't intercept all keystrokes given to a soft keyboard, only those which the soft keyboard decides to send to InputConnection.sendKeyEvent.
1 parent 6ce33dd commit 3cd70f2

File tree

7 files changed

+107
-101
lines changed

7 files changed

+107
-101
lines changed

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

Lines changed: 32 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,8 @@ public AndroidKeyProcessor(
6666
@NonNull TextInputPlugin textInputPlugin) {
6767
this.keyEventChannel = keyEventChannel;
6868
this.textInputPlugin = textInputPlugin;
69-
this.eventResponder = new EventResponder(view);
69+
textInputPlugin.setKeyEventProcessor(this);
70+
this.eventResponder = new EventResponder(view, textInputPlugin);
7071
this.keyEventChannel.setEventResponseHandler(eventResponder);
7172
}
7273

@@ -80,53 +81,33 @@ public void destroy() {
8081
}
8182

8283
/**
83-
* Called when a key up event is received by the {@link FlutterView}.
84+
* Called when a key event is received by the {@link FlutterView} or the {@link
85+
* InputConnectionAdaptor}.
8486
*
8587
* @param keyEvent the Android key event to respond to.
8688
* @return true if the key event should not be propagated to other Android components. Delayed
8789
* synthesis events will return false, so that other components may handle them.
8890
*/
89-
public boolean onKeyUp(@NonNull KeyEvent keyEvent) {
90-
if (eventResponder.dispatchingKeyEvent) {
91-
// Don't handle it if it is from our own delayed event synthesis.
91+
public boolean onKeyEvent(@NonNull KeyEvent keyEvent) {
92+
int action = keyEvent.getAction();
93+
if (action != KeyEvent.ACTION_DOWN && action != KeyEvent.ACTION_UP) {
94+
// There is theoretically a KeyEvent.ACTION_MULTIPLE, but that shouldn't
95+
// be sent anymore anyhow.
9296
return false;
9397
}
94-
95-
Character complexCharacter = applyCombiningCharacterToBaseCharacter(keyEvent.getUnicodeChar());
96-
KeyEventChannel.FlutterKeyEvent flutterEvent =
97-
new KeyEventChannel.FlutterKeyEvent(keyEvent, complexCharacter, eventIdSerial++);
98-
keyEventChannel.keyUp(flutterEvent);
99-
eventResponder.addEvent(flutterEvent.eventId, keyEvent);
100-
return true;
101-
}
102-
103-
/**
104-
* Called when a key down event is received by the {@link FlutterView}.
105-
*
106-
* @param keyEvent the Android key event to respond to.
107-
* @return true if the key event should not be propagated to other Android components. Delayed
108-
* synthesis events will return false, so that other components may handle them.
109-
*/
110-
public boolean onKeyDown(@NonNull KeyEvent keyEvent) {
11198
if (eventResponder.dispatchingKeyEvent) {
11299
// Don't handle it if it is from our own delayed event synthesis.
113100
return false;
114101
}
115102

116-
// If the textInputPlugin is still valid and accepting text, then we'll try
117-
// and send the key event to it, assuming that if the event can be sent,
118-
// that it has been handled.
119-
if (textInputPlugin.getLastInputConnection() != null
120-
&& textInputPlugin.getInputMethodManager().isAcceptingText()) {
121-
if (textInputPlugin.getLastInputConnection().sendKeyEvent(keyEvent)) {
122-
return true;
123-
}
124-
}
125-
126103
Character complexCharacter = applyCombiningCharacterToBaseCharacter(keyEvent.getUnicodeChar());
127104
KeyEventChannel.FlutterKeyEvent flutterEvent =
128105
new KeyEventChannel.FlutterKeyEvent(keyEvent, complexCharacter, eventIdSerial++);
129-
keyEventChannel.keyDown(flutterEvent);
106+
if (action == KeyEvent.ACTION_DOWN) {
107+
keyEventChannel.keyDown(flutterEvent);
108+
} else {
109+
keyEventChannel.keyUp(flutterEvent);
110+
}
130111
eventResponder.addEvent(flutterEvent.eventId, keyEvent);
131112
return true;
132113
}
@@ -196,10 +177,12 @@ private static class EventResponder implements KeyEventChannel.EventResponseHand
196177
private static final long MAX_PENDING_EVENTS = 1000;
197178
final Deque<Entry<Long, KeyEvent>> pendingEvents = new ArrayDeque<Entry<Long, KeyEvent>>();
198179
@NonNull private final View view;
180+
@NonNull private final TextInputPlugin textInputPlugin;
199181
boolean dispatchingKeyEvent = false;
200182

201-
public EventResponder(@NonNull View view) {
183+
public EventResponder(@NonNull View view, @NonNull TextInputPlugin textInputPlugin) {
202184
this.view = view;
185+
this.textInputPlugin = textInputPlugin;
203186
}
204187

205188
/**
@@ -267,12 +250,26 @@ public void addEvent(long id, @NonNull KeyEvent event) {
267250
* @param event the event to be dispatched to the activity.
268251
*/
269252
public void dispatchKeyEvent(KeyEvent event) {
253+
// If the textInputPlugin is still valid and accepting text, then we'll try
254+
// and send the key event to it, assuming that if the event can be sent,
255+
// that it has been handled.
256+
if (textInputPlugin.getLastInputConnection() != null
257+
&& textInputPlugin.getInputMethodManager().isAcceptingText()) {
258+
dispatchingKeyEvent = true;
259+
boolean handled = textInputPlugin.getLastInputConnection().sendKeyEvent(event);
260+
dispatchingKeyEvent = false;
261+
if (handled) {
262+
return;
263+
}
264+
}
265+
270266
// Since the framework didn't handle it, dispatch the key again.
271267
if (view != null) {
272268
// Turn on dispatchingKeyEvent so that we don't dispatch to ourselves and
273269
// send it to the framework again.
274270
dispatchingKeyEvent = true;
275-
view.getRootView().dispatchKeyEvent(event);
271+
272+
view.getRootView().dispatchKeyEventPreIme(event);
276273
dispatchingKeyEvent = false;
277274
}
278275
}

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

Lines changed: 8 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -721,27 +721,7 @@ public boolean checkInputConnectionProxy(View view) {
721721
}
722722

723723
/**
724-
* Invoked when key is released.
725-
*
726-
* <p>This method is typically invoked in response to the release of a physical keyboard key or a
727-
* D-pad button. It is generally not invoked when a virtual software keyboard is used, though a
728-
* software keyboard may choose to invoke this method in some situations.
729-
*
730-
* <p>{@link KeyEvent}s are sent from Android to Flutter. {@link AndroidKeyProcessor} may do some
731-
* additional work with the given {@link KeyEvent}, e.g., combine this {@code keyCode} with the
732-
* previous {@code keyCode} to generate a unicode combined character.
733-
*/
734-
@Override
735-
public boolean onKeyUp(int keyCode, @NonNull KeyEvent event) {
736-
if (!isAttachedToFlutterEngine()) {
737-
return super.onKeyUp(keyCode, event);
738-
}
739-
740-
return androidKeyProcessor.onKeyUp(event) || super.onKeyUp(keyCode, event);
741-
}
742-
743-
/**
744-
* Invoked when key is pressed.
724+
* Invoked when a hardware key is pressed or released, before the IME receives the key.
745725
*
746726
* <p>This method is typically invoked in response to the press of a physical keyboard key or a
747727
* D-pad button. It is generally not invoked when a virtual software keyboard is used, though a
@@ -752,12 +732,13 @@ public boolean onKeyUp(int keyCode, @NonNull KeyEvent event) {
752732
* previous {@code keyCode} to generate a unicode combined character.
753733
*/
754734
@Override
755-
public boolean onKeyDown(int keyCode, @NonNull KeyEvent event) {
756-
if (!isAttachedToFlutterEngine()) {
757-
return super.onKeyDown(keyCode, event);
758-
}
759-
760-
return androidKeyProcessor.onKeyDown(event) || super.onKeyDown(keyCode, event);
735+
public boolean dispatchKeyEventPreIme(KeyEvent event) {
736+
// If the key processor doesn't handle it, then send it on to the
737+
// superclass. The key processor will typically handle all events except
738+
// those where it has re-dispatched the event after receiving a reply from
739+
// the framework that the framework did not handle it.
740+
return (isAttachedToFlutterEngine() && androidKeyProcessor.onKeyEvent(event))
741+
|| super.dispatchKeyEventPreIme(event);
761742
}
762743

763744
/**

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

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,15 @@
2727
import android.view.inputmethod.InputMethodManager;
2828
import android.view.inputmethod.InputMethodSubtype;
2929
import io.flutter.Log;
30+
import io.flutter.embedding.android.AndroidKeyProcessor;
3031
import io.flutter.embedding.engine.FlutterJNI;
3132
import io.flutter.embedding.engine.systemchannels.TextInputChannel;
3233

3334
class InputConnectionAdaptor extends BaseInputConnection {
3435
private final View mFlutterView;
3536
private final int mClient;
3637
private final TextInputChannel textInputChannel;
38+
private final AndroidKeyProcessor keyProcessor;
3739
private final Editable mEditable;
3840
private final EditorInfo mEditorInfo;
3941
private int mBatchCount;
@@ -97,6 +99,7 @@ public InputConnectionAdaptor(
9799
View view,
98100
int client,
99101
TextInputChannel textInputChannel,
102+
AndroidKeyProcessor keyProcessor,
100103
Editable editable,
101104
EditorInfo editorInfo,
102105
FlutterJNI flutterJNI) {
@@ -107,6 +110,7 @@ public InputConnectionAdaptor(
107110
mEditable = editable;
108111
mEditorInfo = editorInfo;
109112
mBatchCount = 0;
113+
this.keyProcessor = keyProcessor;
110114
this.flutterTextUtils = new FlutterTextUtils(flutterJNI);
111115
// We create a dummy Layout with max width so that the selection
112116
// shifting acts as if all text were in one line.
@@ -128,9 +132,10 @@ public InputConnectionAdaptor(
128132
View view,
129133
int client,
130134
TextInputChannel textInputChannel,
135+
AndroidKeyProcessor keyProcessor,
131136
Editable editable,
132137
EditorInfo editorInfo) {
133-
this(view, client, textInputChannel, editable, editorInfo, new FlutterJNI());
138+
this(view, client, textInputChannel, keyProcessor, editable, editorInfo, new FlutterJNI());
134139
}
135140

136141
// Send the current state of the editable to Flutter.
@@ -323,6 +328,14 @@ private static int clampIndexToEditable(int index, Editable editable) {
323328

324329
@Override
325330
public boolean sendKeyEvent(KeyEvent event) {
331+
// Give the key processor a chance to process this event. It will send it
332+
// to the framework to be handled and return true. If the framework ends up
333+
// not handling it, the processor will re-send the event, this time
334+
// returning false so that it can be processed here.
335+
if (keyProcessor != null && keyProcessor.onKeyEvent(event)) {
336+
return true;
337+
}
338+
326339
markDirty();
327340
if (event.getAction() == KeyEvent.ACTION_DOWN) {
328341
if (event.getKeyCode() == KeyEvent.KEYCODE_DEL) {

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

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import androidx.annotation.NonNull;
2929
import androidx.annotation.Nullable;
3030
import androidx.annotation.VisibleForTesting;
31+
import io.flutter.embedding.android.AndroidKeyProcessor;
3132
import io.flutter.embedding.engine.systemchannels.TextInputChannel;
3233
import io.flutter.plugin.platform.PlatformViewsController;
3334
import java.util.HashMap;
@@ -48,6 +49,7 @@ public class TextInputPlugin {
4849
@Nullable private Rect lastClientRect;
4950
private final boolean restartAlwaysRequired;
5051
private ImeSyncDeferringInsetsCallback imeSyncCallback;
52+
private AndroidKeyProcessor keyProcessor;
5153

5254
// When true following calls to createInputConnection will return the cached lastInputConnection
5355
// if the input
@@ -172,6 +174,15 @@ ImeSyncDeferringInsetsCallback getImeSyncCallback() {
172174
return imeSyncCallback;
173175
}
174176

177+
@NonNull
178+
public AndroidKeyProcessor getKeyEventProcessor() {
179+
return keyProcessor;
180+
}
181+
182+
public void setKeyEventProcessor(AndroidKeyProcessor processor) {
183+
keyProcessor = processor;
184+
}
185+
175186
/**
176187
* Use the current platform view input connection until unlockPlatformViewInputConnection is
177188
* called.
@@ -313,7 +324,8 @@ public InputConnection createInputConnection(View view, EditorInfo outAttrs) {
313324
outAttrs.imeOptions |= enterAction;
314325

315326
InputConnectionAdaptor connection =
316-
new InputConnectionAdaptor(view, inputTarget.id, textInputChannel, mEditable, outAttrs);
327+
new InputConnectionAdaptor(
328+
view, inputTarget.id, textInputChannel, keyProcessor, mEditable, outAttrs);
317329
outAttrs.initialSelStart = Selection.getSelectionStart(mEditable);
318330
outAttrs.initialSelEnd = Selection.getSelectionEnd(mEditable);
319331

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

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -269,19 +269,9 @@ public DartExecutor getDartExecutor() {
269269
}
270270

271271
@Override
272-
public boolean onKeyUp(int keyCode, KeyEvent event) {
273-
if (!isAttached()) {
274-
return super.onKeyUp(keyCode, event);
275-
}
276-
return androidKeyProcessor.onKeyUp(event) || super.onKeyUp(keyCode, event);
277-
}
278-
279-
@Override
280-
public boolean onKeyDown(int keyCode, KeyEvent event) {
281-
if (!isAttached()) {
282-
return super.onKeyDown(keyCode, event);
283-
}
284-
return androidKeyProcessor.onKeyDown(event) || super.onKeyDown(keyCode, event);
272+
public boolean dispatchKeyEventPreIme(KeyEvent event) {
273+
return (isAttached() && androidKeyProcessor.onKeyEvent(event))
274+
|| super.dispatchKeyEventPreIme(event);
285275
}
286276

287277
public FlutterNativeView getFlutterNativeView() {

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

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,11 @@ public void respondsTrueWhenHandlingNewEvents() {
5151
AndroidKeyProcessor processor =
5252
new AndroidKeyProcessor(fakeView, fakeKeyEventChannel, mock(TextInputPlugin.class));
5353

54-
boolean result = processor.onKeyDown(new FakeKeyEvent(KeyEvent.ACTION_DOWN, 65));
54+
boolean result = processor.onKeyEvent(new FakeKeyEvent(KeyEvent.ACTION_DOWN, 65));
5555
assertEquals(true, result);
5656
verify(fakeKeyEventChannel, times(1)).keyDown(any(KeyEventChannel.FlutterKeyEvent.class));
5757
verify(fakeKeyEventChannel, times(0)).keyUp(any(KeyEventChannel.FlutterKeyEvent.class));
58-
verify(fakeView, times(0)).dispatchKeyEvent(any(KeyEvent.class));
58+
verify(fakeView, times(0)).dispatchKeyEventPreIme(any(KeyEvent.class));
5959
}
6060

6161
@Test
@@ -97,31 +97,31 @@ public View answer(InvocationOnMock invocation) throws Throwable {
9797
ArgumentCaptor.forClass(KeyEventChannel.FlutterKeyEvent.class);
9898
FakeKeyEvent fakeKeyEvent = new FakeKeyEvent(KeyEvent.ACTION_DOWN, 65);
9999

100-
boolean result = processor.onKeyDown(fakeKeyEvent);
100+
boolean result = processor.onKeyEvent(fakeKeyEvent);
101101
assertEquals(true, result);
102102

103103
// Capture the FlutterKeyEvent so we can find out its event ID to use when
104104
// faking our response.
105105
verify(fakeKeyEventChannel, times(1)).keyDown(eventCaptor.capture());
106106
boolean[] dispatchResult = {true};
107-
when(fakeView.dispatchKeyEvent(any(KeyEvent.class)))
107+
when(fakeView.dispatchKeyEventPreIme(any(KeyEvent.class)))
108108
.then(
109109
new Answer<Boolean>() {
110110
@Override
111111
public Boolean answer(InvocationOnMock invocation) throws Throwable {
112112
KeyEvent event = (KeyEvent) invocation.getArguments()[0];
113113
assertEquals(fakeKeyEvent, event);
114-
dispatchResult[0] = processor.onKeyDown(event);
114+
dispatchResult[0] = processor.onKeyEvent(event);
115115
return dispatchResult[0];
116116
}
117117
});
118118

119119
// Fake a response from the framework.
120120
handlerCaptor.getValue().onKeyEventNotHandled(eventCaptor.getValue().eventId);
121-
verify(fakeView, times(1)).dispatchKeyEvent(fakeKeyEvent);
121+
verify(fakeView, times(1)).dispatchKeyEventPreIme(fakeKeyEvent);
122122
assertEquals(false, dispatchResult[0]);
123123
verify(fakeKeyEventChannel, times(0)).keyUp(any(KeyEventChannel.FlutterKeyEvent.class));
124-
verify(fakeRootView, times(1)).dispatchKeyEvent(fakeKeyEvent);
124+
verify(fakeRootView, times(1)).dispatchKeyEventPreIme(fakeKeyEvent);
125125
}
126126

127127
public void synthesizesEventsWhenKeyUpNotHandled() {
@@ -147,31 +147,31 @@ public View answer(InvocationOnMock invocation) throws Throwable {
147147
ArgumentCaptor.forClass(KeyEventChannel.FlutterKeyEvent.class);
148148
FakeKeyEvent fakeKeyEvent = new FakeKeyEvent(KeyEvent.ACTION_UP, 65);
149149

150-
boolean result = processor.onKeyUp(fakeKeyEvent);
150+
boolean result = processor.onKeyEvent(fakeKeyEvent);
151151
assertEquals(true, result);
152152

153153
// Capture the FlutterKeyEvent so we can find out its event ID to use when
154154
// faking our response.
155155
verify(fakeKeyEventChannel, times(1)).keyUp(eventCaptor.capture());
156156
boolean[] dispatchResult = {true};
157-
when(fakeView.dispatchKeyEvent(any(KeyEvent.class)))
157+
when(fakeView.dispatchKeyEventPreIme(any(KeyEvent.class)))
158158
.then(
159159
new Answer<Boolean>() {
160160
@Override
161161
public Boolean answer(InvocationOnMock invocation) throws Throwable {
162162
KeyEvent event = (KeyEvent) invocation.getArguments()[0];
163163
assertEquals(fakeKeyEvent, event);
164-
dispatchResult[0] = processor.onKeyUp(event);
164+
dispatchResult[0] = processor.onKeyEvent(event);
165165
return dispatchResult[0];
166166
}
167167
});
168168

169169
// Fake a response from the framework.
170170
handlerCaptor.getValue().onKeyEventNotHandled(eventCaptor.getValue().eventId);
171-
verify(fakeView, times(1)).dispatchKeyEvent(fakeKeyEvent);
171+
verify(fakeView, times(1)).dispatchKeyEventPreIme(fakeKeyEvent);
172172
assertEquals(false, dispatchResult[0]);
173173
verify(fakeKeyEventChannel, times(0)).keyUp(any(KeyEventChannel.FlutterKeyEvent.class));
174-
verify(fakeRootView, times(1)).dispatchKeyEvent(fakeKeyEvent);
174+
verify(fakeRootView, times(1)).dispatchKeyEventPreIme(fakeKeyEvent);
175175
}
176176

177177
@NonNull

0 commit comments

Comments
 (0)