forked from firka/flutter
[Android] Return the keyboard pressed state (flutter/engine#42758)
## Description This PR updates the Android engine in order to answer to keyboard pressed state queries from the framework (as implemented in https://github.com/flutter/flutter/pull/122885). This is a rework of https://github.com/flutter/engine/pull/41695 which was reverted in https://github.com/flutter/engine/pull/42346. This issue with https://github.com/flutter/engine/pull/41695 was that the framework side did not get an answer when the channel was setup in the engine without registering a handler (on the engine side) to handle framework requests. The issue was reproducible when the engine initialization was managed by the app (see https://github.com/flutter/flutter/issues/122441#issuecomment-1582052616 for a repro). This PR fixes this issue by changing `flutter/keyboard` lifecycle: the engine now creates the channel and registers a handler just after the channel creation. In order to avoid regression, this PR also updates the channel implemenation (see `KeyboardChannel`) to return an empty `HashMap` when there is no handler registered. ## Related Issue Android engine implementation for https://github.com/flutter/flutter/issues/87391 (see https://github.com/flutter/engine/pull/42346 for Linux implementation) Fixes https://github.com/flutter/flutter/issues/122441 ## Tests Adds 3 tests.
This commit is contained in:
@@ -2384,6 +2384,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
|
||||
@@ -5067,6 +5068,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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -11,7 +11,9 @@ import io.flutter.embedding.android.KeyboardMap.PressingGoal;
|
||||
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 class KeyEmbedderResponder implements KeyboardManager.Responder {
|
||||
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<Long, Long> getPressedState() {
|
||||
return Collections.unmodifiableMap(pressingRecords);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,10 +9,12 @@ import android.view.KeyEvent;
|
||||
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 @@ import java.util.HashSet;
|
||||
* encounter.
|
||||
* </ul>
|
||||
*/
|
||||
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 class KeyboardManager implements InputConnectionAdaptor.KeyboardDelegate
|
||||
new KeyEmbedderResponder(viewDelegate.getBinaryMessenger()),
|
||||
new KeyChannelResponder(new KeyEventChannel(viewDelegate.getBinaryMessenger())),
|
||||
};
|
||||
final KeyboardChannel keyboardChannel = new KeyboardChannel(viewDelegate.getBinaryMessenger());
|
||||
keyboardChannel.setKeyboardMethodHandler(this);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -252,4 +257,15 @@ public class KeyboardManager implements InputConnectionAdaptor.KeyboardDelegate
|
||||
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<Long, Long> getKeyboardState() {
|
||||
KeyEmbedderResponder embedderResponder = (KeyEmbedderResponder) responders[0];
|
||||
return embedderResponder.getPressedState();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
* <p>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<Long, Long> 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<Long, Long> getKeyboardState();
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,7 @@ import io.flutter.util.FakeKeyEvent;
|
||||
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 class KeyboardManagerTest {
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<BinaryMessenger.BinaryMessageHandler> 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());
|
||||
}
|
||||
}
|
||||
@@ -1501,6 +1501,7 @@ public class PlatformViewsControllerTest {
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user