Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions ci/licenses_golden/licenses_flutter
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions shell/platform/android/BUILD.gn
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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);
}
}
Original file line number Diff line number Diff line change
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();
}
}
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
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());
}
}
Original file line number Diff line number Diff line change
@@ -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());
}
}
Original file line number Diff line number Diff line change
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