Skip to content

Commit

Permalink
[Android] Return the keyboard pressed state (#42758)
Browse files Browse the repository at this point in the history
## Description

This PR updates the Android engine in order to answer to keyboard pressed state queries from the framework (as implemented in flutter/flutter#122885).

This is a rework of #41695 which was reverted in #42346.

This issue with #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 flutter/flutter#122441 (comment) 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 flutter/flutter#87391
(see #42346 for Linux implementation)
Fixes flutter/flutter#122441

## Tests

Adds 3 tests.
  • Loading branch information
bleroux committed Jun 21, 2023
1 parent 946f523 commit 62e6d19
Show file tree
Hide file tree
Showing 8 changed files with 194 additions and 1 deletion.
2 changes: 2 additions & 0 deletions ci/licenses_golden/licenses_flutter
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions shell/platform/android/BUILD.gn
Expand Up @@ -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",
Expand Down
Expand Up @@ -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
Expand Down Expand Up @@ -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<Long, Long> getPressedState() {
return Collections.unmodifiableMap(pressingRecords);
}
}
Expand Up @@ -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}.
Expand Down Expand Up @@ -40,7 +42,8 @@
* encounter.
* </ul>
*/
public class KeyboardManager implements InputConnectionAdaptor.KeyboardDelegate {
public class KeyboardManager
implements InputConnectionAdaptor.KeyboardDelegate, KeyboardChannel.KeyboardMethodHandler {
private static final String TAG = "KeyboardManager";

/**
Expand Down Expand Up @@ -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);
}

/**
Expand Down Expand Up @@ -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<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();
}
}
Expand Up @@ -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;
Expand Down Expand Up @@ -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());
}
}
@@ -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());
}
}
Expand Up @@ -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);
Expand Down

0 comments on commit 62e6d19

Please sign in to comment.