未验证 提交 88fd13fc 编写于 作者: G Greg Spencer 提交者: GitHub

Reland: Implement delayed event synthesis key event handling for Android (#20736)

This re-lands the key event synthesis implementation for Android (Original PR: #19024, Revert PR: #19956). The only difference is sending the synthesized key events to the root view instead of the current view.

Without sending it to the root view, the system doesn't have any chance of handling keys like the back button. The event will still not be sent to the framework twice, since we turn off event propagation while re-dispatching the event.
上级 7a890f27
......@@ -417,6 +417,7 @@ action("robolectric_tests") {
sources = [
"test/io/flutter/FlutterTestSuite.java",
"test/io/flutter/SmokeTest.java",
"test/io/flutter/embedding/android/AndroidKeyProcessorTest.java",
"test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java",
"test/io/flutter/embedding/android/FlutterActivityTest.java",
"test/io/flutter/embedding/android/FlutterAndroidComponentTest.java",
......@@ -435,6 +436,7 @@ action("robolectric_tests") {
"test/io/flutter/embedding/engine/loader/ApplicationInfoLoaderTest.java",
"test/io/flutter/embedding/engine/plugins/shim/ShimPluginRegistryTest.java",
"test/io/flutter/embedding/engine/renderer/FlutterRendererTest.java",
"test/io/flutter/embedding/engine/systemchannels/KeyEventChannelTest.java",
"test/io/flutter/embedding/engine/systemchannels/PlatformChannelTest.java",
"test/io/flutter/embedding/engine/systemchannels/RestorationChannelTest.java",
"test/io/flutter/external/FlutterLaunchTests.java",
......
......@@ -4,37 +4,122 @@
package io.flutter.embedding.android;
import android.util.Log;
import android.view.KeyCharacterMap;
import android.view.KeyEvent;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import io.flutter.embedding.engine.systemchannels.KeyEventChannel;
import io.flutter.plugin.editing.TextInputPlugin;
import java.util.AbstractMap.SimpleImmutableEntry;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Map.Entry;
/**
* A class to process key events from Android, passing them to the framework as messages using
* {@link KeyEventChannel}.
*
* <p>A class that sends Android key events to the framework, and re-dispatches those not handled by
* the framework.
*
* <p>Flutter uses asynchronous event handling to avoid blocking the UI thread, but Android requires
* that events are handled synchronously. So, when a key event is received by Flutter, it tells
* Android synchronously that the key has been handled so that it won't propagate to other
* components. Flutter then uses "delayed event synthesis", where it sends the event to the
* framework, and if the framework responds that it has not handled the event, then this class
* synthesizes a new event to send to Android, without handling it this time.
*/
public class AndroidKeyProcessor {
private static final String TAG = "AndroidKeyProcessor";
private static long eventIdSerial = 0;
@NonNull private final KeyEventChannel keyEventChannel;
@NonNull private final TextInputPlugin textInputPlugin;
private int combiningCharacter;
@NonNull private EventResponder eventResponder;
/**
* Constructor for AndroidKeyProcessor.
*
* <p>The view is used as the destination to send the synthesized key to. This means that the the
* next thing in the focus chain will get the event when the framework returns false from
* onKeyDown/onKeyUp
*
* <p>It is possible that that in the middle of the async round trip, the focus chain could
* change, and instead of the native widget that was "next" when the event was fired getting the
* event, it may be the next widget when the event is synthesized that gets it. In practice, this
* shouldn't be a huge problem, as this is an unlikely occurance to happen without user input, and
* it may actually be desired behavior, but it is possible.
*
* @param view takes the activity to use for re-dispatching of events that were not handled by the
* framework.
* @param keyEventChannel the event channel to listen to for new key events.
* @param textInputPlugin a plugin, which, if set, is given key events before the framework is,
* and if it has a valid input connection and is accepting text, then it will handle the event
* and the framework will not receive it.
*/
public AndroidKeyProcessor(
@NonNull KeyEventChannel keyEventChannel, @NonNull TextInputPlugin textInputPlugin) {
@NonNull View view,
@NonNull KeyEventChannel keyEventChannel,
@NonNull TextInputPlugin textInputPlugin) {
this.keyEventChannel = keyEventChannel;
this.textInputPlugin = textInputPlugin;
this.eventResponder = new EventResponder(view);
this.keyEventChannel.setEventResponseHandler(eventResponder);
}
public void onKeyUp(@NonNull KeyEvent keyEvent) {
/**
* Called when a key up event is received by the {@link FlutterView}.
*
* @param keyEvent the Android key event to respond to.
* @return true if the key event should not be propagated to other Android components. Delayed
* synthesis events will return false, so that other components may handle them.
*/
public boolean onKeyUp(@NonNull KeyEvent keyEvent) {
if (eventResponder.dispatchingKeyEvent) {
// Don't handle it if it is from our own delayed event synthesis.
return false;
}
Character complexCharacter = applyCombiningCharacterToBaseCharacter(keyEvent.getUnicodeChar());
keyEventChannel.keyUp(new KeyEventChannel.FlutterKeyEvent(keyEvent, complexCharacter));
KeyEventChannel.FlutterKeyEvent flutterEvent =
new KeyEventChannel.FlutterKeyEvent(keyEvent, complexCharacter, eventIdSerial++);
keyEventChannel.keyUp(flutterEvent);
eventResponder.addEvent(flutterEvent.eventId, keyEvent);
return true;
}
public void onKeyDown(@NonNull KeyEvent keyEvent) {
/**
* Called when a key down event is received by the {@link FlutterView}.
*
* @param keyEvent the Android key event to respond to.
* @return true if the key event should not be propagated to other Android components. Delayed
* synthesis events will return false, so that other components may handle them.
*/
public boolean onKeyDown(@NonNull KeyEvent keyEvent) {
if (eventResponder.dispatchingKeyEvent) {
// Don't handle it if it is from our own delayed event synthesis.
return false;
}
// If the textInputPlugin is still valid and accepting text, then we'll try
// and send the key event to it, assuming that if the event can be sent,
// that it has been handled.
if (textInputPlugin.getLastInputConnection() != null
&& textInputPlugin.getInputMethodManager().isAcceptingText()) {
textInputPlugin.getLastInputConnection().sendKeyEvent(keyEvent);
if (textInputPlugin.getLastInputConnection().sendKeyEvent(keyEvent)) {
return true;
}
}
Character complexCharacter = applyCombiningCharacterToBaseCharacter(keyEvent.getUnicodeChar());
keyEventChannel.keyDown(new KeyEventChannel.FlutterKeyEvent(keyEvent, complexCharacter));
KeyEventChannel.FlutterKeyEvent flutterEvent =
new KeyEventChannel.FlutterKeyEvent(keyEvent, complexCharacter, eventIdSerial++);
keyEventChannel.keyDown(flutterEvent);
eventResponder.addEvent(flutterEvent.eventId, keyEvent);
return true;
}
/**
......@@ -70,7 +155,7 @@ public class AndroidKeyProcessor {
return null;
}
Character complexCharacter = (char) newCharacterCodePoint;
char complexCharacter = (char) newCharacterCodePoint;
boolean isNewCodePointACombiningCharacter =
(newCharacterCodePoint & KeyCharacterMap.COMBINING_ACCENT) != 0;
if (isNewCodePointACombiningCharacter) {
......@@ -82,7 +167,8 @@ public class AndroidKeyProcessor {
combiningCharacter = plainCodePoint;
}
} else {
// The new character is a regular character. Apply combiningCharacter to it, if it exists.
// The new character is a regular character. Apply combiningCharacter to it, if
// it exists.
if (combiningCharacter != 0) {
int combinedChar = KeyCharacterMap.getDeadChar(combiningCharacter, newCharacterCodePoint);
if (combinedChar > 0) {
......@@ -94,4 +180,92 @@ public class AndroidKeyProcessor {
return complexCharacter;
}
private static class EventResponder implements KeyEventChannel.EventResponseHandler {
// The maximum number of pending events that are held before starting to
// complain.
private static final long MAX_PENDING_EVENTS = 1000;
final Deque<Entry<Long, KeyEvent>> pendingEvents = new ArrayDeque<Entry<Long, KeyEvent>>();
@NonNull private final View view;
boolean dispatchingKeyEvent = false;
public EventResponder(@NonNull View view) {
this.view = view;
}
/**
* Removes the pending event with the given id from the cache of pending events.
*
* @param id the id of the event to be removed.
*/
private KeyEvent removePendingEvent(long id) {
if (pendingEvents.getFirst().getKey() != id) {
throw new AssertionError(
"Event response received out of order. Should have seen event "
+ pendingEvents.getFirst().getKey()
+ " first. Instead, received "
+ id);
}
return pendingEvents.removeFirst().getValue();
}
/**
* Called whenever the framework responds that a given key event was handled by the framework.
*
* @param id the event id of the event to be marked as being handled by the framework. Must not
* be null.
*/
@Override
public void onKeyEventHandled(long id) {
removePendingEvent(id);
}
/**
* Called whenever the framework responds that a given key event wasn't handled by the
* framework.
*
* @param id the event id of the event to be marked as not being handled by the framework. Must
* not be null.
*/
@Override
public void onKeyEventNotHandled(long id) {
dispatchKeyEvent(removePendingEvent(id));
}
/** Adds an Android key event with an id to the event responder to wait for a response. */
public void addEvent(long id, @NonNull KeyEvent event) {
if (pendingEvents.size() > 0 && pendingEvents.getFirst().getKey() >= id) {
throw new AssertionError(
"New events must have ids greater than the most recent pending event. New id "
+ id
+ " is less than or equal to the last event id of "
+ pendingEvents.getFirst().getKey());
}
pendingEvents.addLast(new SimpleImmutableEntry<Long, KeyEvent>(id, event));
if (pendingEvents.size() > MAX_PENDING_EVENTS) {
Log.e(
TAG,
"There are "
+ pendingEvents.size()
+ " keyboard events that have not yet received a response. Are responses being "
+ "sent?");
}
}
/**
* Dispatches the event to the activity associated with the context.
*
* @param event the event to be dispatched to the activity.
*/
public void dispatchKeyEvent(KeyEvent event) {
// Since the framework didn't handle it, dispatch the key again.
if (view != null) {
// Turn on dispatchingKeyEvent so that we don't dispatch to ourselves and
// send it to the framework again.
dispatchingKeyEvent = true;
view.getRootView().dispatchKeyEvent(event);
dispatchingKeyEvent = false;
}
}
}
}
......@@ -705,8 +705,7 @@ public class FlutterView extends FrameLayout implements MouseCursorPlugin.MouseC
return super.onKeyUp(keyCode, event);
}
androidKeyProcessor.onKeyUp(event);
return super.onKeyUp(keyCode, event);
return androidKeyProcessor.onKeyUp(event) || super.onKeyUp(keyCode, event);
}
/**
......@@ -726,8 +725,7 @@ public class FlutterView extends FrameLayout implements MouseCursorPlugin.MouseC
return super.onKeyDown(keyCode, event);
}
androidKeyProcessor.onKeyDown(event);
return super.onKeyDown(keyCode, event);
return androidKeyProcessor.onKeyDown(event) || super.onKeyDown(keyCode, event);
}
/**
......@@ -877,7 +875,7 @@ public class FlutterView extends FrameLayout implements MouseCursorPlugin.MouseC
this.flutterEngine.getPlatformViewsController());
localizationPlugin = this.flutterEngine.getLocalizationPlugin();
androidKeyProcessor =
new AndroidKeyProcessor(this.flutterEngine.getKeyEventChannel(), textInputPlugin);
new AndroidKeyProcessor(this, this.flutterEngine.getKeyEventChannel(), textInputPlugin);
androidTouchProcessor =
new AndroidTouchProcessor(this.flutterEngine.getRenderer(), /*trackMotionEvents=*/ false);
accessibilityBridge =
......
......@@ -73,6 +73,7 @@ public class AccessibilityChannel {
break;
}
}
reply.reply(null);
}
};
......
......@@ -9,29 +9,104 @@ import android.view.InputDevice;
import android.view.KeyEvent;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import io.flutter.embedding.engine.dart.DartExecutor;
import io.flutter.Log;
import io.flutter.plugin.common.BasicMessageChannel;
import io.flutter.plugin.common.BinaryMessenger;
import io.flutter.plugin.common.JSONMessageCodec;
import java.util.HashMap;
import java.util.Map;
import org.json.JSONException;
import org.json.JSONObject;
/** TODO(mattcarroll): fill in javadoc for KeyEventChannel. */
/**
* Event message channel for key events to/from the Flutter framework.
*
* <p>Sends key up/down events to the framework, and receives asynchronous messages from the
* framework about whether or not the key was handled.
*/
public class KeyEventChannel {
private static final String TAG = "KeyEventChannel";
@NonNull public final BasicMessageChannel<Object> channel;
/**
* Sets the event response handler to be used to receive key event response messages from the
* framework on this channel.
*/
public void setEventResponseHandler(EventResponseHandler handler) {
this.eventResponseHandler = handler;
}
private EventResponseHandler eventResponseHandler;
/** A handler of incoming key handling messages. */
public interface EventResponseHandler {
/**
* Called whenever the framework responds that a given key event was handled by the framework.
*
* @param id the event id of the event to be marked as being handled by the framework. Must not
* be null.
*/
public void onKeyEventHandled(long id);
public KeyEventChannel(@NonNull DartExecutor dartExecutor) {
/**
* Called whenever the framework responds that a given key event wasn't handled by the
* framework.
*
* @param id the event id of the event to be marked as not being handled by the framework. Must
* not be null.
*/
public void onKeyEventNotHandled(long id);
}
/**
* A constructor that creates a KeyEventChannel with the default message handler.
*
* @param binaryMessenger the binary messenger used to send messages on this channel.
*/
public KeyEventChannel(@NonNull BinaryMessenger binaryMessenger) {
this.channel =
new BasicMessageChannel<>(dartExecutor, "flutter/keyevent", JSONMessageCodec.INSTANCE);
new BasicMessageChannel<>(binaryMessenger, "flutter/keyevent", JSONMessageCodec.INSTANCE);
}
/**
* Creates a reply handler for this an event with the given eventId.
*
* @param eventId the event ID to create a reply for.
*/
BasicMessageChannel.Reply<Object> createReplyHandler(long eventId) {
return message -> {
if (eventResponseHandler == null) {
return;
}
try {
if (message == null) {
eventResponseHandler.onKeyEventNotHandled(eventId);
return;
}
final JSONObject annotatedEvent = (JSONObject) message;
final boolean handled = annotatedEvent.getBoolean("handled");
if (handled) {
eventResponseHandler.onKeyEventHandled(eventId);
} else {
eventResponseHandler.onKeyEventNotHandled(eventId);
}
} catch (JSONException e) {
Log.e(TAG, "Unable to unpack JSON message: " + e);
eventResponseHandler.onKeyEventNotHandled(eventId);
}
};
}
@NonNull public final BasicMessageChannel<Object> channel;
public void keyUp(@NonNull FlutterKeyEvent keyEvent) {
Map<String, Object> message = new HashMap<>();
message.put("type", "keyup");
message.put("keymap", "android");
encodeKeyEvent(keyEvent, message);
channel.send(message);
channel.send(message, createReplyHandler(keyEvent.eventId));
}
public void keyDown(@NonNull FlutterKeyEvent keyEvent) {
......@@ -40,7 +115,7 @@ public class KeyEventChannel {
message.put("keymap", "android");
encodeKeyEvent(keyEvent, message);
channel.send(message);
channel.send(message, createReplyHandler(keyEvent.eventId));
}
private void encodeKeyEvent(
......@@ -61,27 +136,105 @@ public class KeyEventChannel {
message.put("repeatCount", event.repeatCount);
}
/** Key event as defined by Flutter. */
/** A key event as defined by Flutter. */
public static class FlutterKeyEvent {
/**
* The id for the device this event came from.
*
* @see <a
* href="https://developer.android.com/reference/android/view/KeyEvent?hl=en#getDeviceId()">KeyEvent.getDeviceId()</a>
*/
public final int deviceId;
/**
* The flags for this key event.
*
* @see <a
* href="https://developer.android.com/reference/android/view/KeyEvent?hl=en#getFlags()">KeyEvent.getFlags()</a>
*/
public final int flags;
/**
* The code point for the Unicode character produced by this event if no meta keys were pressed
* (by passing 0 to {@code KeyEvent.getUnicodeChar(int)}).
*
* @see <a
* href="https://developer.android.com/reference/android/view/KeyEvent?hl=en#getUnicodeChar(int)">KeyEvent.getUnicodeChar(int)</a>
*/
public final int plainCodePoint;
/**
* The code point for the Unicode character produced by this event, taking into account the meta
* keys currently pressed.
*
* @see <a
* href="https://developer.android.com/reference/android/view/KeyEvent?hl=en#getUnicodeChar()">KeyEvent.getUnicodeChar()</a>
*/
public final int codePoint;
/**
* The Android key code for this event.
*
* @see <a
* href="https://developer.android.com/reference/android/view/KeyEvent?hl=en#getKeyCode()">KeyEvent.getKeyCode()</a>
*/
public final int keyCode;
/**
* The character produced by this event, including any combining characters pressed before it.
*/
@Nullable public final Character complexCharacter;
/**
* The Android scan code for the key pressed.
*
* @see <a
* href="https://developer.android.com/reference/android/view/KeyEvent?hl=en#getScanCode()">KeyEvent.getScanCode()</a>
*/
public final int scanCode;
/**
* The meta key state for the Android key event.
*
* @see <a
* href="https://developer.android.com/reference/android/view/KeyEvent?hl=en#getMetaState()">KeyEvent.getMetaState()</a>
*/
public final int metaState;
/**
* The source of the key event.
*
* @see <a
* href="https://developer.android.com/reference/android/view/KeyEvent?hl=en#getSource()">KeyEvent.getSource()</a>
*/
public final int source;
/**
* The vendorId of the device that produced this key event.
*
* @see <a
* href="https://developer.android.com/reference/android/view/InputDevice?hl=en#getVendorId()">InputDevice.getVendorId()</a>
*/
public final int vendorId;
/**
* The productId of the device that produced this key event.
*
* @see <a
* href="https://developer.android.com/reference/android/view/InputDevice?hl=en#getProductId()">InputDevice.getProductId()</a>
*/
public final int productId;
/**
* The repeat count for this event.
*
* @see <a
* href="https://developer.android.com/reference/android/view/KeyEvent?hl=en#getRepeatCount()">KeyEvent.getRepeatCount()</a>
*/
public final int repeatCount;
/**
* The unique id for this Flutter key event.
*
* <p>This id is used to identify pending events when results are received from the framework.
* This ID does not come from Android.
*/
public final long eventId;
public FlutterKeyEvent(@NonNull KeyEvent androidKeyEvent) {
this(androidKeyEvent, null);
public FlutterKeyEvent(@NonNull KeyEvent androidKeyEvent, long eventId) {
this(androidKeyEvent, null, eventId);
}
public FlutterKeyEvent(
@NonNull KeyEvent androidKeyEvent, @Nullable Character complexCharacter) {
@NonNull KeyEvent androidKeyEvent, @Nullable Character complexCharacter, long eventId) {
this(
androidKeyEvent.getDeviceId(),
androidKeyEvent.getFlags(),
......@@ -92,7 +245,8 @@ public class KeyEventChannel {
androidKeyEvent.getScanCode(),
androidKeyEvent.getMetaState(),
androidKeyEvent.getSource(),
androidKeyEvent.getRepeatCount());
androidKeyEvent.getRepeatCount(),
eventId);
}
public FlutterKeyEvent(
......@@ -105,7 +259,8 @@ public class KeyEventChannel {
int scanCode,
int metaState,
int source,
int repeatCount) {
int repeatCount,
long eventId) {
this.deviceId = deviceId;
this.flags = flags;
this.plainCodePoint = plainCodePoint;
......@@ -116,6 +271,7 @@ public class KeyEventChannel {
this.metaState = metaState;
this.source = source;
this.repeatCount = repeatCount;
this.eventId = eventId;
InputDevice device = InputDevice.getDevice(deviceId);
if (device != null) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
......
......@@ -145,7 +145,7 @@ public class TextInputPlugin {
}
/**
* * Use the current platform view input connection until unlockPlatformViewInputConnection is
* Use the current platform view input connection until unlockPlatformViewInputConnection is
* called.
*
* <p>The current input connection instance is cached and any following call to @{link
......
......@@ -231,7 +231,7 @@ public class FlutterView extends SurfaceView
mMouseCursorPlugin = null;
}
mLocalizationPlugin = new LocalizationPlugin(context, localizationChannel);
androidKeyProcessor = new AndroidKeyProcessor(keyEventChannel, mTextInputPlugin);
androidKeyProcessor = new AndroidKeyProcessor(this, keyEventChannel, mTextInputPlugin);
androidTouchProcessor =
new AndroidTouchProcessor(flutterRenderer, /*trackMotionEvents=*/ false);
platformViewsController.attachToFlutterRenderer(flutterRenderer);
......@@ -270,8 +270,7 @@ public class FlutterView extends SurfaceView
if (!isAttached()) {
return super.onKeyUp(keyCode, event);
}
androidKeyProcessor.onKeyUp(event);
return super.onKeyUp(keyCode, event);
return androidKeyProcessor.onKeyUp(event) || super.onKeyUp(keyCode, event);
}
@Override
......@@ -279,8 +278,7 @@ public class FlutterView extends SurfaceView
if (!isAttached()) {
return super.onKeyDown(keyCode, event);
}
androidKeyProcessor.onKeyDown(event);
return super.onKeyDown(keyCode, event);
return androidKeyProcessor.onKeyDown(event) || super.onKeyDown(keyCode, event);
}
public FlutterNativeView getFlutterNativeView() {
......
......@@ -4,6 +4,7 @@
package io.flutter;
import io.flutter.embedding.android.AndroidKeyProcessorTest;
import io.flutter.embedding.android.FlutterActivityAndFragmentDelegateTest;
import io.flutter.embedding.android.FlutterActivityTest;
import io.flutter.embedding.android.FlutterAndroidComponentTest;
......@@ -17,6 +18,7 @@ import io.flutter.embedding.engine.LocalizationPluginTest;
import io.flutter.embedding.engine.RenderingComponentTest;
import io.flutter.embedding.engine.plugins.shim.ShimPluginRegistryTest;
import io.flutter.embedding.engine.renderer.FlutterRendererTest;
import io.flutter.embedding.engine.systemchannels.KeyEventChannelTest;
import io.flutter.embedding.engine.systemchannels.PlatformChannelTest;
import io.flutter.embedding.engine.systemchannels.RestorationChannelTest;
import io.flutter.external.FlutterLaunchTests;
......@@ -40,6 +42,8 @@ import test.io.flutter.embedding.engine.dart.DartExecutorTest;
@RunWith(Suite.class)
@SuiteClasses({
AccessibilityBridgeTest.class,
AndroidKeyProcessorTest.class,
DartExecutorTest.class,
FlutterActivityAndFragmentDelegateTest.class,
FlutterActivityTest.class,
......@@ -51,26 +55,26 @@ import test.io.flutter.embedding.engine.dart.DartExecutorTest;
FlutterFragmentTest.class,
FlutterJNITest.class,
FlutterLaunchTests.class,
FlutterShellArgsTest.class,
FlutterRendererTest.class,
FlutterShellArgsTest.class,
FlutterViewTest.class,
InputConnectionAdaptorTest.class,
KeyEventChannelTest.class,
LocalizationPluginTest.class,
MouseCursorPluginTest.class,
PlatformChannelTest.class,
PlatformPluginTest.class,
PlatformViewsControllerTest.class,
PluginComponentTest.class,
PreconditionsTest.class,
RenderingComponentTest.class,
StandardMessageCodecTest.class,
StandardMethodCodecTest.class,
RestorationChannelTest.class,
ShimPluginRegistryTest.class,
SingleViewPresentationTest.class,
SmokeTest.class,
StandardMessageCodecTest.class,
StandardMethodCodecTest.class,
TextInputPluginTest.class,
MouseCursorPluginTest.class,
AccessibilityBridgeTest.class,
PlatformChannelTest.class,
RestorationChannelTest.class,
})
/** Runs all of the unit tests listed in the {@code @SuiteClasses} annotation. */
public class FlutterTestSuite {}
package io.flutter.embedding.android;
import static junit.framework.TestCase.assertEquals;
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 static org.mockito.Mockito.when;
import android.annotation.TargetApi;
import android.view.KeyEvent;
import android.view.View;
import androidx.annotation.NonNull;
import io.flutter.embedding.engine.FlutterEngine;
import io.flutter.embedding.engine.FlutterJNI;
import io.flutter.embedding.engine.systemchannels.KeyEventChannel;
import io.flutter.embedding.engine.systemchannels.TextInputChannel;
import io.flutter.plugin.editing.TextInputPlugin;
import io.flutter.util.FakeKeyEvent;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
@Config(manifest = Config.NONE)
@RunWith(RobolectricTestRunner.class)
@TargetApi(28)
public class AndroidKeyProcessorTest {
@Mock FlutterJNI mockFlutterJni;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
when(mockFlutterJni.isAttached()).thenReturn(true);
}
@Test
public void respondsTrueWhenHandlingNewEvents() {
FlutterEngine flutterEngine = mockFlutterEngine();
KeyEventChannel fakeKeyEventChannel = flutterEngine.getKeyEventChannel();
View fakeView = mock(View.class);
AndroidKeyProcessor processor =
new AndroidKeyProcessor(fakeView, fakeKeyEventChannel, mock(TextInputPlugin.class));
boolean result = processor.onKeyDown(new FakeKeyEvent(KeyEvent.ACTION_DOWN, 65));
assertEquals(true, result);
verify(fakeKeyEventChannel, times(1)).keyDown(any(KeyEventChannel.FlutterKeyEvent.class));
verify(fakeKeyEventChannel, times(0)).keyUp(any(KeyEventChannel.FlutterKeyEvent.class));
verify(fakeView, times(0)).dispatchKeyEvent(any(KeyEvent.class));
}
public void synthesizesEventsWhenKeyDownNotHandled() {
FlutterEngine flutterEngine = mockFlutterEngine();
KeyEventChannel fakeKeyEventChannel = flutterEngine.getKeyEventChannel();
View fakeView = mock(View.class);
View fakeRootView = mock(View.class);
when(fakeView.getRootView())
.then(
new Answer<View>() {
@Override
public View answer(InvocationOnMock invocation) throws Throwable {
return fakeRootView;
}
});
ArgumentCaptor<KeyEventChannel.EventResponseHandler> handlerCaptor =
ArgumentCaptor.forClass(KeyEventChannel.EventResponseHandler.class);
verify(fakeKeyEventChannel).setEventResponseHandler(handlerCaptor.capture());
AndroidKeyProcessor processor =
new AndroidKeyProcessor(fakeView, fakeKeyEventChannel, mock(TextInputPlugin.class));
ArgumentCaptor<KeyEventChannel.FlutterKeyEvent> eventCaptor =
ArgumentCaptor.forClass(KeyEventChannel.FlutterKeyEvent.class);
FakeKeyEvent fakeKeyEvent = new FakeKeyEvent(KeyEvent.ACTION_DOWN, 65);
boolean result = processor.onKeyDown(fakeKeyEvent);
assertEquals(true, result);
// Capture the FlutterKeyEvent so we can find out its event ID to use when
// faking our response.
verify(fakeKeyEventChannel, times(1)).keyDown(eventCaptor.capture());
boolean[] dispatchResult = {true};
when(fakeView.dispatchKeyEvent(any(KeyEvent.class)))
.then(
new Answer<Boolean>() {
@Override
public Boolean answer(InvocationOnMock invocation) throws Throwable {
KeyEvent event = (KeyEvent) invocation.getArguments()[0];
assertEquals(fakeKeyEvent, event);
dispatchResult[0] = processor.onKeyDown(event);
return dispatchResult[0];
}
});
// Fake a response from the framework.
handlerCaptor.getValue().onKeyEventNotHandled(eventCaptor.getValue().eventId);
verify(fakeView, times(1)).dispatchKeyEvent(fakeKeyEvent);
assertEquals(false, dispatchResult[0]);
verify(fakeKeyEventChannel, times(0)).keyUp(any(KeyEventChannel.FlutterKeyEvent.class));
verify(fakeRootView, times(1)).dispatchKeyEvent(fakeKeyEvent);
}
public void synthesizesEventsWhenKeyUpNotHandled() {
FlutterEngine flutterEngine = mockFlutterEngine();
KeyEventChannel fakeKeyEventChannel = flutterEngine.getKeyEventChannel();
View fakeView = mock(View.class);
View fakeRootView = mock(View.class);
when(fakeView.getRootView())
.then(
new Answer<View>() {
@Override
public View answer(InvocationOnMock invocation) throws Throwable {
return fakeRootView;
}
});
ArgumentCaptor<KeyEventChannel.EventResponseHandler> handlerCaptor =
ArgumentCaptor.forClass(KeyEventChannel.EventResponseHandler.class);
verify(fakeKeyEventChannel).setEventResponseHandler(handlerCaptor.capture());
AndroidKeyProcessor processor =
new AndroidKeyProcessor(fakeView, fakeKeyEventChannel, mock(TextInputPlugin.class));
ArgumentCaptor<KeyEventChannel.FlutterKeyEvent> eventCaptor =
ArgumentCaptor.forClass(KeyEventChannel.FlutterKeyEvent.class);
FakeKeyEvent fakeKeyEvent = new FakeKeyEvent(KeyEvent.ACTION_UP, 65);
boolean result = processor.onKeyUp(fakeKeyEvent);
assertEquals(true, result);
// Capture the FlutterKeyEvent so we can find out its event ID to use when
// faking our response.
verify(fakeKeyEventChannel, times(1)).keyUp(eventCaptor.capture());
boolean[] dispatchResult = {true};
when(fakeView.dispatchKeyEvent(any(KeyEvent.class)))
.then(
new Answer<Boolean>() {
@Override
public Boolean answer(InvocationOnMock invocation) throws Throwable {
KeyEvent event = (KeyEvent) invocation.getArguments()[0];
assertEquals(fakeKeyEvent, event);
dispatchResult[0] = processor.onKeyUp(event);
return dispatchResult[0];
}
});
// Fake a response from the framework.
handlerCaptor.getValue().onKeyEventNotHandled(eventCaptor.getValue().eventId);
verify(fakeView, times(1)).dispatchKeyEvent(fakeKeyEvent);
assertEquals(false, dispatchResult[0]);
verify(fakeKeyEventChannel, times(0)).keyUp(any(KeyEventChannel.FlutterKeyEvent.class));
verify(fakeRootView, times(1)).dispatchKeyEvent(fakeKeyEvent);
}
@NonNull
private FlutterEngine mockFlutterEngine() {
// Mock FlutterEngine and all of its required direct calls.
FlutterEngine engine = mock(FlutterEngine.class);
when(engine.getKeyEventChannel()).thenReturn(mock(KeyEventChannel.class));
when(engine.getTextInputChannel()).thenReturn(mock(TextInputChannel.class));
return engine;
}
}
......@@ -25,6 +25,7 @@ import io.flutter.embedding.engine.dart.DartExecutor;
import io.flutter.embedding.engine.plugins.activity.ActivityControlSurface;
import io.flutter.embedding.engine.renderer.FlutterRenderer;
import io.flutter.embedding.engine.systemchannels.AccessibilityChannel;
import io.flutter.embedding.engine.systemchannels.KeyEventChannel;
import io.flutter.embedding.engine.systemchannels.LifecycleChannel;
import io.flutter.embedding.engine.systemchannels.LocalizationChannel;
import io.flutter.embedding.engine.systemchannels.MouseCursorChannel;
......@@ -615,19 +616,20 @@ public class FlutterActivityAndFragmentDelegateTest {
// Mock FlutterEngine and all of its required direct calls.
FlutterEngine engine = mock(FlutterEngine.class);
when(engine.getDartExecutor()).thenReturn(mock(DartExecutor.class));
when(engine.getRenderer()).thenReturn(mock(FlutterRenderer.class));
when(engine.getPlatformViewsController()).thenReturn(mock(PlatformViewsController.class));
when(engine.getAccessibilityChannel()).thenReturn(mock(AccessibilityChannel.class));
when(engine.getSettingsChannel()).thenReturn(fakeSettingsChannel);
when(engine.getLocalizationChannel()).thenReturn(mock(LocalizationChannel.class));
when(engine.getActivityControlSurface()).thenReturn(mock(ActivityControlSurface.class));
when(engine.getDartExecutor()).thenReturn(mock(DartExecutor.class));
when(engine.getKeyEventChannel()).thenReturn(mock(KeyEventChannel.class));
when(engine.getLifecycleChannel()).thenReturn(mock(LifecycleChannel.class));
when(engine.getLocalizationChannel()).thenReturn(mock(LocalizationChannel.class));
when(engine.getLocalizationPlugin()).thenReturn(mock(LocalizationPlugin.class));
when(engine.getMouseCursorChannel()).thenReturn(mock(MouseCursorChannel.class));
when(engine.getNavigationChannel()).thenReturn(mock(NavigationChannel.class));
when(engine.getPlatformViewsController()).thenReturn(mock(PlatformViewsController.class));
when(engine.getRenderer()).thenReturn(mock(FlutterRenderer.class));
when(engine.getSettingsChannel()).thenReturn(fakeSettingsChannel);
when(engine.getSystemChannel()).thenReturn(mock(SystemChannel.class));
when(engine.getTextInputChannel()).thenReturn(mock(TextInputChannel.class));
when(engine.getMouseCursorChannel()).thenReturn(mock(MouseCursorChannel.class));
when(engine.getActivityControlSurface()).thenReturn(mock(ActivityControlSurface.class));
when(engine.getLocalizationPlugin()).thenReturn(mock(LocalizationPlugin.class));
return engine;
}
......
package io.flutter.embedding.engine.systemchannels;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
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 android.annotation.TargetApi;
import android.view.KeyEvent;
import androidx.annotation.NonNull;
import io.flutter.plugin.common.BinaryMessenger;
import io.flutter.plugin.common.JSONMessageCodec;
import io.flutter.util.FakeKeyEvent;
import java.nio.ByteBuffer;
import org.json.JSONException;
import org.json.JSONObject;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
@Config(
manifest = Config.NONE,
shadows = {})
@RunWith(RobolectricTestRunner.class)
@TargetApi(24)
public class KeyEventChannelTest {
private void sendReply(boolean handled, BinaryMessenger.BinaryReply messengerReply)
throws JSONException {
JSONObject reply = new JSONObject();
reply.put("handled", true);
ByteBuffer binaryReply = JSONMessageCodec.INSTANCE.encodeMessage(reply);
assertNotNull(binaryReply);
binaryReply.rewind();
messengerReply.reply(binaryReply);
}
@Test
public void keyDownEventIsSentToFramework() throws JSONException {
BinaryMessenger fakeMessenger = mock(BinaryMessenger.class);
KeyEventChannel keyEventChannel = new KeyEventChannel(fakeMessenger);
final boolean[] handled = {false};
final long[] handledId = {-1};
keyEventChannel.setEventResponseHandler(
new KeyEventChannel.EventResponseHandler() {
public void onKeyEventHandled(@NonNull long id) {
handled[0] = true;
handledId[0] = id;
}
public void onKeyEventNotHandled(@NonNull long id) {
handled[0] = false;
handledId[0] = id;
}
});
verify(fakeMessenger, times(0)).send(any(), any(), any());
KeyEvent event = new FakeKeyEvent(KeyEvent.ACTION_DOWN, 65);
KeyEventChannel.FlutterKeyEvent flutterKeyEvent =
new KeyEventChannel.FlutterKeyEvent(event, null, 10);
keyEventChannel.keyDown(flutterKeyEvent);
ArgumentCaptor<ByteBuffer> byteBufferArgumentCaptor = ArgumentCaptor.forClass(ByteBuffer.class);
ArgumentCaptor<BinaryMessenger.BinaryReply> replyArgumentCaptor =
ArgumentCaptor.forClass(BinaryMessenger.BinaryReply.class);
verify(fakeMessenger, times(1))
.send(any(), byteBufferArgumentCaptor.capture(), replyArgumentCaptor.capture());
ByteBuffer capturedMessage = byteBufferArgumentCaptor.getValue();
capturedMessage.rewind();
JSONObject message = (JSONObject) JSONMessageCodec.INSTANCE.decodeMessage(capturedMessage);
assertNotNull(message);
assertEquals("keydown", message.get("type"));
// Simulate a reply, and see that it is handled.
sendReply(true, replyArgumentCaptor.getValue());
assertTrue(handled[0]);
assertEquals(10, handledId[0]);
}
@Test
public void keyUpEventIsSentToFramework() throws JSONException {
BinaryMessenger fakeMessenger = mock(BinaryMessenger.class);
KeyEventChannel keyEventChannel = new KeyEventChannel(fakeMessenger);
final boolean[] handled = {false};
final long[] handledId = {-1};
keyEventChannel.setEventResponseHandler(
new KeyEventChannel.EventResponseHandler() {
public void onKeyEventHandled(long id) {
handled[0] = true;
handledId[0] = id;
}
public void onKeyEventNotHandled(long id) {
handled[0] = false;
handledId[0] = id;
}
});
verify(fakeMessenger, times(0)).send(any(), any(), any());
KeyEvent event = new FakeKeyEvent(KeyEvent.ACTION_UP, 65);
KeyEventChannel.FlutterKeyEvent flutterKeyEvent =
new KeyEventChannel.FlutterKeyEvent(event, null, 10);
keyEventChannel.keyUp(flutterKeyEvent);
ArgumentCaptor<ByteBuffer> byteBufferArgumentCaptor = ArgumentCaptor.forClass(ByteBuffer.class);
ArgumentCaptor<BinaryMessenger.BinaryReply> replyArgumentCaptor =
ArgumentCaptor.forClass(BinaryMessenger.BinaryReply.class);
verify(fakeMessenger, times(1))
.send(any(), byteBufferArgumentCaptor.capture(), replyArgumentCaptor.capture());
ByteBuffer capturedMessage = byteBufferArgumentCaptor.getValue();
capturedMessage.rewind();
JSONObject message = (JSONObject) JSONMessageCodec.INSTANCE.decodeMessage(capturedMessage);
assertNotNull(message);
assertEquals("keyup", message.get("type"));
// Simulate a reply, and see that it is handled.
sendReply(true, replyArgumentCaptor.getValue());
assertTrue(handled[0]);
assertEquals(10, handledId[0]);
}
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册