diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index 164dd3c974c1d..17ea689f6eb22 100755 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -783,7 +783,6 @@ FILE: ../../../flutter/shell/platform/android/io/flutter/app/FlutterApplication. FILE: ../../../flutter/shell/platform/android/io/flutter/app/FlutterFragmentActivity.java FILE: ../../../flutter/shell/platform/android/io/flutter/app/FlutterPlayStoreSplitApplication.java FILE: ../../../flutter/shell/platform/android/io/flutter/app/FlutterPluginRegistry.java -FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/AndroidKeyProcessor.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/AndroidTouchProcessor.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/DrawableSplashScreen.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/ExclusiveAppComponent.java @@ -799,6 +798,8 @@ FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/Flutt FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/FlutterSurfaceView.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/FlutterTextureView.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/FlutterView.java +FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/KeyChannelResponder.java +FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/KeyboardManager.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/MotionEventTracker.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/RenderMode.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/SplashScreen.java diff --git a/shell/platform/android/BUILD.gn b/shell/platform/android/BUILD.gn index 12eb239bb90b8..70f5e4430661a 100644 --- a/shell/platform/android/BUILD.gn +++ b/shell/platform/android/BUILD.gn @@ -128,7 +128,6 @@ android_java_sources = [ "io/flutter/app/FlutterFragmentActivity.java", "io/flutter/app/FlutterPlayStoreSplitApplication.java", "io/flutter/app/FlutterPluginRegistry.java", - "io/flutter/embedding/android/AndroidKeyProcessor.java", "io/flutter/embedding/android/AndroidTouchProcessor.java", "io/flutter/embedding/android/DrawableSplashScreen.java", "io/flutter/embedding/android/ExclusiveAppComponent.java", @@ -144,6 +143,8 @@ android_java_sources = [ "io/flutter/embedding/android/FlutterSurfaceView.java", "io/flutter/embedding/android/FlutterTextureView.java", "io/flutter/embedding/android/FlutterView.java", + "io/flutter/embedding/android/KeyChannelResponder.java", + "io/flutter/embedding/android/KeyboardManager.java", "io/flutter/embedding/android/MotionEventTracker.java", "io/flutter/embedding/android/RenderMode.java", "io/flutter/embedding/android/SplashScreen.java", @@ -458,13 +459,14 @@ action("robolectric_tests") { "test/io/flutter/FlutterTestSuite.java", "test/io/flutter/SmokeTest.java", "test/io/flutter/TestUtils.java", - "test/io/flutter/embedding/android/AndroidKeyProcessorTest.java", "test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java", "test/io/flutter/embedding/android/FlutterActivityTest.java", "test/io/flutter/embedding/android/FlutterAndroidComponentTest.java", "test/io/flutter/embedding/android/FlutterFragmentActivityTest.java", "test/io/flutter/embedding/android/FlutterFragmentTest.java", "test/io/flutter/embedding/android/FlutterViewTest.java", + "test/io/flutter/embedding/android/KeyChannelResponderTest.java", + "test/io/flutter/embedding/android/KeyboardManagerTest.java", "test/io/flutter/embedding/android/RobolectricFlutterActivity.java", "test/io/flutter/embedding/engine/FlutterEngineCacheTest.java", "test/io/flutter/embedding/engine/FlutterEngineConnectionRegistryTest.java", diff --git a/shell/platform/android/io/flutter/embedding/android/AndroidKeyProcessor.java b/shell/platform/android/io/flutter/embedding/android/AndroidKeyProcessor.java deleted file mode 100644 index 00021b9b21343..0000000000000 --- a/shell/platform/android/io/flutter/embedding/android/AndroidKeyProcessor.java +++ /dev/null @@ -1,275 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package io.flutter.embedding.android; - -import android.view.KeyCharacterMap; -import android.view.KeyEvent; -import android.view.View; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import io.flutter.Log; -import io.flutter.embedding.engine.systemchannels.KeyEventChannel; -import io.flutter.plugin.editing.TextInputPlugin; -import java.util.ArrayDeque; -import java.util.Deque; -import java.util.Iterator; - -/** - * A class to process key events from Android, passing them to the framework as messages using - * {@link KeyEventChannel}. - * - *

A class that sends Android key events to the framework, and re-dispatches those not handled by - * the framework. - * - *

Flutter uses asynchronous event handling to avoid blocking the UI thread, but Android requires - * that events are handled synchronously. So, when a key event is received by Flutter, it tells - * Android synchronously that the key has been handled so that it won't propagate to other - * components. Flutter then uses "delayed event synthesis", where it sends the event to the - * framework, and if the framework responds that it has not handled the event, then this class - * synthesizes a new event to send to Android, without handling it this time. - */ -public class AndroidKeyProcessor { - private static final String TAG = "AndroidKeyProcessor"; - - @NonNull private final KeyEventChannel keyEventChannel; - @NonNull private final TextInputPlugin textInputPlugin; - private int combiningCharacter; - @NonNull private EventResponder eventResponder; - - /** - * Constructor for AndroidKeyProcessor. - * - *

The view is used as the destination to send the synthesized key to. This means that the the - * next thing in the focus chain will get the event when the framework returns false from - * onKeyDown/onKeyUp - * - *

It is possible that that in the middle of the async round trip, the focus chain could - * change, and instead of the native widget that was "next" when the event was fired getting the - * event, it may be the next widget when the event is synthesized that gets it. In practice, this - * shouldn't be a huge problem, as this is an unlikely occurrence to happen without user input, - * and it may actually be desired behavior, but it is possible. - * - * @param view takes the activity to use for re-dispatching of events that were not handled by the - * framework. - * @param keyEventChannel the event channel to listen to for new key events. - * @param textInputPlugin a plugin, which, if set, is given key events before the framework is, - * and if it has a valid input connection and is accepting text, then it will handle the event - * and the framework will not receive it. - */ - public AndroidKeyProcessor( - @NonNull View view, - @NonNull KeyEventChannel keyEventChannel, - @NonNull TextInputPlugin textInputPlugin) { - this.keyEventChannel = keyEventChannel; - this.textInputPlugin = textInputPlugin; - textInputPlugin.setKeyEventProcessor(this); - this.eventResponder = new EventResponder(view, textInputPlugin); - this.keyEventChannel.setEventResponseHandler(eventResponder); - } - - /** - * Detaches the key processor from the Flutter engine. - * - *

The AndroidKeyProcessor instance should not be used after calling this. - */ - public void destroy() { - keyEventChannel.setEventResponseHandler(null); - } - - /** - * 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 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 theoretically - // that isn't sent by Android anymore, so this is just for protection in - // case the theory is wrong. - return false; - } - if (isPendingEvent(keyEvent)) { - // If the keyEvent is in the queue of pending events we've seen, and has - // the same id, then we know that this is a re-dispatched keyEvent, and we - // shouldn't respond to it, but we should remove it from tracking now. - eventResponder.removePendingEvent(keyEvent); - return false; - } - - Character complexCharacter = applyCombiningCharacterToBaseCharacter(keyEvent.getUnicodeChar()); - KeyEventChannel.FlutterKeyEvent flutterEvent = - new KeyEventChannel.FlutterKeyEvent(keyEvent, complexCharacter); - - eventResponder.addEvent(keyEvent); - if (action == KeyEvent.ACTION_DOWN) { - keyEventChannel.keyDown(flutterEvent); - } else { - keyEventChannel.keyUp(flutterEvent); - } - return true; - } - - /** - * Returns whether or not the given event is currently being processed by this key processor. This - * is used to determine if a new key event sent to the {@link InputConnectionAdaptor} originates - * from a hardware key event, or a soft keyboard editing event. - * - * @param event the event to check for being the current event. - * @return - */ - public boolean isPendingEvent(@NonNull KeyEvent event) { - return eventResponder.findPendingEvent(event) != null; - } - - /** - * Applies the given Unicode character in {@code newCharacterCodePoint} to a previously entered - * Unicode combining character and returns the combination of these characters if a combination - * exists. - * - *

This method mutates {@link #combiningCharacter} over time to combine characters. - * - *

One of the following things happens in this method: - * - *

- * - *

The following reference explains the concept of a "combining character": - * https://en.wikipedia.org/wiki/Combining_character - */ - @Nullable - private Character applyCombiningCharacterToBaseCharacter(int newCharacterCodePoint) { - if (newCharacterCodePoint == 0) { - return null; - } - - char complexCharacter = (char) newCharacterCodePoint; - boolean isNewCodePointACombiningCharacter = - (newCharacterCodePoint & KeyCharacterMap.COMBINING_ACCENT) != 0; - if (isNewCodePointACombiningCharacter) { - // If a combining character was entered before, combine this one with that one. - int plainCodePoint = newCharacterCodePoint & KeyCharacterMap.COMBINING_ACCENT_MASK; - if (combiningCharacter != 0) { - combiningCharacter = KeyCharacterMap.getDeadChar(combiningCharacter, plainCodePoint); - } else { - combiningCharacter = plainCodePoint; - } - } else { - // The new character is a regular character. Apply combiningCharacter to it, if - // it exists. - if (combiningCharacter != 0) { - int combinedChar = KeyCharacterMap.getDeadChar(combiningCharacter, newCharacterCodePoint); - if (combinedChar > 0) { - complexCharacter = (char) combinedChar; - } - combiningCharacter = 0; - } - } - - return complexCharacter; - } - - private static class EventResponder implements KeyEventChannel.EventResponseHandler { - // The maximum number of pending events that are held before starting to - // complain. - private static final long MAX_PENDING_EVENTS = 1000; - final Deque pendingEvents = new ArrayDeque(); - @NonNull private final View view; - @NonNull private final TextInputPlugin textInputPlugin; - - public EventResponder(@NonNull View view, @NonNull TextInputPlugin textInputPlugin) { - this.view = view; - this.textInputPlugin = textInputPlugin; - } - - /** Removes the first pending event from the cache of pending events. */ - private void removePendingEvent(KeyEvent event) { - pendingEvents.remove(event); - } - - private KeyEvent findPendingEvent(KeyEvent event) { - Iterator iter = pendingEvents.iterator(); - while (iter.hasNext()) { - KeyEvent item = iter.next(); - if (item == event) { - return item; - } - } - return null; - } - - /** - * Called whenever the framework responds that a given key event was handled by the framework. - * - * @param event the event to be marked as being handled by the framework. Must not be null. - */ - @Override - public void onKeyEventHandled(KeyEvent event) { - removePendingEvent(event); - } - - /** - * Called whenever the framework responds that a given key event wasn't handled by the - * framework. - * - * @param event the event to be marked as not being handled by the framework. Must not be null. - */ - @Override - public void onKeyEventNotHandled(KeyEvent event) { - redispatchKeyEvent(findPendingEvent(event)); - } - - /** Adds an Android key event to the event responder to wait for a response. */ - public void addEvent(@NonNull KeyEvent event) { - pendingEvents.addLast(event); - if (pendingEvents.size() > MAX_PENDING_EVENTS) { - Log.e( - TAG, - "There are " - + pendingEvents.size() - + " keyboard events that have not yet received a response. Are responses being " - + "sent?"); - } - } - - /** - * Dispatches the event to the activity associated with the context. - * - * @param event the event to be dispatched to the activity. - */ - private void redispatchKeyEvent(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.getInputMethodManager().isAcceptingText() - && textInputPlugin.getLastInputConnection() != null - && textInputPlugin.getLastInputConnection().sendKeyEvent(event)) { - // The event was handled, so we can remove it from the queue. - removePendingEvent(event); - return; - } - - // Since the framework didn't handle it, dispatch the event again. - if (view != null) { - view.getRootView().dispatchKeyEvent(event); - } - } - } -} diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterView.java b/shell/platform/android/io/flutter/embedding/android/FlutterView.java index 70d5ad60065c1..5e56f9d188969 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterView.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterView.java @@ -104,7 +104,7 @@ public class FlutterView extends FrameLayout implements MouseCursorPlugin.MouseC @Nullable private MouseCursorPlugin mouseCursorPlugin; @Nullable private TextInputPlugin textInputPlugin; @Nullable private LocalizationPlugin localizationPlugin; - @Nullable private AndroidKeyProcessor androidKeyProcessor; + @Nullable private KeyboardManager keyboardManager; @Nullable private AndroidTouchProcessor androidTouchProcessor; @Nullable private AccessibilityBridge accessibilityBridge; @@ -705,7 +705,7 @@ public InputConnection onCreateInputConnection(@NonNull EditorInfo outAttrs) { return super.onCreateInputConnection(outAttrs); } - return textInputPlugin.createInputConnection(this, outAttrs); + return textInputPlugin.createInputConnection(this, keyboardManager, outAttrs); } /** @@ -730,7 +730,7 @@ public boolean checkInputConnectionProxy(View view) { * 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 + *

{@link KeyEvent}s are sent from Android to Flutter. {@link KeyboardManager} 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. */ @@ -747,7 +747,7 @@ public boolean dispatchKeyEvent(KeyEvent event) { // 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)) + return (isAttachedToFlutterEngine() && keyboardManager.handleEvent(event)) || super.dispatchKeyEvent(event); } @@ -975,8 +975,14 @@ public void attachToFlutterEngine(@NonNull FlutterEngine flutterEngine) { this.flutterEngine.getTextInputChannel(), this.flutterEngine.getPlatformViewsController()); localizationPlugin = this.flutterEngine.getLocalizationPlugin(); - androidKeyProcessor = - new AndroidKeyProcessor(this, this.flutterEngine.getKeyEventChannel(), textInputPlugin); + + keyboardManager = + new KeyboardManager( + this, + textInputPlugin, + new KeyChannelResponder[] { + new KeyChannelResponder(flutterEngine.getKeyEventChannel()) + }); androidTouchProcessor = new AndroidTouchProcessor(this.flutterEngine.getRenderer(), /*trackMotionEvents=*/ false); accessibilityBridge = @@ -1060,8 +1066,7 @@ public void detachFromFlutterEngine() { // TODO(mattcarroll): once this is proven to work, move this line ot TextInputPlugin textInputPlugin.getInputMethodManager().restartInput(this); textInputPlugin.destroy(); - - androidKeyProcessor.destroy(); + keyboardManager.destroy(); if (mouseCursorPlugin != null) { mouseCursorPlugin.destroy(); diff --git a/shell/platform/android/io/flutter/embedding/android/KeyChannelResponder.java b/shell/platform/android/io/flutter/embedding/android/KeyChannelResponder.java new file mode 100644 index 0000000000000..d3bd28db79ea0 --- /dev/null +++ b/shell/platform/android/io/flutter/embedding/android/KeyChannelResponder.java @@ -0,0 +1,105 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.embedding.android; + +import android.view.KeyCharacterMap; +import android.view.KeyEvent; +import androidx.annotation.NonNull; +import io.flutter.embedding.engine.systemchannels.KeyEventChannel; + +/** + * A {@link Responder} of {@link KeyboardManager} that handles events by sending the raw information + * through the method channel. + * + *

This class corresponds to the RawKeyboard API in the framework. + */ +public class KeyChannelResponder implements KeyboardManager.Responder { + private static final String TAG = "KeyChannelResponder"; + + @NonNull private final KeyEventChannel keyEventChannel; + private int combiningCharacter; + + public KeyChannelResponder(@NonNull KeyEventChannel keyEventChannel) { + this.keyEventChannel = keyEventChannel; + } + + /** + * Applies the given Unicode character in {@code newCharacterCodePoint} to a previously entered + * Unicode combining character and returns the combination of these characters if a combination + * exists. + * + *

This method mutates {@link #combiningCharacter} over time to combine characters. + * + *

One of the following things happens in this method: + * + *

+ * + *

The following reference explains the concept of a "combining character": + * https://en.wikipedia.org/wiki/Combining_character + */ + Character applyCombiningCharacterToBaseCharacter(int newCharacterCodePoint) { + char complexCharacter = (char) newCharacterCodePoint; + boolean isNewCodePointACombiningCharacter = + (newCharacterCodePoint & KeyCharacterMap.COMBINING_ACCENT) != 0; + if (isNewCodePointACombiningCharacter) { + // If a combining character was entered before, combine this one with that one. + int plainCodePoint = newCharacterCodePoint & KeyCharacterMap.COMBINING_ACCENT_MASK; + if (combiningCharacter != 0) { + combiningCharacter = KeyCharacterMap.getDeadChar(combiningCharacter, plainCodePoint); + } else { + combiningCharacter = plainCodePoint; + } + } else { + // The new character is a regular character. Apply combiningCharacter to it, if + // it exists. + if (combiningCharacter != 0) { + int combinedChar = KeyCharacterMap.getDeadChar(combiningCharacter, newCharacterCodePoint); + if (combinedChar > 0) { + complexCharacter = (char) combinedChar; + } + combiningCharacter = 0; + } + } + + return complexCharacter; + } + + @Override + public void handleEvent( + @NonNull KeyEvent keyEvent, @NonNull OnKeyEventHandledCallback onKeyEventHandledCallback) { + final int action = keyEvent.getAction(); + if (action != KeyEvent.ACTION_DOWN && action != KeyEvent.ACTION_UP) { + // There is theoretically a KeyEvent.ACTION_MULTIPLE, but theoretically + // that isn't sent by Android anymore, so this is just for protection in + // case the theory is wrong. + onKeyEventHandledCallback.onKeyEventHandled(false); + return; + } + + final Character complexCharacter = + applyCombiningCharacterToBaseCharacter(keyEvent.getUnicodeChar()); + KeyEventChannel.FlutterKeyEvent flutterEvent = + new KeyEventChannel.FlutterKeyEvent(keyEvent, complexCharacter); + + final boolean isKeyUp = action != KeyEvent.ACTION_DOWN; + keyEventChannel.sendFlutterKeyEvent( + flutterEvent, + isKeyUp, + (isEventHandled) -> onKeyEventHandledCallback.onKeyEventHandled(isEventHandled)); + } +} diff --git a/shell/platform/android/io/flutter/embedding/android/KeyboardManager.java b/shell/platform/android/io/flutter/embedding/android/KeyboardManager.java new file mode 100644 index 0000000000000..ccfcbc4ff5ca1 --- /dev/null +++ b/shell/platform/android/io/flutter/embedding/android/KeyboardManager.java @@ -0,0 +1,194 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.embedding.android; + +import android.view.KeyEvent; +import android.view.View; +import androidx.annotation.NonNull; +import io.flutter.Log; +import io.flutter.embedding.android.KeyboardManager.Responder.OnKeyEventHandledCallback; +import io.flutter.plugin.editing.TextInputPlugin; +import java.util.HashSet; + +/** + * A class to process {@link KeyEvent}s dispatched to a {@link FlutterView}, either from a hardware + * keyboard or an IME event. + * + *

A class that sends Android {@link KeyEvent} to the a list of {@link Responder}s, and + * re-dispatches those not handled by the primary responders. + * + *

Flutter uses asynchronous event handling to avoid blocking the UI thread, but Android requires + * that events are handled synchronously. So, when the Android system sends new @{link KeyEvent} to + * Flutter, Flutter responds synchronously that the key has been handled so that it won't propagate + * to other components. It then uses "delayed event synthesis", where it sends the event to the + * framework, and if the framework responds that it has not handled the event, then this class + * synthesizes a new event to send to Android, without handling it this time. + * + *

A new {@link KeyEvent} sent to a {@link KeyboardManager} can be propagated to 3 different + * types of responders (in the listed order): + * + *

+ */ +public class KeyboardManager { + private static final String TAG = "KeyboardManager"; + + /** + * Constructor for {@link KeyboardManager} that takes a list of {@link Responder}s. + * + *

The view is used as the destination to send the synthesized key to. This means that the the + * next thing in the focus chain will get the event when the {@link Responder}s return false from + * onKeyDown/onKeyUp. + * + *

It is possible that that in the middle of the async round trip, the focus chain could + * change, and instead of the native widget that was "next" when the event was fired getting the + * event, it may be the next widget when the event is synthesized that gets it. In practice, this + * shouldn't be a huge problem, as this is an unlikely occurrence to happen without user input, + * and it may actually be desired behavior, but it is possible. + * + * @param view takes the activity to use for re-dispatching of events that were not handled by the + * framework. + * @param textInputPlugin a plugin, which, if set, is given key events before the framework is, + * and if it has a valid input connection and is accepting text, then it will handle the event + * and the framework will not receive it. + * @param responders the {@link Responder}s new {@link KeyEvent}s will be first dispatched to. + */ + public KeyboardManager( + View view, @NonNull TextInputPlugin textInputPlugin, Responder[] responders) { + this.view = view; + this.textInputPlugin = textInputPlugin; + this.responders = responders; + } + + /** + * The interface for responding to a {@link KeyEvent} asynchronously. + * + *

Implementers of this interface should be owned by a {@link KeyboardManager}, in order to + * receive key events. + * + *

After receiving a {@link KeyEvent}, the {@link Responder} must call the supplied {@link + * OnKeyEventHandledCallback} exactly once, to inform the {@link KeyboardManager} whether it + * wishes to handle the {@link KeyEvent}. The {@link KeyEvent} will not be propagated to the + * {@link TextInputPlugin} or be redispatched to the view hierachy if any key responders answered + * yes. + * + *

If a {@link Responder} fails to call the {@link OnKeyEventHandledCallback} callback, the + * {@link KeyEvent} will never be sent to the {@link TextInputPlugin}, and the {@link + * KeyboardManager} class can't detect such errors as there is no timeout. + */ + interface Responder { + interface OnKeyEventHandledCallback { + void onKeyEventHandled(Boolean canHandleEvent); + } + + /** + * Informs this {@link Responder} that a new {@link KeyEvent} needs processing. + * + * @param keyEvent the new {@link KeyEvent} this {@link Responder} may be interested in. + * @param onKeyEventHandledCallback the method to call when this {@link Responder} has decided + * whether to handle the {@link keyEvent}. + */ + void handleEvent( + @NonNull KeyEvent keyEvent, @NonNull OnKeyEventHandledCallback onKeyEventHandledCallback); + } + + private class PerEventCallbackBuilder { + private class Callback implements OnKeyEventHandledCallback { + boolean isCalled = false; + + @Override + public void onKeyEventHandled(Boolean canHandleEvent) { + if (isCalled) { + throw new IllegalStateException( + "The onKeyEventHandledCallback should be called exactly once."); + } + isCalled = true; + unrepliedCount -= 1; + isEventHandled |= canHandleEvent; + if (unrepliedCount == 0 && !isEventHandled) { + onUnhandled(keyEvent); + } + } + } + + PerEventCallbackBuilder(@NonNull KeyEvent keyEvent) { + this.keyEvent = keyEvent; + } + + @NonNull final KeyEvent keyEvent; + int unrepliedCount = responders.length; + boolean isEventHandled = false; + + public OnKeyEventHandledCallback buildCallback() { + return new Callback(); + } + } + + @NonNull protected final Responder[] responders; + @NonNull private final HashSet redispatchedEvents = new HashSet<>(); + @NonNull private final TextInputPlugin textInputPlugin; + private final View view; + + public boolean handleEvent(@NonNull KeyEvent keyEvent) { + final boolean isRedispatchedEvent = redispatchedEvents.remove(keyEvent); + if (isRedispatchedEvent) { + return false; + } + + if (responders.length > 0) { + final PerEventCallbackBuilder callbackBuilder = new PerEventCallbackBuilder(keyEvent); + for (final Responder primaryResponder : responders) { + primaryResponder.handleEvent(keyEvent, callbackBuilder.buildCallback()); + } + } else { + onUnhandled(keyEvent); + } + + return true; + } + + public void destroy() { + final int remainingRedispatchCount = redispatchedEvents.size(); + if (remainingRedispatchCount > 0) { + Log.w( + TAG, + "A KeyboardManager was destroyed with " + + String.valueOf(remainingRedispatchCount) + + " unhandled redispatch event(s)."); + } + } + + private void onUnhandled(@NonNull KeyEvent keyEvent) { + if (textInputPlugin.handleKeyEvent(keyEvent) || view == null) { + return; + } + + redispatchedEvents.add(keyEvent); + view.getRootView().dispatchKeyEvent(keyEvent); + if (redispatchedEvents.remove(keyEvent)) { + Log.w(TAG, "A redispatched key event was consumed before reaching KeyboardManager"); + } + } +} diff --git a/shell/platform/android/io/flutter/embedding/engine/systemchannels/KeyEventChannel.java b/shell/platform/android/io/flutter/embedding/engine/systemchannels/KeyEventChannel.java index dbd6bf7f9c924..22345515774e6 100644 --- a/shell/platform/android/io/flutter/embedding/engine/systemchannels/KeyEventChannel.java +++ b/shell/platform/android/io/flutter/embedding/engine/systemchannels/KeyEventChannel.java @@ -27,33 +27,16 @@ public class KeyEventChannel { private static final String TAG = "KeyEventChannel"; - /** - * Sets the event response handler to be used to receive key event response messages from the - * framework on this channel. - */ - public void setEventResponseHandler(EventResponseHandler handler) { - this.eventResponseHandler = handler; - } - - private EventResponseHandler eventResponseHandler; - /** A handler of incoming key handling messages. */ public interface EventResponseHandler { /** - * Called whenever the framework responds that a given key event was handled by the framework. - * - * @param event the event to be marked as being handled by the framework. Must not be null. - */ - public void onKeyEventHandled(KeyEvent event); - - /** - * Called whenever the framework responds that a given key event wasn't handled by the - * framework. + * Called whenever the framework responds that a given key event was handled or not handled by + * the framework. * - * @param event the event to be marked as not being handled by the framework. Must not be null. + * @param isEventHandled whether the framework decides to handle the event. */ - public void onKeyEventNotHandled(KeyEvent event); + public void onFrameworkResponse(boolean isEventHandled); } /** @@ -66,58 +49,19 @@ public KeyEventChannel(@NonNull BinaryMessenger binaryMessenger) { new BasicMessageChannel<>(binaryMessenger, "flutter/keyevent", JSONMessageCodec.INSTANCE); } - /** - * Creates a reply handler for the given key event. - * - * @param event the Android key event to create a reply for. - */ - BasicMessageChannel.Reply createReplyHandler(KeyEvent event) { - return message -> { - if (eventResponseHandler == null) { - return; - } - - try { - if (message == null) { - eventResponseHandler.onKeyEventNotHandled(event); - return; - } - final JSONObject annotatedEvent = (JSONObject) message; - final boolean handled = annotatedEvent.getBoolean("handled"); - if (handled) { - eventResponseHandler.onKeyEventHandled(event); - } else { - eventResponseHandler.onKeyEventNotHandled(event); - } - } catch (JSONException e) { - Log.e(TAG, "Unable to unpack JSON message: " + e); - eventResponseHandler.onKeyEventNotHandled(event); - } - }; - } - @NonNull public final BasicMessageChannel channel; - public void keyUp(@NonNull FlutterKeyEvent keyEvent) { - Map message = new HashMap<>(); - message.put("type", "keyup"); - message.put("keymap", "android"); - encodeKeyEvent(keyEvent, message); - - channel.send(message, createReplyHandler(keyEvent.event)); + public void sendFlutterKeyEvent( + @NonNull FlutterKeyEvent keyEvent, + boolean isKeyUp, + @NonNull EventResponseHandler responseHandler) { + channel.send(encodeKeyEvent(keyEvent, isKeyUp), createReplyHandler(responseHandler)); } - public void keyDown(@NonNull FlutterKeyEvent keyEvent) { + private Map encodeKeyEvent(@NonNull FlutterKeyEvent keyEvent, boolean isKeyUp) { Map message = new HashMap<>(); - message.put("type", "keydown"); + message.put("type", isKeyUp ? "keyup" : "keydown"); message.put("keymap", "android"); - encodeKeyEvent(keyEvent, message); - - channel.send(message, createReplyHandler(keyEvent.event)); - } - - private void encodeKeyEvent( - @NonNull FlutterKeyEvent keyEvent, @NonNull Map message) { message.put("flags", keyEvent.event.getFlags()); message.put("plainCodePoint", keyEvent.event.getUnicodeChar(0x0)); message.put("codePoint", keyEvent.event.getUnicodeChar()); @@ -141,6 +85,28 @@ private void encodeKeyEvent( message.put("productId", productId); message.put("deviceId", keyEvent.event.getDeviceId()); message.put("repeatCount", keyEvent.event.getRepeatCount()); + return message; + } + + /** + * Creates a reply handler for the given key event. + * + * @param responseHandler the completion handler to call when the framework responds. + */ + private static BasicMessageChannel.Reply createReplyHandler( + @NonNull EventResponseHandler responseHandler) { + return message -> { + boolean isEventHandled = false; + try { + if (message != null) { + final JSONObject annotatedEvent = (JSONObject) message; + isEventHandled = annotatedEvent.getBoolean("handled"); + } + } catch (JSONException e) { + Log.e(TAG, "Unable to unpack JSON message: " + e); + } + responseHandler.onFrameworkResponse(isEventHandled); + }; } /** A key event as defined by Flutter. */ diff --git a/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java b/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java index 0b68c27eb3ada..c5a4a3745c29b 100644 --- a/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java +++ b/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java @@ -27,7 +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.android.KeyboardManager; import io.flutter.embedding.engine.FlutterJNI; import io.flutter.embedding.engine.systemchannels.TextInputChannel; @@ -38,7 +38,6 @@ class InputConnectionAdaptor extends BaseInputConnection private final View mFlutterView; private final int mClient; private final TextInputChannel textInputChannel; - private final AndroidKeyProcessor keyProcessor; private final ListenableEditingState mEditable; private final EditorInfo mEditorInfo; private ExtractedTextRequest mExtractRequest; @@ -48,13 +47,14 @@ class InputConnectionAdaptor extends BaseInputConnection private InputMethodManager mImm; private final Layout mLayout; private FlutterTextUtils flutterTextUtils; + private final KeyboardManager keyboardManager; @SuppressWarnings("deprecation") public InputConnectionAdaptor( View view, int client, TextInputChannel textInputChannel, - AndroidKeyProcessor keyProcessor, + KeyboardManager keyboardManager, ListenableEditingState editable, EditorInfo editorInfo, FlutterJNI flutterJNI) { @@ -65,7 +65,7 @@ public InputConnectionAdaptor( mEditable = editable; mEditable.addEditingStateListener(this); mEditorInfo = editorInfo; - this.keyProcessor = keyProcessor; + this.keyboardManager = keyboardManager; 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. @@ -85,10 +85,10 @@ public InputConnectionAdaptor( View view, int client, TextInputChannel textInputChannel, - AndroidKeyProcessor keyProcessor, + KeyboardManager keyboardManager, ListenableEditingState editable, EditorInfo editorInfo) { - this(view, client, textInputChannel, keyProcessor, editable, editorInfo, new FlutterJNI()); + this(view, client, textInputChannel, keyboardManager, editable, editorInfo, new FlutterJNI()); } private ExtractedText getExtractedText(ExtractedTextRequest request) { @@ -290,20 +290,10 @@ private static int clampIndexToEditable(int index, Editable editable) { // occur, and need a chance to be handled by the framework. @Override public boolean sendKeyEvent(KeyEvent event) { - // This gives the key processor a chance to process this event if it came - // from a soft keyboard. 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 to this function. Only do this if the event is not the - // current event, since that indicates that the key processor sent it to us, - // and we only want to call the key processor for events that it doesn't - // already know about (i.e. when events arrive here from a soft keyboard and - // not a hardware keyboard), to avoid a loop. - if (keyProcessor != null - && !keyProcessor.isPendingEvent(event) - && keyProcessor.onKeyEvent(event)) { - return true; - } + return keyboardManager.handleEvent(event); + } + public boolean handleKeyEvent(KeyEvent event) { if (event.getAction() == KeyEvent.ACTION_DOWN) { if (event.getKeyCode() == KeyEvent.KEYCODE_DPAD_LEFT) { return handleHorizontalMovement(true, event.isShiftPressed()); diff --git a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java index b59f8e325693d..f6fc6354152e8 100644 --- a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java +++ b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java @@ -12,6 +12,7 @@ import android.text.Editable; import android.text.InputType; import android.util.SparseArray; +import android.view.KeyEvent; import android.view.View; import android.view.ViewStructure; import android.view.WindowInsets; @@ -25,7 +26,7 @@ import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import io.flutter.Log; -import io.flutter.embedding.android.AndroidKeyProcessor; +import io.flutter.embedding.android.KeyboardManager; import io.flutter.embedding.engine.systemchannels.TextInputChannel; import io.flutter.embedding.engine.systemchannels.TextInputChannel.TextEditState; import io.flutter.plugin.platform.PlatformViewsController; @@ -48,7 +49,6 @@ public class TextInputPlugin implements ListenableEditingState.EditingStateWatch @NonNull private PlatformViewsController platformViewsController; @Nullable private Rect lastClientRect; private ImeSyncDeferringInsetsCallback imeSyncCallback; - private AndroidKeyProcessor keyProcessor; // Initialize the "last seen" text editing values to a non-null value. private TextEditState mLastKnownFrameworkTextEditingState; @@ -175,15 +175,6 @@ 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. @@ -286,7 +277,8 @@ private static int inputTypeFromTextInputType( return textType; } - public InputConnection createInputConnection(View view, EditorInfo outAttrs) { + public InputConnection createInputConnection( + View view, KeyboardManager keyboardManager, EditorInfo outAttrs) { if (inputTarget.type == InputTarget.Type.NO_TARGET) { lastInputConnection = null; return null; @@ -330,7 +322,7 @@ public InputConnection createInputConnection(View view, EditorInfo outAttrs) { InputConnectionAdaptor connection = new InputConnectionAdaptor( - view, inputTarget.id, textInputChannel, keyProcessor, mEditable, outAttrs); + view, inputTarget.id, textInputChannel, keyboardManager, mEditable, outAttrs); outAttrs.initialSelStart = mEditable.getSelectionStart(); outAttrs.initialSelEnd = mEditable.getSelectionEnd(); @@ -557,6 +549,23 @@ public InputTarget(@NonNull Type type, int id) { int id; } + // -------- Start: KeyboardManager Synchronous Responder ------- + public boolean handleKeyEvent(KeyEvent keyEvent) { + if (!getInputMethodManager().isAcceptingText() || lastInputConnection == null) { + return false; + } + + // Send the KeyEvent as an IME KeyEvent. If the input connection is an + // InputConnectionAdaptor then call its handleKeyEvent method (because + // this method will be called by the keyboard manager, and + // InputConnectionAdaptor#sendKeyEvent forwards the key event back to the + // keyboard manager). + return (lastInputConnection instanceof InputConnectionAdaptor) + ? ((InputConnectionAdaptor) lastInputConnection).handleKeyEvent(keyEvent) + : lastInputConnection.sendKeyEvent(keyEvent); + } + // -------- End: KeyboardManager Synchronous Responder ------- + // -------- Start: ListenableEditingState watcher implementation ------- @Override diff --git a/shell/platform/android/io/flutter/view/FlutterView.java b/shell/platform/android/io/flutter/view/FlutterView.java index 59711aabd7880..3988f84c9c3b7 100644 --- a/shell/platform/android/io/flutter/view/FlutterView.java +++ b/shell/platform/android/io/flutter/view/FlutterView.java @@ -42,8 +42,9 @@ import androidx.annotation.UiThread; import io.flutter.Log; import io.flutter.app.FlutterPluginRegistry; -import io.flutter.embedding.android.AndroidKeyProcessor; import io.flutter.embedding.android.AndroidTouchProcessor; +import io.flutter.embedding.android.KeyChannelResponder; +import io.flutter.embedding.android.KeyboardManager; import io.flutter.embedding.engine.dart.DartExecutor; import io.flutter.embedding.engine.renderer.FlutterRenderer; import io.flutter.embedding.engine.renderer.SurfaceTextureWrapper; @@ -127,7 +128,7 @@ static final class ViewportMetrics { private final TextInputPlugin mTextInputPlugin; private final LocalizationPlugin mLocalizationPlugin; private final MouseCursorPlugin mMouseCursorPlugin; - private final AndroidKeyProcessor androidKeyProcessor; + private final KeyboardManager mKeyboardManager; private final AndroidTouchProcessor androidTouchProcessor; private AccessibilityBridge mAccessibilityNodeProvider; private final SurfaceHolder.Callback mSurfaceCallback; @@ -228,13 +229,18 @@ public void onPostResume() { mNativeView.getPluginRegistry().getPlatformViewsController(); mTextInputPlugin = new TextInputPlugin(this, new TextInputChannel(dartExecutor), platformViewsController); + mKeyboardManager = + new KeyboardManager( + this, + mTextInputPlugin, + new KeyChannelResponder[] {new KeyChannelResponder(keyEventChannel)}); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { mMouseCursorPlugin = new MouseCursorPlugin(this, new MouseCursorChannel(dartExecutor)); } else { mMouseCursorPlugin = null; } mLocalizationPlugin = new LocalizationPlugin(context, localizationChannel); - androidKeyProcessor = new AndroidKeyProcessor(this, keyEventChannel, mTextInputPlugin); androidTouchProcessor = new AndroidTouchProcessor(flutterRenderer, /*trackMotionEvents=*/ false); platformViewsController.attachToFlutterRenderer(flutterRenderer); @@ -282,7 +288,7 @@ public boolean dispatchKeyEvent(KeyEvent event) { // 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 (isAttached() && androidKeyProcessor.onKeyEvent(event)) || super.dispatchKeyEvent(event); + return (isAttached() && mKeyboardManager.handleEvent(event)) || super.dispatchKeyEvent(event); } public FlutterNativeView getFlutterNativeView() { @@ -442,7 +448,7 @@ public void destroy() { @Override public InputConnection onCreateInputConnection(EditorInfo outAttrs) { - return mTextInputPlugin.createInputConnection(this, outAttrs); + return mTextInputPlugin.createInputConnection(this, mKeyboardManager, outAttrs); } @Override diff --git a/shell/platform/android/test/io/flutter/FlutterTestSuite.java b/shell/platform/android/test/io/flutter/FlutterTestSuite.java index 446c3d9f62172..c63f9faa20ca6 100644 --- a/shell/platform/android/test/io/flutter/FlutterTestSuite.java +++ b/shell/platform/android/test/io/flutter/FlutterTestSuite.java @@ -4,13 +4,14 @@ package io.flutter; -import io.flutter.embedding.android.AndroidKeyProcessorTest; import io.flutter.embedding.android.FlutterActivityAndFragmentDelegateTest; import io.flutter.embedding.android.FlutterActivityTest; import io.flutter.embedding.android.FlutterAndroidComponentTest; import io.flutter.embedding.android.FlutterFragmentActivityTest; import io.flutter.embedding.android.FlutterFragmentTest; import io.flutter.embedding.android.FlutterViewTest; +import io.flutter.embedding.android.KeyChannelResponderTest; +import io.flutter.embedding.android.KeyboardManagerTest; import io.flutter.embedding.engine.FlutterEngineCacheTest; import io.flutter.embedding.engine.FlutterEngineConnectionRegistryTest; import io.flutter.embedding.engine.FlutterEngineGroupComponentTest; @@ -52,7 +53,6 @@ @RunWith(Suite.class) @SuiteClasses({ AccessibilityBridgeTest.class, - AndroidKeyProcessorTest.class, ApplicationInfoLoaderTest.class, BinaryCodecTest.class, DartExecutorTest.class, @@ -77,6 +77,8 @@ FlutterViewTest.class, InputConnectionAdaptorTest.class, DeferredComponentChannelTest.class, + KeyboardManagerTest.class, + KeyChannelResponderTest.class, KeyEventChannelTest.class, ListenableEditingStateTest.class, LocalizationPluginTest.class, diff --git a/shell/platform/android/test/io/flutter/embedding/android/AndroidKeyProcessorTest.java b/shell/platform/android/test/io/flutter/embedding/android/AndroidKeyProcessorTest.java deleted file mode 100644 index f4fd809642675..0000000000000 --- a/shell/platform/android/test/io/flutter/embedding/android/AndroidKeyProcessorTest.java +++ /dev/null @@ -1,309 +0,0 @@ -package io.flutter.embedding.android; - -import static junit.framework.TestCase.assertEquals; -import static org.mockito.Mockito.any; -import static org.mockito.Mockito.isNull; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.notNull; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import android.annotation.TargetApi; -import android.view.KeyEvent; -import android.view.View; -import androidx.annotation.NonNull; -import io.flutter.embedding.engine.FlutterEngine; -import io.flutter.embedding.engine.FlutterJNI; -import io.flutter.embedding.engine.systemchannels.KeyEventChannel; -import io.flutter.embedding.engine.systemchannels.TextInputChannel; -import io.flutter.plugin.editing.TextInputPlugin; -import io.flutter.util.FakeKeyEvent; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.stubbing.Answer; -import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; - -@Config(manifest = Config.NONE) -@RunWith(RobolectricTestRunner.class) -@TargetApi(28) -public class AndroidKeyProcessorTest { - @Mock FlutterJNI mockFlutterJni; - - @Before - public void setUp() { - MockitoAnnotations.initMocks(this); - when(mockFlutterJni.isAttached()).thenReturn(true); - } - - @Test - public void respondsTrueWhenHandlingNewEvents() { - FlutterEngine flutterEngine = mockFlutterEngine(); - KeyEventChannel fakeKeyEventChannel = flutterEngine.getKeyEventChannel(); - View fakeView = mock(View.class); - - AndroidKeyProcessor processor = - new AndroidKeyProcessor(fakeView, fakeKeyEventChannel, mock(TextInputPlugin.class)); - - 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)); - } - - @Test - public void destroyTest() { - FlutterEngine flutterEngine = mockFlutterEngine(); - KeyEventChannel fakeKeyEventChannel = flutterEngine.getKeyEventChannel(); - View fakeView = mock(View.class); - - AndroidKeyProcessor processor = - new AndroidKeyProcessor(fakeView, fakeKeyEventChannel, mock(TextInputPlugin.class)); - - verify(fakeKeyEventChannel, times(1)) - .setEventResponseHandler(notNull(KeyEventChannel.EventResponseHandler.class)); - processor.destroy(); - verify(fakeKeyEventChannel, times(1)) - .setEventResponseHandler(isNull(KeyEventChannel.EventResponseHandler.class)); - } - - public void removesPendingEventsWhenKeyDownHandled() { - FlutterEngine flutterEngine = mockFlutterEngine(); - KeyEventChannel fakeKeyEventChannel = flutterEngine.getKeyEventChannel(); - View fakeView = mock(View.class); - View fakeRootView = mock(View.class); - when(fakeView.getRootView()) - .then( - new Answer() { - @Override - public View answer(InvocationOnMock invocation) throws Throwable { - return fakeRootView; - } - }); - - ArgumentCaptor handlerCaptor = - ArgumentCaptor.forClass(KeyEventChannel.EventResponseHandler.class); - verify(fakeKeyEventChannel).setEventResponseHandler(handlerCaptor.capture()); - AndroidKeyProcessor processor = - new AndroidKeyProcessor(fakeView, fakeKeyEventChannel, mock(TextInputPlugin.class)); - ArgumentCaptor eventCaptor = - ArgumentCaptor.forClass(KeyEventChannel.FlutterKeyEvent.class); - FakeKeyEvent fakeKeyEvent = new FakeKeyEvent(KeyEvent.ACTION_DOWN, 65); - - boolean result = processor.onKeyEvent(fakeKeyEvent); - assertEquals(true, processor.isPendingEvent(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))) - .then( - new Answer() { - @Override - public Boolean answer(InvocationOnMock invocation) throws Throwable { - KeyEvent event = (KeyEvent) invocation.getArguments()[0]; - assertEquals(fakeKeyEvent, event); - dispatchResult[0] = processor.onKeyEvent(event); - return dispatchResult[0]; - } - }); - - // Fake a response from the framework. - handlerCaptor.getValue().onKeyEventHandled(eventCaptor.getValue().event); - assertEquals(false, processor.isPendingEvent(fakeKeyEvent)); - } - - public void synthesizesEventsWhenKeyDownNotHandled() { - FlutterEngine flutterEngine = mockFlutterEngine(); - KeyEventChannel fakeKeyEventChannel = flutterEngine.getKeyEventChannel(); - View fakeView = mock(View.class); - View fakeRootView = mock(View.class); - when(fakeView.getRootView()) - .then( - new Answer() { - @Override - public View answer(InvocationOnMock invocation) throws Throwable { - return fakeRootView; - } - }); - - ArgumentCaptor handlerCaptor = - ArgumentCaptor.forClass(KeyEventChannel.EventResponseHandler.class); - verify(fakeKeyEventChannel).setEventResponseHandler(handlerCaptor.capture()); - AndroidKeyProcessor processor = - new AndroidKeyProcessor(fakeView, fakeKeyEventChannel, mock(TextInputPlugin.class)); - ArgumentCaptor eventCaptor = - ArgumentCaptor.forClass(KeyEventChannel.FlutterKeyEvent.class); - FakeKeyEvent fakeKeyEvent = new FakeKeyEvent(KeyEvent.ACTION_DOWN, 65); - - boolean result = processor.onKeyEvent(fakeKeyEvent); - assertEquals(true, processor.isPendingEvent(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))) - .then( - new Answer() { - @Override - public Boolean answer(InvocationOnMock invocation) throws Throwable { - KeyEvent event = (KeyEvent) invocation.getArguments()[0]; - assertEquals(fakeKeyEvent, event); - dispatchResult[0] = processor.onKeyEvent(event); - return dispatchResult[0]; - } - }); - - // Fake a response from the framework. - handlerCaptor.getValue().onKeyEventNotHandled(eventCaptor.getValue().event); - assertEquals(true, processor.isPendingEvent(fakeKeyEvent)); - verify(fakeView, times(1)).dispatchKeyEvent(fakeKeyEvent); - assertEquals(false, dispatchResult[0]); - verify(fakeKeyEventChannel, times(0)).keyUp(any(KeyEventChannel.FlutterKeyEvent.class)); - verify(fakeRootView, times(1)).dispatchKeyEvent(fakeKeyEvent); - } - - public void synthesizesEventsWhenKeyUpNotHandled() { - FlutterEngine flutterEngine = mockFlutterEngine(); - KeyEventChannel fakeKeyEventChannel = flutterEngine.getKeyEventChannel(); - View fakeView = mock(View.class); - View fakeRootView = mock(View.class); - when(fakeView.getRootView()) - .then( - new Answer() { - @Override - public View answer(InvocationOnMock invocation) throws Throwable { - return fakeRootView; - } - }); - - ArgumentCaptor handlerCaptor = - ArgumentCaptor.forClass(KeyEventChannel.EventResponseHandler.class); - verify(fakeKeyEventChannel).setEventResponseHandler(handlerCaptor.capture()); - AndroidKeyProcessor processor = - new AndroidKeyProcessor(fakeView, fakeKeyEventChannel, mock(TextInputPlugin.class)); - ArgumentCaptor eventCaptor = - ArgumentCaptor.forClass(KeyEventChannel.FlutterKeyEvent.class); - FakeKeyEvent fakeKeyEvent = new FakeKeyEvent(KeyEvent.ACTION_UP, 65); - - boolean result = processor.onKeyEvent(fakeKeyEvent); - assertEquals(true, processor.isPendingEvent(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))) - .then( - new Answer() { - @Override - public Boolean answer(InvocationOnMock invocation) throws Throwable { - KeyEvent event = (KeyEvent) invocation.getArguments()[0]; - assertEquals(fakeKeyEvent, event); - dispatchResult[0] = processor.onKeyEvent(event); - return dispatchResult[0]; - } - }); - - // Fake a response from the framework. - handlerCaptor.getValue().onKeyEventNotHandled(eventCaptor.getValue().event); - assertEquals(true, processor.isPendingEvent(fakeKeyEvent)); - verify(fakeView, times(1)).dispatchKeyEvent(fakeKeyEvent); - assertEquals(false, dispatchResult[0]); - verify(fakeKeyEventChannel, times(0)).keyUp(any(KeyEventChannel.FlutterKeyEvent.class)); - verify(fakeRootView, times(1)).dispatchKeyEvent(fakeKeyEvent); - } - - public void respondsCorrectlyWhenEventsAreReturnedOutOfOrder() { - FlutterEngine flutterEngine = mockFlutterEngine(); - KeyEventChannel fakeKeyEventChannel = flutterEngine.getKeyEventChannel(); - View fakeView = mock(View.class); - View fakeRootView = mock(View.class); - when(fakeView.getRootView()) - .then( - new Answer() { - @Override - public View answer(InvocationOnMock invocation) throws Throwable { - return fakeRootView; - } - }); - - ArgumentCaptor handlerCaptor = - ArgumentCaptor.forClass(KeyEventChannel.EventResponseHandler.class); - verify(fakeKeyEventChannel).setEventResponseHandler(handlerCaptor.capture()); - AndroidKeyProcessor processor = - new AndroidKeyProcessor(fakeView, fakeKeyEventChannel, mock(TextInputPlugin.class)); - ArgumentCaptor event1Captor = - ArgumentCaptor.forClass(KeyEventChannel.FlutterKeyEvent.class); - ArgumentCaptor event2Captor = - ArgumentCaptor.forClass(KeyEventChannel.FlutterKeyEvent.class); - FakeKeyEvent fakeKeyEvent1 = new FakeKeyEvent(KeyEvent.ACTION_DOWN, 65); - FakeKeyEvent fakeKeyEvent2 = new FakeKeyEvent(KeyEvent.ACTION_DOWN, 20); - - boolean result1 = processor.onKeyEvent(fakeKeyEvent1); - boolean result2 = processor.onKeyEvent(fakeKeyEvent2); - assertEquals(true, processor.isPendingEvent(fakeKeyEvent1)); - assertEquals(true, processor.isPendingEvent(fakeKeyEvent2)); - assertEquals(true, result1); - assertEquals(true, result2); - - // Capture the FlutterKeyEvent so we can find out its event ID to use when - // faking our response. - verify(fakeKeyEventChannel, times(1)).keyDown(event1Captor.capture()); - verify(fakeKeyEventChannel, times(1)).keyDown(event2Captor.capture()); - boolean[] dispatchResult = {true, true}; - when(fakeView.dispatchKeyEvent(any(KeyEvent.class))) - .then( - new Answer() { - @Override - public Boolean answer(InvocationOnMock invocation) throws Throwable { - KeyEvent event = (KeyEvent) invocation.getArguments()[0]; - assertEquals(true, fakeKeyEvent1 == event || fakeKeyEvent2 == event); - if (fakeKeyEvent1 == event) { - dispatchResult[0] = processor.onKeyEvent(fakeKeyEvent1); - return dispatchResult[0]; - } else { - dispatchResult[1] = processor.onKeyEvent(fakeKeyEvent2); - return dispatchResult[1]; - } - } - }); - - assertEquals(true, processor.isPendingEvent(fakeKeyEvent1)); - assertEquals(true, processor.isPendingEvent(fakeKeyEvent2)); - - // Fake a "handled" response from the framework, but do it in reverse order. - handlerCaptor.getValue().onKeyEventNotHandled(event2Captor.getValue().event); - handlerCaptor.getValue().onKeyEventNotHandled(event1Captor.getValue().event); - - verify(fakeView, times(1)).dispatchKeyEvent(fakeKeyEvent1); - verify(fakeView, times(1)).dispatchKeyEvent(fakeKeyEvent2); - assertEquals(false, dispatchResult[0]); - assertEquals(false, dispatchResult[1]); - verify(fakeKeyEventChannel, times(0)).keyUp(any(KeyEventChannel.FlutterKeyEvent.class)); - verify(fakeRootView, times(1)).dispatchKeyEvent(fakeKeyEvent1); - verify(fakeRootView, times(1)).dispatchKeyEvent(fakeKeyEvent2); - } - - @NonNull - private FlutterEngine mockFlutterEngine() { - // Mock FlutterEngine and all of its required direct calls. - FlutterEngine engine = mock(FlutterEngine.class); - when(engine.getKeyEventChannel()).thenReturn(mock(KeyEventChannel.class)); - when(engine.getTextInputChannel()).thenReturn(mock(TextInputChannel.class)); - - return engine; - } -} diff --git a/shell/platform/android/test/io/flutter/embedding/android/KeyChannelResponderTest.java b/shell/platform/android/test/io/flutter/embedding/android/KeyChannelResponderTest.java new file mode 100644 index 0000000000000..0b97651332fa4 --- /dev/null +++ b/shell/platform/android/test/io/flutter/embedding/android/KeyChannelResponderTest.java @@ -0,0 +1,81 @@ +package io.flutter.embedding.android; + +import static junit.framework.TestCase.assertEquals; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.doAnswer; + +import android.annotation.TargetApi; +import android.view.KeyCharacterMap; +import android.view.KeyEvent; +import io.flutter.embedding.engine.systemchannels.KeyEventChannel; +import io.flutter.embedding.engine.systemchannels.KeyEventChannel.EventResponseHandler; +import io.flutter.embedding.engine.systemchannels.KeyEventChannel.FlutterKeyEvent; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +@Config(manifest = Config.NONE) +@RunWith(RobolectricTestRunner.class) +@TargetApi(28) +public class KeyChannelResponderTest { + + private static final int DEAD_KEY = '`' | KeyCharacterMap.COMBINING_ACCENT; + + @Mock KeyEventChannel keyEventChannel; + KeyChannelResponder channelResponder; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + channelResponder = new KeyChannelResponder(keyEventChannel); + } + + @Test + public void primaryResponderTest() { + final int[] completionCallbackInvocationCounter = {0}; + + doAnswer( + invocation -> { + invocation.getArgumentAt(2, EventResponseHandler.class).onFrameworkResponse(true); + return null; + }) + .when(keyEventChannel) + .sendFlutterKeyEvent( + any(FlutterKeyEvent.class), any(boolean.class), any(EventResponseHandler.class)); + + final KeyEvent keyEvent = new KeyEvent(KeyEvent.ACTION_DOWN, 65); + channelResponder.handleEvent( + keyEvent, + (canHandleEvent) -> { + completionCallbackInvocationCounter[0]++; + }); + assertEquals(completionCallbackInvocationCounter[0], 1); + } + + @Test + public void basicCombingCharactersTest() { + assertEquals(0, (int) channelResponder.applyCombiningCharacterToBaseCharacter(0)); + assertEquals('A', (int) channelResponder.applyCombiningCharacterToBaseCharacter('A')); + assertEquals('B', (int) channelResponder.applyCombiningCharacterToBaseCharacter('B')); + assertEquals('B', (int) channelResponder.applyCombiningCharacterToBaseCharacter('B')); + assertEquals(0, (int) channelResponder.applyCombiningCharacterToBaseCharacter(0)); + assertEquals(0, (int) channelResponder.applyCombiningCharacterToBaseCharacter(0)); + + assertEquals('`', (int) channelResponder.applyCombiningCharacterToBaseCharacter(DEAD_KEY)); + assertEquals('`', (int) channelResponder.applyCombiningCharacterToBaseCharacter(DEAD_KEY)); + assertEquals('À', (int) channelResponder.applyCombiningCharacterToBaseCharacter('A')); + + assertEquals('`', (int) channelResponder.applyCombiningCharacterToBaseCharacter(DEAD_KEY)); + assertEquals(0, (int) channelResponder.applyCombiningCharacterToBaseCharacter(0)); + // The 0 input should remove the combining state. + assertEquals('A', (int) channelResponder.applyCombiningCharacterToBaseCharacter('A')); + + assertEquals(0, (int) channelResponder.applyCombiningCharacterToBaseCharacter(0)); + assertEquals('`', (int) channelResponder.applyCombiningCharacterToBaseCharacter(DEAD_KEY)); + assertEquals('À', (int) channelResponder.applyCombiningCharacterToBaseCharacter('A')); + } +} diff --git a/shell/platform/android/test/io/flutter/embedding/android/KeyboardManagerTest.java b/shell/platform/android/test/io/flutter/embedding/android/KeyboardManagerTest.java new file mode 100644 index 0000000000000..11f07569077b3 --- /dev/null +++ b/shell/platform/android/test/io/flutter/embedding/android/KeyboardManagerTest.java @@ -0,0 +1,303 @@ +package io.flutter.embedding.android; + +import static junit.framework.TestCase.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.annotation.TargetApi; +import android.view.KeyEvent; +import android.view.View; +import androidx.annotation.NonNull; +import io.flutter.embedding.android.KeyboardManager.Responder; +import io.flutter.embedding.engine.FlutterEngine; +import io.flutter.embedding.engine.FlutterJNI; +import io.flutter.embedding.engine.systemchannels.KeyEventChannel; +import io.flutter.embedding.engine.systemchannels.TextInputChannel; +import io.flutter.plugin.editing.TextInputPlugin; +import io.flutter.util.FakeKeyEvent; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +@Config(manifest = Config.NONE) +@RunWith(RobolectricTestRunner.class) +@TargetApi(28) +public class KeyboardManagerTest { + static class FakeResponder implements Responder { + KeyEvent mLastKeyEvent; + OnKeyEventHandledCallback mLastKeyEventHandledCallback; + + @Override + public void handleEvent( + @NonNull KeyEvent keyEvent, @NonNull OnKeyEventHandledCallback onKeyEventHandledCallback) { + mLastKeyEvent = keyEvent; + mLastKeyEventHandledCallback = onKeyEventHandledCallback; + } + + void eventHandled(boolean isHandled) { + mLastKeyEventHandledCallback.onKeyEventHandled(isHandled); + } + } + + @Mock FlutterJNI mockFlutterJni; + + FlutterEngine mockEngine; + KeyEventChannel mockKeyEventChannel; + @Mock TextInputPlugin mockTextInputPlugin; + @Mock View mockView; + @Mock View mockRootView; + KeyboardManager keyboardManager; + + @NonNull + private FlutterEngine mockFlutterEngine() { + // Mock FlutterEngine and all of its required direct calls. + FlutterEngine engine = mock(FlutterEngine.class); + when(engine.getKeyEventChannel()).thenReturn(mock(KeyEventChannel.class)); + when(engine.getTextInputChannel()).thenReturn(mock(TextInputChannel.class)); + return engine; + } + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + when(mockFlutterJni.isAttached()).thenReturn(true); + mockEngine = mockFlutterEngine(); + mockKeyEventChannel = mockEngine.getKeyEventChannel(); + when(mockView.getRootView()).thenAnswer(invocation -> mockRootView); + when(mockView.dispatchKeyEvent(any(KeyEvent.class))) + .thenAnswer( + invocation -> keyboardManager.handleEvent((KeyEvent) invocation.getArguments()[0])); + when(mockRootView.dispatchKeyEvent(any(KeyEvent.class))) + .thenAnswer( + invocation -> mockView.dispatchKeyEvent((KeyEvent) invocation.getArguments()[0])); + keyboardManager = + new KeyboardManager( + mockView, + mockTextInputPlugin, + new Responder[] {new KeyChannelResponder(mockKeyEventChannel)}); + } + + // Tests start + + @Test + public void respondsTrueWhenHandlingNewEvents() { + final FakeResponder fakeResponder = new FakeResponder(); + keyboardManager = + new KeyboardManager( + mockView, mockTextInputPlugin, new KeyboardManager.Responder[] {fakeResponder}); + final KeyEvent keyEvent = new FakeKeyEvent(KeyEvent.ACTION_DOWN, 65); + final boolean result = keyboardManager.handleEvent(keyEvent); + + assertEquals(true, result); + assertEquals(keyEvent, fakeResponder.mLastKeyEvent); + // Don't send the key event to the text plugin if the only primary responder + // hasn't responded. + verify(mockTextInputPlugin, times(0)).handleKeyEvent(any(KeyEvent.class)); + verify(mockRootView, times(0)).dispatchKeyEvent(any(KeyEvent.class)); + } + + @Test + public void primaryRespondersHaveTheHighestPrecedence() { + final FakeResponder fakeResponder = new FakeResponder(); + keyboardManager = + new KeyboardManager( + mockView, mockTextInputPlugin, new KeyboardManager.Responder[] {fakeResponder}); + final KeyEvent keyEvent = new FakeKeyEvent(KeyEvent.ACTION_DOWN, 65); + final boolean result = keyboardManager.handleEvent(keyEvent); + + assertEquals(true, result); + assertEquals(keyEvent, fakeResponder.mLastKeyEvent); + + // Don't send the key event to the text plugin if the only primary responder + // hasn't responded. + verify(mockTextInputPlugin, times(0)).handleKeyEvent(any(KeyEvent.class)); + verify(mockRootView, times(0)).dispatchKeyEvent(any(KeyEvent.class)); + + // If a primary responder handles the key event the propagation stops. + assertNotNull(fakeResponder.mLastKeyEventHandledCallback); + fakeResponder.eventHandled(true); + verify(mockTextInputPlugin, times(0)).handleKeyEvent(any(KeyEvent.class)); + verify(mockRootView, times(0)).dispatchKeyEvent(any(KeyEvent.class)); + } + + @Test + public void zeroRespondersTest() { + keyboardManager = + new KeyboardManager(mockView, mockTextInputPlugin, new KeyboardManager.Responder[] {}); + final KeyEvent keyEvent = new FakeKeyEvent(KeyEvent.ACTION_DOWN, 65); + final boolean result = keyboardManager.handleEvent(keyEvent); + assertEquals(true, result); + + // Send the key event to the text plugin since there's 0 primary responders. + verify(mockTextInputPlugin, times(1)).handleKeyEvent(any(KeyEvent.class)); + } + + @Test + public void multipleRespondersTest() { + final FakeResponder fakeResponder1 = new FakeResponder(); + final FakeResponder fakeResponder2 = new FakeResponder(); + keyboardManager = + new KeyboardManager( + mockView, + mockTextInputPlugin, + new KeyboardManager.Responder[] {fakeResponder1, fakeResponder2}); + final KeyEvent keyEvent = new FakeKeyEvent(KeyEvent.ACTION_DOWN, 65); + final boolean result = keyboardManager.handleEvent(keyEvent); + + assertEquals(true, result); + assertEquals(keyEvent, fakeResponder1.mLastKeyEvent); + assertEquals(keyEvent, fakeResponder2.mLastKeyEvent); + + fakeResponder2.eventHandled(false); + // Don't send the key event to the text plugin, since fakeResponder1 + // hasn't responded. + verify(mockTextInputPlugin, times(0)).handleKeyEvent(any(KeyEvent.class)); + + fakeResponder1.eventHandled(false); + verify(mockTextInputPlugin, times(1)).handleKeyEvent(any(KeyEvent.class)); + } + + @Test + public void multipleRespondersTest2() { + final FakeResponder fakeResponder1 = new FakeResponder(); + final FakeResponder fakeResponder2 = new FakeResponder(); + keyboardManager = + new KeyboardManager( + mockView, + mockTextInputPlugin, + new KeyboardManager.Responder[] {fakeResponder1, fakeResponder2}); + final KeyEvent keyEvent = new FakeKeyEvent(KeyEvent.ACTION_DOWN, 65); + final boolean result = keyboardManager.handleEvent(keyEvent); + + fakeResponder2.eventHandled(false); + fakeResponder1.eventHandled(true); + + // Handled by primary responders, propagation stops. + verify(mockTextInputPlugin, times(0)).handleKeyEvent(any(KeyEvent.class)); + } + + @Test + public void multipleRespondersTest3() { + final FakeResponder fakeResponder1 = new FakeResponder(); + final FakeResponder fakeResponder2 = new FakeResponder(); + keyboardManager = + new KeyboardManager( + mockView, + mockTextInputPlugin, + new KeyboardManager.Responder[] {fakeResponder1, fakeResponder2}); + final KeyEvent keyEvent = new FakeKeyEvent(KeyEvent.ACTION_DOWN, 65); + final boolean result = keyboardManager.handleEvent(keyEvent); + + fakeResponder2.eventHandled(false); + + Exception exception = null; + try { + fakeResponder2.eventHandled(false); + } catch (Exception e) { + exception = e; + } + // Throws since the same handle is called twice. + assertNotNull(exception); + } + + @Test + public void textInputPluginHasTheSecondHighestPrecedence() { + final FakeResponder fakeResponder = new FakeResponder(); + keyboardManager = + spy( + new KeyboardManager( + mockView, mockTextInputPlugin, new KeyboardManager.Responder[] {fakeResponder})); + final KeyEvent keyEvent = new FakeKeyEvent(KeyEvent.ACTION_DOWN, 65); + final boolean result = keyboardManager.handleEvent(keyEvent); + + assertEquals(true, result); + assertEquals(keyEvent, fakeResponder.mLastKeyEvent); + + // Don't send the key event to the text plugin if the only primary responder + // hasn't responded. + verify(mockTextInputPlugin, times(0)).handleKeyEvent(any(KeyEvent.class)); + verify(mockRootView, times(0)).dispatchKeyEvent(any(KeyEvent.class)); + + // If no primary responder handles the key event the propagates to the text + // input plugin. + assertNotNull(fakeResponder.mLastKeyEventHandledCallback); + // Let text input plugin handle the key event. + when(mockTextInputPlugin.handleKeyEvent(any())).thenAnswer(invocation -> true); + fakeResponder.eventHandled(false); + + verify(mockTextInputPlugin, times(1)).handleKeyEvent(keyEvent); + verify(mockRootView, times(0)).dispatchKeyEvent(any(KeyEvent.class)); + + // It's not redispatched to the keyboard manager. + verify(keyboardManager, times(1)).handleEvent(any(KeyEvent.class)); + } + + @Test + public void RedispatchKeyEventIfTextInputPluginFailsToHandle() { + final FakeResponder fakeResponder = new FakeResponder(); + keyboardManager = + spy( + new KeyboardManager( + mockView, mockTextInputPlugin, new KeyboardManager.Responder[] {fakeResponder})); + final KeyEvent keyEvent = new FakeKeyEvent(KeyEvent.ACTION_DOWN, 65); + final boolean result = keyboardManager.handleEvent(keyEvent); + + assertEquals(true, result); + assertEquals(keyEvent, fakeResponder.mLastKeyEvent); + + // Don't send the key event to the text plugin if the only primary responder + // hasn't responded. + verify(mockTextInputPlugin, times(0)).handleKeyEvent(any(KeyEvent.class)); + verify(mockRootView, times(0)).dispatchKeyEvent(any(KeyEvent.class)); + + // Neither the primary responders nor text input plugin handles the event. + when(mockTextInputPlugin.handleKeyEvent(any())).thenAnswer(invocation -> false); + fakeResponder.mLastKeyEvent = null; + fakeResponder.eventHandled(false); + + verify(mockTextInputPlugin, times(1)).handleKeyEvent(keyEvent); + verify(mockRootView, times(1)).dispatchKeyEvent(keyEvent); + } + + @Test + public void respondsFalseWhenHandlingRedispatchedEvents() { + final FakeResponder fakeResponder = new FakeResponder(); + keyboardManager = + spy( + new KeyboardManager( + mockView, mockTextInputPlugin, new KeyboardManager.Responder[] {fakeResponder})); + final KeyEvent keyEvent = new FakeKeyEvent(KeyEvent.ACTION_DOWN, 65); + final boolean result = keyboardManager.handleEvent(keyEvent); + + assertEquals(true, result); + assertEquals(keyEvent, fakeResponder.mLastKeyEvent); + + // Don't send the key event to the text plugin if the only primary responder + // hasn't responded. + verify(mockTextInputPlugin, times(0)).handleKeyEvent(any(KeyEvent.class)); + verify(mockRootView, times(0)).dispatchKeyEvent(any(KeyEvent.class)); + + // Neither the primary responders nor text input plugin handles the event. + when(mockTextInputPlugin.handleKeyEvent(any())).thenAnswer(invocation -> false); + fakeResponder.mLastKeyEvent = null; + fakeResponder.eventHandled(false); + + verify(mockTextInputPlugin, times(1)).handleKeyEvent(keyEvent); + verify(mockRootView, times(1)).dispatchKeyEvent(keyEvent); + + // It's redispatched to the keyboard manager, but not the primary + // responders. + verify(keyboardManager, times(2)).handleEvent(any(KeyEvent.class)); + assertNull(fakeResponder.mLastKeyEvent); + } +} diff --git a/shell/platform/android/test/io/flutter/embedding/engine/systemchannels/KeyEventChannelTest.java b/shell/platform/android/test/io/flutter/embedding/engine/systemchannels/KeyEventChannelTest.java index 23645ed9f2829..d0e5817e664ce 100644 --- a/shell/platform/android/test/io/flutter/embedding/engine/systemchannels/KeyEventChannelTest.java +++ b/shell/platform/android/test/io/flutter/embedding/engine/systemchannels/KeyEventChannelTest.java @@ -4,22 +4,23 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.any; -import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import android.annotation.TargetApi; import android.view.KeyEvent; -import androidx.annotation.NonNull; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.JSONMessageCodec; import io.flutter.util.FakeKeyEvent; import java.nio.ByteBuffer; import org.json.JSONException; import org.json.JSONObject; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; @@ -30,6 +31,11 @@ @TargetApi(24) public class KeyEventChannelTest { + KeyEvent keyEvent; + @Mock BinaryMessenger fakeMessenger; + boolean[] handled; + KeyEventChannel keyEventChannel; + private void sendReply(boolean handled, BinaryMessenger.BinaryReply messengerReply) throws JSONException { JSONObject reply = new JSONObject(); @@ -40,30 +46,25 @@ private void sendReply(boolean handled, BinaryMessenger.BinaryReply messengerRep messengerReply.reply(binaryReply); } + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + keyEvent = new FakeKeyEvent(KeyEvent.ACTION_DOWN, 65); + handled = new boolean[] {false}; + keyEventChannel = new KeyEventChannel(fakeMessenger); + } + @Test public void keyDownEventIsSentToFramework() throws JSONException { - BinaryMessenger fakeMessenger = mock(BinaryMessenger.class); - KeyEventChannel keyEventChannel = new KeyEventChannel(fakeMessenger); - final boolean[] handled = {false}; - final KeyEvent[] handledKeyEvents = {null}; - keyEventChannel.setEventResponseHandler( - new KeyEventChannel.EventResponseHandler() { - public void onKeyEventHandled(@NonNull KeyEvent event) { - handled[0] = true; - handledKeyEvents[0] = event; - } - - public void onKeyEventNotHandled(@NonNull KeyEvent event) { - handled[0] = false; - handledKeyEvents[0] = event; - } + KeyEventChannel.FlutterKeyEvent flutterKeyEvent = + new KeyEventChannel.FlutterKeyEvent(keyEvent, null); + keyEventChannel.sendFlutterKeyEvent( + flutterKeyEvent, + false, + (isHandled) -> { + handled[0] = isHandled; }); - verify(fakeMessenger, times(0)).send(any(), any(), any()); - KeyEvent event = new FakeKeyEvent(KeyEvent.ACTION_DOWN, 65); - KeyEventChannel.FlutterKeyEvent flutterKeyEvent = - new KeyEventChannel.FlutterKeyEvent(event, null); - keyEventChannel.keyDown(flutterKeyEvent); ArgumentCaptor byteBufferArgumentCaptor = ArgumentCaptor.forClass(ByteBuffer.class); ArgumentCaptor replyArgumentCaptor = ArgumentCaptor.forClass(BinaryMessenger.BinaryReply.class); @@ -78,33 +79,20 @@ public void onKeyEventNotHandled(@NonNull KeyEvent event) { // Simulate a reply, and see that it is handled. sendReply(true, replyArgumentCaptor.getValue()); assertTrue(handled[0]); - assertEquals(event, handledKeyEvents[0]); } @Test public void keyUpEventIsSentToFramework() throws JSONException { - BinaryMessenger fakeMessenger = mock(BinaryMessenger.class); - KeyEventChannel keyEventChannel = new KeyEventChannel(fakeMessenger); - final boolean[] handled = {false}; - final KeyEvent[] handledKeyEvents = {null}; - keyEventChannel.setEventResponseHandler( - new KeyEventChannel.EventResponseHandler() { - public void onKeyEventHandled(@NonNull KeyEvent event) { - handled[0] = true; - handledKeyEvents[0] = event; - } - - public void onKeyEventNotHandled(@NonNull KeyEvent event) { - handled[0] = false; - handledKeyEvents[0] = event; - } + keyEvent = new FakeKeyEvent(KeyEvent.ACTION_UP, 65); + KeyEventChannel.FlutterKeyEvent flutterKeyEvent = + new KeyEventChannel.FlutterKeyEvent(keyEvent, null); + keyEventChannel.sendFlutterKeyEvent( + flutterKeyEvent, + false, + (isHandled) -> { + handled[0] = isHandled; }); - verify(fakeMessenger, times(0)).send(any(), any(), any()); - KeyEvent event = new FakeKeyEvent(KeyEvent.ACTION_UP, 65); - KeyEventChannel.FlutterKeyEvent flutterKeyEvent = - new KeyEventChannel.FlutterKeyEvent(event, null); - keyEventChannel.keyUp(flutterKeyEvent); ArgumentCaptor byteBufferArgumentCaptor = ArgumentCaptor.forClass(ByteBuffer.class); ArgumentCaptor replyArgumentCaptor = ArgumentCaptor.forClass(BinaryMessenger.BinaryReply.class); @@ -114,11 +102,10 @@ public void onKeyEventNotHandled(@NonNull KeyEvent event) { capturedMessage.rewind(); JSONObject message = (JSONObject) JSONMessageCodec.INSTANCE.decodeMessage(capturedMessage); assertNotNull(message); - assertEquals("keyup", message.get("type")); + assertEquals("keydown", message.get("type")); // Simulate a reply, and see that it is handled. sendReply(true, replyArgumentCaptor.getValue()); assertTrue(handled[0]); - assertEquals(event, handledKeyEvents[0]); } } 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 a0f98f9e68dd7..25c7106fbba72 100644 --- a/shell/platform/android/test/io/flutter/plugin/editing/InputConnectionAdaptorTest.java +++ b/shell/platform/android/test/io/flutter/plugin/editing/InputConnectionAdaptorTest.java @@ -32,7 +32,7 @@ import android.view.inputmethod.ExtractedTextRequest; import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputMethodManager; -import io.flutter.embedding.android.AndroidKeyProcessor; +import io.flutter.embedding.android.KeyboardManager; import io.flutter.embedding.engine.FlutterJNI; import io.flutter.embedding.engine.dart.DartExecutor; import io.flutter.embedding.engine.systemchannels.TextInputChannel; @@ -43,9 +43,12 @@ import java.nio.ByteBuffer; import org.json.JSONArray; import org.json.JSONException; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; @@ -60,6 +63,7 @@ shadows = {ShadowClipboardManager.class, InputConnectionAdaptorTest.TestImm.class}) @RunWith(RobolectricTestRunner.class) public class InputConnectionAdaptorTest { + @Mock KeyboardManager mockKeyboardManager; // Verifies the method and arguments for a captured method call. private void verifyMethodCall(ByteBuffer buffer, String methodName, String[] expectedArgs) throws JSONException { @@ -75,6 +79,11 @@ private void verifyMethodCall(ByteBuffer buffer, String methodName, String[] exp } } + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + } + @Test public void inputConnectionAdaptor_ReceivesEnter() throws NullPointerException { View testView = new View(RuntimeEnvironment.application); @@ -82,7 +91,6 @@ 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); ListenableEditingState mEditable = new ListenableEditingState(null, testView); Selection.setSelection(mEditable, 0, 0); ListenableEditingState spyEditable = spy(mEditable); @@ -91,11 +99,11 @@ public void inputConnectionAdaptor_ReceivesEnter() throws NullPointerException { InputConnectionAdaptor inputConnectionAdaptor = new InputConnectionAdaptor( - testView, inputTargetId, textInputChannel, mockKeyProcessor, spyEditable, outAttrs); + testView, inputTargetId, textInputChannel, mockKeyboardManager, spyEditable, outAttrs); // Send an enter key and make sure the Editable received it. FakeKeyEvent keyEvent = new FakeKeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER); - inputConnectionAdaptor.sendKeyEvent(keyEvent); + inputConnectionAdaptor.handleKeyEvent(keyEvent); verify(spyEditable, times(1)).insert(eq(0), anyString()); } @@ -172,11 +180,16 @@ 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); ListenableEditingState editable = sampleEditable(0, 0); InputConnectionAdaptor adaptor = new InputConnectionAdaptor( - testView, client, textInputChannel, mockKeyProcessor, editable, null, mockFlutterJNI); + testView, + client, + textInputChannel, + mockKeyboardManager, + editable, + null, + mockFlutterJNI); adaptor.performPrivateCommand("actionCommand", null); ArgumentCaptor channelCaptor = ArgumentCaptor.forClass(String.class); @@ -200,11 +213,16 @@ 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); ListenableEditingState editable = sampleEditable(0, 0); InputConnectionAdaptor adaptor = new InputConnectionAdaptor( - testView, client, textInputChannel, mockKeyProcessor, editable, null, mockFlutterJNI); + testView, + client, + textInputChannel, + mockKeyboardManager, + editable, + null, + mockFlutterJNI); Bundle bundle = new Bundle(); byte[] buffer = new byte[] {'a', 'b', 'c', 'd'}; @@ -234,11 +252,16 @@ 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); ListenableEditingState editable = sampleEditable(0, 0); InputConnectionAdaptor adaptor = new InputConnectionAdaptor( - testView, client, textInputChannel, mockKeyProcessor, editable, null, mockFlutterJNI); + testView, + client, + textInputChannel, + mockKeyboardManager, + editable, + null, + mockFlutterJNI); Bundle bundle = new Bundle(); byte b = 3; @@ -266,11 +289,16 @@ 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); ListenableEditingState editable = sampleEditable(0, 0); InputConnectionAdaptor adaptor = new InputConnectionAdaptor( - testView, client, textInputChannel, mockKeyProcessor, editable, null, mockFlutterJNI); + testView, + client, + textInputChannel, + mockKeyboardManager, + editable, + null, + mockFlutterJNI); Bundle bundle = new Bundle(); char[] buffer = new char[] {'a', 'b', 'c', 'd'}; @@ -301,11 +329,16 @@ 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); ListenableEditingState editable = sampleEditable(0, 0); InputConnectionAdaptor adaptor = new InputConnectionAdaptor( - testView, client, textInputChannel, mockKeyProcessor, editable, null, mockFlutterJNI); + testView, + client, + textInputChannel, + mockKeyboardManager, + editable, + null, + mockFlutterJNI); Bundle bundle = new Bundle(); char b = 'a'; @@ -333,11 +366,16 @@ 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); ListenableEditingState editable = sampleEditable(0, 0); InputConnectionAdaptor adaptor = new InputConnectionAdaptor( - testView, client, textInputChannel, mockKeyProcessor, editable, null, mockFlutterJNI); + testView, + client, + textInputChannel, + mockKeyboardManager, + editable, + null, + mockFlutterJNI); Bundle bundle = new Bundle(); CharSequence charSequence1 = new StringBuffer("abc"); @@ -369,11 +407,16 @@ 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); ListenableEditingState editable = sampleEditable(0, 0); InputConnectionAdaptor adaptor = new InputConnectionAdaptor( - testView, client, textInputChannel, mockKeyProcessor, editable, null, mockFlutterJNI); + testView, + client, + textInputChannel, + mockKeyboardManager, + editable, + null, + mockFlutterJNI); Bundle bundle = new Bundle(); CharSequence charSequence = new StringBuffer("abc"); @@ -403,11 +446,16 @@ 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); ListenableEditingState editable = sampleEditable(0, 0); InputConnectionAdaptor adaptor = new InputConnectionAdaptor( - testView, client, textInputChannel, mockKeyProcessor, editable, null, mockFlutterJNI); + testView, + client, + textInputChannel, + mockKeyboardManager, + editable, + null, + mockFlutterJNI); Bundle bundle = new Bundle(); float value = 0.5f; @@ -435,11 +483,16 @@ 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); ListenableEditingState editable = sampleEditable(0, 0); InputConnectionAdaptor adaptor = new InputConnectionAdaptor( - testView, client, textInputChannel, mockKeyProcessor, editable, null, mockFlutterJNI); + testView, + client, + textInputChannel, + mockKeyboardManager, + editable, + null, + mockFlutterJNI); Bundle bundle = new Bundle(); float[] value = {0.5f, 0.6f}; @@ -470,7 +523,7 @@ public void testSendKeyEvent_shiftKeyUpCancelsSelection() { InputConnectionAdaptor adaptor = sampleInputConnectionAdaptor(editable); KeyEvent shiftKeyUp = new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_SHIFT_LEFT); - boolean didConsume = adaptor.sendKeyEvent(shiftKeyUp); + boolean didConsume = adaptor.handleKeyEvent(shiftKeyUp); assertTrue(didConsume); assertEquals(selEnd, Selection.getSelectionStart(editable)); @@ -484,7 +537,7 @@ public void testSendKeyEvent_leftKeyMovesCaretLeft() { InputConnectionAdaptor adaptor = sampleInputConnectionAdaptor(editable); KeyEvent leftKeyDown = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_LEFT); - boolean didConsume = adaptor.sendKeyEvent(leftKeyDown); + boolean didConsume = adaptor.handleKeyEvent(leftKeyDown); assertTrue(didConsume); assertEquals(selStart - 1, Selection.getSelectionStart(editable)); @@ -501,134 +554,134 @@ public void testSendKeyEvent_leftKeyMovesCaretLeftComplexEmoji() { boolean didConsume; // Normal Character - didConsume = adaptor.sendKeyEvent(downKeyDown); + didConsume = adaptor.handleKeyEvent(downKeyDown); assertTrue(didConsume); assertEquals(Selection.getSelectionStart(editable), 74); // Non-Spacing Mark - didConsume = adaptor.sendKeyEvent(downKeyDown); + didConsume = adaptor.handleKeyEvent(downKeyDown); assertTrue(didConsume); assertEquals(Selection.getSelectionStart(editable), 73); - didConsume = adaptor.sendKeyEvent(downKeyDown); + didConsume = adaptor.handleKeyEvent(downKeyDown); assertTrue(didConsume); assertEquals(Selection.getSelectionStart(editable), 72); // Keycap - didConsume = adaptor.sendKeyEvent(downKeyDown); + didConsume = adaptor.handleKeyEvent(downKeyDown); assertTrue(didConsume); assertEquals(Selection.getSelectionStart(editable), 69); // Keycap with invalid base adaptor.setSelection(68, 68); - didConsume = adaptor.sendKeyEvent(downKeyDown); + didConsume = adaptor.handleKeyEvent(downKeyDown); assertTrue(didConsume); assertEquals(Selection.getSelectionStart(editable), 66); adaptor.setSelection(67, 67); - didConsume = adaptor.sendKeyEvent(downKeyDown); + didConsume = adaptor.handleKeyEvent(downKeyDown); assertTrue(didConsume); assertEquals(Selection.getSelectionStart(editable), 66); // Zero Width Joiner - didConsume = adaptor.sendKeyEvent(downKeyDown); + didConsume = adaptor.handleKeyEvent(downKeyDown); assertTrue(didConsume); assertEquals(Selection.getSelectionStart(editable), 55); // Zero Width Joiner with invalid base - didConsume = adaptor.sendKeyEvent(downKeyDown); + didConsume = adaptor.handleKeyEvent(downKeyDown); assertTrue(didConsume); assertEquals(Selection.getSelectionStart(editable), 53); - didConsume = adaptor.sendKeyEvent(downKeyDown); + didConsume = adaptor.handleKeyEvent(downKeyDown); assertTrue(didConsume); assertEquals(Selection.getSelectionStart(editable), 52); - didConsume = adaptor.sendKeyEvent(downKeyDown); + didConsume = adaptor.handleKeyEvent(downKeyDown); assertTrue(didConsume); assertEquals(Selection.getSelectionStart(editable), 51); // ----- Start Emoji Tag Sequence with invalid base testing ---- // Delete base tag adaptor.setSelection(39, 39); - didConsume = adaptor.sendKeyEvent(downKeyDown); + didConsume = adaptor.handleKeyEvent(downKeyDown); assertTrue(didConsume); assertEquals(Selection.getSelectionStart(editable), 37); // Delete the sequence adaptor.setSelection(49, 49); for (int i = 0; i < 6; i++) { - didConsume = adaptor.sendKeyEvent(downKeyDown); + didConsume = adaptor.handleKeyEvent(downKeyDown); assertTrue(didConsume); } assertEquals(Selection.getSelectionStart(editable), 37); // ----- End Emoji Tag Sequence with invalid base testing ---- // Emoji Tag Sequence - didConsume = adaptor.sendKeyEvent(downKeyDown); + didConsume = adaptor.handleKeyEvent(downKeyDown); assertTrue(didConsume); assertEquals(Selection.getSelectionStart(editable), 23); // Variation Selector with invalid base adaptor.setSelection(22, 22); - didConsume = adaptor.sendKeyEvent(downKeyDown); + didConsume = adaptor.handleKeyEvent(downKeyDown); assertTrue(didConsume); assertEquals(Selection.getSelectionStart(editable), 21); adaptor.setSelection(22, 22); - didConsume = adaptor.sendKeyEvent(downKeyDown); + didConsume = adaptor.handleKeyEvent(downKeyDown); assertTrue(didConsume); assertEquals(Selection.getSelectionStart(editable), 21); // Variation Selector - didConsume = adaptor.sendKeyEvent(downKeyDown); + didConsume = adaptor.handleKeyEvent(downKeyDown); assertTrue(didConsume); assertEquals(Selection.getSelectionStart(editable), 19); // Emoji Modifier - didConsume = adaptor.sendKeyEvent(downKeyDown); + didConsume = adaptor.handleKeyEvent(downKeyDown); assertTrue(didConsume); assertEquals(Selection.getSelectionStart(editable), 16); // Emoji Modifier with invalid base adaptor.setSelection(14, 14); - didConsume = adaptor.sendKeyEvent(downKeyDown); + didConsume = adaptor.handleKeyEvent(downKeyDown); assertTrue(didConsume); assertEquals(Selection.getSelectionStart(editable), 13); adaptor.setSelection(14, 14); - didConsume = adaptor.sendKeyEvent(downKeyDown); + didConsume = adaptor.handleKeyEvent(downKeyDown); assertTrue(didConsume); assertEquals(Selection.getSelectionStart(editable), 13); // Line Feed adaptor.setSelection(12, 12); - didConsume = adaptor.sendKeyEvent(downKeyDown); + didConsume = adaptor.handleKeyEvent(downKeyDown); assertTrue(didConsume); assertEquals(Selection.getSelectionStart(editable), 11); // Carriage Return adaptor.setSelection(12, 12); - didConsume = adaptor.sendKeyEvent(downKeyDown); + didConsume = adaptor.handleKeyEvent(downKeyDown); assertTrue(didConsume); assertEquals(Selection.getSelectionStart(editable), 11); // Carriage Return and Line Feed - didConsume = adaptor.sendKeyEvent(downKeyDown); + didConsume = adaptor.handleKeyEvent(downKeyDown); assertTrue(didConsume); assertEquals(Selection.getSelectionStart(editable), 9); // Regional Indicator Symbol odd - didConsume = adaptor.sendKeyEvent(downKeyDown); + didConsume = adaptor.handleKeyEvent(downKeyDown); assertTrue(didConsume); assertEquals(Selection.getSelectionStart(editable), 7); // Regional Indicator Symbol even - didConsume = adaptor.sendKeyEvent(downKeyDown); + didConsume = adaptor.handleKeyEvent(downKeyDown); assertTrue(didConsume); assertEquals(Selection.getSelectionStart(editable), 3); // Simple Emoji - didConsume = adaptor.sendKeyEvent(downKeyDown); + didConsume = adaptor.handleKeyEvent(downKeyDown); assertTrue(didConsume); assertEquals(Selection.getSelectionStart(editable), 1); // First CodePoint - didConsume = adaptor.sendKeyEvent(downKeyDown); + didConsume = adaptor.handleKeyEvent(downKeyDown); assertTrue(didConsume); assertEquals(Selection.getSelectionStart(editable), 0); } @@ -641,7 +694,7 @@ public void testSendKeyEvent_leftKeyExtendsSelectionLeft() { InputConnectionAdaptor adaptor = sampleInputConnectionAdaptor(editable); KeyEvent leftKeyDown = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_LEFT); - boolean didConsume = adaptor.sendKeyEvent(leftKeyDown); + boolean didConsume = adaptor.handleKeyEvent(leftKeyDown); assertTrue(didConsume); assertEquals(selStart, Selection.getSelectionStart(editable)); @@ -657,7 +710,7 @@ public void testSendKeyEvent_shiftLeftKeyStartsSelectionLeft() { KeyEvent shiftLeftKeyDown = new KeyEvent( 0, 0, KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_LEFT, 0, KeyEvent.META_SHIFT_ON); - boolean didConsume = adaptor.sendKeyEvent(shiftLeftKeyDown); + boolean didConsume = adaptor.handleKeyEvent(shiftLeftKeyDown); assertTrue(didConsume); assertEquals(selStart, Selection.getSelectionStart(editable)); @@ -671,7 +724,7 @@ public void testSendKeyEvent_rightKeyMovesCaretRight() { InputConnectionAdaptor adaptor = sampleInputConnectionAdaptor(editable); KeyEvent rightKeyDown = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_RIGHT); - boolean didConsume = adaptor.sendKeyEvent(rightKeyDown); + boolean didConsume = adaptor.handleKeyEvent(rightKeyDown); assertTrue(didConsume); assertEquals(selStart + 1, Selection.getSelectionStart(editable)); @@ -692,26 +745,26 @@ public void testSendKeyEvent_rightKeyMovesCaretRightComplexRegion() { boolean didConsume; // The cursor moves over two region indicators at a time. - didConsume = adaptor.sendKeyEvent(downKeyDown); + didConsume = adaptor.handleKeyEvent(downKeyDown); assertTrue(didConsume); assertEquals(Selection.getSelectionStart(editable), 4); - didConsume = adaptor.sendKeyEvent(downKeyDown); + didConsume = adaptor.handleKeyEvent(downKeyDown); assertTrue(didConsume); assertEquals(Selection.getSelectionStart(editable), 8); - didConsume = adaptor.sendKeyEvent(downKeyDown); + didConsume = adaptor.handleKeyEvent(downKeyDown); assertTrue(didConsume); assertEquals(Selection.getSelectionStart(editable), 12); // When there is only one region indicator left with no pair, the cursor // moves over that single region indicator. - didConsume = adaptor.sendKeyEvent(downKeyDown); + didConsume = adaptor.handleKeyEvent(downKeyDown); assertTrue(didConsume); assertEquals(Selection.getSelectionStart(editable), 14); // If the cursor is placed in the middle of a region indicator pair, it // moves over only the second half of the pair. adaptor.setSelection(6, 6); - didConsume = adaptor.sendKeyEvent(downKeyDown); + didConsume = adaptor.handleKeyEvent(downKeyDown); assertTrue(didConsume); assertEquals(Selection.getSelectionStart(editable), 8); } @@ -726,71 +779,71 @@ public void testSendKeyEvent_rightKeyMovesCaretRightComplexEmoji() { boolean didConsume; // First CodePoint - didConsume = adaptor.sendKeyEvent(downKeyDown); + didConsume = adaptor.handleKeyEvent(downKeyDown); assertTrue(didConsume); assertEquals(Selection.getSelectionStart(editable), 1); // Simple Emoji - didConsume = adaptor.sendKeyEvent(downKeyDown); + didConsume = adaptor.handleKeyEvent(downKeyDown); assertTrue(didConsume); assertEquals(Selection.getSelectionStart(editable), 3); // Regional Indicator Symbol even - didConsume = adaptor.sendKeyEvent(downKeyDown); + didConsume = adaptor.handleKeyEvent(downKeyDown); assertTrue(didConsume); assertEquals(Selection.getSelectionStart(editable), 7); // Regional Indicator Symbol odd - didConsume = adaptor.sendKeyEvent(downKeyDown); + didConsume = adaptor.handleKeyEvent(downKeyDown); assertTrue(didConsume); assertEquals(Selection.getSelectionStart(editable), 9); // Carriage Return - didConsume = adaptor.sendKeyEvent(downKeyDown); + didConsume = adaptor.handleKeyEvent(downKeyDown); assertTrue(didConsume); assertEquals(Selection.getSelectionStart(editable), 10); // Line Feed and Carriage Return - didConsume = adaptor.sendKeyEvent(downKeyDown); + didConsume = adaptor.handleKeyEvent(downKeyDown); assertTrue(didConsume); assertEquals(Selection.getSelectionStart(editable), 12); // Line Feed - didConsume = adaptor.sendKeyEvent(downKeyDown); + didConsume = adaptor.handleKeyEvent(downKeyDown); assertTrue(didConsume); assertEquals(Selection.getSelectionStart(editable), 13); // Modified Emoji - didConsume = adaptor.sendKeyEvent(downKeyDown); + didConsume = adaptor.handleKeyEvent(downKeyDown); assertTrue(didConsume); assertEquals(Selection.getSelectionStart(editable), 16); // Emoji Modifier adaptor.setSelection(14, 14); - didConsume = adaptor.sendKeyEvent(downKeyDown); + didConsume = adaptor.handleKeyEvent(downKeyDown); assertTrue(didConsume); assertEquals(Selection.getSelectionStart(editable), 16); // Emoji Modifier with invalid base adaptor.setSelection(18, 18); - didConsume = adaptor.sendKeyEvent(downKeyDown); + didConsume = adaptor.handleKeyEvent(downKeyDown); assertTrue(didConsume); assertEquals(Selection.getSelectionStart(editable), 19); // Variation Selector - didConsume = adaptor.sendKeyEvent(downKeyDown); + didConsume = adaptor.handleKeyEvent(downKeyDown); assertTrue(didConsume); assertEquals(Selection.getSelectionStart(editable), 21); // Variation Selector with invalid base adaptor.setSelection(22, 22); - didConsume = adaptor.sendKeyEvent(downKeyDown); + didConsume = adaptor.handleKeyEvent(downKeyDown); assertTrue(didConsume); assertEquals(Selection.getSelectionStart(editable), 23); // Emoji Tag Sequence for (int i = 0; i < 7; i++) { - didConsume = adaptor.sendKeyEvent(downKeyDown); + didConsume = adaptor.handleKeyEvent(downKeyDown); assertTrue(didConsume); assertEquals(Selection.getSelectionStart(editable), 25 + 2 * i); } @@ -800,7 +853,7 @@ public void testSendKeyEvent_rightKeyMovesCaretRightComplexEmoji() { // Pass the sequence adaptor.setSelection(39, 39); for (int i = 0; i < 6; i++) { - didConsume = adaptor.sendKeyEvent(downKeyDown); + didConsume = adaptor.handleKeyEvent(downKeyDown); assertTrue(didConsume); assertEquals(Selection.getSelectionStart(editable), 41 + 2 * i); } @@ -808,45 +861,45 @@ public void testSendKeyEvent_rightKeyMovesCaretRightComplexEmoji() { // ----- End Emoji Tag Sequence with invalid base testing ---- // Zero Width Joiner with invalid base - didConsume = adaptor.sendKeyEvent(downKeyDown); + didConsume = adaptor.handleKeyEvent(downKeyDown); assertTrue(didConsume); assertEquals(Selection.getSelectionStart(editable), 52); - didConsume = adaptor.sendKeyEvent(downKeyDown); + didConsume = adaptor.handleKeyEvent(downKeyDown); assertTrue(didConsume); assertEquals(Selection.getSelectionStart(editable), 53); - didConsume = adaptor.sendKeyEvent(downKeyDown); + didConsume = adaptor.handleKeyEvent(downKeyDown); assertTrue(didConsume); assertEquals(Selection.getSelectionStart(editable), 55); // Zero Width Joiner - didConsume = adaptor.sendKeyEvent(downKeyDown); + didConsume = adaptor.handleKeyEvent(downKeyDown); assertTrue(didConsume); assertEquals(Selection.getSelectionStart(editable), 66); // Keycap with invalid base adaptor.setSelection(67, 67); - didConsume = adaptor.sendKeyEvent(downKeyDown); + didConsume = adaptor.handleKeyEvent(downKeyDown); assertTrue(didConsume); assertEquals(Selection.getSelectionStart(editable), 68); - didConsume = adaptor.sendKeyEvent(downKeyDown); + didConsume = adaptor.handleKeyEvent(downKeyDown); assertTrue(didConsume); assertEquals(Selection.getSelectionStart(editable), 69); // Keycap - didConsume = adaptor.sendKeyEvent(downKeyDown); + didConsume = adaptor.handleKeyEvent(downKeyDown); assertTrue(didConsume); assertEquals(Selection.getSelectionStart(editable), 72); // Non-Spacing Mark - didConsume = adaptor.sendKeyEvent(downKeyDown); + didConsume = adaptor.handleKeyEvent(downKeyDown); assertTrue(didConsume); assertEquals(Selection.getSelectionStart(editable), 73); - didConsume = adaptor.sendKeyEvent(downKeyDown); + didConsume = adaptor.handleKeyEvent(downKeyDown); assertTrue(didConsume); assertEquals(Selection.getSelectionStart(editable), 74); // Normal Character - didConsume = adaptor.sendKeyEvent(downKeyDown); + didConsume = adaptor.handleKeyEvent(downKeyDown); assertTrue(didConsume); assertEquals(Selection.getSelectionStart(editable), 75); } @@ -859,7 +912,7 @@ public void testSendKeyEvent_rightKeyExtendsSelectionRight() { InputConnectionAdaptor adaptor = sampleInputConnectionAdaptor(editable); KeyEvent rightKeyDown = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_RIGHT); - boolean didConsume = adaptor.sendKeyEvent(rightKeyDown); + boolean didConsume = adaptor.handleKeyEvent(rightKeyDown); assertTrue(didConsume); assertEquals(selStart, Selection.getSelectionStart(editable)); @@ -875,7 +928,7 @@ public void testSendKeyEvent_shiftRightKeyStartsSelectionRight() { KeyEvent shiftRightKeyDown = new KeyEvent( 0, 0, KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_RIGHT, 0, KeyEvent.META_SHIFT_ON); - boolean didConsume = adaptor.sendKeyEvent(shiftRightKeyDown); + boolean didConsume = adaptor.handleKeyEvent(shiftRightKeyDown); assertTrue(didConsume); assertEquals(selStart, Selection.getSelectionStart(editable)); @@ -889,7 +942,7 @@ public void testSendKeyEvent_upKeyMovesCaretUp() { InputConnectionAdaptor adaptor = sampleInputConnectionAdaptor(editable); KeyEvent upKeyDown = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_UP); - boolean didConsume = adaptor.sendKeyEvent(upKeyDown); + boolean didConsume = adaptor.handleKeyEvent(upKeyDown); assertTrue(didConsume); // Checks the caret moved left (to some previous character). Selection.moveUp() behaves @@ -904,7 +957,7 @@ public void testSendKeyEvent_downKeyMovesCaretDown() { InputConnectionAdaptor adaptor = sampleInputConnectionAdaptor(editable); KeyEvent downKeyDown = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_DOWN); - boolean didConsume = adaptor.sendKeyEvent(downKeyDown); + boolean didConsume = adaptor.handleKeyEvent(downKeyDown); assertTrue(didConsume); // Checks the caret moved right (to some following character). Selection.moveDown() behaves @@ -919,25 +972,25 @@ public void testSendKeyEvent_MovementKeysAreNopWhenNoSelection() { InputConnectionAdaptor adaptor = sampleInputConnectionAdaptor(editable); KeyEvent keyEvent = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_DOWN); - boolean didConsume = adaptor.sendKeyEvent(keyEvent); + boolean didConsume = adaptor.handleKeyEvent(keyEvent); assertFalse(didConsume); assertEquals(Selection.getSelectionStart(editable), -1); assertEquals(Selection.getSelectionEnd(editable), -1); keyEvent = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_UP); - didConsume = adaptor.sendKeyEvent(keyEvent); + didConsume = adaptor.handleKeyEvent(keyEvent); assertFalse(didConsume); assertEquals(Selection.getSelectionStart(editable), -1); assertEquals(Selection.getSelectionEnd(editable), -1); keyEvent = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_LEFT); - didConsume = adaptor.sendKeyEvent(keyEvent); + didConsume = adaptor.handleKeyEvent(keyEvent); assertFalse(didConsume); assertEquals(Selection.getSelectionStart(editable), -1); assertEquals(Selection.getSelectionEnd(editable), -1); keyEvent = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_RIGHT); - didConsume = adaptor.sendKeyEvent(keyEvent); + didConsume = adaptor.handleKeyEvent(keyEvent); assertFalse(didConsume); assertEquals(Selection.getSelectionStart(editable), -1); assertEquals(Selection.getSelectionEnd(editable), -1); @@ -964,13 +1017,12 @@ public void testExtractedText_monitoring() { } ListenableEditingState editable = sampleEditable(5, 5); View testView = new View(RuntimeEnvironment.application); - AndroidKeyProcessor mockKeyProcessor = mock(AndroidKeyProcessor.class); InputConnectionAdaptor adaptor = new InputConnectionAdaptor( testView, 1, mock(TextInputChannel.class), - mockKeyProcessor, + mockKeyboardManager, editable, new EditorInfo()); TestImm testImm = @@ -1020,7 +1072,6 @@ public void testCursorAnchorInfo() { return; } - AndroidKeyProcessor mockKeyProcessor = mock(AndroidKeyProcessor.class); ListenableEditingState editable = sampleEditable(5, 5); View testView = new View(RuntimeEnvironment.application); InputConnectionAdaptor adaptor = @@ -1028,7 +1079,7 @@ public void testCursorAnchorInfo() { testView, 1, mock(TextInputChannel.class), - mockKeyProcessor, + mockKeyboardManager, editable, new EditorInfo()); TestImm testImm = @@ -1064,30 +1115,27 @@ public void testCursorAnchorInfo() { @Test public void testSendKeyEvent_sendSoftKeyEvents() { ListenableEditingState editable = sampleEditable(5, 5); - AndroidKeyProcessor mockKeyProcessor = mock(AndroidKeyProcessor.class); - when(mockKeyProcessor.isPendingEvent(any())).thenReturn(true); - InputConnectionAdaptor adaptor = sampleInputConnectionAdaptor(editable, mockKeyProcessor); + InputConnectionAdaptor adaptor = sampleInputConnectionAdaptor(editable, mockKeyboardManager); KeyEvent shiftKeyDown = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_SHIFT_LEFT); - boolean didConsume = adaptor.sendKeyEvent(shiftKeyDown); + boolean didConsume = adaptor.handleKeyEvent(shiftKeyDown); assertFalse(didConsume); - verify(mockKeyProcessor, never()).onKeyEvent(shiftKeyDown); + verify(mockKeyboardManager, never()).handleEvent(shiftKeyDown); } @Test public void testSendKeyEvent_sendHardwareKeyEvents() { ListenableEditingState editable = sampleEditable(5, 5); - AndroidKeyProcessor mockKeyProcessor = mock(AndroidKeyProcessor.class); - when(mockKeyProcessor.isPendingEvent(any())).thenReturn(false); - when(mockKeyProcessor.onKeyEvent(any())).thenReturn(true); - InputConnectionAdaptor adaptor = sampleInputConnectionAdaptor(editable, mockKeyProcessor); + when(mockKeyboardManager.handleEvent(any())).thenReturn(true); + InputConnectionAdaptor adaptor = sampleInputConnectionAdaptor(editable, mockKeyboardManager); KeyEvent shiftKeyDown = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_SHIFT_LEFT); + // Call sendKeyEvent instead of handleKeyEvent. boolean didConsume = adaptor.sendKeyEvent(shiftKeyDown); assertTrue(didConsume); - verify(mockKeyProcessor, times(1)).onKeyEvent(shiftKeyDown); + verify(mockKeyboardManager, times(1)).handleEvent(shiftKeyDown); } @Test @@ -1098,7 +1146,7 @@ public void testSendKeyEvent_delKeyNotConsumed() { KeyEvent downKeyDown = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL); for (int i = 0; i < 4; i++) { - boolean didConsume = adaptor.sendKeyEvent(downKeyDown); + boolean didConsume = adaptor.handleKeyEvent(downKeyDown); assertFalse(didConsume); } assertEquals(5, Selection.getSelectionStart(editable)); @@ -1110,7 +1158,7 @@ public void testDoesNotConsumeBackButton() { InputConnectionAdaptor adaptor = sampleInputConnectionAdaptor(editable); FakeKeyEvent keyEvent = new FakeKeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_BACK); - boolean didConsume = adaptor.sendKeyEvent(keyEvent); + boolean didConsume = adaptor.handleKeyEvent(keyEvent); assertFalse(didConsume); } @@ -1158,11 +1206,11 @@ private static ListenableEditingState sampleEditable(int selStart, int selEnd, S private static InputConnectionAdaptor sampleInputConnectionAdaptor( ListenableEditingState editable) { - return sampleInputConnectionAdaptor(editable, mock(AndroidKeyProcessor.class)); + return sampleInputConnectionAdaptor(editable, mock(KeyboardManager.class)); } private static InputConnectionAdaptor sampleInputConnectionAdaptor( - ListenableEditingState editable, AndroidKeyProcessor mockKeyProcessor) { + ListenableEditingState editable, KeyboardManager mockKeyboardManager) { View testView = new View(RuntimeEnvironment.application); int client = 0; TextInputChannel textInputChannel = mock(TextInputChannel.class); @@ -1183,7 +1231,7 @@ private static InputConnectionAdaptor sampleInputConnectionAdaptor( .thenAnswer( (invocation) -> Emoji.isRegionalIndicatorSymbol((int) invocation.getArguments()[0])); return new InputConnectionAdaptor( - testView, client, textInputChannel, mockKeyProcessor, editable, null, mockFlutterJNI); + testView, client, textInputChannel, mockKeyboardManager, editable, null, mockFlutterJNI); } private class TestTextInputChannel extends TextInputChannel { diff --git a/shell/platform/android/test/io/flutter/plugin/editing/ListenableEditingStateTest.java b/shell/platform/android/test/io/flutter/plugin/editing/ListenableEditingStateTest.java index d542a27e151bb..356382f2b4f27 100644 --- a/shell/platform/android/test/io/flutter/plugin/editing/ListenableEditingStateTest.java +++ b/shell/platform/android/test/io/flutter/plugin/editing/ListenableEditingStateTest.java @@ -10,11 +10,14 @@ import android.view.View; import android.view.inputmethod.BaseInputConnection; import android.view.inputmethod.EditorInfo; -import io.flutter.embedding.android.AndroidKeyProcessor; +import io.flutter.embedding.android.KeyboardManager; import io.flutter.embedding.engine.systemchannels.TextInputChannel; import java.util.ArrayList; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; @@ -22,6 +25,8 @@ @Config(manifest = Config.NONE) @RunWith(RobolectricTestRunner.class) public class ListenableEditingStateTest { + @Mock KeyboardManager mockKeyboardManager; + private BaseInputConnection getTestInputConnection(View view, Editable mEditable) { new View(RuntimeEnvironment.application); return new BaseInputConnection(view, true) { @@ -32,6 +37,11 @@ public Editable getEditable() { }; } + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + } + // -------- Start: Test BatchEditing ------- @Test public void testBatchEditing() { @@ -239,13 +249,12 @@ public void endBatchEdit() { final Listener listener = new Listener(); final View testView = new View(RuntimeEnvironment.application); - final AndroidKeyProcessor mockKeyProcessor = mock(AndroidKeyProcessor.class); final InputConnectionAdaptor inputConnection = new InputConnectionAdaptor( testView, 0, mock(TextInputChannel.class), - mockKeyProcessor, + mockKeyboardManager, editingState, new EditorInfo()); @@ -266,13 +275,12 @@ public void inputMethod_testSetSelection() { new ListenableEditingState(null, new View(RuntimeEnvironment.application)); final Listener listener = new Listener(); final View testView = new View(RuntimeEnvironment.application); - final AndroidKeyProcessor mockKeyProcessor = mock(AndroidKeyProcessor.class); final InputConnectionAdaptor inputConnection = new InputConnectionAdaptor( testView, 0, mock(TextInputChannel.class), - mockKeyProcessor, + mockKeyboardManager, editingState, new EditorInfo()); editingState.replace(0, editingState.length(), "initial text"); @@ -302,13 +310,12 @@ public void inputMethod_testSetComposition() { new ListenableEditingState(null, new View(RuntimeEnvironment.application)); final Listener listener = new Listener(); final View testView = new View(RuntimeEnvironment.application); - final AndroidKeyProcessor mockKeyProcessor = mock(AndroidKeyProcessor.class); final InputConnectionAdaptor inputConnection = new InputConnectionAdaptor( testView, 0, mock(TextInputChannel.class), - mockKeyProcessor, + mockKeyboardManager, editingState, new EditorInfo()); editingState.replace(0, editingState.length(), "initial text"); @@ -364,13 +371,12 @@ public void inputMethod_testCommitText() { new ListenableEditingState(null, new View(RuntimeEnvironment.application)); final Listener listener = new Listener(); final View testView = new View(RuntimeEnvironment.application); - final AndroidKeyProcessor mockKeyProcessor = mock(AndroidKeyProcessor.class); final InputConnectionAdaptor inputConnection = new InputConnectionAdaptor( testView, 0, mock(TextInputChannel.class), - mockKeyProcessor, + mockKeyboardManager, editingState, new EditorInfo()); editingState.replace(0, editingState.length(), "initial text"); diff --git a/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java b/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java index 7e8436418ae96..972d41236e95b 100644 --- a/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java +++ b/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java @@ -40,8 +40,8 @@ import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputMethodManager; import android.view.inputmethod.InputMethodSubtype; -import io.flutter.embedding.android.AndroidKeyProcessor; import io.flutter.embedding.android.FlutterView; +import io.flutter.embedding.android.KeyboardManager; import io.flutter.embedding.engine.FlutterEngine; import io.flutter.embedding.engine.FlutterJNI; import io.flutter.embedding.engine.dart.DartExecutor; @@ -210,7 +210,8 @@ public void inputConnectionAdaptor_RepeatFilter() throws NullPointerException { .updateEditingState(anyInt(), any(), anyInt(), anyInt(), anyInt(), anyInt()); InputConnectionAdaptor inputConnectionAdaptor = - (InputConnectionAdaptor) textInputPlugin.createInputConnection(testView, outAttrs); + (InputConnectionAdaptor) + textInputPlugin.createInputConnection(testView, mock(KeyboardManager.class), outAttrs); inputConnectionAdaptor.beginBatchEdit(); verify(textInputChannel, times(0)) @@ -376,7 +377,9 @@ public void setTextInputEditingState_restartsIMEOnlyWhenFrameworkChangesComposin textInputPlugin.setTextInputEditingState( testView, new TextInputChannel.TextEditState("", 0, 0, -1, -1)); assertEquals(1, testImm.getRestartCount(testView)); - InputConnection connection = textInputPlugin.createInputConnection(testView, new EditorInfo()); + InputConnection connection = + textInputPlugin.createInputConnection( + testView, mock(KeyboardManager.class), new EditorInfo()); connection.setComposingText("POWERRRRR", 1); textInputPlugin.setTextInputEditingState( @@ -520,9 +523,12 @@ public void inputConnection_createsActionFromEnter() throws JSONException { any(BinaryMessenger.BinaryReply.class)); assertEquals("flutter/textinput", channelCaptor.getValue()); verifyMethodCall(bufferCaptor.getValue(), "TextInputClient.requestExistingInputState", null); - InputConnection connection = textInputPlugin.createInputConnection(testView, new EditorInfo()); + InputConnectionAdaptor connection = + (InputConnectionAdaptor) + textInputPlugin.createInputConnection( + testView, mock(KeyboardManager.class), new EditorInfo()); - connection.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER)); + connection.handleKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER)); verify(dartExecutor, times(2)) .send( channelCaptor.capture(), @@ -533,9 +539,9 @@ public void inputConnection_createsActionFromEnter() throws JSONException { bufferCaptor.getValue(), "TextInputClient.performAction", new String[] {"0", "TextInputAction.done"}); - connection.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_ENTER)); + connection.handleKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_ENTER)); - connection.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_NUMPAD_ENTER)); + connection.handleKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_NUMPAD_ENTER)); verify(dartExecutor, times(3)) .send( channelCaptor.capture(), @@ -585,7 +591,9 @@ public void inputConnection_finishComposingTextUpdatesIMM() throws JSONException // There's a pending restart since we initialized the text input client. Flush that now. textInputPlugin.setTextInputEditingState( testView, new TextInputChannel.TextEditState("text", 0, 0, -1, -1)); - InputConnection connection = textInputPlugin.createInputConnection(testView, new EditorInfo()); + InputConnection connection = + textInputPlugin.createInputConnection( + testView, mock(KeyboardManager.class), new EditorInfo()); connection.requestCursorUpdates( InputConnection.CURSOR_UPDATE_MONITOR | InputConnection.CURSOR_UPDATE_IMMEDIATE); @@ -789,13 +797,13 @@ public void autofill_testLifeCycle() { // The input method updates the text, call notifyValueChanged. testAfm.resetStates(); - final AndroidKeyProcessor mockKeyProcessor = mock(AndroidKeyProcessor.class); + final KeyboardManager mockKeyboardManager = mock(KeyboardManager.class); InputConnectionAdaptor adaptor = new InputConnectionAdaptor( testView, 0, mock(TextInputChannel.class), - mockKeyProcessor, + mockKeyboardManager, (ListenableEditingState) textInputPlugin.getEditable(), new EditorInfo()); adaptor.commitText("input from IME ", 1); diff --git a/tools/android_lint/project.xml b/tools/android_lint/project.xml index 29b512015262b..7ac27f2309285 100644 --- a/tools/android_lint/project.xml +++ b/tools/android_lint/project.xml @@ -4,81 +4,186 @@ + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + - - - + + - + + + + - - - - - - - - - + + + + + + + - - - - - - - + + + + + + - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +