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:
+ *
+ *
+ * {@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}
+ * callback. More than one {@link PrimaryResponder} is allowed to reply true and handle the
+ * same {@link KeyEvent}.
+ * {@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.
+ * "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}.
+ *
+ */
+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);