Skip to content

Commit 672765b

Browse files
committed
Revert "Revert "Reland: Use dispatchKeyEventPreIme, and handle keys sent to InputConnection.sendKeyEvent on Android (flutter#21979)" (flutter#22004)"
This reverts commit 3658bd7.
1 parent d5b7269 commit 672765b

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)