diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index 5000803f207e8..ba1a4919d5a08 100644 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -2382,6 +2382,7 @@ ORIGIN: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/rend ORIGIN: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/renderer/SurfaceTextureWrapper.java + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/DeferredComponentChannel.java + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/KeyEventChannel.java + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/KeyboardChannel.java + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/LifecycleChannel.java + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/LocalizationChannel.java + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/MouseCursorChannel.java + ../../../flutter/LICENSE @@ -5063,6 +5064,7 @@ FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/render FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/AccessibilityChannel.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/DeferredComponentChannel.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/KeyEventChannel.java +FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/KeyboardChannel.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/LifecycleChannel.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/LocalizationChannel.java FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/MouseCursorChannel.java diff --git a/shell/platform/android/BUILD.gn b/shell/platform/android/BUILD.gn index da4811bdcfb0e..db8a0c5099f3c 100644 --- a/shell/platform/android/BUILD.gn +++ b/shell/platform/android/BUILD.gn @@ -252,6 +252,7 @@ android_java_sources = [ "io/flutter/embedding/engine/systemchannels/AccessibilityChannel.java", "io/flutter/embedding/engine/systemchannels/DeferredComponentChannel.java", "io/flutter/embedding/engine/systemchannels/KeyEventChannel.java", + "io/flutter/embedding/engine/systemchannels/KeyboardChannel.java", "io/flutter/embedding/engine/systemchannels/LifecycleChannel.java", "io/flutter/embedding/engine/systemchannels/LocalizationChannel.java", "io/flutter/embedding/engine/systemchannels/MouseCursorChannel.java", diff --git a/shell/platform/android/io/flutter/embedding/android/KeyEmbedderResponder.java b/shell/platform/android/io/flutter/embedding/android/KeyEmbedderResponder.java index 2a5d5b1a87400..aa1fdec32d514 100644 --- a/shell/platform/android/io/flutter/embedding/android/KeyEmbedderResponder.java +++ b/shell/platform/android/io/flutter/embedding/android/KeyEmbedderResponder.java @@ -11,7 +11,9 @@ import io.flutter.embedding.android.KeyboardMap.TogglingGoal; import io.flutter.plugin.common.BinaryMessenger; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; +import java.util.Map; /** * A {@link KeyboardManager.Responder} of {@link KeyboardManager} that handles events by sending @@ -405,4 +407,14 @@ public void handleEvent( onKeyEventHandledCallback.onKeyEventHandled(true); } } + + /** + * Returns an unmodifiable view of the pressed state. + * + * @return A map whose keys are physical keyboard key IDs and values are the corresponding logical + * keyboard key IDs. + */ + public Map getPressedState() { + return Collections.unmodifiableMap(pressingRecords); + } } diff --git a/shell/platform/android/io/flutter/embedding/android/KeyboardManager.java b/shell/platform/android/io/flutter/embedding/android/KeyboardManager.java index 76e344a4e5d26..ffafb7692e52d 100644 --- a/shell/platform/android/io/flutter/embedding/android/KeyboardManager.java +++ b/shell/platform/android/io/flutter/embedding/android/KeyboardManager.java @@ -9,10 +9,12 @@ import androidx.annotation.NonNull; import io.flutter.Log; import io.flutter.embedding.engine.systemchannels.KeyEventChannel; +import io.flutter.embedding.engine.systemchannels.KeyboardChannel; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.editing.InputConnectionAdaptor; import io.flutter.plugin.editing.TextInputPlugin; import java.util.HashSet; +import java.util.Map; /** * Processes keyboard events and cooperate with {@link TextInputPlugin}. @@ -40,7 +42,8 @@ * encounter. * */ -public class KeyboardManager implements InputConnectionAdaptor.KeyboardDelegate { +public class KeyboardManager + implements InputConnectionAdaptor.KeyboardDelegate, KeyboardChannel.KeyboardMethodHandler { private static final String TAG = "KeyboardManager"; /** @@ -119,6 +122,8 @@ public KeyboardManager(@NonNull ViewDelegate viewDelegate) { new KeyEmbedderResponder(viewDelegate.getBinaryMessenger()), new KeyChannelResponder(new KeyEventChannel(viewDelegate.getBinaryMessenger())), }; + final KeyboardChannel keyboardChannel = new KeyboardChannel(viewDelegate.getBinaryMessenger()); + keyboardChannel.setKeyboardMethodHandler(this); } /** @@ -252,4 +257,15 @@ private void onUnhandled(@NonNull KeyEvent keyEvent) { Log.w(TAG, "A redispatched key event was consumed before reaching KeyboardManager"); } } + + /** + * Returns an unmodifiable view of the pressed state. + * + * @return A map whose keys are physical keyboard key IDs and values are the corresponding logical + * keyboard key IDs. + */ + public Map getKeyboardState() { + KeyEmbedderResponder embedderResponder = (KeyEmbedderResponder) responders[0]; + return embedderResponder.getPressedState(); + } } diff --git a/shell/platform/android/io/flutter/embedding/engine/systemchannels/KeyboardChannel.java b/shell/platform/android/io/flutter/embedding/engine/systemchannels/KeyboardChannel.java new file mode 100644 index 0000000000000..a60e30885c246 --- /dev/null +++ b/shell/platform/android/io/flutter/embedding/engine/systemchannels/KeyboardChannel.java @@ -0,0 +1,75 @@ +// 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.engine.systemchannels; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; +import io.flutter.plugin.common.StandardMethodCodec; +import java.util.HashMap; +import java.util.Map; + +/** + * Event message channel for keyboard events to/from the Flutter framework. + * + *

Receives asynchronous messages from the framework to query the engine known pressed state. + */ +public class KeyboardChannel { + public final MethodChannel channel; + private KeyboardMethodHandler keyboardMethodHandler; + + @NonNull + public final MethodChannel.MethodCallHandler parsingMethodHandler = + new MethodChannel.MethodCallHandler() { + Map pressedState = new HashMap<>(); + + @Override + public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { + if (keyboardMethodHandler == null) { + // Returns an empty pressed state when the engine did not get a chance to register + // a method handler for this channel. + result.success(pressedState); + } else { + switch (call.method) { + case "getKeyboardState": + try { + pressedState = keyboardMethodHandler.getKeyboardState(); + } catch (IllegalStateException exception) { + result.error("error", exception.getMessage(), null); + } + result.success(pressedState); + break; + default: + result.notImplemented(); + break; + } + } + } + }; + + public KeyboardChannel(@NonNull BinaryMessenger messenger) { + channel = new MethodChannel(messenger, "flutter/keyboard", StandardMethodCodec.INSTANCE); + channel.setMethodCallHandler(parsingMethodHandler); + } + + /** + * Sets the {@link KeyboardMethodHandler} which receives all requests to query the keyboard state. + */ + public void setKeyboardMethodHandler(@Nullable KeyboardMethodHandler keyboardMethodHandler) { + this.keyboardMethodHandler = keyboardMethodHandler; + } + + public interface KeyboardMethodHandler { + /** + * Returns the keyboard pressed states. + * + * @return A map whose keys are physical keyboard key IDs and values are the corresponding + * logical keyboard key IDs. + */ + Map getKeyboardState(); + } +} 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 799ae861b2352..850196e6f2077 100644 --- a/shell/platform/android/test/io/flutter/embedding/android/KeyboardManagerTest.java +++ b/shell/platform/android/test/io/flutter/embedding/android/KeyboardManagerTest.java @@ -25,6 +25,7 @@ import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.stream.Collectors; @@ -1564,4 +1565,22 @@ public void synchronizeCapsLock() { calls.get(0).keyData, Type.kUp, PHYSICAL_CAPS_LOCK, LOGICAL_CAPS_LOCK, null, false); calls.clear(); } + + @Test + public void getKeyboardState() { + final KeyboardTester tester = new KeyboardTester(); + + tester.respondToTextInputWith(true); // Suppress redispatching. + + // Initial pressed state is empty. + assertEquals(tester.keyboardManager.getKeyboardState(), Map.of()); + + tester.keyboardManager.handleEvent( + new FakeKeyEvent(ACTION_DOWN, SCAN_KEY_A, KEYCODE_A, 1, 'a', 0)); + assertEquals(tester.keyboardManager.getKeyboardState(), Map.of(PHYSICAL_KEY_A, LOGICAL_KEY_A)); + + tester.keyboardManager.handleEvent( + new FakeKeyEvent(ACTION_UP, SCAN_KEY_A, KEYCODE_A, 0, 'a', 0)); + assertEquals(tester.keyboardManager.getKeyboardState(), Map.of()); + } } diff --git a/shell/platform/android/test/io/flutter/embedding/engine/systemchannels/KeyboardChannelTest.java b/shell/platform/android/test/io/flutter/embedding/engine/systemchannels/KeyboardChannelTest.java new file mode 100644 index 0000000000000..96766a31b11c8 --- /dev/null +++ b/shell/platform/android/test/io/flutter/embedding/engine/systemchannels/KeyboardChannelTest.java @@ -0,0 +1,67 @@ +package io.flutter.embedding.android; + +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 androidx.test.ext.junit.runners.AndroidJUnit4; +import io.flutter.embedding.engine.dart.DartExecutor; +import io.flutter.embedding.engine.systemchannels.KeyboardChannel; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; +import io.flutter.plugin.common.StandardMethodCodec; +import java.nio.ByteBuffer; +import java.util.HashMap; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.robolectric.annotation.Config; + +@Config(manifest = Config.NONE) +@RunWith(AndroidJUnit4.class) +public class KeyboardChannelTest { + + private static BinaryMessenger.BinaryReply sendToBinaryMessageHandler( + BinaryMessenger.BinaryMessageHandler binaryMessageHandler, String method, Object args) { + MethodCall methodCall = new MethodCall(method, args); + ByteBuffer encodedMethodCall = StandardMethodCodec.INSTANCE.encodeMethodCall(methodCall); + BinaryMessenger.BinaryReply reply = mock(BinaryMessenger.BinaryReply.class); + binaryMessageHandler.onMessage((ByteBuffer) encodedMethodCall.flip(), reply); + return reply; + } + + @Test + public void respondsToGetKeyboardStateChannelMessage() { + ArgumentCaptor binaryMessageHandlerCaptor = + ArgumentCaptor.forClass(BinaryMessenger.BinaryMessageHandler.class); + DartExecutor mockBinaryMessenger = mock(DartExecutor.class); + KeyboardChannel.KeyboardMethodHandler mockHandler = + mock(KeyboardChannel.KeyboardMethodHandler.class); + KeyboardChannel keyboardChannel = new KeyboardChannel(mockBinaryMessenger); + + verify(mockBinaryMessenger, times(1)) + .setMessageHandler(any(String.class), binaryMessageHandlerCaptor.capture()); + + BinaryMessenger.BinaryMessageHandler binaryMessageHandler = + binaryMessageHandlerCaptor.getValue(); + + keyboardChannel.setKeyboardMethodHandler(mockHandler); + sendToBinaryMessageHandler(binaryMessageHandler, "getKeyboardState", null); + + verify(mockHandler, times(1)).getKeyboardState(); + } + + @Test + public void repliesWhenNoKeyboardChannelHandler() { + // Regression test for https://github.com/flutter/flutter/issues/122441#issuecomment-1582052616. + + KeyboardChannel keyboardChannel = new KeyboardChannel(mock(DartExecutor.class)); + MethodCall methodCall = new MethodCall("getKeyboardState", null); + MethodChannel.Result result = mock(MethodChannel.Result.class); + keyboardChannel.parsingMethodHandler.onMethodCall(methodCall, result); + + verify(result).success(new HashMap()); + } +} diff --git a/shell/platform/android/test/io/flutter/plugin/platform/PlatformViewsControllerTest.java b/shell/platform/android/test/io/flutter/plugin/platform/PlatformViewsControllerTest.java index 77cdbe1299585..623b45ad4f974 100644 --- a/shell/platform/android/test/io/flutter/plugin/platform/PlatformViewsControllerTest.java +++ b/shell/platform/android/test/io/flutter/plugin/platform/PlatformViewsControllerTest.java @@ -1501,6 +1501,7 @@ public void release() {} when(engine.getPlatformViewsController()).thenReturn(platformViewsController); when(engine.getLocalizationPlugin()).thenReturn(mock(LocalizationPlugin.class)); when(engine.getAccessibilityChannel()).thenReturn(mock(AccessibilityChannel.class)); + when(engine.getDartExecutor()).thenReturn(executor); flutterView.attachToFlutterEngine(engine); platformViewsController.attachToView(flutterView);