From d4b9518bd1ef017812ac057ef4e397c954da41f9 Mon Sep 17 00:00:00 2001 From: LongCat is Looong <31859944+LongCatIsLooong@users.noreply.github.com> Date: Wed, 14 Apr 2021 21:06:43 -0700 Subject: [PATCH 01/12] WIP --- ci/licenses_golden/licenses_flutter | 2 + shell/platform/android/BUILD.gn | 4 + .../android/AndroidKeyProcessor.java | 177 +--------- .../embedding/android/FlutterView.java | 12 +- .../android/KeyChannelResponder.java | 44 +++ .../embedding/android/KeyboardManager.java | 174 ++++++++++ .../systemchannels/KeyEventChannel.java | 95 ++---- .../editing/InputConnectionAdaptor.java | 28 +- .../plugin/editing/TextInputPlugin.java | 34 +- .../android/io/flutter/view/FlutterView.java | 10 +- .../test/io/flutter/FlutterTestSuite.java | 4 + .../android/AndroidKeyProcessorTest.java | 310 ++---------------- .../android/KeyChannelResponderTest.java | 54 +++ .../android/KeyboardManagerTest.java | 308 +++++++++++++++++ .../systemchannels/KeyEventChannelTest.java | 77 ++--- .../editing/InputConnectionAdaptorTest.java | 266 +++++++++------ .../editing/ListenableEditingStateTest.java | 24 +- .../plugin/editing/TextInputPluginTest.java | 15 +- tools/android_lint/project.xml | 225 +++++++++---- 19 files changed, 1068 insertions(+), 795 deletions(-) create mode 100644 shell/platform/android/io/flutter/embedding/android/KeyChannelResponder.java create mode 100644 shell/platform/android/io/flutter/embedding/android/KeyboardManager.java create mode 100644 shell/platform/android/test/io/flutter/embedding/android/KeyChannelResponderTest.java create mode 100644 shell/platform/android/test/io/flutter/embedding/android/KeyboardManagerTest.java diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index 093f8c362cfd8..5ba2ac4d49de4 100755 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -759,6 +759,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/KeyboardManager.java +FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/KeyChannelResponder.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 69ab05fc049b5..e0253d862b258 100644 --- a/shell/platform/android/BUILD.gn +++ b/shell/platform/android/BUILD.gn @@ -143,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/KeyboardManager.java", + "io/flutter/embedding/android/KeyChannelResponder.java", "io/flutter/embedding/android/MotionEventTracker.java", "io/flutter/embedding/android/RenderMode.java", "io/flutter/embedding/android/SplashScreen.java", @@ -463,6 +465,8 @@ action("robolectric_tests") { "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/KeyboardManagerTest.java", + "test/io/flutter/embedding/android/KeyChannelResponderTest.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 index 00021b9b21343..44e80aee86e6c 100644 --- a/shell/platform/android/io/flutter/embedding/android/AndroidKeyProcessor.java +++ b/shell/platform/android/io/flutter/embedding/android/AndroidKeyProcessor.java @@ -5,16 +5,7 @@ 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 @@ -30,13 +21,8 @@ * 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; +class AndroidKeyProcessor { private int combiningCharacter; - @NonNull private EventResponder eventResponder; /** * Constructor for AndroidKeyProcessor. @@ -58,74 +44,6 @@ public class AndroidKeyProcessor { * 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 @@ -155,11 +73,7 @@ public boolean isPendingEvent(@NonNull KeyEvent event) { * https://en.wikipedia.org/wiki/Combining_character */ @Nullable - private Character applyCombiningCharacterToBaseCharacter(int newCharacterCodePoint) { - if (newCharacterCodePoint == 0) { - return null; - } - + Character applyCombiningCharacterToBaseCharacter(int newCharacterCodePoint) { char complexCharacter = (char) newCharacterCodePoint; boolean isNewCodePointACombiningCharacter = (newCharacterCodePoint & KeyCharacterMap.COMBINING_ACCENT) != 0; @@ -185,91 +99,4 @@ private Character applyCombiningCharacterToBaseCharacter(int newCharacterCodePoi 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 ea02a08fd23b8..aa0887ad35250 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterView.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterView.java @@ -101,7 +101,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; @@ -744,7 +744,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); } @@ -894,8 +894,9 @@ 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, flutterEngine.getKeyEventChannel()); androidTouchProcessor = new AndroidTouchProcessor(this.flutterEngine.getRenderer(), /*trackMotionEvents=*/ false); accessibilityBridge = @@ -979,8 +980,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..562a0d3e7244f --- /dev/null +++ b/shell/platform/android/io/flutter/embedding/android/KeyChannelResponder.java @@ -0,0 +1,44 @@ +package io.flutter.embedding.android; + +import android.view.KeyEvent; +import androidx.annotation.NonNull; +import io.flutter.embedding.engine.systemchannels.KeyEventChannel; + +/** + * A light wrapper around a {@link KeyEventChannel} that turns it into a {@link PrimaryResponder}. + */ +class KeyChannelResponder implements KeyboardManager.PrimaryResponder { + private static final String TAG = "KeyChannelResponder"; + + @NonNull private final KeyEventChannel keyEventChannel; + private final AndroidKeyProcessor keyProcessor = new AndroidKeyProcessor(); + + KeyChannelResponder(@NonNull KeyEventChannel keyEventChannel) { + this.keyEventChannel = keyEventChannel; + } + + @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 = + keyProcessor.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)); + return; + } +} 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..d763773eead51 --- /dev/null +++ b/shell/platform/android/io/flutter/embedding/android/KeyboardManager.java @@ -0,0 +1,174 @@ +// 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.PrimaryResponder.OnKeyEventHandledCallback; +import io.flutter.embedding.engine.systemchannels.KeyEventChannel; +import io.flutter.plugin.editing.TextInputPlugin; +import java.util.HashSet; + +/** + * A class to process {@link KeyEvent}s dispatched to a {@link FlutterView}. + * + *

A class that sends Android key events to the currently registered {@link PrimaryResponder}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 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. + * + *

A new {@link KeyEvent} sent to a {@link KeyboardManager} may be processed by 3 different types + * of "responder"s: + * + *

