From be4ff16b99c9a1611f7461ebc85f5363431be2f2 Mon Sep 17 00:00:00 2001 From: Greg Spencer Date: Mon, 14 Sep 2020 11:24:20 -0700 Subject: [PATCH 1/3] Switch to using pre-IME event dispatch --- .../android/AndroidKeyProcessor.java | 2 +- .../android/io/flutter/view/FlutterView.java | 22 +++++++++++++++---- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/shell/platform/android/io/flutter/embedding/android/AndroidKeyProcessor.java b/shell/platform/android/io/flutter/embedding/android/AndroidKeyProcessor.java index 14d7effe56c11..8a8c136859d01 100644 --- a/shell/platform/android/io/flutter/embedding/android/AndroidKeyProcessor.java +++ b/shell/platform/android/io/flutter/embedding/android/AndroidKeyProcessor.java @@ -272,7 +272,7 @@ public void dispatchKeyEvent(KeyEvent event) { // Turn on dispatchingKeyEvent so that we don't dispatch to ourselves and // send it to the framework again. dispatchingKeyEvent = true; - view.getRootView().dispatchKeyEvent(event); + view.getRootView().dispatchKeyEventPreIme(event); dispatchingKeyEvent = false; } } diff --git a/shell/platform/android/io/flutter/view/FlutterView.java b/shell/platform/android/io/flutter/view/FlutterView.java index 9899e1714b255..5ace9be982179 100644 --- a/shell/platform/android/io/flutter/view/FlutterView.java +++ b/shell/platform/android/io/flutter/view/FlutterView.java @@ -275,12 +275,26 @@ public boolean onKeyUp(int keyCode, KeyEvent event) { return androidKeyProcessor.onKeyUp(event) || super.onKeyUp(keyCode, event); } - @Override - public boolean onKeyDown(int keyCode, KeyEvent event) { + // @Override + // public boolean onKeyDown(int keyCode, KeyEvent event) { + // if (!isAttached()) { + // return super.onKeyDown(keyCode, event); + // } + // return androidKeyProcessor.onKeyDown(event) || super.onKeyDown(keyCode, event); + // } + + @override + public boolean dispatchKeyEventPreIme(KeyEvent event) { if (!isAttached()) { - return super.onKeyDown(keyCode, event); + return super.dispatchKeyEventPreIme(event); + } + int action = event.getAction(); + if (action == ACTION_DOWN) { + return androidKeyProcessor.onKeyDown(event) || super.dispatchKeyEventPreIme(keyCode, event); + } else if (action == ACTION_UP) { + return androidKeyProcessor.onKeyUp(event) || super.dispatchKeyEventPreIme(keyCode, event); } - return androidKeyProcessor.onKeyDown(event) || super.onKeyDown(keyCode, event); + return super.dispatchKeyEventPreIme(event); } public FlutterNativeView getFlutterNativeView() { From 2c8727e9973e2e85154edef8b42fd243b426b549 Mon Sep 17 00:00:00 2001 From: Greg Spencer Date: Mon, 14 Sep 2020 16:08:52 -0700 Subject: [PATCH 2/3] Handle input sent to InputConnectionAdaptor too --- .../android/AndroidKeyProcessor.java | 65 +++++++++---------- .../embedding/android/FlutterView.java | 33 ++-------- .../editing/InputConnectionAdaptor.java | 22 ++++++- .../plugin/editing/TextInputPlugin.java | 14 +++- .../android/io/flutter/view/FlutterView.java | 28 +------- 5 files changed, 72 insertions(+), 90 deletions(-) diff --git a/shell/platform/android/io/flutter/embedding/android/AndroidKeyProcessor.java b/shell/platform/android/io/flutter/embedding/android/AndroidKeyProcessor.java index 8a8c136859d01..c2c5549596518 100644 --- a/shell/platform/android/io/flutter/embedding/android/AndroidKeyProcessor.java +++ b/shell/platform/android/io/flutter/embedding/android/AndroidKeyProcessor.java @@ -66,7 +66,8 @@ public AndroidKeyProcessor( @NonNull TextInputPlugin textInputPlugin) { this.keyEventChannel = keyEventChannel; this.textInputPlugin = textInputPlugin; - this.eventResponder = new EventResponder(view); + textInputPlugin.setKeyEventProcessor(this); + this.eventResponder = new EventResponder(view, textInputPlugin); this.keyEventChannel.setEventResponseHandler(eventResponder); } @@ -80,53 +81,33 @@ public void destroy() { } /** - * Called when a key up event is received by the {@link FlutterView}. + * Called when a key event is received by the {@link FlutterView} or the {@link + * InputConnectionAdaptor}. * * @param keyEvent the Android key event to respond to. * @return true if the key event should not be propagated to other Android components. Delayed * synthesis events will return false, so that other components may handle them. */ - public boolean onKeyUp(@NonNull KeyEvent keyEvent) { - if (eventResponder.dispatchingKeyEvent) { - // Don't handle it if it is from our own delayed event synthesis. + public boolean onKeyEvent(@NonNull KeyEvent keyEvent) { + int action = keyEvent.getAction(); + if (action != KeyEvent.ACTION_DOWN && action != KeyEvent.ACTION_UP) { + // There is theoretically a KeyEvent.ACTION_MULTIPLE, but that shouldn't + // be sent anymore anyhow. return false; } - - Character complexCharacter = applyCombiningCharacterToBaseCharacter(keyEvent.getUnicodeChar()); - KeyEventChannel.FlutterKeyEvent flutterEvent = - new KeyEventChannel.FlutterKeyEvent(keyEvent, complexCharacter, eventIdSerial++); - keyEventChannel.keyUp(flutterEvent); - eventResponder.addEvent(flutterEvent.eventId, keyEvent); - return true; - } - - /** - * Called when a key down event is received by the {@link FlutterView}. - * - * @param keyEvent the Android key event to respond to. - * @return true if the key event should not be propagated to other Android components. Delayed - * synthesis events will return false, so that other components may handle them. - */ - public boolean onKeyDown(@NonNull KeyEvent keyEvent) { if (eventResponder.dispatchingKeyEvent) { // Don't handle it if it is from our own delayed event synthesis. return false; } - // If the textInputPlugin is still valid and accepting text, then we'll try - // and send the key event to it, assuming that if the event can be sent, - // that it has been handled. - if (textInputPlugin.getLastInputConnection() != null - && textInputPlugin.getInputMethodManager().isAcceptingText()) { - if (textInputPlugin.getLastInputConnection().sendKeyEvent(keyEvent)) { - return true; - } - } - Character complexCharacter = applyCombiningCharacterToBaseCharacter(keyEvent.getUnicodeChar()); KeyEventChannel.FlutterKeyEvent flutterEvent = new KeyEventChannel.FlutterKeyEvent(keyEvent, complexCharacter, eventIdSerial++); - keyEventChannel.keyDown(flutterEvent); + if (action == KeyEvent.ACTION_DOWN) { + keyEventChannel.keyDown(flutterEvent); + } else { + keyEventChannel.keyUp(flutterEvent); + } eventResponder.addEvent(flutterEvent.eventId, keyEvent); return true; } @@ -196,10 +177,12 @@ private static class EventResponder implements KeyEventChannel.EventResponseHand private static final long MAX_PENDING_EVENTS = 1000; final Deque> pendingEvents = new ArrayDeque>(); @NonNull private final View view; + @NonNull private final TextInputPlugin textInputPlugin; boolean dispatchingKeyEvent = false; - public EventResponder(@NonNull View view) { + public EventResponder(@NonNull View view, @NonNull TextInputPlugin textInputPlugin) { this.view = view; + this.textInputPlugin = textInputPlugin; } /** @@ -267,11 +250,25 @@ public void addEvent(long id, @NonNull KeyEvent event) { * @param event the event to be dispatched to the activity. */ public void dispatchKeyEvent(KeyEvent event) { + // If the textInputPlugin is still valid and accepting text, then we'll try + // and send the key event to it, assuming that if the event can be sent, + // that it has been handled. + if (textInputPlugin.getLastInputConnection() != null + && textInputPlugin.getInputMethodManager().isAcceptingText()) { + dispatchingKeyEvent = true; + boolean handled = textInputPlugin.getLastInputConnection().sendKeyEvent(event); + dispatchingKeyEvent = false; + if (handled) { + return; + } + } + // Since the framework didn't handle it, dispatch the key again. if (view != null) { // Turn on dispatchingKeyEvent so that we don't dispatch to ourselves and // send it to the framework again. dispatchingKeyEvent = true; + view.getRootView().dispatchKeyEventPreIme(event); dispatchingKeyEvent = false; } diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterView.java b/shell/platform/android/io/flutter/embedding/android/FlutterView.java index 48d9243cf86c2..a5bd6874e2421 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterView.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterView.java @@ -721,27 +721,7 @@ public boolean checkInputConnectionProxy(View view) { } /** - * Invoked when key is released. - * - *

This method is typically invoked in response to the release of a physical keyboard key or a - * D-pad button. It is generally not invoked when a virtual software keyboard is used, though a - * software keyboard may choose to invoke this method in some situations. - * - *

{@link KeyEvent}s are sent from Android to Flutter. {@link AndroidKeyProcessor} may do some - * additional work with the given {@link KeyEvent}, e.g., combine this {@code keyCode} with the - * previous {@code keyCode} to generate a unicode combined character. - */ - @Override - public boolean onKeyUp(int keyCode, @NonNull KeyEvent event) { - if (!isAttachedToFlutterEngine()) { - return super.onKeyUp(keyCode, event); - } - - return androidKeyProcessor.onKeyUp(event) || super.onKeyUp(keyCode, event); - } - - /** - * Invoked when key is pressed. + * Invoked when key is pressed or released. * *

This method is typically invoked in response to the press of a physical keyboard key or a * 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) { * previous {@code keyCode} to generate a unicode combined character. */ @Override - public boolean onKeyDown(int keyCode, @NonNull KeyEvent event) { - if (!isAttachedToFlutterEngine()) { - return super.onKeyDown(keyCode, event); - } + public boolean dispatchKeyEventPreIme(KeyEvent event) { + return (isAttachedToFlutterEngine() && androidKeyProcessor.onKeyEvent(event)) + || super.dispatchKeyEventPreIme(event); + } - return androidKeyProcessor.onKeyDown(event) || super.onKeyDown(keyCode, event); + public AndroidKeyProcessor getKeyProcessor() { + return androidKeyProcessor; } /** diff --git a/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java b/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java index d702e2b7855ea..a23d6882f1c69 100644 --- a/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java +++ b/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java @@ -27,13 +27,17 @@ import android.view.inputmethod.InputMethodManager; import android.view.inputmethod.InputMethodSubtype; import io.flutter.Log; +import io.flutter.embedding.android.AndroidKeyProcessor; import io.flutter.embedding.engine.FlutterJNI; import io.flutter.embedding.engine.systemchannels.TextInputChannel; class InputConnectionAdaptor extends BaseInputConnection { + private static final String TAG = "InputConnectionAdaptor"; + private final View mFlutterView; private final int mClient; private final TextInputChannel textInputChannel; + private final TextInputPlugin textInputPlugin; private final Editable mEditable; private final EditorInfo mEditorInfo; private int mBatchCount; @@ -99,7 +103,8 @@ public InputConnectionAdaptor( TextInputChannel textInputChannel, Editable editable, EditorInfo editorInfo, - FlutterJNI flutterJNI) { + FlutterJNI flutterJNI, + TextInputPlugin textInputPlugin) { super(view, true); mFlutterView = view; mClient = client; @@ -107,6 +112,7 @@ public InputConnectionAdaptor( mEditable = editable; mEditorInfo = editorInfo; mBatchCount = 0; + this.textInputPlugin = textInputPlugin; this.flutterTextUtils = new FlutterTextUtils(flutterJNI); // We create a dummy Layout with max width so that the selection // shifting acts as if all text were in one line. @@ -129,8 +135,9 @@ public InputConnectionAdaptor( int client, TextInputChannel textInputChannel, Editable editable, - EditorInfo editorInfo) { - this(view, client, textInputChannel, editable, editorInfo, new FlutterJNI()); + EditorInfo editorInfo, + TextInputPlugin textInputPlugin) { + this(view, client, textInputChannel, editable, editorInfo, new FlutterJNI(), textInputPlugin); } // Send the current state of the editable to Flutter. @@ -323,6 +330,15 @@ private static int clampIndexToEditable(int index, Editable editable) { @Override public boolean sendKeyEvent(KeyEvent event) { + String type = event.getAction() == KeyEvent.ACTION_DOWN ? "DOWN" : "UP"; + int action = event.getAction(); + AndroidKeyProcessor processor = textInputPlugin.getKeyEventProcessor(); + if (processor != null) { + if (processor.onKeyEvent(event)) { + return true; + } + } + markDirty(); if (event.getAction() == KeyEvent.ACTION_DOWN) { if (event.getKeyCode() == KeyEvent.KEYCODE_DEL) { diff --git a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java index 3090c6fba81fa..bb19e5e054b7c 100644 --- a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java +++ b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java @@ -33,6 +33,7 @@ import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.annotation.VisibleForTesting; +import io.flutter.embedding.android.AndroidKeyProcessor; import io.flutter.embedding.engine.systemchannels.TextInputChannel; import io.flutter.plugin.platform.PlatformViewsController; import java.util.HashMap; @@ -54,6 +55,7 @@ public class TextInputPlugin { @Nullable private Rect lastClientRect; private final boolean restartAlwaysRequired; private ImeSyncDeferringInsetsCallback imeSyncCallback; + private AndroidKeyProcessor keyProcessor; // When true following calls to createInputConnection will return the cached lastInputConnection // if the input @@ -327,6 +329,15 @@ ImeSyncDeferringInsetsCallback getImeSyncCallback() { return imeSyncCallback; } + @NonNull + public AndroidKeyProcessor getKeyEventProcessor() { + return keyProcessor; + } + + public void setKeyEventProcessor(AndroidKeyProcessor processor) { + keyProcessor = processor; + } + /** * Use the current platform view input connection until unlockPlatformViewInputConnection is * called. @@ -469,7 +480,8 @@ public InputConnection createInputConnection(View view, EditorInfo outAttrs) { outAttrs.imeOptions |= enterAction; InputConnectionAdaptor connection = - new InputConnectionAdaptor(view, inputTarget.id, textInputChannel, mEditable, outAttrs); + new InputConnectionAdaptor( + view, inputTarget.id, textInputChannel, mEditable, outAttrs, this); outAttrs.initialSelStart = Selection.getSelectionStart(mEditable); outAttrs.initialSelEnd = Selection.getSelectionEnd(mEditable); diff --git a/shell/platform/android/io/flutter/view/FlutterView.java b/shell/platform/android/io/flutter/view/FlutterView.java index 5ace9be982179..172c4492d665b 100644 --- a/shell/platform/android/io/flutter/view/FlutterView.java +++ b/shell/platform/android/io/flutter/view/FlutterView.java @@ -268,33 +268,9 @@ public DartExecutor getDartExecutor() { } @Override - public boolean onKeyUp(int keyCode, KeyEvent event) { - if (!isAttached()) { - return super.onKeyUp(keyCode, event); - } - return androidKeyProcessor.onKeyUp(event) || super.onKeyUp(keyCode, event); - } - - // @Override - // public boolean onKeyDown(int keyCode, KeyEvent event) { - // if (!isAttached()) { - // return super.onKeyDown(keyCode, event); - // } - // return androidKeyProcessor.onKeyDown(event) || super.onKeyDown(keyCode, event); - // } - - @override public boolean dispatchKeyEventPreIme(KeyEvent event) { - if (!isAttached()) { - return super.dispatchKeyEventPreIme(event); - } - int action = event.getAction(); - if (action == ACTION_DOWN) { - return androidKeyProcessor.onKeyDown(event) || super.dispatchKeyEventPreIme(keyCode, event); - } else if (action == ACTION_UP) { - return androidKeyProcessor.onKeyUp(event) || super.dispatchKeyEventPreIme(keyCode, event); - } - return super.dispatchKeyEventPreIme(event); + return (isAttached() && androidKeyProcessor.onKeyEvent(event)) + || super.dispatchKeyEventPreIme(event); } public FlutterNativeView getFlutterNativeView() { From 9ddf1b2b1001fcbee2bf6c6fd28a6efe5784237e Mon Sep 17 00:00:00 2001 From: Greg Spencer Date: Mon, 14 Sep 2020 16:37:16 -0700 Subject: [PATCH 3/3] Fix tests --- .../embedding/android/FlutterView.java | 10 ++--- .../editing/InputConnectionAdaptor.java | 29 +++++++-------- .../plugin/editing/TextInputPlugin.java | 2 +- .../android/AndroidKeyProcessorTest.java | 24 ++++++------ .../editing/InputConnectionAdaptorTest.java | 37 +++++++++++++------ 5 files changed, 56 insertions(+), 46 deletions(-) diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterView.java b/shell/platform/android/io/flutter/embedding/android/FlutterView.java index a5bd6874e2421..78cc4e7e716d7 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterView.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterView.java @@ -721,7 +721,7 @@ public boolean checkInputConnectionProxy(View view) { } /** - * Invoked when key is pressed or released. + * Invoked when a hardware key is pressed or released, before the IME receives the key. * *

This method is typically invoked in response to the press of a physical keyboard key or a * D-pad button. It is generally not invoked when a virtual software keyboard is used, though a @@ -733,14 +733,14 @@ public boolean checkInputConnectionProxy(View view) { */ @Override public boolean dispatchKeyEventPreIme(KeyEvent event) { + // If the key processor doesn't handle it, then send it on to the + // superclass. The key processor will typically handle all events except + // those where it has re-dispatched the event after receiving a reply from + // the framework that the framework did not handle it. return (isAttachedToFlutterEngine() && androidKeyProcessor.onKeyEvent(event)) || super.dispatchKeyEventPreIme(event); } - public AndroidKeyProcessor getKeyProcessor() { - return androidKeyProcessor; - } - /** * Invoked by Android when a user touch event occurs. * diff --git a/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java b/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java index a23d6882f1c69..26cb5d9d984e7 100644 --- a/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java +++ b/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java @@ -32,12 +32,10 @@ import io.flutter.embedding.engine.systemchannels.TextInputChannel; class InputConnectionAdaptor extends BaseInputConnection { - private static final String TAG = "InputConnectionAdaptor"; - private final View mFlutterView; private final int mClient; private final TextInputChannel textInputChannel; - private final TextInputPlugin textInputPlugin; + private final AndroidKeyProcessor keyProcessor; private final Editable mEditable; private final EditorInfo mEditorInfo; private int mBatchCount; @@ -101,10 +99,10 @@ public InputConnectionAdaptor( View view, int client, TextInputChannel textInputChannel, + AndroidKeyProcessor keyProcessor, Editable editable, EditorInfo editorInfo, - FlutterJNI flutterJNI, - TextInputPlugin textInputPlugin) { + FlutterJNI flutterJNI) { super(view, true); mFlutterView = view; mClient = client; @@ -112,7 +110,7 @@ public InputConnectionAdaptor( mEditable = editable; mEditorInfo = editorInfo; mBatchCount = 0; - this.textInputPlugin = textInputPlugin; + this.keyProcessor = keyProcessor; this.flutterTextUtils = new FlutterTextUtils(flutterJNI); // We create a dummy Layout with max width so that the selection // shifting acts as if all text were in one line. @@ -134,10 +132,10 @@ public InputConnectionAdaptor( View view, int client, TextInputChannel textInputChannel, + AndroidKeyProcessor keyProcessor, Editable editable, - EditorInfo editorInfo, - TextInputPlugin textInputPlugin) { - this(view, client, textInputChannel, editable, editorInfo, new FlutterJNI(), textInputPlugin); + EditorInfo editorInfo) { + this(view, client, textInputChannel, keyProcessor, editable, editorInfo, new FlutterJNI()); } // Send the current state of the editable to Flutter. @@ -330,13 +328,12 @@ private static int clampIndexToEditable(int index, Editable editable) { @Override public boolean sendKeyEvent(KeyEvent event) { - String type = event.getAction() == KeyEvent.ACTION_DOWN ? "DOWN" : "UP"; - int action = event.getAction(); - AndroidKeyProcessor processor = textInputPlugin.getKeyEventProcessor(); - if (processor != null) { - if (processor.onKeyEvent(event)) { - return true; - } + // Give the key processor a chance to process this event. It will send it + // to the framework to be handled and return true. If the framework ends up + // not handling it, the processor will re-send the event, this time + // returning false so that it can be processed here. + if (keyProcessor != null && keyProcessor.onKeyEvent(event)) { + return true; } markDirty(); diff --git a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java index bb19e5e054b7c..abbece5439490 100644 --- a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java +++ b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java @@ -481,7 +481,7 @@ public InputConnection createInputConnection(View view, EditorInfo outAttrs) { InputConnectionAdaptor connection = new InputConnectionAdaptor( - view, inputTarget.id, textInputChannel, mEditable, outAttrs, this); + view, inputTarget.id, textInputChannel, keyProcessor, mEditable, outAttrs); outAttrs.initialSelStart = Selection.getSelectionStart(mEditable); outAttrs.initialSelEnd = Selection.getSelectionEnd(mEditable); diff --git a/shell/platform/android/test/io/flutter/embedding/android/AndroidKeyProcessorTest.java b/shell/platform/android/test/io/flutter/embedding/android/AndroidKeyProcessorTest.java index 8eddb009dc828..74feb6c804663 100644 --- a/shell/platform/android/test/io/flutter/embedding/android/AndroidKeyProcessorTest.java +++ b/shell/platform/android/test/io/flutter/embedding/android/AndroidKeyProcessorTest.java @@ -51,11 +51,11 @@ public void respondsTrueWhenHandlingNewEvents() { AndroidKeyProcessor processor = new AndroidKeyProcessor(fakeView, fakeKeyEventChannel, mock(TextInputPlugin.class)); - boolean result = processor.onKeyDown(new FakeKeyEvent(KeyEvent.ACTION_DOWN, 65)); + boolean result = processor.onKeyEvent(new FakeKeyEvent(KeyEvent.ACTION_DOWN, 65)); assertEquals(true, result); verify(fakeKeyEventChannel, times(1)).keyDown(any(KeyEventChannel.FlutterKeyEvent.class)); verify(fakeKeyEventChannel, times(0)).keyUp(any(KeyEventChannel.FlutterKeyEvent.class)); - verify(fakeView, times(0)).dispatchKeyEvent(any(KeyEvent.class)); + verify(fakeView, times(0)).dispatchKeyEventPreIme(any(KeyEvent.class)); } @Test @@ -97,31 +97,31 @@ public View answer(InvocationOnMock invocation) throws Throwable { ArgumentCaptor.forClass(KeyEventChannel.FlutterKeyEvent.class); FakeKeyEvent fakeKeyEvent = new FakeKeyEvent(KeyEvent.ACTION_DOWN, 65); - boolean result = processor.onKeyDown(fakeKeyEvent); + boolean result = processor.onKeyEvent(fakeKeyEvent); assertEquals(true, result); // Capture the FlutterKeyEvent so we can find out its event ID to use when // faking our response. verify(fakeKeyEventChannel, times(1)).keyDown(eventCaptor.capture()); boolean[] dispatchResult = {true}; - when(fakeView.dispatchKeyEvent(any(KeyEvent.class))) + when(fakeView.dispatchKeyEventPreIme(any(KeyEvent.class))) .then( new Answer() { @Override public Boolean answer(InvocationOnMock invocation) throws Throwable { KeyEvent event = (KeyEvent) invocation.getArguments()[0]; assertEquals(fakeKeyEvent, event); - dispatchResult[0] = processor.onKeyDown(event); + dispatchResult[0] = processor.onKeyEvent(event); return dispatchResult[0]; } }); // Fake a response from the framework. handlerCaptor.getValue().onKeyEventNotHandled(eventCaptor.getValue().eventId); - verify(fakeView, times(1)).dispatchKeyEvent(fakeKeyEvent); + verify(fakeView, times(1)).dispatchKeyEventPreIme(fakeKeyEvent); assertEquals(false, dispatchResult[0]); verify(fakeKeyEventChannel, times(0)).keyUp(any(KeyEventChannel.FlutterKeyEvent.class)); - verify(fakeRootView, times(1)).dispatchKeyEvent(fakeKeyEvent); + verify(fakeRootView, times(1)).dispatchKeyEventPreIme(fakeKeyEvent); } public void synthesizesEventsWhenKeyUpNotHandled() { @@ -147,31 +147,31 @@ public View answer(InvocationOnMock invocation) throws Throwable { ArgumentCaptor.forClass(KeyEventChannel.FlutterKeyEvent.class); FakeKeyEvent fakeKeyEvent = new FakeKeyEvent(KeyEvent.ACTION_UP, 65); - boolean result = processor.onKeyUp(fakeKeyEvent); + boolean result = processor.onKeyEvent(fakeKeyEvent); assertEquals(true, result); // Capture the FlutterKeyEvent so we can find out its event ID to use when // faking our response. verify(fakeKeyEventChannel, times(1)).keyUp(eventCaptor.capture()); boolean[] dispatchResult = {true}; - when(fakeView.dispatchKeyEvent(any(KeyEvent.class))) + when(fakeView.dispatchKeyEventPreIme(any(KeyEvent.class))) .then( new Answer() { @Override public Boolean answer(InvocationOnMock invocation) throws Throwable { KeyEvent event = (KeyEvent) invocation.getArguments()[0]; assertEquals(fakeKeyEvent, event); - dispatchResult[0] = processor.onKeyUp(event); + dispatchResult[0] = processor.onKeyEvent(event); return dispatchResult[0]; } }); // Fake a response from the framework. handlerCaptor.getValue().onKeyEventNotHandled(eventCaptor.getValue().eventId); - verify(fakeView, times(1)).dispatchKeyEvent(fakeKeyEvent); + verify(fakeView, times(1)).dispatchKeyEventPreIme(fakeKeyEvent); assertEquals(false, dispatchResult[0]); verify(fakeKeyEventChannel, times(0)).keyUp(any(KeyEventChannel.FlutterKeyEvent.class)); - verify(fakeRootView, times(1)).dispatchKeyEvent(fakeKeyEvent); + verify(fakeRootView, times(1)).dispatchKeyEventPreIme(fakeKeyEvent); } @NonNull diff --git a/shell/platform/android/test/io/flutter/plugin/editing/InputConnectionAdaptorTest.java b/shell/platform/android/test/io/flutter/plugin/editing/InputConnectionAdaptorTest.java index 2052a47934c0d..a2d8eb963a42c 100644 --- a/shell/platform/android/test/io/flutter/plugin/editing/InputConnectionAdaptorTest.java +++ b/shell/platform/android/test/io/flutter/plugin/editing/InputConnectionAdaptorTest.java @@ -25,6 +25,7 @@ import android.view.View; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.ExtractedText; +import io.flutter.embedding.android.AndroidKeyProcessor; import io.flutter.embedding.engine.FlutterJNI; import io.flutter.embedding.engine.dart.DartExecutor; import io.flutter.embedding.engine.systemchannels.TextInputChannel; @@ -68,6 +69,7 @@ public void inputConnectionAdaptor_ReceivesEnter() throws NullPointerException { DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJni, mock(AssetManager.class))); int inputTargetId = 0; TextInputChannel textInputChannel = new TextInputChannel(dartExecutor); + AndroidKeyProcessor mockKeyProcessor = mock(AndroidKeyProcessor.class); Editable mEditable = Editable.Factory.getInstance().newEditable(""); Editable spyEditable = spy(mEditable); EditorInfo outAttrs = new EditorInfo(); @@ -75,7 +77,7 @@ public void inputConnectionAdaptor_ReceivesEnter() throws NullPointerException { InputConnectionAdaptor inputConnectionAdaptor = new InputConnectionAdaptor( - testView, inputTargetId, textInputChannel, spyEditable, outAttrs); + testView, inputTargetId, textInputChannel, mockKeyProcessor, spyEditable, outAttrs); // Send an enter key and make sure the Editable received it. FakeKeyEvent keyEvent = new FakeKeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER); @@ -156,10 +158,11 @@ public void testPerformPrivateCommand_dataIsNull() throws JSONException { FlutterJNI mockFlutterJNI = mock(FlutterJNI.class); DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJNI, mock(AssetManager.class))); TextInputChannel textInputChannel = new TextInputChannel(dartExecutor); + AndroidKeyProcessor mockKeyProcessor = mock(AndroidKeyProcessor.class); Editable editable = sampleEditable(0, 0); InputConnectionAdaptor adaptor = new InputConnectionAdaptor( - testView, client, textInputChannel, editable, null, mockFlutterJNI); + testView, client, textInputChannel, mockKeyProcessor, editable, null, mockFlutterJNI); adaptor.performPrivateCommand("actionCommand", null); ArgumentCaptor channelCaptor = ArgumentCaptor.forClass(String.class); @@ -183,10 +186,11 @@ public void testPerformPrivateCommand_dataIsByteArray() throws JSONException { FlutterJNI mockFlutterJNI = mock(FlutterJNI.class); DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJNI, mock(AssetManager.class))); TextInputChannel textInputChannel = new TextInputChannel(dartExecutor); + AndroidKeyProcessor mockKeyProcessor = mock(AndroidKeyProcessor.class); Editable editable = sampleEditable(0, 0); InputConnectionAdaptor adaptor = new InputConnectionAdaptor( - testView, client, textInputChannel, editable, null, mockFlutterJNI); + testView, client, textInputChannel, mockKeyProcessor, editable, null, mockFlutterJNI); Bundle bundle = new Bundle(); byte[] buffer = new byte[] {'a', 'b', 'c', 'd'}; @@ -216,10 +220,11 @@ public void testPerformPrivateCommand_dataIsByte() throws JSONException { FlutterJNI mockFlutterJNI = mock(FlutterJNI.class); DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJNI, mock(AssetManager.class))); TextInputChannel textInputChannel = new TextInputChannel(dartExecutor); + AndroidKeyProcessor mockKeyProcessor = mock(AndroidKeyProcessor.class); Editable editable = sampleEditable(0, 0); InputConnectionAdaptor adaptor = new InputConnectionAdaptor( - testView, client, textInputChannel, editable, null, mockFlutterJNI); + testView, client, textInputChannel, mockKeyProcessor, editable, null, mockFlutterJNI); Bundle bundle = new Bundle(); byte b = 3; @@ -247,10 +252,11 @@ public void testPerformPrivateCommand_dataIsCharArray() throws JSONException { FlutterJNI mockFlutterJNI = mock(FlutterJNI.class); DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJNI, mock(AssetManager.class))); TextInputChannel textInputChannel = new TextInputChannel(dartExecutor); + AndroidKeyProcessor mockKeyProcessor = mock(AndroidKeyProcessor.class); Editable editable = sampleEditable(0, 0); InputConnectionAdaptor adaptor = new InputConnectionAdaptor( - testView, client, textInputChannel, editable, null, mockFlutterJNI); + testView, client, textInputChannel, mockKeyProcessor, editable, null, mockFlutterJNI); Bundle bundle = new Bundle(); char[] buffer = new char[] {'a', 'b', 'c', 'd'}; @@ -281,10 +287,11 @@ public void testPerformPrivateCommand_dataIsChar() throws JSONException { FlutterJNI mockFlutterJNI = mock(FlutterJNI.class); DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJNI, mock(AssetManager.class))); TextInputChannel textInputChannel = new TextInputChannel(dartExecutor); + AndroidKeyProcessor mockKeyProcessor = mock(AndroidKeyProcessor.class); Editable editable = sampleEditable(0, 0); InputConnectionAdaptor adaptor = new InputConnectionAdaptor( - testView, client, textInputChannel, editable, null, mockFlutterJNI); + testView, client, textInputChannel, mockKeyProcessor, editable, null, mockFlutterJNI); Bundle bundle = new Bundle(); char b = 'a'; @@ -312,10 +319,11 @@ public void testPerformPrivateCommand_dataIsCharSequenceArray() throws JSONExcep FlutterJNI mockFlutterJNI = mock(FlutterJNI.class); DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJNI, mock(AssetManager.class))); TextInputChannel textInputChannel = new TextInputChannel(dartExecutor); + AndroidKeyProcessor mockKeyProcessor = mock(AndroidKeyProcessor.class); Editable editable = sampleEditable(0, 0); InputConnectionAdaptor adaptor = new InputConnectionAdaptor( - testView, client, textInputChannel, editable, null, mockFlutterJNI); + testView, client, textInputChannel, mockKeyProcessor, editable, null, mockFlutterJNI); Bundle bundle = new Bundle(); CharSequence charSequence1 = new StringBuffer("abc"); @@ -347,10 +355,11 @@ public void testPerformPrivateCommand_dataIsCharSequence() throws JSONException FlutterJNI mockFlutterJNI = mock(FlutterJNI.class); DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJNI, mock(AssetManager.class))); TextInputChannel textInputChannel = new TextInputChannel(dartExecutor); + AndroidKeyProcessor mockKeyProcessor = mock(AndroidKeyProcessor.class); Editable editable = sampleEditable(0, 0); InputConnectionAdaptor adaptor = new InputConnectionAdaptor( - testView, client, textInputChannel, editable, null, mockFlutterJNI); + testView, client, textInputChannel, mockKeyProcessor, editable, null, mockFlutterJNI); Bundle bundle = new Bundle(); CharSequence charSequence = new StringBuffer("abc"); @@ -380,10 +389,11 @@ public void testPerformPrivateCommand_dataIsFloat() throws JSONException { FlutterJNI mockFlutterJNI = mock(FlutterJNI.class); DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJNI, mock(AssetManager.class))); TextInputChannel textInputChannel = new TextInputChannel(dartExecutor); + AndroidKeyProcessor mockKeyProcessor = mock(AndroidKeyProcessor.class); Editable editable = sampleEditable(0, 0); InputConnectionAdaptor adaptor = new InputConnectionAdaptor( - testView, client, textInputChannel, editable, null, mockFlutterJNI); + testView, client, textInputChannel, mockKeyProcessor, editable, null, mockFlutterJNI); Bundle bundle = new Bundle(); float value = 0.5f; @@ -411,10 +421,11 @@ public void testPerformPrivateCommand_dataIsFloatArray() throws JSONException { FlutterJNI mockFlutterJNI = mock(FlutterJNI.class); DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJNI, mock(AssetManager.class))); TextInputChannel textInputChannel = new TextInputChannel(dartExecutor); + AndroidKeyProcessor mockKeyProcessor = mock(AndroidKeyProcessor.class); Editable editable = sampleEditable(0, 0); InputConnectionAdaptor adaptor = new InputConnectionAdaptor( - testView, client, textInputChannel, editable, null, mockFlutterJNI); + testView, client, textInputChannel, mockKeyProcessor, editable, null, mockFlutterJNI); Bundle bundle = new Bundle(); float[] value = {0.5f, 0.6f}; @@ -907,6 +918,7 @@ public void inputConnectionAdaptor_RepeatFilter() throws NullPointerException { DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJni, mock(AssetManager.class))); int inputTargetId = 0; TestTextInputChannel textInputChannel = new TestTextInputChannel(dartExecutor); + AndroidKeyProcessor mockKeyProcessor = mock(AndroidKeyProcessor.class); Editable mEditable = Editable.Factory.getInstance().newEditable(""); Editable spyEditable = spy(mEditable); EditorInfo outAttrs = new EditorInfo(); @@ -914,7 +926,7 @@ public void inputConnectionAdaptor_RepeatFilter() throws NullPointerException { InputConnectionAdaptor inputConnectionAdaptor = new InputConnectionAdaptor( - testView, inputTargetId, textInputChannel, spyEditable, outAttrs); + testView, inputTargetId, textInputChannel, mockKeyProcessor, spyEditable, outAttrs); inputConnectionAdaptor.beginBatchEdit(); assertEquals(textInputChannel.updateEditingStateInvocations, 0); @@ -1159,6 +1171,7 @@ private static InputConnectionAdaptor sampleInputConnectionAdaptor(Editable edit int client = 0; TextInputChannel textInputChannel = mock(TextInputChannel.class); FlutterJNI mockFlutterJNI = mock(FlutterJNI.class); + AndroidKeyProcessor mockKeyProcessor = mock(AndroidKeyProcessor.class); when(mockFlutterJNI.nativeFlutterTextUtilsIsEmoji(anyInt())) .thenAnswer((invocation) -> Emoji.isEmoji((int) invocation.getArguments()[0])); when(mockFlutterJNI.nativeFlutterTextUtilsIsEmojiModifier(anyInt())) @@ -1175,7 +1188,7 @@ private static InputConnectionAdaptor sampleInputConnectionAdaptor(Editable edit .thenAnswer( (invocation) -> Emoji.isRegionalIndicatorSymbol((int) invocation.getArguments()[0])); return new InputConnectionAdaptor( - testView, client, textInputChannel, editable, null, mockFlutterJNI); + testView, client, textInputChannel, mockKeyProcessor, editable, null, mockFlutterJNI); } private class TestTextInputChannel extends TextInputChannel {