diff --git a/shell/platform/android/io/flutter/embedding/android/AndroidKeyProcessor.java b/shell/platform/android/io/flutter/embedding/android/AndroidKeyProcessor.java index 7de46d4d9ebdb..ebf3a94068745 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,12 +250,26 @@ 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().dispatchKeyEvent(event); + + 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..78cc4e7e716d7 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 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 @@ -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); - } - - return androidKeyProcessor.onKeyDown(event) || super.onKeyDown(keyCode, event); + 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); } /** diff --git a/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java b/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java index d702e2b7855ea..26cb5d9d984e7 100644 --- a/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java +++ b/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java @@ -27,6 +27,7 @@ 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; @@ -34,6 +35,7 @@ class InputConnectionAdaptor extends BaseInputConnection { private final View mFlutterView; private final int mClient; private final TextInputChannel textInputChannel; + private final AndroidKeyProcessor keyProcessor; private final Editable mEditable; private final EditorInfo mEditorInfo; private int mBatchCount; @@ -97,6 +99,7 @@ public InputConnectionAdaptor( View view, int client, TextInputChannel textInputChannel, + AndroidKeyProcessor keyProcessor, Editable editable, EditorInfo editorInfo, FlutterJNI flutterJNI) { @@ -107,6 +110,7 @@ public InputConnectionAdaptor( mEditable = editable; mEditorInfo = editorInfo; mBatchCount = 0; + 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. @@ -128,9 +132,10 @@ public InputConnectionAdaptor( View view, int client, TextInputChannel textInputChannel, + AndroidKeyProcessor keyProcessor, Editable editable, EditorInfo editorInfo) { - this(view, client, textInputChannel, editable, editorInfo, new FlutterJNI()); + this(view, client, textInputChannel, keyProcessor, editable, editorInfo, new FlutterJNI()); } // Send the current state of the editable to Flutter. @@ -323,6 +328,14 @@ private static int clampIndexToEditable(int index, Editable editable) { @Override public boolean sendKeyEvent(KeyEvent event) { + // 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(); 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 2dc272a7d74be..b58cb8c67c399 100644 --- a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java +++ b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java @@ -28,6 +28,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; 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; @@ -48,6 +49,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 @@ -172,6 +174,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. @@ -313,7 +324,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, keyProcessor, mEditable, outAttrs); 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 b630ad242a51f..282ec161ea44c 100644 --- a/shell/platform/android/io/flutter/view/FlutterView.java +++ b/shell/platform/android/io/flutter/view/FlutterView.java @@ -269,19 +269,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); + public boolean dispatchKeyEventPreIme(KeyEvent event) { + return (isAttached() && androidKeyProcessor.onKeyEvent(event)) + || super.dispatchKeyEventPreIme(event); } public FlutterNativeView getFlutterNativeView() { 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 {