+ */ +public class KeyboardManager { + private static final String TAG = "KeyboardManager"; + + KeyboardManager( + View view, @NonNull TextInputPlugin textInputPlugin, PrimaryResponder[] primaryResponders) { + this.view = view; + this.textInputPlugin = textInputPlugin; + this.primaryResponders = primaryResponders; + } + + public KeyboardManager( + View view, @NonNull TextInputPlugin textInputPlugin, KeyEventChannel keyEventChannel) { + this( + view, + textInputPlugin, + new KeyboardManager.PrimaryResponder[] {new KeyChannelResponder(keyEventChannel)}); + } + + /** + * The interface for responding to a {@link KeyEvent} asynchronously. + * + *

Implementers of this interface should be added to a {@link KeyboardManager} using the {@link + * KeyboardManager#addPrimaryResponder(PrimaryResponder)}, in order to receive key events. + * + *

After receiving a {@link KeyEvent}, the {@link PrimaryResponder} must call the supplied + * {@link OnKeyEventHandledCallback} to inform the {@link KeyboardManager} whether it is capable + * of handling the {@link KeyEvent}. + * + *

If a {@link PrimaryResponder} 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 PrimaryResponder { + interface OnKeyEventHandledCallback { + void onKeyEventHandled(Boolean canHandleEvent); + } + + /** + * Informs this {@link PrimaryResponder} that a new {@link KeyEvent} needs processing. + * + * @param keyEvent the new {@link KeyEvent} this {@link PrimaryResponder} may be interested in. + * @param onKeyEventHandledCallback the method to call when this {@link PrimaryResponder} has + * decided whether to handle {@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 = primaryResponders.length; + boolean isEventHandled = false; + + public OnKeyEventHandledCallback buildCallback() { + return new Callback(); + } + } + + @NonNull protected final PrimaryResponder[] primaryResponders; + @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 !isRedispatchedEvent; + } + + if (primaryResponders.length > 0) { + final PerEventCallbackBuilder callbackBuilder = new PerEventCallbackBuilder(keyEvent); + for (final PrimaryResponder primaryResponder : primaryResponders) { + primaryResponder.handleEvent(keyEvent, callbackBuilder.buildCallback()); + } + } else { + onUnhandled(keyEvent); + } + + return !isRedispatchedEvent; + } + + 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..86dba6120d426 100644 --- a/shell/platform/android/io/flutter/embedding/engine/systemchannels/KeyEventChannel.java +++ b/shell/platform/android/io/flutter/embedding/engine/systemchannels/KeyEventChannel.java @@ -27,16 +27,6 @@ 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 { @@ -45,15 +35,7 @@ public interface EventResponseHandler { * * @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. - * - * @param event the event to be marked as not being handled by the framework. Must not be null. - */ - public void onKeyEventNotHandled(KeyEvent event); + public void onFrameworkResponse(boolean isEventHandled); } /** @@ -66,58 +48,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 +84,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..9da722392c432 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 mKeyboardManager; @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; + mKeyboardManager = 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 mKeyboardManager.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..db73de74d1e5a 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,7 @@ public class TextInputPlugin implements ListenableEditingState.EditingStateWatch @NonNull private PlatformViewsController platformViewsController; @Nullable private Rect lastClientRect; private ImeSyncDeferringInsetsCallback imeSyncCallback; - private AndroidKeyProcessor keyProcessor; + private KeyboardManager mKeyboardManager; // Initialize the "last seen" text editing values to a non-null value. private TextEditState mLastKnownFrameworkTextEditingState; @@ -175,15 +176,9 @@ ImeSyncDeferringInsetsCallback getImeSyncCallback() { return imeSyncCallback; } - @NonNull - public AndroidKeyProcessor getKeyEventProcessor() { - return keyProcessor; - } - - public void setKeyEventProcessor(AndroidKeyProcessor processor) { - keyProcessor = processor; + public void setKeyboardManager(KeyboardManager processor) { + mKeyboardManager = processor; } - /** * Use the current platform view input connection until unlockPlatformViewInputConnection is * called. @@ -330,7 +325,7 @@ public InputConnection createInputConnection(View view, EditorInfo outAttrs) { InputConnectionAdaptor connection = new InputConnectionAdaptor( - view, inputTarget.id, textInputChannel, keyProcessor, mEditable, outAttrs); + view, inputTarget.id, textInputChannel, mKeyboardManager, mEditable, outAttrs); outAttrs.initialSelStart = mEditable.getSelectionStart(); outAttrs.initialSelEnd = mEditable.getSelectionEnd(); @@ -557,6 +552,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..00a6783cb8dbd 100644 --- a/shell/platform/android/io/flutter/view/FlutterView.java +++ b/shell/platform/android/io/flutter/view/FlutterView.java @@ -42,8 +42,8 @@ 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.KeyboardManager; import io.flutter.embedding.engine.dart.DartExecutor; import io.flutter.embedding.engine.renderer.FlutterRenderer; import io.flutter.embedding.engine.renderer.SurfaceTextureWrapper; @@ -127,7 +127,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 +228,15 @@ public void onPostResume() { mNativeView.getPluginRegistry().getPlatformViewsController(); mTextInputPlugin = new TextInputPlugin(this, new TextInputChannel(dartExecutor), platformViewsController); + mKeyboardManager = new KeyboardManager(this, mTextInputPlugin, keyEventChannel); + mTextInputPlugin.setKeyboardManager(mKeyboardManager); + 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 +284,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() { diff --git a/shell/platform/android/test/io/flutter/FlutterTestSuite.java b/shell/platform/android/test/io/flutter/FlutterTestSuite.java index b4adff91c685e..76819d852c1fa 100644 --- a/shell/platform/android/test/io/flutter/FlutterTestSuite.java +++ b/shell/platform/android/test/io/flutter/FlutterTestSuite.java @@ -11,6 +11,8 @@ 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; @@ -75,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 index f4fd809642675..667c4018de666 100644 --- a/shell/platform/android/test/io/flutter/embedding/android/AndroidKeyProcessorTest.java +++ b/shell/platform/android/test/io/flutter/embedding/android/AndroidKeyProcessorTest.java @@ -1,32 +1,12 @@ 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 android.view.KeyCharacterMap; 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; @@ -34,276 +14,34 @@ @RunWith(RobolectricTestRunner.class) @TargetApi(28) public class AndroidKeyProcessorTest { - @Mock FlutterJNI mockFlutterJni; + AndroidKeyProcessor keyProcessor; + private static final int DEAD_KEY = '`' | KeyCharacterMap.COMBINING_ACCENT; @Before public void setUp() { - MockitoAnnotations.initMocks(this); - when(mockFlutterJni.isAttached()).thenReturn(true); + keyProcessor = new AndroidKeyProcessor(); } @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; + public void basicCombingCharactersTest() { + assertEquals(0, (int) keyProcessor.applyCombiningCharacterToBaseCharacter(0)); + assertEquals('A', (int) keyProcessor.applyCombiningCharacterToBaseCharacter('A')); + assertEquals('B', (int) keyProcessor.applyCombiningCharacterToBaseCharacter('B')); + assertEquals('B', (int) keyProcessor.applyCombiningCharacterToBaseCharacter('B')); + assertEquals(0, (int) keyProcessor.applyCombiningCharacterToBaseCharacter(0)); + assertEquals(0, (int) keyProcessor.applyCombiningCharacterToBaseCharacter(0)); + + assertEquals('`', (int) keyProcessor.applyCombiningCharacterToBaseCharacter(DEAD_KEY)); + assertEquals('`', (int) keyProcessor.applyCombiningCharacterToBaseCharacter(DEAD_KEY)); + assertEquals('À', (int) keyProcessor.applyCombiningCharacterToBaseCharacter('A')); + + assertEquals('`', (int) keyProcessor.applyCombiningCharacterToBaseCharacter(DEAD_KEY)); + assertEquals(0, (int) keyProcessor.applyCombiningCharacterToBaseCharacter(0)); + // The 0 input should remove the combining state. + assertEquals('A', (int) keyProcessor.applyCombiningCharacterToBaseCharacter('A')); + + assertEquals(0, (int) keyProcessor.applyCombiningCharacterToBaseCharacter(0)); + assertEquals('`', (int) keyProcessor.applyCombiningCharacterToBaseCharacter(DEAD_KEY)); + assertEquals('À', (int) keyProcessor.applyCombiningCharacterToBaseCharacter('A')); } } 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..32c97ef08a939 --- /dev/null +++ b/shell/platform/android/test/io/flutter/embedding/android/KeyChannelResponderTest.java @@ -0,0 +1,54 @@ +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.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 { + @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); + } +} 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..b8bb1d0d81bfc --- /dev/null +++ b/shell/platform/android/test/io/flutter/embedding/android/KeyboardManagerTest.java @@ -0,0 +1,308 @@ +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.PrimaryResponder; +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 PrimaryResponder { + 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(); + // mockView = mock(View.class); + // mockRootView = mock(View.class); + 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, mockKeyEventChannel); + } + + // Tests start + + @Test + public void respondsTrueWhenHandlingNewEvents() { + final FakeResponder fakeResponder = new FakeResponder(); + keyboardManager = + new KeyboardManager( + mockView, mockTextInputPlugin, new KeyboardManager.PrimaryResponder[] {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.PrimaryResponder[] {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 zeroPrimaryRespondersTest() { + keyboardManager = + new KeyboardManager( + mockView, mockTextInputPlugin, new KeyboardManager.PrimaryResponder[] {}); + 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 multiplePrimaryRespondersTest() { + final FakeResponder fakeResponder1 = new FakeResponder(); + final FakeResponder fakeResponder2 = new FakeResponder(); + keyboardManager = + new KeyboardManager( + mockView, + mockTextInputPlugin, + new KeyboardManager.PrimaryResponder[] {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 multiplePrimaryRespondersTest2() { + final FakeResponder fakeResponder1 = new FakeResponder(); + final FakeResponder fakeResponder2 = new FakeResponder(); + keyboardManager = + new KeyboardManager( + mockView, + mockTextInputPlugin, + new KeyboardManager.PrimaryResponder[] {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 multiplePrimaryRespondersTest3() { + final FakeResponder fakeResponder1 = new FakeResponder(); + final FakeResponder fakeResponder2 = new FakeResponder(); + keyboardManager = + new KeyboardManager( + mockView, + mockTextInputPlugin, + new KeyboardManager.PrimaryResponder[] {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.PrimaryResponder[] {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.PrimaryResponder[] {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.PrimaryResponder[] {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..c70bb7186fb09 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; @@ -520,9 +520,10 @@ 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, 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 +534,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(), @@ -789,13 +790,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..b236687705401 100644 --- a/tools/android_lint/project.xml +++ b/tools/android_lint/project.xml @@ -4,81 +4,188 @@ + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + - - - + + - + + + + - - - - - - - - - + + + + + + + - - - - - - - + + + + + + - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From d52523dcb770ef45499b6c4eac9bdd5da908766e Mon Sep 17 00:00:00 2001 From: LongCat is Looong <31859944+LongCatIsLooong@users.noreply.github.com> Date: Fri, 16 Apr 2021 13:57:35 -0700 Subject: [PATCH 02/12] split AndroidKeyProcessor into different classes --- .../android/AndroidKeyProcessor.java | 38 +----------- .../android/KeyChannelResponder.java | 9 +-- .../embedding/android/KeyboardManager.java | 62 ++++++++++++++----- .../android/KeyboardManagerTest.java | 2 - 4 files changed, 52 insertions(+), 59 deletions(-) diff --git a/shell/platform/android/io/flutter/embedding/android/AndroidKeyProcessor.java b/shell/platform/android/io/flutter/embedding/android/AndroidKeyProcessor.java index 44e80aee86e6c..4031aaa1b2ff0 100644 --- a/shell/platform/android/io/flutter/embedding/android/AndroidKeyProcessor.java +++ b/shell/platform/android/io/flutter/embedding/android/AndroidKeyProcessor.java @@ -5,46 +5,11 @@ package io.flutter.embedding.android; import android.view.KeyCharacterMap; -import androidx.annotation.Nullable; -/** - * 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. - */ +/** A class for handling combining characters in a {@link KeyEvent} stream. */ class AndroidKeyProcessor { private int combiningCharacter; - /** - * 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. - */ - /** * 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 @@ -72,7 +37,6 @@ class AndroidKeyProcessor { *

The following reference explains the concept of a "combining character": * https://en.wikipedia.org/wiki/Combining_character */ - @Nullable Character applyCombiningCharacterToBaseCharacter(int newCharacterCodePoint) { char complexCharacter = (char) newCharacterCodePoint; boolean isNewCodePointACombiningCharacter = diff --git a/shell/platform/android/io/flutter/embedding/android/KeyChannelResponder.java b/shell/platform/android/io/flutter/embedding/android/KeyChannelResponder.java index 562a0d3e7244f..d1e110349ec6d 100644 --- a/shell/platform/android/io/flutter/embedding/android/KeyChannelResponder.java +++ b/shell/platform/android/io/flutter/embedding/android/KeyChannelResponder.java @@ -1,12 +1,14 @@ +// 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 androidx.annotation.NonNull; import io.flutter.embedding.engine.systemchannels.KeyEventChannel; -/** - * A light wrapper around a {@link KeyEventChannel} that turns it into a {@link PrimaryResponder}. - */ +/** A light wrapper around a {@link KeyEventChannel}, turning it into a {@link PrimaryResponder}. */ class KeyChannelResponder implements KeyboardManager.PrimaryResponder { private static final String TAG = "KeyChannelResponder"; @@ -39,6 +41,5 @@ public void handleEvent( flutterEvent, isKeyUp, (isEventHandled) -> onKeyEventHandledCallback.onKeyEventHandled(isEventHandled)); - return; } } diff --git a/shell/platform/android/io/flutter/embedding/android/KeyboardManager.java b/shell/platform/android/io/flutter/embedding/android/KeyboardManager.java index d763773eead51..2c793527229be 100644 --- a/shell/platform/android/io/flutter/embedding/android/KeyboardManager.java +++ b/shell/platform/android/io/flutter/embedding/android/KeyboardManager.java @@ -14,10 +14,11 @@ import java.util.HashSet; /** - * A class to process {@link KeyEvent}s dispatched to a {@link FlutterView}. + * 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 key events to the currently registered {@link PrimaryResponder}s, - * and re-dispatches those not handled by the primary responders. + *

A class that sends Android {@link KeyEvent} to the currently registered {@link + * PrimaryResponder}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 a key event is received by Flutter, it tells @@ -26,19 +27,25 @@ * 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} may be processed by 3 different types - * of "responder"s: + *

A new {@link KeyEvent} sent to a {@link KeyboardManager} can be propagated to 3 different + * types of "responder"s: * *

    - *
  • {@link PrimaryResponder}s: the {@link KeyboardManager} calls the {@link - * PrimaryResponder#handleEvent(KeyEvent, OnKeyEventHandledCallback)} method on the currently - * registered {@link PrimaryResponder}s. When each {@link PrimaryResponder} has decided wether - * to handle the key event, it must call the supplied {@link OnKeyEventHandledCallback} + *
  • {@link PrimaryResponder}s: An immutable list of key responders in a {@link KeyboardManager} + * that each implements the {@link PrimaryResponder} interface. a {@link PrimaryResponder} is + * capable of handling {@link KeyEvent}s asynchronously. + *

    When a new {@link KeyEvent} is received, {@link KeyboardManager} calls the {@link + * PrimaryResponder#handleEvent(KeyEvent, OnKeyEventHandledCallback)} method on its {@link + * PrimaryResponder}s. Each {@link PrimaryResponder} must call the supplied {@link + * OnKeyEventHandledCallback} exactly once, when it has decided wether to handle the key event * callback. More than one {@link PrimaryResponder} is allowed to reply true and handle the * same {@link KeyEvent}. + *

    Typically a {@link KeyboardManager} uses a {@link KeyChannelResponder} as its only + * {@link PrimaryResponder}. *

  • {@link TextInputPlugin}: if every {@link PrimaryResponder} has replied false to a {@link - * KeyEvent}, the {@link KeyEvent} will be sent to the currently focused editable text field - * in {@link TextInputPlugin}, if any. + * KeyEvent}, or if the {@link KeyboardManager} has zero {@link PrimaryResponder}s, the {@link + * KeyEvent} will be sent to the currently focused editable text field in {@link + * TextInputPlugin}, if any. *
  • "Redispatch": if there's no currently focused text field in {@link TextInputPlugin}, * or the text field does not handle the {@link KeyEvent} either, the {@link KeyEvent} will be * sent back to the top of the activity's view hierachy, allowing the {@link KeyEvent} to be @@ -56,6 +63,27 @@ public class KeyboardManager { this.primaryResponders = primaryResponders; } + /** + * Constructor for {@link KeyboardManager} that uses a {@link KeyChannelResponder} as its only + * {@link PrimaryResponder}. + * + *

    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 KeyChannelResponder} 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 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 keyEventChannel the event channel to listen to for new key events. + */ public KeyboardManager( View view, @NonNull TextInputPlugin textInputPlugin, KeyEventChannel keyEventChannel) { this( @@ -67,12 +95,14 @@ public KeyboardManager( /** * The interface for responding to a {@link KeyEvent} asynchronously. * - *

    Implementers of this interface should be added to a {@link KeyboardManager} using the {@link - * KeyboardManager#addPrimaryResponder(PrimaryResponder)}, in order to receive key events. + *

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

    After receiving a {@link KeyEvent}, the {@link PrimaryResponder} must call the supplied - * {@link OnKeyEventHandledCallback} to inform the {@link KeyboardManager} whether it is capable - * of handling the {@link KeyEvent}. + * {@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 the key responder answered + * yes. * *

    If a {@link PrimaryResponder} fails to call the {@link OnKeyEventHandledCallback} callback, * the {@link KeyEvent} will never be sent to the {@link TextInputPlugin}, and the {@link @@ -88,7 +118,7 @@ interface OnKeyEventHandledCallback { * * @param keyEvent the new {@link KeyEvent} this {@link PrimaryResponder} may be interested in. * @param onKeyEventHandledCallback the method to call when this {@link PrimaryResponder} has - * decided whether to handle {@link keyEvent}. + * decided whether to handle the {@link keyEvent}. */ void handleEvent( @NonNull KeyEvent keyEvent, @NonNull OnKeyEventHandledCallback onKeyEventHandledCallback); diff --git a/shell/platform/android/test/io/flutter/embedding/android/KeyboardManagerTest.java b/shell/platform/android/test/io/flutter/embedding/android/KeyboardManagerTest.java index b8bb1d0d81bfc..b799c790c8a4e 100644 --- a/shell/platform/android/test/io/flutter/embedding/android/KeyboardManagerTest.java +++ b/shell/platform/android/test/io/flutter/embedding/android/KeyboardManagerTest.java @@ -73,8 +73,6 @@ public void setUp() { when(mockFlutterJni.isAttached()).thenReturn(true); mockEngine = mockFlutterEngine(); mockKeyEventChannel = mockEngine.getKeyEventChannel(); - // mockView = mock(View.class); - // mockRootView = mock(View.class); when(mockView.getRootView()).thenAnswer(invocation -> mockRootView); when(mockView.dispatchKeyEvent(any(KeyEvent.class))) .thenAnswer( From c3deeee40e2fef9bf2c4a2aa2db013b561c6d0d1 Mon Sep 17 00:00:00 2001 From: LongCat is Looong <31859944+LongCatIsLooong@users.noreply.github.com> Date: Fri, 16 Apr 2021 15:10:25 -0700 Subject: [PATCH 03/12] fix GN format --- shell/platform/android/BUILD.gn | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/shell/platform/android/BUILD.gn b/shell/platform/android/BUILD.gn index e0253d862b258..84a784e2d8e43 100644 --- a/shell/platform/android/BUILD.gn +++ b/shell/platform/android/BUILD.gn @@ -143,8 +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/KeyboardManager.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", @@ -465,8 +465,8 @@ action("robolectric_tests") { "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/KeyboardManagerTest.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", From e9501bef43f3f961e2772bacb3c6ed424c854155 Mon Sep 17 00:00:00 2001 From: LongCat is Looong <31859944+LongCatIsLooong@users.noreply.github.com> Date: Fri, 16 Apr 2021 15:55:38 -0700 Subject: [PATCH 04/12] fix license --- ci/licenses_golden/licenses_flutter | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index 5ba2ac4d49de4..489a19a023935 100755 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -759,8 +759,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/KeyboardManager.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 From a9a01baf22c95a8a9d9eb577c1c61474b79dab4b Mon Sep 17 00:00:00 2001 From: LongCat is Looong <31859944+LongCatIsLooong@users.noreply.github.com> Date: Fri, 16 Apr 2021 16:09:53 -0700 Subject: [PATCH 05/12] minor documentation changes --- .../embedding/android/KeyboardManager.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/shell/platform/android/io/flutter/embedding/android/KeyboardManager.java b/shell/platform/android/io/flutter/embedding/android/KeyboardManager.java index 2c793527229be..01ca70da93073 100644 --- a/shell/platform/android/io/flutter/embedding/android/KeyboardManager.java +++ b/shell/platform/android/io/flutter/embedding/android/KeyboardManager.java @@ -17,8 +17,8 @@ * 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 currently registered {@link - * PrimaryResponder}s, and re-dispatches those not handled by the primary responders. + *

    A class that sends Android {@link KeyEvent} to the a list of {@link PrimaryResponder}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 a key event is received by Flutter, it tells @@ -28,12 +28,12 @@ * 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 "responder"s: + * types of "responder"s (in the listed order): * *

      *
    • {@link PrimaryResponder}s: An immutable list of key responders in a {@link KeyboardManager} - * that each implements the {@link PrimaryResponder} interface. a {@link PrimaryResponder} is - * capable of handling {@link KeyEvent}s asynchronously. + * that each implements the {@link PrimaryResponder} interface. A {@link PrimaryResponder} is + * a key responder that's capable of handling {@link KeyEvent}s asynchronously. *

      When a new {@link KeyEvent} is received, {@link KeyboardManager} calls the {@link * PrimaryResponder#handleEvent(KeyEvent, OnKeyEventHandledCallback)} method on its {@link * PrimaryResponder}s. Each {@link PrimaryResponder} must call the supplied {@link @@ -48,9 +48,9 @@ * TextInputPlugin}, if any. *

    • "Redispatch": if there's no currently focused text field in {@link TextInputPlugin}, * or the text field does not handle the {@link KeyEvent} either, the {@link KeyEvent} will be - * sent back to the top of the activity's view hierachy, allowing the {@link KeyEvent} to be - * "redispatched", only this time the {@link KeyboardManager} will not try to handle the - * redispatched {@link KeyEvent}. + * sent back to the top of the activity's view hierachy, allowing it to be "redispatched", + * only this time the {@link KeyboardManager} will not try to handle the redispatched {@link + * KeyEvent}. *
    */ public class KeyboardManager { From 6732e0fe0707185728be3e03df92dbbb0163770c Mon Sep 17 00:00:00 2001 From: LongCat is Looong <31859944+LongCatIsLooong@users.noreply.github.com> Date: Wed, 21 Apr 2021 20:30:27 -0700 Subject: [PATCH 06/12] review --- .../android/io/flutter/embedding/android/KeyboardManager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shell/platform/android/io/flutter/embedding/android/KeyboardManager.java b/shell/platform/android/io/flutter/embedding/android/KeyboardManager.java index 01ca70da93073..e6176e7cd442a 100644 --- a/shell/platform/android/io/flutter/embedding/android/KeyboardManager.java +++ b/shell/platform/android/io/flutter/embedding/android/KeyboardManager.java @@ -164,7 +164,7 @@ public OnKeyEventHandledCallback buildCallback() { public boolean handleEvent(@NonNull KeyEvent keyEvent) { final boolean isRedispatchedEvent = redispatchedEvents.remove(keyEvent); if (isRedispatchedEvent) { - return !isRedispatchedEvent; + return false; } if (primaryResponders.length > 0) { From b0c0e0b503ce8654eba0c88f9294fd075e388e0d Mon Sep 17 00:00:00 2001 From: LongCat is Looong <31859944+LongCatIsLooong@users.noreply.github.com> Date: Mon, 26 Apr 2021 16:07:19 -0700 Subject: [PATCH 07/12] review --- ci/licenses_golden/licenses_flutter | 1 - shell/platform/android/BUILD.gn | 2 - .../android/AndroidKeyProcessor.java | 66 ----------------- .../embedding/android/FlutterView.java | 2 +- .../android/KeyChannelResponder.java | 70 +++++++++++++++++-- .../embedding/android/KeyboardManager.java | 52 +++++++------- .../systemchannels/KeyEventChannel.java | 5 +- .../plugin/editing/TextInputPlugin.java | 8 +-- .../test/io/flutter/FlutterTestSuite.java | 2 - .../android/AndroidKeyProcessorTest.java | 47 ------------- .../android/KeyChannelResponderTest.java | 27 +++++++ tools/android_lint/project.xml | 2 - 12 files changed, 125 insertions(+), 159 deletions(-) delete mode 100644 shell/platform/android/io/flutter/embedding/android/AndroidKeyProcessor.java delete mode 100644 shell/platform/android/test/io/flutter/embedding/android/AndroidKeyProcessorTest.java diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index 489a19a023935..965081f7e7ed3 100755 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -743,7 +743,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 diff --git a/shell/platform/android/BUILD.gn b/shell/platform/android/BUILD.gn index 84a784e2d8e43..394421b46b87a 100644 --- a/shell/platform/android/BUILD.gn +++ b/shell/platform/android/BUILD.gn @@ -127,7 +127,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", @@ -458,7 +457,6 @@ action("robolectric_tests") { "test/io/flutter/FlutterInjectorTest.java", "test/io/flutter/FlutterTestSuite.java", "test/io/flutter/SmokeTest.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", 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 4031aaa1b2ff0..0000000000000 --- a/shell/platform/android/io/flutter/embedding/android/AndroidKeyProcessor.java +++ /dev/null @@ -1,66 +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; - -/** A class for handling combining characters in a {@link KeyEvent} stream. */ -class AndroidKeyProcessor { - private int combiningCharacter; - - /** - * 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: - * - *

      - *
    • If no previous {@link #combiningCharacter} exists and the {@code newCharacterCodePoint} - * is not a combining character, then {@code newCharacterCodePoint} is returned. - *
    • If no previous {@link #combiningCharacter} exists and the {@code newCharacterCodePoint} - * is a combining character, then {@code newCharacterCodePoint} is saved as the {@link - * #combiningCharacter} and null is returned. - *
    • If a previous {@link #combiningCharacter} exists and the {@code newCharacterCodePoint} is - * also a combining character, then the {@code newCharacterCodePoint} is combined with the - * existing {@link #combiningCharacter} and null is returned. - *
    • If a previous {@link #combiningCharacter} exists and the {@code newCharacterCodePoint} is - * not a combining character, then the {@link #combiningCharacter} is applied to the regular - * {@code newCharacterCodePoint} and the resulting complex character is returned. The {@link - * #combiningCharacter} is cleared. - *
    - * - *

    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; - } -} diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterView.java b/shell/platform/android/io/flutter/embedding/android/FlutterView.java index aa0887ad35250..4409c954997ab 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterView.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterView.java @@ -727,7 +727,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. */ diff --git a/shell/platform/android/io/flutter/embedding/android/KeyChannelResponder.java b/shell/platform/android/io/flutter/embedding/android/KeyChannelResponder.java index d1e110349ec6d..5fee44c3ab113 100644 --- a/shell/platform/android/io/flutter/embedding/android/KeyChannelResponder.java +++ b/shell/platform/android/io/flutter/embedding/android/KeyChannelResponder.java @@ -8,19 +8,78 @@ import androidx.annotation.NonNull; import io.flutter.embedding.engine.systemchannels.KeyEventChannel; -/** A light wrapper around a {@link KeyEventChannel}, turning it into a {@link PrimaryResponder}. */ -class KeyChannelResponder implements KeyboardManager.PrimaryResponder { +/** +* 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. +*/ +class KeyChannelResponder implements KeyboardManager.Responder { private static final String TAG = "KeyChannelResponder"; @NonNull private final KeyEventChannel keyEventChannel; - private final AndroidKeyProcessor keyProcessor = new AndroidKeyProcessor(); + private int combiningCharacter; 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: + * + *

      + *
    • If no previous {@link #combiningCharacter} exists and the {@code newCharacterCodePoint} + * is not a combining character, then {@code newCharacterCodePoint} is returned. + *
    • If no previous {@link #combiningCharacter} exists and the {@code newCharacterCodePoint} + * is a combining character, then {@code newCharacterCodePoint} is saved as the {@link + * #combiningCharacter} and null is returned. + *
    • If a previous {@link #combiningCharacter} exists and the {@code newCharacterCodePoint} is + * also a combining character, then the {@code newCharacterCodePoint} is combined with the + * existing {@link #combiningCharacter} and null is returned. + *
    • If a previous {@link #combiningCharacter} exists and the {@code newCharacterCodePoint} is + * not a combining character, then the {@link #combiningCharacter} is applied to the regular + * {@code newCharacterCodePoint} and the resulting complex character is returned. The {@link + * #combiningCharacter} is cleared. + *
    + * + *

    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( + void handleEvent( @NonNull KeyEvent keyEvent, @NonNull OnKeyEventHandledCallback onKeyEventHandledCallback) { final int action = keyEvent.getAction(); if (action != KeyEvent.ACTION_DOWN && action != KeyEvent.ACTION_UP) { @@ -31,8 +90,7 @@ public void handleEvent( return; } - final Character complexCharacter = - keyProcessor.applyCombiningCharacterToBaseCharacter(keyEvent.getUnicodeChar()); + final Character complexCharacter = applyCombiningCharacterToBaseCharacter(keyEvent.getUnicodeChar()); KeyEventChannel.FlutterKeyEvent flutterEvent = new KeyEventChannel.FlutterKeyEvent(keyEvent, complexCharacter); diff --git a/shell/platform/android/io/flutter/embedding/android/KeyboardManager.java b/shell/platform/android/io/flutter/embedding/android/KeyboardManager.java index e6176e7cd442a..5f8a934c168a8 100644 --- a/shell/platform/android/io/flutter/embedding/android/KeyboardManager.java +++ b/shell/platform/android/io/flutter/embedding/android/KeyboardManager.java @@ -8,7 +8,7 @@ import android.view.View; import androidx.annotation.NonNull; import io.flutter.Log; -import io.flutter.embedding.android.KeyboardManager.PrimaryResponder.OnKeyEventHandledCallback; +import io.flutter.embedding.android.KeyboardManager.Responder.OnKeyEventHandledCallback; import io.flutter.embedding.engine.systemchannels.KeyEventChannel; import io.flutter.plugin.editing.TextInputPlugin; import java.util.HashSet; @@ -17,33 +17,33 @@ * 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 PrimaryResponder}s, and + *

    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 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 + * 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 "responder"s (in the listed order): + * types of responders (in the listed order): * *

      - *
    • {@link PrimaryResponder}s: An immutable list of key responders in a {@link KeyboardManager} - * that each implements the {@link PrimaryResponder} interface. A {@link PrimaryResponder} is + *
    • {@link Responder}s: An immutable list of key responders in a {@link KeyboardManager} + * that each implements the {@link Responder} interface. A {@link Responder} is * a key responder that's capable of handling {@link KeyEvent}s asynchronously. *

      When a new {@link KeyEvent} is received, {@link KeyboardManager} calls the {@link - * PrimaryResponder#handleEvent(KeyEvent, OnKeyEventHandledCallback)} method on its {@link - * PrimaryResponder}s. Each {@link PrimaryResponder} must call the supplied {@link + * Responder#handleEvent(KeyEvent, OnKeyEventHandledCallback)} method on its {@link + * Responder}s. Each {@link Responder} must call the supplied {@link * OnKeyEventHandledCallback} exactly once, when it has decided wether to handle the key event - * callback. More than one {@link PrimaryResponder} is allowed to reply true and handle the + * callback. More than one {@link Responder} is allowed to reply true and handle the * same {@link KeyEvent}. *

      Typically a {@link KeyboardManager} uses a {@link KeyChannelResponder} as its only - * {@link PrimaryResponder}. - *

    • {@link TextInputPlugin}: if every {@link PrimaryResponder} has replied false to a {@link - * KeyEvent}, or if the {@link KeyboardManager} has zero {@link PrimaryResponder}s, the {@link + * {@link Responder}. + *
    • {@link TextInputPlugin}: if every {@link Responder} has replied false to a {@link + * KeyEvent}, or if the {@link KeyboardManager} has zero {@link Responder}s, the {@link * KeyEvent} will be sent to the currently focused editable text field in {@link * TextInputPlugin}, if any. *
    • "Redispatch": if there's no currently focused text field in {@link TextInputPlugin}, @@ -57,7 +57,7 @@ public class KeyboardManager { private static final String TAG = "KeyboardManager"; KeyboardManager( - View view, @NonNull TextInputPlugin textInputPlugin, PrimaryResponder[] primaryResponders) { + View view, @NonNull TextInputPlugin textInputPlugin, Responder[] primaryResponders) { this.view = view; this.textInputPlugin = textInputPlugin; this.primaryResponders = primaryResponders; @@ -65,7 +65,7 @@ public class KeyboardManager { /** * Constructor for {@link KeyboardManager} that uses a {@link KeyChannelResponder} as its only - * {@link PrimaryResponder}. + * {@link Responder}. * *

      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 KeyChannelResponder} returns @@ -89,7 +89,7 @@ public KeyboardManager( this( view, textInputPlugin, - new KeyboardManager.PrimaryResponder[] {new KeyChannelResponder(keyEventChannel)}); + new KeyboardManager.Responder[] {new KeyChannelResponder(keyEventChannel)}); } /** @@ -98,26 +98,26 @@ public KeyboardManager( *

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

      After receiving a {@link KeyEvent}, the {@link PrimaryResponder} must call the supplied + *

      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 the key responder answered + * {@link TextInputPlugin} or be redispatched to the view hierachy if any key responders answered * yes. * - *

      If a {@link PrimaryResponder} fails to call the {@link OnKeyEventHandledCallback} callback, + *

      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 PrimaryResponder { + interface Responder { interface OnKeyEventHandledCallback { void onKeyEventHandled(Boolean canHandleEvent); } /** - * Informs this {@link PrimaryResponder} that a new {@link KeyEvent} needs processing. + * Informs this {@link Responder} that a new {@link KeyEvent} needs processing. * - * @param keyEvent the new {@link KeyEvent} this {@link PrimaryResponder} may be interested in. - * @param onKeyEventHandledCallback the method to call when this {@link PrimaryResponder} has + * @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( @@ -156,7 +156,7 @@ public OnKeyEventHandledCallback buildCallback() { } } - @NonNull protected final PrimaryResponder[] primaryResponders; + @NonNull protected final Responder[] primaryResponders; @NonNull private final HashSet redispatchedEvents = new HashSet<>(); @NonNull private final TextInputPlugin textInputPlugin; private final View view; @@ -169,7 +169,7 @@ public boolean handleEvent(@NonNull KeyEvent keyEvent) { if (primaryResponders.length > 0) { final PerEventCallbackBuilder callbackBuilder = new PerEventCallbackBuilder(keyEvent); - for (final PrimaryResponder primaryResponder : primaryResponders) { + for (final Responder primaryResponder : primaryResponders) { primaryResponder.handleEvent(keyEvent, callbackBuilder.buildCallback()); } } else { 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 86dba6120d426..249f339a6d988 100644 --- a/shell/platform/android/io/flutter/embedding/engine/systemchannels/KeyEventChannel.java +++ b/shell/platform/android/io/flutter/embedding/engine/systemchannels/KeyEventChannel.java @@ -31,9 +31,10 @@ public class KeyEventChannel { public interface EventResponseHandler { /** - * Called whenever the framework responds that a given key event was 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 being handled by the framework. Must not be null. + * @param isEventHandled whether the framework decides to handle the event. */ public void onFrameworkResponse(boolean isEventHandled); } diff --git a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java index db73de74d1e5a..8d22c74e736ea 100644 --- a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java +++ b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java @@ -49,7 +49,7 @@ public class TextInputPlugin implements ListenableEditingState.EditingStateWatch @NonNull private PlatformViewsController platformViewsController; @Nullable private Rect lastClientRect; private ImeSyncDeferringInsetsCallback imeSyncCallback; - private KeyboardManager mKeyboardManager; + private KeyboardManager keyboardManager; // Initialize the "last seen" text editing values to a non-null value. private TextEditState mLastKnownFrameworkTextEditingState; @@ -176,8 +176,8 @@ ImeSyncDeferringInsetsCallback getImeSyncCallback() { return imeSyncCallback; } - public void setKeyboardManager(KeyboardManager processor) { - mKeyboardManager = processor; + public void setKeyboardManager(KeyboardManager keyboardManager) { + this.keyboardManager = keyboardManager; } /** * Use the current platform view input connection until unlockPlatformViewInputConnection is @@ -325,7 +325,7 @@ public InputConnection createInputConnection(View view, EditorInfo outAttrs) { InputConnectionAdaptor connection = new InputConnectionAdaptor( - view, inputTarget.id, textInputChannel, mKeyboardManager, mEditable, outAttrs); + view, inputTarget.id, textInputChannel, keyboardManager, mEditable, outAttrs); outAttrs.initialSelStart = mEditable.getSelectionStart(); outAttrs.initialSelEnd = mEditable.getSelectionEnd(); diff --git a/shell/platform/android/test/io/flutter/FlutterTestSuite.java b/shell/platform/android/test/io/flutter/FlutterTestSuite.java index 76819d852c1fa..840c176714e33 100644 --- a/shell/platform/android/test/io/flutter/FlutterTestSuite.java +++ b/shell/platform/android/test/io/flutter/FlutterTestSuite.java @@ -4,7 +4,6 @@ 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; @@ -53,7 +52,6 @@ @RunWith(Suite.class) @SuiteClasses({ AccessibilityBridgeTest.class, - AndroidKeyProcessorTest.class, ApplicationInfoLoaderTest.class, DartExecutorTest.class, DartMessengerTest.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 667c4018de666..0000000000000 --- a/shell/platform/android/test/io/flutter/embedding/android/AndroidKeyProcessorTest.java +++ /dev/null @@ -1,47 +0,0 @@ -package io.flutter.embedding.android; - -import static junit.framework.TestCase.assertEquals; - -import android.annotation.TargetApi; -import android.view.KeyCharacterMap; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; - -@Config(manifest = Config.NONE) -@RunWith(RobolectricTestRunner.class) -@TargetApi(28) -public class AndroidKeyProcessorTest { - AndroidKeyProcessor keyProcessor; - private static final int DEAD_KEY = '`' | KeyCharacterMap.COMBINING_ACCENT; - - @Before - public void setUp() { - keyProcessor = new AndroidKeyProcessor(); - } - - @Test - public void basicCombingCharactersTest() { - assertEquals(0, (int) keyProcessor.applyCombiningCharacterToBaseCharacter(0)); - assertEquals('A', (int) keyProcessor.applyCombiningCharacterToBaseCharacter('A')); - assertEquals('B', (int) keyProcessor.applyCombiningCharacterToBaseCharacter('B')); - assertEquals('B', (int) keyProcessor.applyCombiningCharacterToBaseCharacter('B')); - assertEquals(0, (int) keyProcessor.applyCombiningCharacterToBaseCharacter(0)); - assertEquals(0, (int) keyProcessor.applyCombiningCharacterToBaseCharacter(0)); - - assertEquals('`', (int) keyProcessor.applyCombiningCharacterToBaseCharacter(DEAD_KEY)); - assertEquals('`', (int) keyProcessor.applyCombiningCharacterToBaseCharacter(DEAD_KEY)); - assertEquals('À', (int) keyProcessor.applyCombiningCharacterToBaseCharacter('A')); - - assertEquals('`', (int) keyProcessor.applyCombiningCharacterToBaseCharacter(DEAD_KEY)); - assertEquals(0, (int) keyProcessor.applyCombiningCharacterToBaseCharacter(0)); - // The 0 input should remove the combining state. - assertEquals('A', (int) keyProcessor.applyCombiningCharacterToBaseCharacter('A')); - - assertEquals(0, (int) keyProcessor.applyCombiningCharacterToBaseCharacter(0)); - assertEquals('`', (int) keyProcessor.applyCombiningCharacterToBaseCharacter(DEAD_KEY)); - assertEquals('À', (int) keyProcessor.applyCombiningCharacterToBaseCharacter('A')); - } -} diff --git a/shell/platform/android/test/io/flutter/embedding/android/KeyChannelResponderTest.java b/shell/platform/android/test/io/flutter/embedding/android/KeyChannelResponderTest.java index 32c97ef08a939..0b97651332fa4 100644 --- a/shell/platform/android/test/io/flutter/embedding/android/KeyChannelResponderTest.java +++ b/shell/platform/android/test/io/flutter/embedding/android/KeyChannelResponderTest.java @@ -5,6 +5,7 @@ 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; @@ -21,6 +22,9 @@ @RunWith(RobolectricTestRunner.class) @TargetApi(28) public class KeyChannelResponderTest { + + private static final int DEAD_KEY = '`' | KeyCharacterMap.COMBINING_ACCENT; + @Mock KeyEventChannel keyEventChannel; KeyChannelResponder channelResponder; @@ -51,4 +55,27 @@ public void primaryResponderTest() { }); 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/tools/android_lint/project.xml b/tools/android_lint/project.xml index b236687705401..7ac27f2309285 100644 --- a/tools/android_lint/project.xml +++ b/tools/android_lint/project.xml @@ -133,7 +133,6 @@ - @@ -185,7 +184,6 @@ - From 6c1dde065cc2370ff22e49ed602e70b594815c7a Mon Sep 17 00:00:00 2001 From: LongCat is Looong <31859944+LongCatIsLooong@users.noreply.github.com> Date: Mon, 26 Apr 2021 17:02:23 -0700 Subject: [PATCH 08/12] format --- .../io/flutter/embedding/android/KeyChannelResponder.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/shell/platform/android/io/flutter/embedding/android/KeyChannelResponder.java b/shell/platform/android/io/flutter/embedding/android/KeyChannelResponder.java index 374733feed5db..ab6d1c5b0b57b 100644 --- a/shell/platform/android/io/flutter/embedding/android/KeyChannelResponder.java +++ b/shell/platform/android/io/flutter/embedding/android/KeyChannelResponder.java @@ -4,6 +4,7 @@ package io.flutter.embedding.android; +import android.view.KeyCharacterMap; import android.view.KeyEvent; import androidx.annotation.NonNull; import io.flutter.embedding.engine.systemchannels.KeyEventChannel; @@ -79,7 +80,7 @@ Character applyCombiningCharacterToBaseCharacter(int newCharacterCodePoint) { } @Override - void handleEvent( + public void handleEvent( @NonNull KeyEvent keyEvent, @NonNull OnKeyEventHandledCallback onKeyEventHandledCallback) { final int action = keyEvent.getAction(); if (action != KeyEvent.ACTION_DOWN && action != KeyEvent.ACTION_UP) { From 001d9b77eb270fb09ab48a4f94e66511820d74c2 Mon Sep 17 00:00:00 2001 From: LongCat is Looong <31859944+LongCatIsLooong@users.noreply.github.com> Date: Wed, 28 Apr 2021 16:18:18 -0700 Subject: [PATCH 09/12] review --- .../embedding/android/FlutterView.java | 5 ++- .../android/KeyChannelResponder.java | 4 +-- .../embedding/android/KeyboardManager.java | 31 +++++++++---------- .../android/io/flutter/view/FlutterView.java | 4 ++- 4 files changed, 24 insertions(+), 20 deletions(-) diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterView.java b/shell/platform/android/io/flutter/embedding/android/FlutterView.java index 59f9000ff39d6..63b185ae70dec 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterView.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterView.java @@ -977,7 +977,10 @@ public void attachToFlutterEngine(@NonNull FlutterEngine flutterEngine) { localizationPlugin = this.flutterEngine.getLocalizationPlugin(); keyboardManager = - new KeyboardManager(this, textInputPlugin, flutterEngine.getKeyEventChannel()); + new KeyboardManager( + this, + mTextInputPlugin, + new Responder[] {new KeyChannelResponder(flutterEngine.getKeyEventChannel())}); androidTouchProcessor = new AndroidTouchProcessor(this.flutterEngine.getRenderer(), /*trackMotionEvents=*/ false); accessibilityBridge = diff --git a/shell/platform/android/io/flutter/embedding/android/KeyChannelResponder.java b/shell/platform/android/io/flutter/embedding/android/KeyChannelResponder.java index ab6d1c5b0b57b..d3bd28db79ea0 100644 --- a/shell/platform/android/io/flutter/embedding/android/KeyChannelResponder.java +++ b/shell/platform/android/io/flutter/embedding/android/KeyChannelResponder.java @@ -15,13 +15,13 @@ * *

      This class corresponds to the RawKeyboard API in the framework. */ -class KeyChannelResponder implements KeyboardManager.Responder { +public class KeyChannelResponder implements KeyboardManager.Responder { private static final String TAG = "KeyChannelResponder"; @NonNull private final KeyEventChannel keyEventChannel; private int combiningCharacter; - KeyChannelResponder(@NonNull KeyEventChannel keyEventChannel) { + public KeyChannelResponder(@NonNull KeyEventChannel keyEventChannel) { this.keyEventChannel = keyEventChannel; } diff --git a/shell/platform/android/io/flutter/embedding/android/KeyboardManager.java b/shell/platform/android/io/flutter/embedding/android/KeyboardManager.java index fe5250ae25917..6120b0f939d27 100644 --- a/shell/platform/android/io/flutter/embedding/android/KeyboardManager.java +++ b/shell/platform/android/io/flutter/embedding/android/KeyboardManager.java @@ -56,20 +56,12 @@ public class KeyboardManager { private static final String TAG = "KeyboardManager"; - KeyboardManager( - View view, @NonNull TextInputPlugin textInputPlugin, Responder[] primaryResponders) { - this.view = view; - this.textInputPlugin = textInputPlugin; - this.primaryResponders = primaryResponders; - } - /** - * Constructor for {@link KeyboardManager} that uses a {@link KeyChannelResponder} as its only - * {@link Responder}. + * 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 KeyChannelResponder} returns - * false from onKeyDown/onKeyUp. + * 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 @@ -82,9 +74,16 @@ public class KeyboardManager { * @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 keyEventChannel the event channel to listen to for new key events. + * @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; + } + + KeyboardManager( View view, @NonNull TextInputPlugin textInputPlugin, KeyEventChannel keyEventChannel) { this( view, @@ -148,7 +147,7 @@ public void onKeyEventHandled(Boolean canHandleEvent) { } @NonNull final KeyEvent keyEvent; - int unrepliedCount = primaryResponders.length; + int unrepliedCount = responders.length; boolean isEventHandled = false; public OnKeyEventHandledCallback buildCallback() { @@ -156,7 +155,7 @@ public OnKeyEventHandledCallback buildCallback() { } } - @NonNull protected final Responder[] primaryResponders; + @NonNull protected final Responder[] responders; @NonNull private final HashSet redispatchedEvents = new HashSet<>(); @NonNull private final TextInputPlugin textInputPlugin; private final View view; @@ -167,9 +166,9 @@ public boolean handleEvent(@NonNull KeyEvent keyEvent) { return false; } - if (primaryResponders.length > 0) { + if (responders.length > 0) { final PerEventCallbackBuilder callbackBuilder = new PerEventCallbackBuilder(keyEvent); - for (final Responder primaryResponder : primaryResponders) { + for (final Responder primaryResponder : responders) { primaryResponder.handleEvent(keyEvent, callbackBuilder.buildCallback()); } } else { diff --git a/shell/platform/android/io/flutter/view/FlutterView.java b/shell/platform/android/io/flutter/view/FlutterView.java index 00a6783cb8dbd..238005a26cb1f 100644 --- a/shell/platform/android/io/flutter/view/FlutterView.java +++ b/shell/platform/android/io/flutter/view/FlutterView.java @@ -228,7 +228,9 @@ public void onPostResume() { mNativeView.getPluginRegistry().getPlatformViewsController(); mTextInputPlugin = new TextInputPlugin(this, new TextInputChannel(dartExecutor), platformViewsController); - mKeyboardManager = new KeyboardManager(this, mTextInputPlugin, keyEventChannel); + mKeyboardManager = + new KeyboardManager( + this, mTextInputPlugin, new Responder[] {new KeyChannelResponder(keyEventChannel)}); mTextInputPlugin.setKeyboardManager(mKeyboardManager); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { From 14055bfc7ece4c0dc35aa83bf0a8d880037dcb57 Mon Sep 17 00:00:00 2001 From: LongCat is Looong <31859944+LongCatIsLooong@users.noreply.github.com> Date: Tue, 11 May 2021 01:50:24 -0700 Subject: [PATCH 10/12] review --- .../embedding/android/KeyboardManager.java | 15 +++------------ .../embedding/android/KeyboardManagerTest.java | 6 +++++- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/shell/platform/android/io/flutter/embedding/android/KeyboardManager.java b/shell/platform/android/io/flutter/embedding/android/KeyboardManager.java index 6120b0f939d27..9ff8d9a7e110b 100644 --- a/shell/platform/android/io/flutter/embedding/android/KeyboardManager.java +++ b/shell/platform/android/io/flutter/embedding/android/KeyboardManager.java @@ -9,7 +9,6 @@ import androidx.annotation.NonNull; import io.flutter.Log; import io.flutter.embedding.android.KeyboardManager.Responder.OnKeyEventHandledCallback; -import io.flutter.embedding.engine.systemchannels.KeyEventChannel; import io.flutter.plugin.editing.TextInputPlugin; import java.util.HashSet; @@ -37,9 +36,9 @@ *

      When a new {@link KeyEvent} is received, {@link KeyboardManager} calls the {@link * Responder#handleEvent(KeyEvent, OnKeyEventHandledCallback)} method on its {@link * Responder}s. Each {@link Responder} must call the supplied {@link - * OnKeyEventHandledCallback} exactly once, when it has decided wether to handle the key event - * callback. More than one {@link Responder} is allowed to reply true and handle the same - * {@link KeyEvent}. + * OnKeyEventHandledCallback} exactly once, when it has decided whether to handle the key + * event callback. More than one {@link Responder} is allowed to reply true and handle the + * same {@link KeyEvent}. *

      Typically a {@link KeyboardManager} uses a {@link KeyChannelResponder} as its only * {@link Responder}. *

    • {@link TextInputPlugin}: if every {@link Responder} has replied false to a {@link @@ -83,14 +82,6 @@ public KeyboardManager( this.responders = responders; } - KeyboardManager( - View view, @NonNull TextInputPlugin textInputPlugin, KeyEventChannel keyEventChannel) { - this( - view, - textInputPlugin, - new KeyboardManager.Responder[] {new KeyChannelResponder(keyEventChannel)}); - } - /** * The interface for responding to a {@link KeyEvent} asynchronously. * diff --git a/shell/platform/android/test/io/flutter/embedding/android/KeyboardManagerTest.java b/shell/platform/android/test/io/flutter/embedding/android/KeyboardManagerTest.java index b799c790c8a4e..8db1c231c5f5c 100644 --- a/shell/platform/android/test/io/flutter/embedding/android/KeyboardManagerTest.java +++ b/shell/platform/android/test/io/flutter/embedding/android/KeyboardManagerTest.java @@ -80,7 +80,11 @@ public void setUp() { when(mockRootView.dispatchKeyEvent(any(KeyEvent.class))) .thenAnswer( invocation -> mockView.dispatchKeyEvent((KeyEvent) invocation.getArguments()[0])); - keyboardManager = new KeyboardManager(mockView, mockTextInputPlugin, mockKeyEventChannel); + keyboardManager = + new KeyboardManager( + mockView, + mockTextInputPlugin, + new Responder[] {new KeyChannelResponder(flutterEngine.getKeyEventChannel())}); } // Tests start From fbf7eeb76dcdb4d54d3655dbd6563fccafda3eda Mon Sep 17 00:00:00 2001 From: LongCat is Looong <31859944+LongCatIsLooong@users.noreply.github.com> Date: Fri, 4 Jun 2021 00:41:35 -0700 Subject: [PATCH 11/12] remove setKeyboardManager --- .../io/flutter/embedding/android/FlutterView.java | 4 ++-- .../embedding/android/KeyboardManager.java | 2 +- .../plugin/editing/InputConnectionAdaptor.java | 6 +++--- .../flutter/plugin/editing/TextInputPlugin.java | 7 ++----- .../android/io/flutter/view/FlutterView.java | 4 ++-- .../plugin/editing/TextInputPluginTest.java | 15 +++++++++++---- 6 files changed, 21 insertions(+), 17 deletions(-) diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterView.java b/shell/platform/android/io/flutter/embedding/android/FlutterView.java index 63b185ae70dec..2af337914a0e3 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterView.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterView.java @@ -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); } /** @@ -979,7 +979,7 @@ public void attachToFlutterEngine(@NonNull FlutterEngine flutterEngine) { keyboardManager = new KeyboardManager( this, - mTextInputPlugin, + textInputPlugin, new Responder[] {new KeyChannelResponder(flutterEngine.getKeyEventChannel())}); androidTouchProcessor = new AndroidTouchProcessor(this.flutterEngine.getRenderer(), /*trackMotionEvents=*/ false); diff --git a/shell/platform/android/io/flutter/embedding/android/KeyboardManager.java b/shell/platform/android/io/flutter/embedding/android/KeyboardManager.java index 9ff8d9a7e110b..ccfcbc4ff5ca1 100644 --- a/shell/platform/android/io/flutter/embedding/android/KeyboardManager.java +++ b/shell/platform/android/io/flutter/embedding/android/KeyboardManager.java @@ -166,7 +166,7 @@ public boolean handleEvent(@NonNull KeyEvent keyEvent) { onUnhandled(keyEvent); } - return !isRedispatchedEvent; + return true; } public void destroy() { diff --git a/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java b/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java index 9da722392c432..c5a4a3745c29b 100644 --- a/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java +++ b/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java @@ -47,7 +47,7 @@ class InputConnectionAdaptor extends BaseInputConnection private InputMethodManager mImm; private final Layout mLayout; private FlutterTextUtils flutterTextUtils; - private final KeyboardManager mKeyboardManager; + private final KeyboardManager keyboardManager; @SuppressWarnings("deprecation") public InputConnectionAdaptor( @@ -65,7 +65,7 @@ public InputConnectionAdaptor( mEditable = editable; mEditable.addEditingStateListener(this); mEditorInfo = editorInfo; - mKeyboardManager = keyboardManager; + 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. @@ -290,7 +290,7 @@ 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) { - return mKeyboardManager.handleEvent(event); + return keyboardManager.handleEvent(event); } public boolean handleKeyEvent(KeyEvent event) { diff --git a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java index 8d22c74e736ea..f6fc6354152e8 100644 --- a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java +++ b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java @@ -49,7 +49,6 @@ public class TextInputPlugin implements ListenableEditingState.EditingStateWatch @NonNull private PlatformViewsController platformViewsController; @Nullable private Rect lastClientRect; private ImeSyncDeferringInsetsCallback imeSyncCallback; - private KeyboardManager keyboardManager; // Initialize the "last seen" text editing values to a non-null value. private TextEditState mLastKnownFrameworkTextEditingState; @@ -176,9 +175,6 @@ ImeSyncDeferringInsetsCallback getImeSyncCallback() { return imeSyncCallback; } - public void setKeyboardManager(KeyboardManager keyboardManager) { - this.keyboardManager = keyboardManager; - } /** * Use the current platform view input connection until unlockPlatformViewInputConnection is * called. @@ -281,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; diff --git a/shell/platform/android/io/flutter/view/FlutterView.java b/shell/platform/android/io/flutter/view/FlutterView.java index 238005a26cb1f..0d8f2139df970 100644 --- a/shell/platform/android/io/flutter/view/FlutterView.java +++ b/shell/platform/android/io/flutter/view/FlutterView.java @@ -43,6 +43,7 @@ import io.flutter.Log; import io.flutter.app.FlutterPluginRegistry; 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; @@ -231,7 +232,6 @@ public void onPostResume() { mKeyboardManager = new KeyboardManager( this, mTextInputPlugin, new Responder[] {new KeyChannelResponder(keyEventChannel)}); - mTextInputPlugin.setKeyboardManager(mKeyboardManager); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { mMouseCursorPlugin = new MouseCursorPlugin(this, new MouseCursorChannel(dartExecutor)); @@ -446,7 +446,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/plugin/editing/TextInputPluginTest.java b/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java index c70bb7186fb09..972d41236e95b 100644 --- a/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java +++ b/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java @@ -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( @@ -521,7 +524,9 @@ public void inputConnection_createsActionFromEnter() throws JSONException { assertEquals("flutter/textinput", channelCaptor.getValue()); verifyMethodCall(bufferCaptor.getValue(), "TextInputClient.requestExistingInputState", null); InputConnectionAdaptor connection = - (InputConnectionAdaptor) textInputPlugin.createInputConnection(testView, new EditorInfo()); + (InputConnectionAdaptor) + textInputPlugin.createInputConnection( + testView, mock(KeyboardManager.class), new EditorInfo()); connection.handleKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER)); verify(dartExecutor, times(2)) @@ -586,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); From e98ade9f11df610278d224fb71ffedb39bf11397 Mon Sep 17 00:00:00 2001 From: LongCat is Looong <31859944+LongCatIsLooong@users.noreply.github.com> Date: Fri, 4 Jun 2021 14:08:00 -0700 Subject: [PATCH 12/12] resolve symbols --- .../embedding/android/FlutterView.java | 4 +- .../android/io/flutter/view/FlutterView.java | 4 +- .../android/KeyboardManagerTest.java | 39 ++++++++----------- 3 files changed, 22 insertions(+), 25 deletions(-) diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterView.java b/shell/platform/android/io/flutter/embedding/android/FlutterView.java index 2af337914a0e3..5e56f9d188969 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterView.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterView.java @@ -980,7 +980,9 @@ public void attachToFlutterEngine(@NonNull FlutterEngine flutterEngine) { new KeyboardManager( this, textInputPlugin, - new Responder[] {new KeyChannelResponder(flutterEngine.getKeyEventChannel())}); + new KeyChannelResponder[] { + new KeyChannelResponder(flutterEngine.getKeyEventChannel()) + }); androidTouchProcessor = new AndroidTouchProcessor(this.flutterEngine.getRenderer(), /*trackMotionEvents=*/ false); accessibilityBridge = diff --git a/shell/platform/android/io/flutter/view/FlutterView.java b/shell/platform/android/io/flutter/view/FlutterView.java index 0d8f2139df970..3988f84c9c3b7 100644 --- a/shell/platform/android/io/flutter/view/FlutterView.java +++ b/shell/platform/android/io/flutter/view/FlutterView.java @@ -231,7 +231,9 @@ public void onPostResume() { new TextInputPlugin(this, new TextInputChannel(dartExecutor), platformViewsController); mKeyboardManager = new KeyboardManager( - this, mTextInputPlugin, new Responder[] {new KeyChannelResponder(keyEventChannel)}); + this, + mTextInputPlugin, + new KeyChannelResponder[] {new KeyChannelResponder(keyEventChannel)}); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { mMouseCursorPlugin = new MouseCursorPlugin(this, new MouseCursorChannel(dartExecutor)); diff --git a/shell/platform/android/test/io/flutter/embedding/android/KeyboardManagerTest.java b/shell/platform/android/test/io/flutter/embedding/android/KeyboardManagerTest.java index 8db1c231c5f5c..11f07569077b3 100644 --- a/shell/platform/android/test/io/flutter/embedding/android/KeyboardManagerTest.java +++ b/shell/platform/android/test/io/flutter/embedding/android/KeyboardManagerTest.java @@ -14,7 +14,7 @@ import android.view.KeyEvent; import android.view.View; import androidx.annotation.NonNull; -import io.flutter.embedding.android.KeyboardManager.PrimaryResponder; +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; @@ -33,7 +33,7 @@ @RunWith(RobolectricTestRunner.class) @TargetApi(28) public class KeyboardManagerTest { - static class FakeResponder implements PrimaryResponder { + static class FakeResponder implements Responder { KeyEvent mLastKeyEvent; OnKeyEventHandledCallback mLastKeyEventHandledCallback; @@ -84,7 +84,7 @@ public void setUp() { new KeyboardManager( mockView, mockTextInputPlugin, - new Responder[] {new KeyChannelResponder(flutterEngine.getKeyEventChannel())}); + new Responder[] {new KeyChannelResponder(mockKeyEventChannel)}); } // Tests start @@ -94,7 +94,7 @@ public void respondsTrueWhenHandlingNewEvents() { final FakeResponder fakeResponder = new FakeResponder(); keyboardManager = new KeyboardManager( - mockView, mockTextInputPlugin, new KeyboardManager.PrimaryResponder[] {fakeResponder}); + mockView, mockTextInputPlugin, new KeyboardManager.Responder[] {fakeResponder}); final KeyEvent keyEvent = new FakeKeyEvent(KeyEvent.ACTION_DOWN, 65); final boolean result = keyboardManager.handleEvent(keyEvent); @@ -111,7 +111,7 @@ public void primaryRespondersHaveTheHighestPrecedence() { final FakeResponder fakeResponder = new FakeResponder(); keyboardManager = new KeyboardManager( - mockView, mockTextInputPlugin, new KeyboardManager.PrimaryResponder[] {fakeResponder}); + mockView, mockTextInputPlugin, new KeyboardManager.Responder[] {fakeResponder}); final KeyEvent keyEvent = new FakeKeyEvent(KeyEvent.ACTION_DOWN, 65); final boolean result = keyboardManager.handleEvent(keyEvent); @@ -131,10 +131,9 @@ public void primaryRespondersHaveTheHighestPrecedence() { } @Test - public void zeroPrimaryRespondersTest() { + public void zeroRespondersTest() { keyboardManager = - new KeyboardManager( - mockView, mockTextInputPlugin, new KeyboardManager.PrimaryResponder[] {}); + 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); @@ -144,14 +143,14 @@ public void zeroPrimaryRespondersTest() { } @Test - public void multiplePrimaryRespondersTest() { + public void multipleRespondersTest() { final FakeResponder fakeResponder1 = new FakeResponder(); final FakeResponder fakeResponder2 = new FakeResponder(); keyboardManager = new KeyboardManager( mockView, mockTextInputPlugin, - new KeyboardManager.PrimaryResponder[] {fakeResponder1, fakeResponder2}); + new KeyboardManager.Responder[] {fakeResponder1, fakeResponder2}); final KeyEvent keyEvent = new FakeKeyEvent(KeyEvent.ACTION_DOWN, 65); final boolean result = keyboardManager.handleEvent(keyEvent); @@ -169,14 +168,14 @@ public void multiplePrimaryRespondersTest() { } @Test - public void multiplePrimaryRespondersTest2() { + public void multipleRespondersTest2() { final FakeResponder fakeResponder1 = new FakeResponder(); final FakeResponder fakeResponder2 = new FakeResponder(); keyboardManager = new KeyboardManager( mockView, mockTextInputPlugin, - new KeyboardManager.PrimaryResponder[] {fakeResponder1, fakeResponder2}); + new KeyboardManager.Responder[] {fakeResponder1, fakeResponder2}); final KeyEvent keyEvent = new FakeKeyEvent(KeyEvent.ACTION_DOWN, 65); final boolean result = keyboardManager.handleEvent(keyEvent); @@ -188,14 +187,14 @@ public void multiplePrimaryRespondersTest2() { } @Test - public void multiplePrimaryRespondersTest3() { + public void multipleRespondersTest3() { final FakeResponder fakeResponder1 = new FakeResponder(); final FakeResponder fakeResponder2 = new FakeResponder(); keyboardManager = new KeyboardManager( mockView, mockTextInputPlugin, - new KeyboardManager.PrimaryResponder[] {fakeResponder1, fakeResponder2}); + new KeyboardManager.Responder[] {fakeResponder1, fakeResponder2}); final KeyEvent keyEvent = new FakeKeyEvent(KeyEvent.ACTION_DOWN, 65); final boolean result = keyboardManager.handleEvent(keyEvent); @@ -217,9 +216,7 @@ public void textInputPluginHasTheSecondHighestPrecedence() { keyboardManager = spy( new KeyboardManager( - mockView, - mockTextInputPlugin, - new KeyboardManager.PrimaryResponder[] {fakeResponder})); + mockView, mockTextInputPlugin, new KeyboardManager.Responder[] {fakeResponder})); final KeyEvent keyEvent = new FakeKeyEvent(KeyEvent.ACTION_DOWN, 65); final boolean result = keyboardManager.handleEvent(keyEvent); @@ -251,9 +248,7 @@ public void RedispatchKeyEventIfTextInputPluginFailsToHandle() { keyboardManager = spy( new KeyboardManager( - mockView, - mockTextInputPlugin, - new KeyboardManager.PrimaryResponder[] {fakeResponder})); + mockView, mockTextInputPlugin, new KeyboardManager.Responder[] {fakeResponder})); final KeyEvent keyEvent = new FakeKeyEvent(KeyEvent.ACTION_DOWN, 65); final boolean result = keyboardManager.handleEvent(keyEvent); @@ -280,9 +275,7 @@ public void respondsFalseWhenHandlingRedispatchedEvents() { keyboardManager = spy( new KeyboardManager( - mockView, - mockTextInputPlugin, - new KeyboardManager.PrimaryResponder[] {fakeResponder})); + mockView, mockTextInputPlugin, new KeyboardManager.Responder[] {fakeResponder})); final KeyEvent keyEvent = new FakeKeyEvent(KeyEvent.ACTION_DOWN, 65); final boolean result = keyboardManager.handleEvent(keyEvent);