Implement delayed event synthesis key event handling for Android (flutter/engine#19024)
This implements the design in flutter.dev/go/handling-synchronous-keyboard-events for Android. I started with Android, but this will be used for all platforms as we add them. The related framework PR is: flutter/flutter#59358 (which has already landed)
This commit is contained in:
@@ -430,6 +430,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",
|
||||
@@ -447,6 +448,7 @@ 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,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) {
|
||||
Character complexCharacter = applyCombiningCharacterToBaseCharacter(keyEvent.getUnicodeChar());
|
||||
keyEventChannel.keyUp(new KeyEventChannel.FlutterKeyEvent(keyEvent, complexCharacter));
|
||||
}
|
||||
|
||||
public void onKeyDown(@NonNull KeyEvent keyEvent) {
|
||||
if (textInputPlugin.getLastInputConnection() != null
|
||||
&& textInputPlugin.getInputMethodManager().isAcceptingText()) {
|
||||
textInputPlugin.getLastInputConnection().sendKeyEvent(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.keyDown(new KeyEventChannel.FlutterKeyEvent(keyEvent, complexCharacter));
|
||||
KeyEventChannel.FlutterKeyEvent flutterEvent =
|
||||
new KeyEventChannel.FlutterKeyEvent(keyEvent, complexCharacter, eventIdSerial++);
|
||||
keyEventChannel.keyUp(flutterEvent);
|
||||
eventResponder.addEvent(flutterEvent.eventId, keyEvent);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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()) {
|
||||
if (textInputPlugin.getLastInputConnection().sendKeyEvent(keyEvent)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
Character complexCharacter = applyCombiningCharacterToBaseCharacter(keyEvent.getUnicodeChar());
|
||||
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.dispatchKeyEvent(event);
|
||||
dispatchingKeyEvent = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -679,8 +679,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);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -700,8 +699,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);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -851,7 +849,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";
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<>(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 KeyEventChannel(@NonNull DartExecutor dartExecutor) {
|
||||
this.channel =
|
||||
new BasicMessageChannel<>(dartExecutor, "flutter/keyevent", JSONMessageCodec.INSTANCE);
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
@@ -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(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.RestorationChannelTest;
|
||||
import io.flutter.external.FlutterLaunchTests;
|
||||
import io.flutter.plugin.common.StandardMessageCodecTest;
|
||||
@@ -39,6 +41,8 @@ import test.io.flutter.embedding.engine.dart.DartExecutorTest;
|
||||
|
||||
@RunWith(Suite.class)
|
||||
@SuiteClasses({
|
||||
AccessibilityBridgeTest.class,
|
||||
AndroidKeyProcessorTest.class,
|
||||
DartExecutorTest.class,
|
||||
FlutterActivityAndFragmentDelegateTest.class,
|
||||
FlutterActivityTest.class,
|
||||
@@ -50,25 +54,25 @@ 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,
|
||||
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,
|
||||
RestorationChannelTest.class,
|
||||
})
|
||||
/** Runs all of the unit tests listed in the {@code @SuiteClasses} annotation. */
|
||||
public class FlutterTestSuite {}
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
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,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;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
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]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user