Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit 62e6d19

Browse files
authored
[Android] Return the keyboard pressed state (#42758)
## 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.
1 parent 946f523 commit 62e6d19

8 files changed

Lines changed: 194 additions & 1 deletion

File tree

ci/licenses_golden/licenses_flutter

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2384,6 +2384,7 @@ ORIGIN: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/rend
23842384
ORIGIN: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/renderer/SurfaceTextureWrapper.java + ../../../flutter/LICENSE
23852385
ORIGIN: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/DeferredComponentChannel.java + ../../../flutter/LICENSE
23862386
ORIGIN: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/KeyEventChannel.java + ../../../flutter/LICENSE
2387+
ORIGIN: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/KeyboardChannel.java + ../../../flutter/LICENSE
23872388
ORIGIN: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/LifecycleChannel.java + ../../../flutter/LICENSE
23882389
ORIGIN: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/LocalizationChannel.java + ../../../flutter/LICENSE
23892390
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
50675068
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/AccessibilityChannel.java
50685069
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/DeferredComponentChannel.java
50695070
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/KeyEventChannel.java
5071+
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/KeyboardChannel.java
50705072
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/LifecycleChannel.java
50715073
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/LocalizationChannel.java
50725074
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/MouseCursorChannel.java

shell/platform/android/BUILD.gn

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,7 @@ android_java_sources = [
252252
"io/flutter/embedding/engine/systemchannels/AccessibilityChannel.java",
253253
"io/flutter/embedding/engine/systemchannels/DeferredComponentChannel.java",
254254
"io/flutter/embedding/engine/systemchannels/KeyEventChannel.java",
255+
"io/flutter/embedding/engine/systemchannels/KeyboardChannel.java",
255256
"io/flutter/embedding/engine/systemchannels/LifecycleChannel.java",
256257
"io/flutter/embedding/engine/systemchannels/LocalizationChannel.java",
257258
"io/flutter/embedding/engine/systemchannels/MouseCursorChannel.java",

shell/platform/android/io/flutter/embedding/android/KeyEmbedderResponder.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@
1111
import io.flutter.embedding.android.KeyboardMap.TogglingGoal;
1212
import io.flutter.plugin.common.BinaryMessenger;
1313
import java.util.ArrayList;
14+
import java.util.Collections;
1415
import java.util.HashMap;
16+
import java.util.Map;
1517

1618
/**
1719
* A {@link KeyboardManager.Responder} of {@link KeyboardManager} that handles events by sending
@@ -405,4 +407,14 @@ public void handleEvent(
405407
onKeyEventHandledCallback.onKeyEventHandled(true);
406408
}
407409
}
410+
411+
/**
412+
* Returns an unmodifiable view of the pressed state.
413+
*
414+
* @return A map whose keys are physical keyboard key IDs and values are the corresponding logical
415+
* keyboard key IDs.
416+
*/
417+
public Map<Long, Long> getPressedState() {
418+
return Collections.unmodifiableMap(pressingRecords);
419+
}
408420
}

shell/platform/android/io/flutter/embedding/android/KeyboardManager.java

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,12 @@
99
import androidx.annotation.NonNull;
1010
import io.flutter.Log;
1111
import io.flutter.embedding.engine.systemchannels.KeyEventChannel;
12+
import io.flutter.embedding.engine.systemchannels.KeyboardChannel;
1213
import io.flutter.plugin.common.BinaryMessenger;
1314
import io.flutter.plugin.editing.InputConnectionAdaptor;
1415
import io.flutter.plugin.editing.TextInputPlugin;
1516
import java.util.HashSet;
17+
import java.util.Map;
1618

1719
/**
1820
* Processes keyboard events and cooperate with {@link TextInputPlugin}.
@@ -40,7 +42,8 @@
4042
* encounter.
4143
* </ul>
4244
*/
43-
public class KeyboardManager implements InputConnectionAdaptor.KeyboardDelegate {
45+
public class KeyboardManager
46+
implements InputConnectionAdaptor.KeyboardDelegate, KeyboardChannel.KeyboardMethodHandler {
4447
private static final String TAG = "KeyboardManager";
4548

4649
/**
@@ -119,6 +122,8 @@ public KeyboardManager(@NonNull ViewDelegate viewDelegate) {
119122
new KeyEmbedderResponder(viewDelegate.getBinaryMessenger()),
120123
new KeyChannelResponder(new KeyEventChannel(viewDelegate.getBinaryMessenger())),
121124
};
125+
final KeyboardChannel keyboardChannel = new KeyboardChannel(viewDelegate.getBinaryMessenger());
126+
keyboardChannel.setKeyboardMethodHandler(this);
122127
}
123128

124129
/**
@@ -252,4 +257,15 @@ private void onUnhandled(@NonNull KeyEvent keyEvent) {
252257
Log.w(TAG, "A redispatched key event was consumed before reaching KeyboardManager");
253258
}
254259
}
260+
261+
/**
262+
* Returns an unmodifiable view of the pressed state.
263+
*
264+
* @return A map whose keys are physical keyboard key IDs and values are the corresponding logical
265+
* keyboard key IDs.
266+
*/
267+
public Map<Long, Long> getKeyboardState() {
268+
KeyEmbedderResponder embedderResponder = (KeyEmbedderResponder) responders[0];
269+
return embedderResponder.getPressedState();
270+
}
255271
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
package io.flutter.embedding.engine.systemchannels;
6+
7+
import androidx.annotation.NonNull;
8+
import androidx.annotation.Nullable;
9+
import io.flutter.plugin.common.BinaryMessenger;
10+
import io.flutter.plugin.common.MethodCall;
11+
import io.flutter.plugin.common.MethodChannel;
12+
import io.flutter.plugin.common.StandardMethodCodec;
13+
import java.util.HashMap;
14+
import java.util.Map;
15+
16+
/**
17+
* Event message channel for keyboard events to/from the Flutter framework.
18+
*
19+
* <p>Receives asynchronous messages from the framework to query the engine known pressed state.
20+
*/
21+
public class KeyboardChannel {
22+
public final MethodChannel channel;
23+
private KeyboardMethodHandler keyboardMethodHandler;
24+
25+
@NonNull
26+
public final MethodChannel.MethodCallHandler parsingMethodHandler =
27+
new MethodChannel.MethodCallHandler() {
28+
Map<Long, Long> pressedState = new HashMap<>();
29+
30+
@Override
31+
public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
32+
if (keyboardMethodHandler == null) {
33+
// Returns an empty pressed state when the engine did not get a chance to register
34+
// a method handler for this channel.
35+
result.success(pressedState);
36+
} else {
37+
switch (call.method) {
38+
case "getKeyboardState":
39+
try {
40+
pressedState = keyboardMethodHandler.getKeyboardState();
41+
} catch (IllegalStateException exception) {
42+
result.error("error", exception.getMessage(), null);
43+
}
44+
result.success(pressedState);
45+
break;
46+
default:
47+
result.notImplemented();
48+
break;
49+
}
50+
}
51+
}
52+
};
53+
54+
public KeyboardChannel(@NonNull BinaryMessenger messenger) {
55+
channel = new MethodChannel(messenger, "flutter/keyboard", StandardMethodCodec.INSTANCE);
56+
channel.setMethodCallHandler(parsingMethodHandler);
57+
}
58+
59+
/**
60+
* Sets the {@link KeyboardMethodHandler} which receives all requests to query the keyboard state.
61+
*/
62+
public void setKeyboardMethodHandler(@Nullable KeyboardMethodHandler keyboardMethodHandler) {
63+
this.keyboardMethodHandler = keyboardMethodHandler;
64+
}
65+
66+
public interface KeyboardMethodHandler {
67+
/**
68+
* Returns the keyboard pressed states.
69+
*
70+
* @return A map whose keys are physical keyboard key IDs and values are the corresponding
71+
* logical keyboard key IDs.
72+
*/
73+
Map<Long, Long> getKeyboardState();
74+
}
75+
}

shell/platform/android/test/io/flutter/embedding/android/KeyboardManagerTest.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import java.nio.ByteBuffer;
2626
import java.util.ArrayList;
2727
import java.util.List;
28+
import java.util.Map;
2829
import java.util.function.BiConsumer;
2930
import java.util.function.Consumer;
3031
import java.util.stream.Collectors;
@@ -1564,4 +1565,22 @@ public void synchronizeCapsLock() {
15641565
calls.get(0).keyData, Type.kUp, PHYSICAL_CAPS_LOCK, LOGICAL_CAPS_LOCK, null, false);
15651566
calls.clear();
15661567
}
1568+
1569+
@Test
1570+
public void getKeyboardState() {
1571+
final KeyboardTester tester = new KeyboardTester();
1572+
1573+
tester.respondToTextInputWith(true); // Suppress redispatching.
1574+
1575+
// Initial pressed state is empty.
1576+
assertEquals(tester.keyboardManager.getKeyboardState(), Map.of());
1577+
1578+
tester.keyboardManager.handleEvent(
1579+
new FakeKeyEvent(ACTION_DOWN, SCAN_KEY_A, KEYCODE_A, 1, 'a', 0));
1580+
assertEquals(tester.keyboardManager.getKeyboardState(), Map.of(PHYSICAL_KEY_A, LOGICAL_KEY_A));
1581+
1582+
tester.keyboardManager.handleEvent(
1583+
new FakeKeyEvent(ACTION_UP, SCAN_KEY_A, KEYCODE_A, 0, 'a', 0));
1584+
assertEquals(tester.keyboardManager.getKeyboardState(), Map.of());
1585+
}
15671586
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package io.flutter.embedding.android;
2+
3+
import static org.mockito.Mockito.any;
4+
import static org.mockito.Mockito.mock;
5+
import static org.mockito.Mockito.times;
6+
import static org.mockito.Mockito.verify;
7+
8+
import androidx.test.ext.junit.runners.AndroidJUnit4;
9+
import io.flutter.embedding.engine.dart.DartExecutor;
10+
import io.flutter.embedding.engine.systemchannels.KeyboardChannel;
11+
import io.flutter.plugin.common.BinaryMessenger;
12+
import io.flutter.plugin.common.MethodCall;
13+
import io.flutter.plugin.common.MethodChannel;
14+
import io.flutter.plugin.common.StandardMethodCodec;
15+
import java.nio.ByteBuffer;
16+
import java.util.HashMap;
17+
import org.junit.Test;
18+
import org.junit.runner.RunWith;
19+
import org.mockito.ArgumentCaptor;
20+
import org.robolectric.annotation.Config;
21+
22+
@Config(manifest = Config.NONE)
23+
@RunWith(AndroidJUnit4.class)
24+
public class KeyboardChannelTest {
25+
26+
private static BinaryMessenger.BinaryReply sendToBinaryMessageHandler(
27+
BinaryMessenger.BinaryMessageHandler binaryMessageHandler, String method, Object args) {
28+
MethodCall methodCall = new MethodCall(method, args);
29+
ByteBuffer encodedMethodCall = StandardMethodCodec.INSTANCE.encodeMethodCall(methodCall);
30+
BinaryMessenger.BinaryReply reply = mock(BinaryMessenger.BinaryReply.class);
31+
binaryMessageHandler.onMessage((ByteBuffer) encodedMethodCall.flip(), reply);
32+
return reply;
33+
}
34+
35+
@Test
36+
public void respondsToGetKeyboardStateChannelMessage() {
37+
ArgumentCaptor<BinaryMessenger.BinaryMessageHandler> binaryMessageHandlerCaptor =
38+
ArgumentCaptor.forClass(BinaryMessenger.BinaryMessageHandler.class);
39+
DartExecutor mockBinaryMessenger = mock(DartExecutor.class);
40+
KeyboardChannel.KeyboardMethodHandler mockHandler =
41+
mock(KeyboardChannel.KeyboardMethodHandler.class);
42+
KeyboardChannel keyboardChannel = new KeyboardChannel(mockBinaryMessenger);
43+
44+
verify(mockBinaryMessenger, times(1))
45+
.setMessageHandler(any(String.class), binaryMessageHandlerCaptor.capture());
46+
47+
BinaryMessenger.BinaryMessageHandler binaryMessageHandler =
48+
binaryMessageHandlerCaptor.getValue();
49+
50+
keyboardChannel.setKeyboardMethodHandler(mockHandler);
51+
sendToBinaryMessageHandler(binaryMessageHandler, "getKeyboardState", null);
52+
53+
verify(mockHandler, times(1)).getKeyboardState();
54+
}
55+
56+
@Test
57+
public void repliesWhenNoKeyboardChannelHandler() {
58+
// Regression test for https://github.com/flutter/flutter/issues/122441#issuecomment-1582052616.
59+
60+
KeyboardChannel keyboardChannel = new KeyboardChannel(mock(DartExecutor.class));
61+
MethodCall methodCall = new MethodCall("getKeyboardState", null);
62+
MethodChannel.Result result = mock(MethodChannel.Result.class);
63+
keyboardChannel.parsingMethodHandler.onMethodCall(methodCall, result);
64+
65+
verify(result).success(new HashMap());
66+
}
67+
}

shell/platform/android/test/io/flutter/plugin/platform/PlatformViewsControllerTest.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1501,6 +1501,7 @@ public void release() {}
15011501
when(engine.getPlatformViewsController()).thenReturn(platformViewsController);
15021502
when(engine.getLocalizationPlugin()).thenReturn(mock(LocalizationPlugin.class));
15031503
when(engine.getAccessibilityChannel()).thenReturn(mock(AccessibilityChannel.class));
1504+
when(engine.getDartExecutor()).thenReturn(executor);
15041505

15051506
flutterView.attachToFlutterEngine(engine);
15061507
platformViewsController.attachToView(flutterView);

0 commit comments

Comments
 (0)