未验证 提交 18200e7e 编写于 作者: G Greg Spencer 提交者: GitHub

Revert "Implement delayed event synthesis key event handling for Android (#19024)" (#19956)

This reverts commit 8825f917 because it breaks flutter_gallery__back_button_memory and a customer test.
上级 0318e7e0
......@@ -430,7 +430,6 @@ 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",
......@@ -448,7 +447,6 @@ action("robolectric_tests") {
"test/io/flutter/embedding/engine/dart/DartExecutorTest.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/RestorationChannelTest.java",
"test/io/flutter/external/FlutterLaunchTests.java",
"test/io/flutter/plugin/common/StandardMessageCodecTest.java",
......
......@@ -4,122 +4,37 @@
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 View view,
@NonNull KeyEventChannel keyEventChannel,
@NonNull TextInputPlugin textInputPlugin) {
@NonNull KeyEventChannel keyEventChannel, @NonNull TextInputPlugin textInputPlugin) {
this.keyEventChannel = keyEventChannel;
this.textInputPlugin = textInputPlugin;
this.eventResponder = new EventResponder(view);
this.keyEventChannel.setEventResponseHandler(eventResponder);
}
/**
* 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;
}
public void onKeyUp(@NonNull KeyEvent keyEvent) {
Character complexCharacter = applyCombiningCharacterToBaseCharacter(keyEvent.getUnicodeChar());
KeyEventChannel.FlutterKeyEvent flutterEvent =
new KeyEventChannel.FlutterKeyEvent(keyEvent, complexCharacter, eventIdSerial++);
keyEventChannel.keyUp(flutterEvent);
eventResponder.addEvent(flutterEvent.eventId, keyEvent);
return true;
keyEventChannel.keyUp(new KeyEventChannel.FlutterKeyEvent(keyEvent, complexCharacter));
}
/**
* 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.
public void onKeyDown(@NonNull KeyEvent keyEvent) {
if (textInputPlugin.getLastInputConnection() != null
&& textInputPlugin.getInputMethodManager().isAcceptingText()) {
if (textInputPlugin.getLastInputConnection().sendKeyEvent(keyEvent)) {
return true;
}
textInputPlugin.getLastInputConnection().sendKeyEvent(keyEvent);
}
Character complexCharacter = applyCombiningCharacterToBaseCharacter(keyEvent.getUnicodeChar());
KeyEventChannel.FlutterKeyEvent flutterEvent =
new KeyEventChannel.FlutterKeyEvent(keyEvent, complexCharacter, eventIdSerial++);
keyEventChannel.keyDown(flutterEvent);
eventResponder.addEvent(flutterEvent.eventId, keyEvent);
return true;
keyEventChannel.keyDown(new KeyEventChannel.FlutterKeyEvent(keyEvent, complexCharacter));
}
/**
......@@ -155,7 +70,7 @@ public class AndroidKeyProcessor {
return null;
}
char complexCharacter = (char) newCharacterCodePoint;
Character complexCharacter = (char) newCharacterCodePoint;
boolean isNewCodePointACombiningCharacter =
(newCharacterCodePoint & KeyCharacterMap.COMBINING_ACCENT) != 0;
if (isNewCodePointACombiningCharacter) {
......@@ -167,8 +82,7 @@ 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) {
......@@ -180,92 +94,4 @@ 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.dispatchKeyEvent(event);
dispatchingKeyEvent = false;
}
}
}
}
......@@ -679,7 +679,8 @@ public class FlutterView extends FrameLayout implements MouseCursorPlugin.MouseC
return super.onKeyUp(keyCode, event);
}
return androidKeyProcessor.onKeyUp(event) || super.onKeyUp(keyCode, event);
androidKeyProcessor.onKeyUp(event);
return super.onKeyUp(keyCode, event);
}
/**
......@@ -699,7 +700,8 @@ public class FlutterView extends FrameLayout implements MouseCursorPlugin.MouseC
return super.onKeyDown(keyCode, event);
}
return androidKeyProcessor.onKeyDown(event) || super.onKeyDown(keyCode, event);
androidKeyProcessor.onKeyDown(event);
return super.onKeyDown(keyCode, event);
}
/**
......@@ -849,7 +851,7 @@ public class FlutterView extends FrameLayout implements MouseCursorPlugin.MouseC
this.flutterEngine.getPlatformViewsController());
localizationPlugin = this.flutterEngine.getLocalizationPlugin();
androidKeyProcessor =
new AndroidKeyProcessor(this, this.flutterEngine.getKeyEventChannel(), textInputPlugin);
new AndroidKeyProcessor(this.flutterEngine.getKeyEventChannel(), textInputPlugin);
androidTouchProcessor =
new AndroidTouchProcessor(this.flutterEngine.getRenderer(), /*trackMotionEvents=*/ false);
accessibilityBridge =
......
......@@ -73,7 +73,6 @@ public class AccessibilityChannel {
break;
}
}
reply.reply(null);
}
};
......
......@@ -9,104 +9,29 @@ import android.view.InputDevice;
import android.view.KeyEvent;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import io.flutter.Log;
import io.flutter.embedding.engine.dart.DartExecutor;
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;
/**
* 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.
*/
/** TODO(mattcarroll): fill in javadoc for KeyEventChannel. */
public class KeyEventChannel {
private static final String TAG = "KeyEventChannel";
/**
* 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);
/**
* 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);
}
@NonNull public final BasicMessageChannel<Object> channel;
/**
* 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) {
public KeyEventChannel(@NonNull DartExecutor dartExecutor) {
this.channel =
new BasicMessageChannel<>(binaryMessenger, "flutter/keyevent", JSONMessageCodec.INSTANCE);
new BasicMessageChannel<>(dartExecutor, "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, createReplyHandler(keyEvent.eventId));
channel.send(message);
}
public void keyDown(@NonNull FlutterKeyEvent keyEvent) {
......@@ -115,7 +40,7 @@ public class KeyEventChannel {
message.put("keymap", "android");
encodeKeyEvent(keyEvent, message);
channel.send(message, createReplyHandler(keyEvent.eventId));
channel.send(message);
}
private void encodeKeyEvent(
......@@ -136,105 +61,27 @@ public class KeyEventChannel {
message.put("repeatCount", event.repeatCount);
}
/** A key event as defined by Flutter. */
/** 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, long eventId) {
this(androidKeyEvent, null, eventId);
public FlutterKeyEvent(@NonNull KeyEvent androidKeyEvent) {
this(androidKeyEvent, null);
}
public FlutterKeyEvent(
@NonNull KeyEvent androidKeyEvent, @Nullable Character complexCharacter, long eventId) {
@NonNull KeyEvent androidKeyEvent, @Nullable Character complexCharacter) {
this(
androidKeyEvent.getDeviceId(),
androidKeyEvent.getFlags(),
......@@ -245,8 +92,7 @@ public class KeyEventChannel {
androidKeyEvent.getScanCode(),
androidKeyEvent.getMetaState(),
androidKeyEvent.getSource(),
androidKeyEvent.getRepeatCount(),
eventId);
androidKeyEvent.getRepeatCount());
}
public FlutterKeyEvent(
......@@ -259,8 +105,7 @@ public class KeyEventChannel {
int scanCode,
int metaState,
int source,
int repeatCount,
long eventId) {
int repeatCount) {
this.deviceId = deviceId;
this.flags = flags;
this.plainCodePoint = plainCodePoint;
......@@ -271,7 +116,6 @@ 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) {
......
......@@ -127,7 +127,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(this, keyEventChannel, mTextInputPlugin);
androidKeyProcessor = new AndroidKeyProcessor(keyEventChannel, mTextInputPlugin);
androidTouchProcessor =
new AndroidTouchProcessor(flutterRenderer, /*trackMotionEvents=*/ false);
platformViewsController.attachToFlutterRenderer(flutterRenderer);
......@@ -270,7 +270,8 @@ public class FlutterView extends SurfaceView
if (!isAttached()) {
return super.onKeyUp(keyCode, event);
}
return androidKeyProcessor.onKeyUp(event) || super.onKeyUp(keyCode, event);
androidKeyProcessor.onKeyUp(event);
return super.onKeyUp(keyCode, event);
}
@Override
......@@ -278,7 +279,8 @@ public class FlutterView extends SurfaceView
if (!isAttached()) {
return super.onKeyDown(keyCode, event);
}
return androidKeyProcessor.onKeyDown(event) || super.onKeyDown(keyCode, event);
androidKeyProcessor.onKeyDown(event);
return super.onKeyDown(keyCode, event);
}
public FlutterNativeView getFlutterNativeView() {
......
......@@ -4,7 +4,6 @@
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;
......@@ -18,7 +17,6 @@ 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.RestorationChannelTest;
import io.flutter.external.FlutterLaunchTests;
import io.flutter.plugin.common.StandardMessageCodecTest;
......@@ -41,8 +39,6 @@ import test.io.flutter.embedding.engine.dart.DartExecutorTest;
@RunWith(Suite.class)
@SuiteClasses({
AccessibilityBridgeTest.class,
AndroidKeyProcessorTest.class,
DartExecutorTest.class,
FlutterActivityAndFragmentDelegateTest.class,
FlutterActivityTest.class,
......@@ -54,25 +50,25 @@ import test.io.flutter.embedding.engine.dart.DartExecutorTest;
FlutterFragmentTest.class,
FlutterJNITest.class,
FlutterLaunchTests.class,
FlutterRendererTest.class,
FlutterShellArgsTest.class,
FlutterRendererTest.class,
FlutterViewTest.class,
InputConnectionAdaptorTest.class,
KeyEventChannelTest.class,
LocalizationPluginTest.class,
MouseCursorPluginTest.class,
PlatformPluginTest.class,
PlatformViewsControllerTest.class,
PluginComponentTest.class,
PreconditionsTest.class,
RenderingComponentTest.class,
RestorationChannelTest.class,
StandardMessageCodecTest.class,
StandardMethodCodecTest.class,
ShimPluginRegistryTest.class,
SingleViewPresentationTest.class,
SmokeTest.class,
StandardMessageCodecTest.class,
StandardMethodCodecTest.class,
TextInputPluginTest.class,
MouseCursorPluginTest.class,
AccessibilityBridgeTest.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);
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));
}
public void synthesizesEventsWhenKeyUpNotHandled() {
FlutterEngine flutterEngine = mockFlutterEngine();
KeyEventChannel fakeKeyEventChannel = flutterEngine.getKeyEventChannel();
View fakeView = mock(View.class);
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));
}
@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,7 +25,6 @@ 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;
......@@ -616,20 +615,19 @@ public class FlutterActivityAndFragmentDelegateTest {
// Mock FlutterEngine and all of its required direct calls.
FlutterEngine engine = mock(FlutterEngine.class);
when(engine.getAccessibilityChannel()).thenReturn(mock(AccessibilityChannel.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.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.getLifecycleChannel()).thenReturn(mock(LifecycleChannel.class));
when(engine.getNavigationChannel()).thenReturn(mock(NavigationChannel.class));
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.
先完成此消息的编辑!
想要评论请 注册