[Android KeyEvents] Split AndroidKeyProcessor into separate classes (flutter/engine#25628)

This commit is contained in:
LongCatIsLooong 2021-06-04 18:54:01 -07:00 committed by GitHub
parent 270cc23b0f
commit f2d4eff14c
19 changed files with 1176 additions and 942 deletions

View File

@ -783,7 +783,6 @@ FILE: ../../../flutter/shell/platform/android/io/flutter/app/FlutterApplication.
FILE: ../../../flutter/shell/platform/android/io/flutter/app/FlutterFragmentActivity.java
FILE: ../../../flutter/shell/platform/android/io/flutter/app/FlutterPlayStoreSplitApplication.java
FILE: ../../../flutter/shell/platform/android/io/flutter/app/FlutterPluginRegistry.java
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/AndroidKeyProcessor.java
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/AndroidTouchProcessor.java
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/DrawableSplashScreen.java
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/ExclusiveAppComponent.java
@ -799,6 +798,8 @@ FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/Flutt
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/FlutterSurfaceView.java
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/FlutterTextureView.java
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/FlutterView.java
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/KeyChannelResponder.java
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/KeyboardManager.java
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/MotionEventTracker.java
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/RenderMode.java
FILE: ../../../flutter/shell/platform/android/io/flutter/embedding/android/SplashScreen.java

View File

@ -128,7 +128,6 @@ android_java_sources = [
"io/flutter/app/FlutterFragmentActivity.java",
"io/flutter/app/FlutterPlayStoreSplitApplication.java",
"io/flutter/app/FlutterPluginRegistry.java",
"io/flutter/embedding/android/AndroidKeyProcessor.java",
"io/flutter/embedding/android/AndroidTouchProcessor.java",
"io/flutter/embedding/android/DrawableSplashScreen.java",
"io/flutter/embedding/android/ExclusiveAppComponent.java",
@ -144,6 +143,8 @@ android_java_sources = [
"io/flutter/embedding/android/FlutterSurfaceView.java",
"io/flutter/embedding/android/FlutterTextureView.java",
"io/flutter/embedding/android/FlutterView.java",
"io/flutter/embedding/android/KeyChannelResponder.java",
"io/flutter/embedding/android/KeyboardManager.java",
"io/flutter/embedding/android/MotionEventTracker.java",
"io/flutter/embedding/android/RenderMode.java",
"io/flutter/embedding/android/SplashScreen.java",
@ -458,13 +459,14 @@ action("robolectric_tests") {
"test/io/flutter/FlutterTestSuite.java",
"test/io/flutter/SmokeTest.java",
"test/io/flutter/TestUtils.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",
"test/io/flutter/embedding/android/FlutterFragmentActivityTest.java",
"test/io/flutter/embedding/android/FlutterFragmentTest.java",
"test/io/flutter/embedding/android/FlutterViewTest.java",
"test/io/flutter/embedding/android/KeyChannelResponderTest.java",
"test/io/flutter/embedding/android/KeyboardManagerTest.java",
"test/io/flutter/embedding/android/RobolectricFlutterActivity.java",
"test/io/flutter/embedding/engine/FlutterEngineCacheTest.java",
"test/io/flutter/embedding/engine/FlutterEngineConnectionRegistryTest.java",

View File

@ -1,275 +0,0 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package io.flutter.embedding.android;
import android.view.KeyCharacterMap;
import android.view.KeyEvent;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import io.flutter.Log;
import io.flutter.embedding.engine.systemchannels.KeyEventChannel;
import io.flutter.plugin.editing.TextInputPlugin;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Iterator;
/**
* 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";
@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 occurrence 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) {
this.keyEventChannel = keyEventChannel;
this.textInputPlugin = textInputPlugin;
textInputPlugin.setKeyEventProcessor(this);
this.eventResponder = new EventResponder(view, textInputPlugin);
this.keyEventChannel.setEventResponseHandler(eventResponder);
}
/**
* Detaches the key processor from the Flutter engine.
*
* <p>The AndroidKeyProcessor instance should not be used after calling this.
*/
public void destroy() {
keyEventChannel.setEventResponseHandler(null);
}
/**
* Called when a key event is received by the {@link FlutterView} or the {@link
* InputConnectionAdaptor}.
*
* @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 onKeyEvent(@NonNull KeyEvent keyEvent) {
int action = keyEvent.getAction();
if (action != KeyEvent.ACTION_DOWN && action != KeyEvent.ACTION_UP) {
// There is theoretically a KeyEvent.ACTION_MULTIPLE, but theoretically
// that isn't sent by Android anymore, so this is just for protection in
// case the theory is wrong.
return false;
}
if (isPendingEvent(keyEvent)) {
// If the keyEvent is in the queue of pending events we've seen, and has
// the same id, then we know that this is a re-dispatched keyEvent, and we
// shouldn't respond to it, but we should remove it from tracking now.
eventResponder.removePendingEvent(keyEvent);
return false;
}
Character complexCharacter = applyCombiningCharacterToBaseCharacter(keyEvent.getUnicodeChar());
KeyEventChannel.FlutterKeyEvent flutterEvent =
new KeyEventChannel.FlutterKeyEvent(keyEvent, complexCharacter);
eventResponder.addEvent(keyEvent);
if (action == KeyEvent.ACTION_DOWN) {
keyEventChannel.keyDown(flutterEvent);
} else {
keyEventChannel.keyUp(flutterEvent);
}
return true;
}
/**
* Returns whether or not the given event is currently being processed by this key processor. This
* is used to determine if a new key event sent to the {@link InputConnectionAdaptor} originates
* from a hardware key event, or a soft keyboard editing event.
*
* @param event the event to check for being the current event.
* @return
*/
public boolean isPendingEvent(@NonNull KeyEvent event) {
return eventResponder.findPendingEvent(event) != null;
}
/**
* Applies the given Unicode character in {@code newCharacterCodePoint} to a previously entered
* Unicode combining character and returns the combination of these characters if a combination
* exists.
*
* <p>This method mutates {@link #combiningCharacter} over time to combine characters.
*
* <p>One of the following things happens in this method:
*
* <ul>
* <li>If no previous {@link #combiningCharacter} exists and the {@code newCharacterCodePoint}
* is not a combining character, then {@code newCharacterCodePoint} is returned.
* <li>If no previous {@link #combiningCharacter} exists and the {@code newCharacterCodePoint}
* is a combining character, then {@code newCharacterCodePoint} is saved as the {@link
* #combiningCharacter} and null is returned.
* <li>If a previous {@link #combiningCharacter} exists and the {@code newCharacterCodePoint} is
* also a combining character, then the {@code newCharacterCodePoint} is combined with the
* existing {@link #combiningCharacter} and null is returned.
* <li>If a previous {@link #combiningCharacter} exists and the {@code newCharacterCodePoint} is
* not a combining character, then the {@link #combiningCharacter} is applied to the regular
* {@code newCharacterCodePoint} and the resulting complex character is returned. The {@link
* #combiningCharacter} is cleared.
* </ul>
*
* <p>The following reference explains the concept of a "combining character":
* https://en.wikipedia.org/wiki/Combining_character
*/
@Nullable
private Character applyCombiningCharacterToBaseCharacter(int newCharacterCodePoint) {
if (newCharacterCodePoint == 0) {
return null;
}
char complexCharacter = (char) newCharacterCodePoint;
boolean isNewCodePointACombiningCharacter =
(newCharacterCodePoint & KeyCharacterMap.COMBINING_ACCENT) != 0;
if (isNewCodePointACombiningCharacter) {
// If a combining character was entered before, combine this one with that one.
int plainCodePoint = newCharacterCodePoint & KeyCharacterMap.COMBINING_ACCENT_MASK;
if (combiningCharacter != 0) {
combiningCharacter = KeyCharacterMap.getDeadChar(combiningCharacter, plainCodePoint);
} else {
combiningCharacter = plainCodePoint;
}
} else {
// 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) {
complexCharacter = (char) combinedChar;
}
combiningCharacter = 0;
}
}
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<KeyEvent> pendingEvents = new ArrayDeque<KeyEvent>();
@NonNull private final View view;
@NonNull private final TextInputPlugin textInputPlugin;
public EventResponder(@NonNull View view, @NonNull TextInputPlugin textInputPlugin) {
this.view = view;
this.textInputPlugin = textInputPlugin;
}
/** Removes the first pending event from the cache of pending events. */
private void removePendingEvent(KeyEvent event) {
pendingEvents.remove(event);
}
private KeyEvent findPendingEvent(KeyEvent event) {
Iterator<KeyEvent> iter = pendingEvents.iterator();
while (iter.hasNext()) {
KeyEvent item = iter.next();
if (item == event) {
return item;
}
}
return null;
}
/**
* Called whenever the framework responds that a given key event was handled by the framework.
*
* @param event the event to be marked as being handled by the framework. Must not be null.
*/
@Override
public void onKeyEventHandled(KeyEvent event) {
removePendingEvent(event);
}
/**
* Called whenever the framework responds that a given key event wasn't handled by the
* framework.
*
* @param event the event to be marked as not being handled by the framework. Must not be null.
*/
@Override
public void onKeyEventNotHandled(KeyEvent event) {
redispatchKeyEvent(findPendingEvent(event));
}
/** Adds an Android key event to the event responder to wait for a response. */
public void addEvent(@NonNull KeyEvent event) {
pendingEvents.addLast(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.
*/
private void redispatchKeyEvent(KeyEvent event) {
// 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.getInputMethodManager().isAcceptingText()
&& textInputPlugin.getLastInputConnection() != null
&& textInputPlugin.getLastInputConnection().sendKeyEvent(event)) {
// The event was handled, so we can remove it from the queue.
removePendingEvent(event);
return;
}
// Since the framework didn't handle it, dispatch the event again.
if (view != null) {
view.getRootView().dispatchKeyEvent(event);
}
}
}
}

View File

@ -104,7 +104,7 @@ public class FlutterView extends FrameLayout implements MouseCursorPlugin.MouseC
@Nullable private MouseCursorPlugin mouseCursorPlugin;
@Nullable private TextInputPlugin textInputPlugin;
@Nullable private LocalizationPlugin localizationPlugin;
@Nullable private AndroidKeyProcessor androidKeyProcessor;
@Nullable private KeyboardManager keyboardManager;
@Nullable private AndroidTouchProcessor androidTouchProcessor;
@Nullable private AccessibilityBridge accessibilityBridge;
@ -705,7 +705,7 @@ public class FlutterView extends FrameLayout implements MouseCursorPlugin.MouseC
return super.onCreateInputConnection(outAttrs);
}
return textInputPlugin.createInputConnection(this, outAttrs);
return textInputPlugin.createInputConnection(this, keyboardManager, outAttrs);
}
/**
@ -730,7 +730,7 @@ public class FlutterView extends FrameLayout implements MouseCursorPlugin.MouseC
* D-pad button. It is generally not invoked when a virtual software keyboard is used, though a
* software keyboard may choose to invoke this method in some situations.
*
* <p>{@link KeyEvent}s are sent from Android to Flutter. {@link AndroidKeyProcessor} may do some
* <p>{@link KeyEvent}s are sent from Android to Flutter. {@link KeyboardManager} may do some
* additional work with the given {@link KeyEvent}, e.g., combine this {@code keyCode} with the
* previous {@code keyCode} to generate a unicode combined character.
*/
@ -747,7 +747,7 @@ public class FlutterView extends FrameLayout implements MouseCursorPlugin.MouseC
// superclass. The key processor will typically handle all events except
// those where it has re-dispatched the event after receiving a reply from
// the framework that the framework did not handle it.
return (isAttachedToFlutterEngine() && androidKeyProcessor.onKeyEvent(event))
return (isAttachedToFlutterEngine() && keyboardManager.handleEvent(event))
|| super.dispatchKeyEvent(event);
}
@ -975,8 +975,14 @@ public class FlutterView extends FrameLayout implements MouseCursorPlugin.MouseC
this.flutterEngine.getTextInputChannel(),
this.flutterEngine.getPlatformViewsController());
localizationPlugin = this.flutterEngine.getLocalizationPlugin();
androidKeyProcessor =
new AndroidKeyProcessor(this, this.flutterEngine.getKeyEventChannel(), textInputPlugin);
keyboardManager =
new KeyboardManager(
this,
textInputPlugin,
new KeyChannelResponder[] {
new KeyChannelResponder(flutterEngine.getKeyEventChannel())
});
androidTouchProcessor =
new AndroidTouchProcessor(this.flutterEngine.getRenderer(), /*trackMotionEvents=*/ false);
accessibilityBridge =
@ -1060,8 +1066,7 @@ public class FlutterView extends FrameLayout implements MouseCursorPlugin.MouseC
// TODO(mattcarroll): once this is proven to work, move this line ot TextInputPlugin
textInputPlugin.getInputMethodManager().restartInput(this);
textInputPlugin.destroy();
androidKeyProcessor.destroy();
keyboardManager.destroy();
if (mouseCursorPlugin != null) {
mouseCursorPlugin.destroy();

View File

@ -0,0 +1,105 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package io.flutter.embedding.android;
import android.view.KeyCharacterMap;
import android.view.KeyEvent;
import androidx.annotation.NonNull;
import io.flutter.embedding.engine.systemchannels.KeyEventChannel;
/**
* A {@link Responder} of {@link KeyboardManager} that handles events by sending the raw information
* through the method channel.
*
* <p>This class corresponds to the RawKeyboard API in the framework.
*/
public class KeyChannelResponder implements KeyboardManager.Responder {
private static final String TAG = "KeyChannelResponder";
@NonNull private final KeyEventChannel keyEventChannel;
private int combiningCharacter;
public KeyChannelResponder(@NonNull KeyEventChannel keyEventChannel) {
this.keyEventChannel = keyEventChannel;
}
/**
* Applies the given Unicode character in {@code newCharacterCodePoint} to a previously entered
* Unicode combining character and returns the combination of these characters if a combination
* exists.
*
* <p>This method mutates {@link #combiningCharacter} over time to combine characters.
*
* <p>One of the following things happens in this method:
*
* <ul>
* <li>If no previous {@link #combiningCharacter} exists and the {@code newCharacterCodePoint}
* is not a combining character, then {@code newCharacterCodePoint} is returned.
* <li>If no previous {@link #combiningCharacter} exists and the {@code newCharacterCodePoint}
* is a combining character, then {@code newCharacterCodePoint} is saved as the {@link
* #combiningCharacter} and null is returned.
* <li>If a previous {@link #combiningCharacter} exists and the {@code newCharacterCodePoint} is
* also a combining character, then the {@code newCharacterCodePoint} is combined with the
* existing {@link #combiningCharacter} and null is returned.
* <li>If a previous {@link #combiningCharacter} exists and the {@code newCharacterCodePoint} is
* not a combining character, then the {@link #combiningCharacter} is applied to the regular
* {@code newCharacterCodePoint} and the resulting complex character is returned. The {@link
* #combiningCharacter} is cleared.
* </ul>
*
* <p>The following reference explains the concept of a "combining character":
* https://en.wikipedia.org/wiki/Combining_character
*/
Character applyCombiningCharacterToBaseCharacter(int newCharacterCodePoint) {
char complexCharacter = (char) newCharacterCodePoint;
boolean isNewCodePointACombiningCharacter =
(newCharacterCodePoint & KeyCharacterMap.COMBINING_ACCENT) != 0;
if (isNewCodePointACombiningCharacter) {
// If a combining character was entered before, combine this one with that one.
int plainCodePoint = newCharacterCodePoint & KeyCharacterMap.COMBINING_ACCENT_MASK;
if (combiningCharacter != 0) {
combiningCharacter = KeyCharacterMap.getDeadChar(combiningCharacter, plainCodePoint);
} else {
combiningCharacter = plainCodePoint;
}
} else {
// 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) {
complexCharacter = (char) combinedChar;
}
combiningCharacter = 0;
}
}
return complexCharacter;
}
@Override
public void handleEvent(
@NonNull KeyEvent keyEvent, @NonNull OnKeyEventHandledCallback onKeyEventHandledCallback) {
final int action = keyEvent.getAction();
if (action != KeyEvent.ACTION_DOWN && action != KeyEvent.ACTION_UP) {
// There is theoretically a KeyEvent.ACTION_MULTIPLE, but theoretically
// that isn't sent by Android anymore, so this is just for protection in
// case the theory is wrong.
onKeyEventHandledCallback.onKeyEventHandled(false);
return;
}
final Character complexCharacter =
applyCombiningCharacterToBaseCharacter(keyEvent.getUnicodeChar());
KeyEventChannel.FlutterKeyEvent flutterEvent =
new KeyEventChannel.FlutterKeyEvent(keyEvent, complexCharacter);
final boolean isKeyUp = action != KeyEvent.ACTION_DOWN;
keyEventChannel.sendFlutterKeyEvent(
flutterEvent,
isKeyUp,
(isEventHandled) -> onKeyEventHandledCallback.onKeyEventHandled(isEventHandled));
}
}

View File

@ -0,0 +1,194 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package io.flutter.embedding.android;
import android.view.KeyEvent;
import android.view.View;
import androidx.annotation.NonNull;
import io.flutter.Log;
import io.flutter.embedding.android.KeyboardManager.Responder.OnKeyEventHandledCallback;
import io.flutter.plugin.editing.TextInputPlugin;
import java.util.HashSet;
/**
* A class to process {@link KeyEvent}s dispatched to a {@link FlutterView}, either from a hardware
* keyboard or an IME event.
*
* <p>A class that sends Android {@link KeyEvent} to the a list of {@link Responder}s, and
* re-dispatches those not handled by the primary responders.
*
* <p>Flutter uses asynchronous event handling to avoid blocking the UI thread, but Android requires
* that events are handled synchronously. So, when the Android system sends new @{link KeyEvent} to
* Flutter, Flutter responds synchronously that the key has been handled so that it won't propagate
* to other components. It 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.
*
* <p>A new {@link KeyEvent} sent to a {@link KeyboardManager} can be propagated to 3 different
* types of responders (in the listed order):
*
* <ul>
* <li>{@link Responder}s: An immutable list of key responders in a {@link KeyboardManager} that
* each implements the {@link Responder} interface. A {@link Responder} is a key responder
* that's capable of handling {@link KeyEvent}s asynchronously.
* <p>When a new {@link KeyEvent} is received, {@link KeyboardManager} calls the {@link
* Responder#handleEvent(KeyEvent, OnKeyEventHandledCallback)} method on its {@link
* Responder}s. Each {@link Responder} must call the supplied {@link
* OnKeyEventHandledCallback} exactly once, when it has decided whether to handle the key
* event callback. More than one {@link Responder} is allowed to reply true and handle the
* same {@link KeyEvent}.
* <p>Typically a {@link KeyboardManager} uses a {@link KeyChannelResponder} as its only
* {@link Responder}.
* <li>{@link TextInputPlugin}: if every {@link Responder} has replied false to a {@link
* KeyEvent}, or if the {@link KeyboardManager} has zero {@link Responder}s, the {@link
* KeyEvent} will be sent to the currently focused editable text field in {@link
* TextInputPlugin}, if any.
* <li><b>"Redispatch"</b>: if there's no currently focused text field in {@link TextInputPlugin},
* or the text field does not handle the {@link KeyEvent} either, the {@link KeyEvent} will be
* sent back to the top of the activity's view hierachy, allowing it to be "redispatched",
* only this time the {@link KeyboardManager} will not try to handle the redispatched {@link
* KeyEvent}.
* </ul>
*/
public class KeyboardManager {
private static final String TAG = "KeyboardManager";
/**
* Constructor for {@link KeyboardManager} that takes a list of {@link Responder}s.
*
* <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 {@link Responder}s return 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 occurrence 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 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.
* @param responders the {@link Responder}s new {@link KeyEvent}s will be first dispatched to.
*/
public KeyboardManager(
View view, @NonNull TextInputPlugin textInputPlugin, Responder[] responders) {
this.view = view;
this.textInputPlugin = textInputPlugin;
this.responders = responders;
}
/**
* The interface for responding to a {@link KeyEvent} asynchronously.
*
* <p>Implementers of this interface should be owned by a {@link KeyboardManager}, in order to
* receive key events.
*
* <p>After receiving a {@link KeyEvent}, the {@link Responder} must call the supplied {@link
* OnKeyEventHandledCallback} exactly once, to inform the {@link KeyboardManager} whether it
* wishes to handle the {@link KeyEvent}. The {@link KeyEvent} will not be propagated to the
* {@link TextInputPlugin} or be redispatched to the view hierachy if any key responders answered
* yes.
*
* <p>If a {@link Responder} fails to call the {@link OnKeyEventHandledCallback} callback, the
* {@link KeyEvent} will never be sent to the {@link TextInputPlugin}, and the {@link
* KeyboardManager} class can't detect such errors as there is no timeout.
*/
interface Responder {
interface OnKeyEventHandledCallback {
void onKeyEventHandled(Boolean canHandleEvent);
}
/**
* Informs this {@link Responder} that a new {@link KeyEvent} needs processing.
*
* @param keyEvent the new {@link KeyEvent} this {@link Responder} may be interested in.
* @param onKeyEventHandledCallback the method to call when this {@link Responder} has decided
* whether to handle the {@link keyEvent}.
*/
void handleEvent(
@NonNull KeyEvent keyEvent, @NonNull OnKeyEventHandledCallback onKeyEventHandledCallback);
}
private class PerEventCallbackBuilder {
private class Callback implements OnKeyEventHandledCallback {
boolean isCalled = false;
@Override
public void onKeyEventHandled(Boolean canHandleEvent) {
if (isCalled) {
throw new IllegalStateException(
"The onKeyEventHandledCallback should be called exactly once.");
}
isCalled = true;
unrepliedCount -= 1;
isEventHandled |= canHandleEvent;
if (unrepliedCount == 0 && !isEventHandled) {
onUnhandled(keyEvent);
}
}
}
PerEventCallbackBuilder(@NonNull KeyEvent keyEvent) {
this.keyEvent = keyEvent;
}
@NonNull final KeyEvent keyEvent;
int unrepliedCount = responders.length;
boolean isEventHandled = false;
public OnKeyEventHandledCallback buildCallback() {
return new Callback();
}
}
@NonNull protected final Responder[] responders;
@NonNull private final HashSet<KeyEvent> redispatchedEvents = new HashSet<>();
@NonNull private final TextInputPlugin textInputPlugin;
private final View view;
public boolean handleEvent(@NonNull KeyEvent keyEvent) {
final boolean isRedispatchedEvent = redispatchedEvents.remove(keyEvent);
if (isRedispatchedEvent) {
return false;
}
if (responders.length > 0) {
final PerEventCallbackBuilder callbackBuilder = new PerEventCallbackBuilder(keyEvent);
for (final Responder primaryResponder : responders) {
primaryResponder.handleEvent(keyEvent, callbackBuilder.buildCallback());
}
} else {
onUnhandled(keyEvent);
}
return true;
}
public void destroy() {
final int remainingRedispatchCount = redispatchedEvents.size();
if (remainingRedispatchCount > 0) {
Log.w(
TAG,
"A KeyboardManager was destroyed with "
+ String.valueOf(remainingRedispatchCount)
+ " unhandled redispatch event(s).");
}
}
private void onUnhandled(@NonNull KeyEvent keyEvent) {
if (textInputPlugin.handleKeyEvent(keyEvent) || view == null) {
return;
}
redispatchedEvents.add(keyEvent);
view.getRootView().dispatchKeyEvent(keyEvent);
if (redispatchedEvents.remove(keyEvent)) {
Log.w(TAG, "A redispatched key event was consumed before reaching KeyboardManager");
}
}
}

View File

@ -27,33 +27,16 @@ import org.json.JSONObject;
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.
* Called whenever the framework responds that a given key event was handled or not handled by
* the framework.
*
* @param event the event to be marked as being handled by the framework. Must not be null.
* @param isEventHandled whether the framework decides to handle the event.
*/
public void onKeyEventHandled(KeyEvent event);
/**
* Called whenever the framework responds that a given key event wasn't handled by the
* framework.
*
* @param event the event to be marked as not being handled by the framework. Must not be null.
*/
public void onKeyEventNotHandled(KeyEvent event);
public void onFrameworkResponse(boolean isEventHandled);
}
/**
@ -66,58 +49,19 @@ public class KeyEventChannel {
new BasicMessageChannel<>(binaryMessenger, "flutter/keyevent", JSONMessageCodec.INSTANCE);
}
/**
* Creates a reply handler for the given key event.
*
* @param event the Android key event to create a reply for.
*/
BasicMessageChannel.Reply<Object> createReplyHandler(KeyEvent event) {
return message -> {
if (eventResponseHandler == null) {
return;
}
try {
if (message == null) {
eventResponseHandler.onKeyEventNotHandled(event);
return;
}
final JSONObject annotatedEvent = (JSONObject) message;
final boolean handled = annotatedEvent.getBoolean("handled");
if (handled) {
eventResponseHandler.onKeyEventHandled(event);
} else {
eventResponseHandler.onKeyEventNotHandled(event);
}
} catch (JSONException e) {
Log.e(TAG, "Unable to unpack JSON message: " + e);
eventResponseHandler.onKeyEventNotHandled(event);
}
};
}
@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.event));
public void sendFlutterKeyEvent(
@NonNull FlutterKeyEvent keyEvent,
boolean isKeyUp,
@NonNull EventResponseHandler responseHandler) {
channel.send(encodeKeyEvent(keyEvent, isKeyUp), createReplyHandler(responseHandler));
}
public void keyDown(@NonNull FlutterKeyEvent keyEvent) {
private Map<String, Object> encodeKeyEvent(@NonNull FlutterKeyEvent keyEvent, boolean isKeyUp) {
Map<String, Object> message = new HashMap<>();
message.put("type", "keydown");
message.put("type", isKeyUp ? "keyup" : "keydown");
message.put("keymap", "android");
encodeKeyEvent(keyEvent, message);
channel.send(message, createReplyHandler(keyEvent.event));
}
private void encodeKeyEvent(
@NonNull FlutterKeyEvent keyEvent, @NonNull Map<String, Object> message) {
message.put("flags", keyEvent.event.getFlags());
message.put("plainCodePoint", keyEvent.event.getUnicodeChar(0x0));
message.put("codePoint", keyEvent.event.getUnicodeChar());
@ -141,6 +85,28 @@ public class KeyEventChannel {
message.put("productId", productId);
message.put("deviceId", keyEvent.event.getDeviceId());
message.put("repeatCount", keyEvent.event.getRepeatCount());
return message;
}
/**
* Creates a reply handler for the given key event.
*
* @param responseHandler the completion handler to call when the framework responds.
*/
private static BasicMessageChannel.Reply<Object> createReplyHandler(
@NonNull EventResponseHandler responseHandler) {
return message -> {
boolean isEventHandled = false;
try {
if (message != null) {
final JSONObject annotatedEvent = (JSONObject) message;
isEventHandled = annotatedEvent.getBoolean("handled");
}
} catch (JSONException e) {
Log.e(TAG, "Unable to unpack JSON message: " + e);
}
responseHandler.onFrameworkResponse(isEventHandled);
};
}
/** A key event as defined by Flutter. */

View File

@ -27,7 +27,7 @@ import android.view.inputmethod.ExtractedTextRequest;
import android.view.inputmethod.InputMethodManager;
import android.view.inputmethod.InputMethodSubtype;
import io.flutter.Log;
import io.flutter.embedding.android.AndroidKeyProcessor;
import io.flutter.embedding.android.KeyboardManager;
import io.flutter.embedding.engine.FlutterJNI;
import io.flutter.embedding.engine.systemchannels.TextInputChannel;
@ -38,7 +38,6 @@ class InputConnectionAdaptor extends BaseInputConnection
private final View mFlutterView;
private final int mClient;
private final TextInputChannel textInputChannel;
private final AndroidKeyProcessor keyProcessor;
private final ListenableEditingState mEditable;
private final EditorInfo mEditorInfo;
private ExtractedTextRequest mExtractRequest;
@ -48,13 +47,14 @@ class InputConnectionAdaptor extends BaseInputConnection
private InputMethodManager mImm;
private final Layout mLayout;
private FlutterTextUtils flutterTextUtils;
private final KeyboardManager keyboardManager;
@SuppressWarnings("deprecation")
public InputConnectionAdaptor(
View view,
int client,
TextInputChannel textInputChannel,
AndroidKeyProcessor keyProcessor,
KeyboardManager keyboardManager,
ListenableEditingState editable,
EditorInfo editorInfo,
FlutterJNI flutterJNI) {
@ -65,7 +65,7 @@ class InputConnectionAdaptor extends BaseInputConnection
mEditable = editable;
mEditable.addEditingStateListener(this);
mEditorInfo = editorInfo;
this.keyProcessor = keyProcessor;
this.keyboardManager = keyboardManager;
this.flutterTextUtils = new FlutterTextUtils(flutterJNI);
// We create a dummy Layout with max width so that the selection
// shifting acts as if all text were in one line.
@ -85,10 +85,10 @@ class InputConnectionAdaptor extends BaseInputConnection
View view,
int client,
TextInputChannel textInputChannel,
AndroidKeyProcessor keyProcessor,
KeyboardManager keyboardManager,
ListenableEditingState editable,
EditorInfo editorInfo) {
this(view, client, textInputChannel, keyProcessor, editable, editorInfo, new FlutterJNI());
this(view, client, textInputChannel, keyboardManager, editable, editorInfo, new FlutterJNI());
}
private ExtractedText getExtractedText(ExtractedTextRequest request) {
@ -290,20 +290,10 @@ class InputConnectionAdaptor extends BaseInputConnection
// occur, and need a chance to be handled by the framework.
@Override
public boolean sendKeyEvent(KeyEvent event) {
// This gives the key processor a chance to process this event if it came
// from a soft keyboard. It will send it to the framework to be handled and
// return true. If the framework ends up not handling it, the processor will
// re-send the event to this function. Only do this if the event is not the
// current event, since that indicates that the key processor sent it to us,
// and we only want to call the key processor for events that it doesn't
// already know about (i.e. when events arrive here from a soft keyboard and
// not a hardware keyboard), to avoid a loop.
if (keyProcessor != null
&& !keyProcessor.isPendingEvent(event)
&& keyProcessor.onKeyEvent(event)) {
return true;
}
return keyboardManager.handleEvent(event);
}
public boolean handleKeyEvent(KeyEvent event) {
if (event.getAction() == KeyEvent.ACTION_DOWN) {
if (event.getKeyCode() == KeyEvent.KEYCODE_DPAD_LEFT) {
return handleHorizontalMovement(true, event.isShiftPressed());

View File

@ -12,6 +12,7 @@ import android.os.Bundle;
import android.text.Editable;
import android.text.InputType;
import android.util.SparseArray;
import android.view.KeyEvent;
import android.view.View;
import android.view.ViewStructure;
import android.view.WindowInsets;
@ -25,7 +26,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import io.flutter.Log;
import io.flutter.embedding.android.AndroidKeyProcessor;
import io.flutter.embedding.android.KeyboardManager;
import io.flutter.embedding.engine.systemchannels.TextInputChannel;
import io.flutter.embedding.engine.systemchannels.TextInputChannel.TextEditState;
import io.flutter.plugin.platform.PlatformViewsController;
@ -48,7 +49,6 @@ public class TextInputPlugin implements ListenableEditingState.EditingStateWatch
@NonNull private PlatformViewsController platformViewsController;
@Nullable private Rect lastClientRect;
private ImeSyncDeferringInsetsCallback imeSyncCallback;
private AndroidKeyProcessor keyProcessor;
// Initialize the "last seen" text editing values to a non-null value.
private TextEditState mLastKnownFrameworkTextEditingState;
@ -175,15 +175,6 @@ public class TextInputPlugin implements ListenableEditingState.EditingStateWatch
return imeSyncCallback;
}
@NonNull
public AndroidKeyProcessor getKeyEventProcessor() {
return keyProcessor;
}
public void setKeyEventProcessor(AndroidKeyProcessor processor) {
keyProcessor = processor;
}
/**
* Use the current platform view input connection until unlockPlatformViewInputConnection is
* called.
@ -286,7 +277,8 @@ public class TextInputPlugin implements ListenableEditingState.EditingStateWatch
return textType;
}
public InputConnection createInputConnection(View view, EditorInfo outAttrs) {
public InputConnection createInputConnection(
View view, KeyboardManager keyboardManager, EditorInfo outAttrs) {
if (inputTarget.type == InputTarget.Type.NO_TARGET) {
lastInputConnection = null;
return null;
@ -330,7 +322,7 @@ public class TextInputPlugin implements ListenableEditingState.EditingStateWatch
InputConnectionAdaptor connection =
new InputConnectionAdaptor(
view, inputTarget.id, textInputChannel, keyProcessor, mEditable, outAttrs);
view, inputTarget.id, textInputChannel, keyboardManager, mEditable, outAttrs);
outAttrs.initialSelStart = mEditable.getSelectionStart();
outAttrs.initialSelEnd = mEditable.getSelectionEnd();
@ -557,6 +549,23 @@ public class TextInputPlugin implements ListenableEditingState.EditingStateWatch
int id;
}
// -------- Start: KeyboardManager Synchronous Responder -------
public boolean handleKeyEvent(KeyEvent keyEvent) {
if (!getInputMethodManager().isAcceptingText() || lastInputConnection == null) {
return false;
}
// Send the KeyEvent as an IME KeyEvent. If the input connection is an
// InputConnectionAdaptor then call its handleKeyEvent method (because
// this method will be called by the keyboard manager, and
// InputConnectionAdaptor#sendKeyEvent forwards the key event back to the
// keyboard manager).
return (lastInputConnection instanceof InputConnectionAdaptor)
? ((InputConnectionAdaptor) lastInputConnection).handleKeyEvent(keyEvent)
: lastInputConnection.sendKeyEvent(keyEvent);
}
// -------- End: KeyboardManager Synchronous Responder -------
// -------- Start: ListenableEditingState watcher implementation -------
@Override

View File

@ -42,8 +42,9 @@ import androidx.annotation.RequiresApi;
import androidx.annotation.UiThread;
import io.flutter.Log;
import io.flutter.app.FlutterPluginRegistry;
import io.flutter.embedding.android.AndroidKeyProcessor;
import io.flutter.embedding.android.AndroidTouchProcessor;
import io.flutter.embedding.android.KeyChannelResponder;
import io.flutter.embedding.android.KeyboardManager;
import io.flutter.embedding.engine.dart.DartExecutor;
import io.flutter.embedding.engine.renderer.FlutterRenderer;
import io.flutter.embedding.engine.renderer.SurfaceTextureWrapper;
@ -127,7 +128,7 @@ public class FlutterView extends SurfaceView
private final TextInputPlugin mTextInputPlugin;
private final LocalizationPlugin mLocalizationPlugin;
private final MouseCursorPlugin mMouseCursorPlugin;
private final AndroidKeyProcessor androidKeyProcessor;
private final KeyboardManager mKeyboardManager;
private final AndroidTouchProcessor androidTouchProcessor;
private AccessibilityBridge mAccessibilityNodeProvider;
private final SurfaceHolder.Callback mSurfaceCallback;
@ -228,13 +229,18 @@ public class FlutterView extends SurfaceView
mNativeView.getPluginRegistry().getPlatformViewsController();
mTextInputPlugin =
new TextInputPlugin(this, new TextInputChannel(dartExecutor), platformViewsController);
mKeyboardManager =
new KeyboardManager(
this,
mTextInputPlugin,
new KeyChannelResponder[] {new KeyChannelResponder(keyEventChannel)});
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
mMouseCursorPlugin = new MouseCursorPlugin(this, new MouseCursorChannel(dartExecutor));
} else {
mMouseCursorPlugin = null;
}
mLocalizationPlugin = new LocalizationPlugin(context, localizationChannel);
androidKeyProcessor = new AndroidKeyProcessor(this, keyEventChannel, mTextInputPlugin);
androidTouchProcessor =
new AndroidTouchProcessor(flutterRenderer, /*trackMotionEvents=*/ false);
platformViewsController.attachToFlutterRenderer(flutterRenderer);
@ -282,7 +288,7 @@ public class FlutterView extends SurfaceView
// superclass. The key processor will typically handle all events except
// those where it has re-dispatched the event after receiving a reply from
// the framework that the framework did not handle it.
return (isAttached() && androidKeyProcessor.onKeyEvent(event)) || super.dispatchKeyEvent(event);
return (isAttached() && mKeyboardManager.handleEvent(event)) || super.dispatchKeyEvent(event);
}
public FlutterNativeView getFlutterNativeView() {
@ -442,7 +448,7 @@ public class FlutterView extends SurfaceView
@Override
public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
return mTextInputPlugin.createInputConnection(this, outAttrs);
return mTextInputPlugin.createInputConnection(this, mKeyboardManager, outAttrs);
}
@Override

View File

@ -4,13 +4,14 @@
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;
import io.flutter.embedding.android.FlutterFragmentActivityTest;
import io.flutter.embedding.android.FlutterFragmentTest;
import io.flutter.embedding.android.FlutterViewTest;
import io.flutter.embedding.android.KeyChannelResponderTest;
import io.flutter.embedding.android.KeyboardManagerTest;
import io.flutter.embedding.engine.FlutterEngineCacheTest;
import io.flutter.embedding.engine.FlutterEngineConnectionRegistryTest;
import io.flutter.embedding.engine.FlutterEngineGroupComponentTest;
@ -52,7 +53,6 @@ import test.io.flutter.embedding.engine.PluginComponentTest;
@RunWith(Suite.class)
@SuiteClasses({
AccessibilityBridgeTest.class,
AndroidKeyProcessorTest.class,
ApplicationInfoLoaderTest.class,
BinaryCodecTest.class,
DartExecutorTest.class,
@ -77,6 +77,8 @@ import test.io.flutter.embedding.engine.PluginComponentTest;
FlutterViewTest.class,
InputConnectionAdaptorTest.class,
DeferredComponentChannelTest.class,
KeyboardManagerTest.class,
KeyChannelResponderTest.class,
KeyEventChannelTest.class,
ListenableEditingStateTest.class,
LocalizationPluginTest.class,

View File

@ -1,309 +0,0 @@
package io.flutter.embedding.android;
import static junit.framework.TestCase.assertEquals;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.isNull;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.notNull;
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.onKeyEvent(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));
}
@Test
public void destroyTest() {
FlutterEngine flutterEngine = mockFlutterEngine();
KeyEventChannel fakeKeyEventChannel = flutterEngine.getKeyEventChannel();
View fakeView = mock(View.class);
AndroidKeyProcessor processor =
new AndroidKeyProcessor(fakeView, fakeKeyEventChannel, mock(TextInputPlugin.class));
verify(fakeKeyEventChannel, times(1))
.setEventResponseHandler(notNull(KeyEventChannel.EventResponseHandler.class));
processor.destroy();
verify(fakeKeyEventChannel, times(1))
.setEventResponseHandler(isNull(KeyEventChannel.EventResponseHandler.class));
}
public void removesPendingEventsWhenKeyDownHandled() {
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.onKeyEvent(fakeKeyEvent);
assertEquals(true, processor.isPendingEvent(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.onKeyEvent(event);
return dispatchResult[0];
}
});
// Fake a response from the framework.
handlerCaptor.getValue().onKeyEventHandled(eventCaptor.getValue().event);
assertEquals(false, processor.isPendingEvent(fakeKeyEvent));
}
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.onKeyEvent(fakeKeyEvent);
assertEquals(true, processor.isPendingEvent(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.onKeyEvent(event);
return dispatchResult[0];
}
});
// Fake a response from the framework.
handlerCaptor.getValue().onKeyEventNotHandled(eventCaptor.getValue().event);
assertEquals(true, processor.isPendingEvent(fakeKeyEvent));
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.onKeyEvent(fakeKeyEvent);
assertEquals(true, processor.isPendingEvent(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.onKeyEvent(event);
return dispatchResult[0];
}
});
// Fake a response from the framework.
handlerCaptor.getValue().onKeyEventNotHandled(eventCaptor.getValue().event);
assertEquals(true, processor.isPendingEvent(fakeKeyEvent));
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 respondsCorrectlyWhenEventsAreReturnedOutOfOrder() {
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> event1Captor =
ArgumentCaptor.forClass(KeyEventChannel.FlutterKeyEvent.class);
ArgumentCaptor<KeyEventChannel.FlutterKeyEvent> event2Captor =
ArgumentCaptor.forClass(KeyEventChannel.FlutterKeyEvent.class);
FakeKeyEvent fakeKeyEvent1 = new FakeKeyEvent(KeyEvent.ACTION_DOWN, 65);
FakeKeyEvent fakeKeyEvent2 = new FakeKeyEvent(KeyEvent.ACTION_DOWN, 20);
boolean result1 = processor.onKeyEvent(fakeKeyEvent1);
boolean result2 = processor.onKeyEvent(fakeKeyEvent2);
assertEquals(true, processor.isPendingEvent(fakeKeyEvent1));
assertEquals(true, processor.isPendingEvent(fakeKeyEvent2));
assertEquals(true, result1);
assertEquals(true, result2);
// Capture the FlutterKeyEvent so we can find out its event ID to use when
// faking our response.
verify(fakeKeyEventChannel, times(1)).keyDown(event1Captor.capture());
verify(fakeKeyEventChannel, times(1)).keyDown(event2Captor.capture());
boolean[] dispatchResult = {true, 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(true, fakeKeyEvent1 == event || fakeKeyEvent2 == event);
if (fakeKeyEvent1 == event) {
dispatchResult[0] = processor.onKeyEvent(fakeKeyEvent1);
return dispatchResult[0];
} else {
dispatchResult[1] = processor.onKeyEvent(fakeKeyEvent2);
return dispatchResult[1];
}
}
});
assertEquals(true, processor.isPendingEvent(fakeKeyEvent1));
assertEquals(true, processor.isPendingEvent(fakeKeyEvent2));
// Fake a "handled" response from the framework, but do it in reverse order.
handlerCaptor.getValue().onKeyEventNotHandled(event2Captor.getValue().event);
handlerCaptor.getValue().onKeyEventNotHandled(event1Captor.getValue().event);
verify(fakeView, times(1)).dispatchKeyEvent(fakeKeyEvent1);
verify(fakeView, times(1)).dispatchKeyEvent(fakeKeyEvent2);
assertEquals(false, dispatchResult[0]);
assertEquals(false, dispatchResult[1]);
verify(fakeKeyEventChannel, times(0)).keyUp(any(KeyEventChannel.FlutterKeyEvent.class));
verify(fakeRootView, times(1)).dispatchKeyEvent(fakeKeyEvent1);
verify(fakeRootView, times(1)).dispatchKeyEvent(fakeKeyEvent2);
}
@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;
}
}

View File

@ -0,0 +1,81 @@
package io.flutter.embedding.android;
import static junit.framework.TestCase.assertEquals;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.doAnswer;
import android.annotation.TargetApi;
import android.view.KeyCharacterMap;
import android.view.KeyEvent;
import io.flutter.embedding.engine.systemchannels.KeyEventChannel;
import io.flutter.embedding.engine.systemchannels.KeyEventChannel.EventResponseHandler;
import io.flutter.embedding.engine.systemchannels.KeyEventChannel.FlutterKeyEvent;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
@Config(manifest = Config.NONE)
@RunWith(RobolectricTestRunner.class)
@TargetApi(28)
public class KeyChannelResponderTest {
private static final int DEAD_KEY = '`' | KeyCharacterMap.COMBINING_ACCENT;
@Mock KeyEventChannel keyEventChannel;
KeyChannelResponder channelResponder;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
channelResponder = new KeyChannelResponder(keyEventChannel);
}
@Test
public void primaryResponderTest() {
final int[] completionCallbackInvocationCounter = {0};
doAnswer(
invocation -> {
invocation.getArgumentAt(2, EventResponseHandler.class).onFrameworkResponse(true);
return null;
})
.when(keyEventChannel)
.sendFlutterKeyEvent(
any(FlutterKeyEvent.class), any(boolean.class), any(EventResponseHandler.class));
final KeyEvent keyEvent = new KeyEvent(KeyEvent.ACTION_DOWN, 65);
channelResponder.handleEvent(
keyEvent,
(canHandleEvent) -> {
completionCallbackInvocationCounter[0]++;
});
assertEquals(completionCallbackInvocationCounter[0], 1);
}
@Test
public void basicCombingCharactersTest() {
assertEquals(0, (int) channelResponder.applyCombiningCharacterToBaseCharacter(0));
assertEquals('A', (int) channelResponder.applyCombiningCharacterToBaseCharacter('A'));
assertEquals('B', (int) channelResponder.applyCombiningCharacterToBaseCharacter('B'));
assertEquals('B', (int) channelResponder.applyCombiningCharacterToBaseCharacter('B'));
assertEquals(0, (int) channelResponder.applyCombiningCharacterToBaseCharacter(0));
assertEquals(0, (int) channelResponder.applyCombiningCharacterToBaseCharacter(0));
assertEquals('`', (int) channelResponder.applyCombiningCharacterToBaseCharacter(DEAD_KEY));
assertEquals('`', (int) channelResponder.applyCombiningCharacterToBaseCharacter(DEAD_KEY));
assertEquals('À', (int) channelResponder.applyCombiningCharacterToBaseCharacter('A'));
assertEquals('`', (int) channelResponder.applyCombiningCharacterToBaseCharacter(DEAD_KEY));
assertEquals(0, (int) channelResponder.applyCombiningCharacterToBaseCharacter(0));
// The 0 input should remove the combining state.
assertEquals('A', (int) channelResponder.applyCombiningCharacterToBaseCharacter('A'));
assertEquals(0, (int) channelResponder.applyCombiningCharacterToBaseCharacter(0));
assertEquals('`', (int) channelResponder.applyCombiningCharacterToBaseCharacter(DEAD_KEY));
assertEquals('À', (int) channelResponder.applyCombiningCharacterToBaseCharacter('A'));
}
}

View File

@ -0,0 +1,303 @@
package io.flutter.embedding.android;
import static junit.framework.TestCase.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
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.android.KeyboardManager.Responder;
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.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
@Config(manifest = Config.NONE)
@RunWith(RobolectricTestRunner.class)
@TargetApi(28)
public class KeyboardManagerTest {
static class FakeResponder implements Responder {
KeyEvent mLastKeyEvent;
OnKeyEventHandledCallback mLastKeyEventHandledCallback;
@Override
public void handleEvent(
@NonNull KeyEvent keyEvent, @NonNull OnKeyEventHandledCallback onKeyEventHandledCallback) {
mLastKeyEvent = keyEvent;
mLastKeyEventHandledCallback = onKeyEventHandledCallback;
}
void eventHandled(boolean isHandled) {
mLastKeyEventHandledCallback.onKeyEventHandled(isHandled);
}
}
@Mock FlutterJNI mockFlutterJni;
FlutterEngine mockEngine;
KeyEventChannel mockKeyEventChannel;
@Mock TextInputPlugin mockTextInputPlugin;
@Mock View mockView;
@Mock View mockRootView;
KeyboardManager keyboardManager;
@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;
}
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
when(mockFlutterJni.isAttached()).thenReturn(true);
mockEngine = mockFlutterEngine();
mockKeyEventChannel = mockEngine.getKeyEventChannel();
when(mockView.getRootView()).thenAnswer(invocation -> mockRootView);
when(mockView.dispatchKeyEvent(any(KeyEvent.class)))
.thenAnswer(
invocation -> keyboardManager.handleEvent((KeyEvent) invocation.getArguments()[0]));
when(mockRootView.dispatchKeyEvent(any(KeyEvent.class)))
.thenAnswer(
invocation -> mockView.dispatchKeyEvent((KeyEvent) invocation.getArguments()[0]));
keyboardManager =
new KeyboardManager(
mockView,
mockTextInputPlugin,
new Responder[] {new KeyChannelResponder(mockKeyEventChannel)});
}
// Tests start
@Test
public void respondsTrueWhenHandlingNewEvents() {
final FakeResponder fakeResponder = new FakeResponder();
keyboardManager =
new KeyboardManager(
mockView, mockTextInputPlugin, new KeyboardManager.Responder[] {fakeResponder});
final KeyEvent keyEvent = new FakeKeyEvent(KeyEvent.ACTION_DOWN, 65);
final boolean result = keyboardManager.handleEvent(keyEvent);
assertEquals(true, result);
assertEquals(keyEvent, fakeResponder.mLastKeyEvent);
// Don't send the key event to the text plugin if the only primary responder
// hasn't responded.
verify(mockTextInputPlugin, times(0)).handleKeyEvent(any(KeyEvent.class));
verify(mockRootView, times(0)).dispatchKeyEvent(any(KeyEvent.class));
}
@Test
public void primaryRespondersHaveTheHighestPrecedence() {
final FakeResponder fakeResponder = new FakeResponder();
keyboardManager =
new KeyboardManager(
mockView, mockTextInputPlugin, new KeyboardManager.Responder[] {fakeResponder});
final KeyEvent keyEvent = new FakeKeyEvent(KeyEvent.ACTION_DOWN, 65);
final boolean result = keyboardManager.handleEvent(keyEvent);
assertEquals(true, result);
assertEquals(keyEvent, fakeResponder.mLastKeyEvent);
// Don't send the key event to the text plugin if the only primary responder
// hasn't responded.
verify(mockTextInputPlugin, times(0)).handleKeyEvent(any(KeyEvent.class));
verify(mockRootView, times(0)).dispatchKeyEvent(any(KeyEvent.class));
// If a primary responder handles the key event the propagation stops.
assertNotNull(fakeResponder.mLastKeyEventHandledCallback);
fakeResponder.eventHandled(true);
verify(mockTextInputPlugin, times(0)).handleKeyEvent(any(KeyEvent.class));
verify(mockRootView, times(0)).dispatchKeyEvent(any(KeyEvent.class));
}
@Test
public void zeroRespondersTest() {
keyboardManager =
new KeyboardManager(mockView, mockTextInputPlugin, new KeyboardManager.Responder[] {});
final KeyEvent keyEvent = new FakeKeyEvent(KeyEvent.ACTION_DOWN, 65);
final boolean result = keyboardManager.handleEvent(keyEvent);
assertEquals(true, result);
// Send the key event to the text plugin since there's 0 primary responders.
verify(mockTextInputPlugin, times(1)).handleKeyEvent(any(KeyEvent.class));
}
@Test
public void multipleRespondersTest() {
final FakeResponder fakeResponder1 = new FakeResponder();
final FakeResponder fakeResponder2 = new FakeResponder();
keyboardManager =
new KeyboardManager(
mockView,
mockTextInputPlugin,
new KeyboardManager.Responder[] {fakeResponder1, fakeResponder2});
final KeyEvent keyEvent = new FakeKeyEvent(KeyEvent.ACTION_DOWN, 65);
final boolean result = keyboardManager.handleEvent(keyEvent);
assertEquals(true, result);
assertEquals(keyEvent, fakeResponder1.mLastKeyEvent);
assertEquals(keyEvent, fakeResponder2.mLastKeyEvent);
fakeResponder2.eventHandled(false);
// Don't send the key event to the text plugin, since fakeResponder1
// hasn't responded.
verify(mockTextInputPlugin, times(0)).handleKeyEvent(any(KeyEvent.class));
fakeResponder1.eventHandled(false);
verify(mockTextInputPlugin, times(1)).handleKeyEvent(any(KeyEvent.class));
}
@Test
public void multipleRespondersTest2() {
final FakeResponder fakeResponder1 = new FakeResponder();
final FakeResponder fakeResponder2 = new FakeResponder();
keyboardManager =
new KeyboardManager(
mockView,
mockTextInputPlugin,
new KeyboardManager.Responder[] {fakeResponder1, fakeResponder2});
final KeyEvent keyEvent = new FakeKeyEvent(KeyEvent.ACTION_DOWN, 65);
final boolean result = keyboardManager.handleEvent(keyEvent);
fakeResponder2.eventHandled(false);
fakeResponder1.eventHandled(true);
// Handled by primary responders, propagation stops.
verify(mockTextInputPlugin, times(0)).handleKeyEvent(any(KeyEvent.class));
}
@Test
public void multipleRespondersTest3() {
final FakeResponder fakeResponder1 = new FakeResponder();
final FakeResponder fakeResponder2 = new FakeResponder();
keyboardManager =
new KeyboardManager(
mockView,
mockTextInputPlugin,
new KeyboardManager.Responder[] {fakeResponder1, fakeResponder2});
final KeyEvent keyEvent = new FakeKeyEvent(KeyEvent.ACTION_DOWN, 65);
final boolean result = keyboardManager.handleEvent(keyEvent);
fakeResponder2.eventHandled(false);
Exception exception = null;
try {
fakeResponder2.eventHandled(false);
} catch (Exception e) {
exception = e;
}
// Throws since the same handle is called twice.
assertNotNull(exception);
}
@Test
public void textInputPluginHasTheSecondHighestPrecedence() {
final FakeResponder fakeResponder = new FakeResponder();
keyboardManager =
spy(
new KeyboardManager(
mockView, mockTextInputPlugin, new KeyboardManager.Responder[] {fakeResponder}));
final KeyEvent keyEvent = new FakeKeyEvent(KeyEvent.ACTION_DOWN, 65);
final boolean result = keyboardManager.handleEvent(keyEvent);
assertEquals(true, result);
assertEquals(keyEvent, fakeResponder.mLastKeyEvent);
// Don't send the key event to the text plugin if the only primary responder
// hasn't responded.
verify(mockTextInputPlugin, times(0)).handleKeyEvent(any(KeyEvent.class));
verify(mockRootView, times(0)).dispatchKeyEvent(any(KeyEvent.class));
// If no primary responder handles the key event the propagates to the text
// input plugin.
assertNotNull(fakeResponder.mLastKeyEventHandledCallback);
// Let text input plugin handle the key event.
when(mockTextInputPlugin.handleKeyEvent(any())).thenAnswer(invocation -> true);
fakeResponder.eventHandled(false);
verify(mockTextInputPlugin, times(1)).handleKeyEvent(keyEvent);
verify(mockRootView, times(0)).dispatchKeyEvent(any(KeyEvent.class));
// It's not redispatched to the keyboard manager.
verify(keyboardManager, times(1)).handleEvent(any(KeyEvent.class));
}
@Test
public void RedispatchKeyEventIfTextInputPluginFailsToHandle() {
final FakeResponder fakeResponder = new FakeResponder();
keyboardManager =
spy(
new KeyboardManager(
mockView, mockTextInputPlugin, new KeyboardManager.Responder[] {fakeResponder}));
final KeyEvent keyEvent = new FakeKeyEvent(KeyEvent.ACTION_DOWN, 65);
final boolean result = keyboardManager.handleEvent(keyEvent);
assertEquals(true, result);
assertEquals(keyEvent, fakeResponder.mLastKeyEvent);
// Don't send the key event to the text plugin if the only primary responder
// hasn't responded.
verify(mockTextInputPlugin, times(0)).handleKeyEvent(any(KeyEvent.class));
verify(mockRootView, times(0)).dispatchKeyEvent(any(KeyEvent.class));
// Neither the primary responders nor text input plugin handles the event.
when(mockTextInputPlugin.handleKeyEvent(any())).thenAnswer(invocation -> false);
fakeResponder.mLastKeyEvent = null;
fakeResponder.eventHandled(false);
verify(mockTextInputPlugin, times(1)).handleKeyEvent(keyEvent);
verify(mockRootView, times(1)).dispatchKeyEvent(keyEvent);
}
@Test
public void respondsFalseWhenHandlingRedispatchedEvents() {
final FakeResponder fakeResponder = new FakeResponder();
keyboardManager =
spy(
new KeyboardManager(
mockView, mockTextInputPlugin, new KeyboardManager.Responder[] {fakeResponder}));
final KeyEvent keyEvent = new FakeKeyEvent(KeyEvent.ACTION_DOWN, 65);
final boolean result = keyboardManager.handleEvent(keyEvent);
assertEquals(true, result);
assertEquals(keyEvent, fakeResponder.mLastKeyEvent);
// Don't send the key event to the text plugin if the only primary responder
// hasn't responded.
verify(mockTextInputPlugin, times(0)).handleKeyEvent(any(KeyEvent.class));
verify(mockRootView, times(0)).dispatchKeyEvent(any(KeyEvent.class));
// Neither the primary responders nor text input plugin handles the event.
when(mockTextInputPlugin.handleKeyEvent(any())).thenAnswer(invocation -> false);
fakeResponder.mLastKeyEvent = null;
fakeResponder.eventHandled(false);
verify(mockTextInputPlugin, times(1)).handleKeyEvent(keyEvent);
verify(mockRootView, times(1)).dispatchKeyEvent(keyEvent);
// It's redispatched to the keyboard manager, but not the primary
// responders.
verify(keyboardManager, times(2)).handleEvent(any(KeyEvent.class));
assertNull(fakeResponder.mLastKeyEvent);
}
}

View File

@ -4,22 +4,23 @@ 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.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
@ -30,6 +31,11 @@ import org.robolectric.annotation.Config;
@TargetApi(24)
public class KeyEventChannelTest {
KeyEvent keyEvent;
@Mock BinaryMessenger fakeMessenger;
boolean[] handled;
KeyEventChannel keyEventChannel;
private void sendReply(boolean handled, BinaryMessenger.BinaryReply messengerReply)
throws JSONException {
JSONObject reply = new JSONObject();
@ -40,30 +46,25 @@ public class KeyEventChannelTest {
messengerReply.reply(binaryReply);
}
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
keyEvent = new FakeKeyEvent(KeyEvent.ACTION_DOWN, 65);
handled = new boolean[] {false};
keyEventChannel = new KeyEventChannel(fakeMessenger);
}
@Test
public void keyDownEventIsSentToFramework() throws JSONException {
BinaryMessenger fakeMessenger = mock(BinaryMessenger.class);
KeyEventChannel keyEventChannel = new KeyEventChannel(fakeMessenger);
final boolean[] handled = {false};
final KeyEvent[] handledKeyEvents = {null};
keyEventChannel.setEventResponseHandler(
new KeyEventChannel.EventResponseHandler() {
public void onKeyEventHandled(@NonNull KeyEvent event) {
handled[0] = true;
handledKeyEvents[0] = event;
}
public void onKeyEventNotHandled(@NonNull KeyEvent event) {
handled[0] = false;
handledKeyEvents[0] = event;
}
});
verify(fakeMessenger, times(0)).send(any(), any(), any());
KeyEvent event = new FakeKeyEvent(KeyEvent.ACTION_DOWN, 65);
KeyEventChannel.FlutterKeyEvent flutterKeyEvent =
new KeyEventChannel.FlutterKeyEvent(event, null);
keyEventChannel.keyDown(flutterKeyEvent);
new KeyEventChannel.FlutterKeyEvent(keyEvent, null);
keyEventChannel.sendFlutterKeyEvent(
flutterKeyEvent,
false,
(isHandled) -> {
handled[0] = isHandled;
});
ArgumentCaptor<ByteBuffer> byteBufferArgumentCaptor = ArgumentCaptor.forClass(ByteBuffer.class);
ArgumentCaptor<BinaryMessenger.BinaryReply> replyArgumentCaptor =
ArgumentCaptor.forClass(BinaryMessenger.BinaryReply.class);
@ -78,33 +79,20 @@ public class KeyEventChannelTest {
// Simulate a reply, and see that it is handled.
sendReply(true, replyArgumentCaptor.getValue());
assertTrue(handled[0]);
assertEquals(event, handledKeyEvents[0]);
}
@Test
public void keyUpEventIsSentToFramework() throws JSONException {
BinaryMessenger fakeMessenger = mock(BinaryMessenger.class);
KeyEventChannel keyEventChannel = new KeyEventChannel(fakeMessenger);
final boolean[] handled = {false};
final KeyEvent[] handledKeyEvents = {null};
keyEventChannel.setEventResponseHandler(
new KeyEventChannel.EventResponseHandler() {
public void onKeyEventHandled(@NonNull KeyEvent event) {
handled[0] = true;
handledKeyEvents[0] = event;
}
public void onKeyEventNotHandled(@NonNull KeyEvent event) {
handled[0] = false;
handledKeyEvents[0] = event;
}
});
verify(fakeMessenger, times(0)).send(any(), any(), any());
KeyEvent event = new FakeKeyEvent(KeyEvent.ACTION_UP, 65);
keyEvent = new FakeKeyEvent(KeyEvent.ACTION_UP, 65);
KeyEventChannel.FlutterKeyEvent flutterKeyEvent =
new KeyEventChannel.FlutterKeyEvent(event, null);
keyEventChannel.keyUp(flutterKeyEvent);
new KeyEventChannel.FlutterKeyEvent(keyEvent, null);
keyEventChannel.sendFlutterKeyEvent(
flutterKeyEvent,
false,
(isHandled) -> {
handled[0] = isHandled;
});
ArgumentCaptor<ByteBuffer> byteBufferArgumentCaptor = ArgumentCaptor.forClass(ByteBuffer.class);
ArgumentCaptor<BinaryMessenger.BinaryReply> replyArgumentCaptor =
ArgumentCaptor.forClass(BinaryMessenger.BinaryReply.class);
@ -114,11 +102,10 @@ public class KeyEventChannelTest {
capturedMessage.rewind();
JSONObject message = (JSONObject) JSONMessageCodec.INSTANCE.decodeMessage(capturedMessage);
assertNotNull(message);
assertEquals("keyup", message.get("type"));
assertEquals("keydown", message.get("type"));
// Simulate a reply, and see that it is handled.
sendReply(true, replyArgumentCaptor.getValue());
assertTrue(handled[0]);
assertEquals(event, handledKeyEvents[0]);
}
}

View File

@ -32,7 +32,7 @@ import android.view.inputmethod.ExtractedText;
import android.view.inputmethod.ExtractedTextRequest;
import android.view.inputmethod.InputConnection;
import android.view.inputmethod.InputMethodManager;
import io.flutter.embedding.android.AndroidKeyProcessor;
import io.flutter.embedding.android.KeyboardManager;
import io.flutter.embedding.engine.FlutterJNI;
import io.flutter.embedding.engine.dart.DartExecutor;
import io.flutter.embedding.engine.systemchannels.TextInputChannel;
@ -43,9 +43,12 @@ import io.flutter.util.FakeKeyEvent;
import java.nio.ByteBuffer;
import org.json.JSONArray;
import org.json.JSONException;
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.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
@ -60,6 +63,7 @@ import org.robolectric.shadows.ShadowInputMethodManager;
shadows = {ShadowClipboardManager.class, InputConnectionAdaptorTest.TestImm.class})
@RunWith(RobolectricTestRunner.class)
public class InputConnectionAdaptorTest {
@Mock KeyboardManager mockKeyboardManager;
// Verifies the method and arguments for a captured method call.
private void verifyMethodCall(ByteBuffer buffer, String methodName, String[] expectedArgs)
throws JSONException {
@ -75,6 +79,11 @@ public class InputConnectionAdaptorTest {
}
}
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
}
@Test
public void inputConnectionAdaptor_ReceivesEnter() throws NullPointerException {
View testView = new View(RuntimeEnvironment.application);
@ -82,7 +91,6 @@ public class InputConnectionAdaptorTest {
DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJni, mock(AssetManager.class)));
int inputTargetId = 0;
TextInputChannel textInputChannel = new TextInputChannel(dartExecutor);
AndroidKeyProcessor mockKeyProcessor = mock(AndroidKeyProcessor.class);
ListenableEditingState mEditable = new ListenableEditingState(null, testView);
Selection.setSelection(mEditable, 0, 0);
ListenableEditingState spyEditable = spy(mEditable);
@ -91,11 +99,11 @@ public class InputConnectionAdaptorTest {
InputConnectionAdaptor inputConnectionAdaptor =
new InputConnectionAdaptor(
testView, inputTargetId, textInputChannel, mockKeyProcessor, spyEditable, outAttrs);
testView, inputTargetId, textInputChannel, mockKeyboardManager, spyEditable, outAttrs);
// Send an enter key and make sure the Editable received it.
FakeKeyEvent keyEvent = new FakeKeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER);
inputConnectionAdaptor.sendKeyEvent(keyEvent);
inputConnectionAdaptor.handleKeyEvent(keyEvent);
verify(spyEditable, times(1)).insert(eq(0), anyString());
}
@ -172,11 +180,16 @@ public class InputConnectionAdaptorTest {
FlutterJNI mockFlutterJNI = mock(FlutterJNI.class);
DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJNI, mock(AssetManager.class)));
TextInputChannel textInputChannel = new TextInputChannel(dartExecutor);
AndroidKeyProcessor mockKeyProcessor = mock(AndroidKeyProcessor.class);
ListenableEditingState editable = sampleEditable(0, 0);
InputConnectionAdaptor adaptor =
new InputConnectionAdaptor(
testView, client, textInputChannel, mockKeyProcessor, editable, null, mockFlutterJNI);
testView,
client,
textInputChannel,
mockKeyboardManager,
editable,
null,
mockFlutterJNI);
adaptor.performPrivateCommand("actionCommand", null);
ArgumentCaptor<String> channelCaptor = ArgumentCaptor.forClass(String.class);
@ -200,11 +213,16 @@ public class InputConnectionAdaptorTest {
FlutterJNI mockFlutterJNI = mock(FlutterJNI.class);
DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJNI, mock(AssetManager.class)));
TextInputChannel textInputChannel = new TextInputChannel(dartExecutor);
AndroidKeyProcessor mockKeyProcessor = mock(AndroidKeyProcessor.class);
ListenableEditingState editable = sampleEditable(0, 0);
InputConnectionAdaptor adaptor =
new InputConnectionAdaptor(
testView, client, textInputChannel, mockKeyProcessor, editable, null, mockFlutterJNI);
testView,
client,
textInputChannel,
mockKeyboardManager,
editable,
null,
mockFlutterJNI);
Bundle bundle = new Bundle();
byte[] buffer = new byte[] {'a', 'b', 'c', 'd'};
@ -234,11 +252,16 @@ public class InputConnectionAdaptorTest {
FlutterJNI mockFlutterJNI = mock(FlutterJNI.class);
DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJNI, mock(AssetManager.class)));
TextInputChannel textInputChannel = new TextInputChannel(dartExecutor);
AndroidKeyProcessor mockKeyProcessor = mock(AndroidKeyProcessor.class);
ListenableEditingState editable = sampleEditable(0, 0);
InputConnectionAdaptor adaptor =
new InputConnectionAdaptor(
testView, client, textInputChannel, mockKeyProcessor, editable, null, mockFlutterJNI);
testView,
client,
textInputChannel,
mockKeyboardManager,
editable,
null,
mockFlutterJNI);
Bundle bundle = new Bundle();
byte b = 3;
@ -266,11 +289,16 @@ public class InputConnectionAdaptorTest {
FlutterJNI mockFlutterJNI = mock(FlutterJNI.class);
DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJNI, mock(AssetManager.class)));
TextInputChannel textInputChannel = new TextInputChannel(dartExecutor);
AndroidKeyProcessor mockKeyProcessor = mock(AndroidKeyProcessor.class);
ListenableEditingState editable = sampleEditable(0, 0);
InputConnectionAdaptor adaptor =
new InputConnectionAdaptor(
testView, client, textInputChannel, mockKeyProcessor, editable, null, mockFlutterJNI);
testView,
client,
textInputChannel,
mockKeyboardManager,
editable,
null,
mockFlutterJNI);
Bundle bundle = new Bundle();
char[] buffer = new char[] {'a', 'b', 'c', 'd'};
@ -301,11 +329,16 @@ public class InputConnectionAdaptorTest {
FlutterJNI mockFlutterJNI = mock(FlutterJNI.class);
DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJNI, mock(AssetManager.class)));
TextInputChannel textInputChannel = new TextInputChannel(dartExecutor);
AndroidKeyProcessor mockKeyProcessor = mock(AndroidKeyProcessor.class);
ListenableEditingState editable = sampleEditable(0, 0);
InputConnectionAdaptor adaptor =
new InputConnectionAdaptor(
testView, client, textInputChannel, mockKeyProcessor, editable, null, mockFlutterJNI);
testView,
client,
textInputChannel,
mockKeyboardManager,
editable,
null,
mockFlutterJNI);
Bundle bundle = new Bundle();
char b = 'a';
@ -333,11 +366,16 @@ public class InputConnectionAdaptorTest {
FlutterJNI mockFlutterJNI = mock(FlutterJNI.class);
DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJNI, mock(AssetManager.class)));
TextInputChannel textInputChannel = new TextInputChannel(dartExecutor);
AndroidKeyProcessor mockKeyProcessor = mock(AndroidKeyProcessor.class);
ListenableEditingState editable = sampleEditable(0, 0);
InputConnectionAdaptor adaptor =
new InputConnectionAdaptor(
testView, client, textInputChannel, mockKeyProcessor, editable, null, mockFlutterJNI);
testView,
client,
textInputChannel,
mockKeyboardManager,
editable,
null,
mockFlutterJNI);
Bundle bundle = new Bundle();
CharSequence charSequence1 = new StringBuffer("abc");
@ -369,11 +407,16 @@ public class InputConnectionAdaptorTest {
FlutterJNI mockFlutterJNI = mock(FlutterJNI.class);
DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJNI, mock(AssetManager.class)));
TextInputChannel textInputChannel = new TextInputChannel(dartExecutor);
AndroidKeyProcessor mockKeyProcessor = mock(AndroidKeyProcessor.class);
ListenableEditingState editable = sampleEditable(0, 0);
InputConnectionAdaptor adaptor =
new InputConnectionAdaptor(
testView, client, textInputChannel, mockKeyProcessor, editable, null, mockFlutterJNI);
testView,
client,
textInputChannel,
mockKeyboardManager,
editable,
null,
mockFlutterJNI);
Bundle bundle = new Bundle();
CharSequence charSequence = new StringBuffer("abc");
@ -403,11 +446,16 @@ public class InputConnectionAdaptorTest {
FlutterJNI mockFlutterJNI = mock(FlutterJNI.class);
DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJNI, mock(AssetManager.class)));
TextInputChannel textInputChannel = new TextInputChannel(dartExecutor);
AndroidKeyProcessor mockKeyProcessor = mock(AndroidKeyProcessor.class);
ListenableEditingState editable = sampleEditable(0, 0);
InputConnectionAdaptor adaptor =
new InputConnectionAdaptor(
testView, client, textInputChannel, mockKeyProcessor, editable, null, mockFlutterJNI);
testView,
client,
textInputChannel,
mockKeyboardManager,
editable,
null,
mockFlutterJNI);
Bundle bundle = new Bundle();
float value = 0.5f;
@ -435,11 +483,16 @@ public class InputConnectionAdaptorTest {
FlutterJNI mockFlutterJNI = mock(FlutterJNI.class);
DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJNI, mock(AssetManager.class)));
TextInputChannel textInputChannel = new TextInputChannel(dartExecutor);
AndroidKeyProcessor mockKeyProcessor = mock(AndroidKeyProcessor.class);
ListenableEditingState editable = sampleEditable(0, 0);
InputConnectionAdaptor adaptor =
new InputConnectionAdaptor(
testView, client, textInputChannel, mockKeyProcessor, editable, null, mockFlutterJNI);
testView,
client,
textInputChannel,
mockKeyboardManager,
editable,
null,
mockFlutterJNI);
Bundle bundle = new Bundle();
float[] value = {0.5f, 0.6f};
@ -470,7 +523,7 @@ public class InputConnectionAdaptorTest {
InputConnectionAdaptor adaptor = sampleInputConnectionAdaptor(editable);
KeyEvent shiftKeyUp = new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_SHIFT_LEFT);
boolean didConsume = adaptor.sendKeyEvent(shiftKeyUp);
boolean didConsume = adaptor.handleKeyEvent(shiftKeyUp);
assertTrue(didConsume);
assertEquals(selEnd, Selection.getSelectionStart(editable));
@ -484,7 +537,7 @@ public class InputConnectionAdaptorTest {
InputConnectionAdaptor adaptor = sampleInputConnectionAdaptor(editable);
KeyEvent leftKeyDown = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_LEFT);
boolean didConsume = adaptor.sendKeyEvent(leftKeyDown);
boolean didConsume = adaptor.handleKeyEvent(leftKeyDown);
assertTrue(didConsume);
assertEquals(selStart - 1, Selection.getSelectionStart(editable));
@ -501,134 +554,134 @@ public class InputConnectionAdaptorTest {
boolean didConsume;
// Normal Character
didConsume = adaptor.sendKeyEvent(downKeyDown);
didConsume = adaptor.handleKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 74);
// Non-Spacing Mark
didConsume = adaptor.sendKeyEvent(downKeyDown);
didConsume = adaptor.handleKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 73);
didConsume = adaptor.sendKeyEvent(downKeyDown);
didConsume = adaptor.handleKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 72);
// Keycap
didConsume = adaptor.sendKeyEvent(downKeyDown);
didConsume = adaptor.handleKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 69);
// Keycap with invalid base
adaptor.setSelection(68, 68);
didConsume = adaptor.sendKeyEvent(downKeyDown);
didConsume = adaptor.handleKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 66);
adaptor.setSelection(67, 67);
didConsume = adaptor.sendKeyEvent(downKeyDown);
didConsume = adaptor.handleKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 66);
// Zero Width Joiner
didConsume = adaptor.sendKeyEvent(downKeyDown);
didConsume = adaptor.handleKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 55);
// Zero Width Joiner with invalid base
didConsume = adaptor.sendKeyEvent(downKeyDown);
didConsume = adaptor.handleKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 53);
didConsume = adaptor.sendKeyEvent(downKeyDown);
didConsume = adaptor.handleKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 52);
didConsume = adaptor.sendKeyEvent(downKeyDown);
didConsume = adaptor.handleKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 51);
// ----- Start Emoji Tag Sequence with invalid base testing ----
// Delete base tag
adaptor.setSelection(39, 39);
didConsume = adaptor.sendKeyEvent(downKeyDown);
didConsume = adaptor.handleKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 37);
// Delete the sequence
adaptor.setSelection(49, 49);
for (int i = 0; i < 6; i++) {
didConsume = adaptor.sendKeyEvent(downKeyDown);
didConsume = adaptor.handleKeyEvent(downKeyDown);
assertTrue(didConsume);
}
assertEquals(Selection.getSelectionStart(editable), 37);
// ----- End Emoji Tag Sequence with invalid base testing ----
// Emoji Tag Sequence
didConsume = adaptor.sendKeyEvent(downKeyDown);
didConsume = adaptor.handleKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 23);
// Variation Selector with invalid base
adaptor.setSelection(22, 22);
didConsume = adaptor.sendKeyEvent(downKeyDown);
didConsume = adaptor.handleKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 21);
adaptor.setSelection(22, 22);
didConsume = adaptor.sendKeyEvent(downKeyDown);
didConsume = adaptor.handleKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 21);
// Variation Selector
didConsume = adaptor.sendKeyEvent(downKeyDown);
didConsume = adaptor.handleKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 19);
// Emoji Modifier
didConsume = adaptor.sendKeyEvent(downKeyDown);
didConsume = adaptor.handleKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 16);
// Emoji Modifier with invalid base
adaptor.setSelection(14, 14);
didConsume = adaptor.sendKeyEvent(downKeyDown);
didConsume = adaptor.handleKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 13);
adaptor.setSelection(14, 14);
didConsume = adaptor.sendKeyEvent(downKeyDown);
didConsume = adaptor.handleKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 13);
// Line Feed
adaptor.setSelection(12, 12);
didConsume = adaptor.sendKeyEvent(downKeyDown);
didConsume = adaptor.handleKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 11);
// Carriage Return
adaptor.setSelection(12, 12);
didConsume = adaptor.sendKeyEvent(downKeyDown);
didConsume = adaptor.handleKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 11);
// Carriage Return and Line Feed
didConsume = adaptor.sendKeyEvent(downKeyDown);
didConsume = adaptor.handleKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 9);
// Regional Indicator Symbol odd
didConsume = adaptor.sendKeyEvent(downKeyDown);
didConsume = adaptor.handleKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 7);
// Regional Indicator Symbol even
didConsume = adaptor.sendKeyEvent(downKeyDown);
didConsume = adaptor.handleKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 3);
// Simple Emoji
didConsume = adaptor.sendKeyEvent(downKeyDown);
didConsume = adaptor.handleKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 1);
// First CodePoint
didConsume = adaptor.sendKeyEvent(downKeyDown);
didConsume = adaptor.handleKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 0);
}
@ -641,7 +694,7 @@ public class InputConnectionAdaptorTest {
InputConnectionAdaptor adaptor = sampleInputConnectionAdaptor(editable);
KeyEvent leftKeyDown = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_LEFT);
boolean didConsume = adaptor.sendKeyEvent(leftKeyDown);
boolean didConsume = adaptor.handleKeyEvent(leftKeyDown);
assertTrue(didConsume);
assertEquals(selStart, Selection.getSelectionStart(editable));
@ -657,7 +710,7 @@ public class InputConnectionAdaptorTest {
KeyEvent shiftLeftKeyDown =
new KeyEvent(
0, 0, KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_LEFT, 0, KeyEvent.META_SHIFT_ON);
boolean didConsume = adaptor.sendKeyEvent(shiftLeftKeyDown);
boolean didConsume = adaptor.handleKeyEvent(shiftLeftKeyDown);
assertTrue(didConsume);
assertEquals(selStart, Selection.getSelectionStart(editable));
@ -671,7 +724,7 @@ public class InputConnectionAdaptorTest {
InputConnectionAdaptor adaptor = sampleInputConnectionAdaptor(editable);
KeyEvent rightKeyDown = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_RIGHT);
boolean didConsume = adaptor.sendKeyEvent(rightKeyDown);
boolean didConsume = adaptor.handleKeyEvent(rightKeyDown);
assertTrue(didConsume);
assertEquals(selStart + 1, Selection.getSelectionStart(editable));
@ -692,26 +745,26 @@ public class InputConnectionAdaptorTest {
boolean didConsume;
// The cursor moves over two region indicators at a time.
didConsume = adaptor.sendKeyEvent(downKeyDown);
didConsume = adaptor.handleKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 4);
didConsume = adaptor.sendKeyEvent(downKeyDown);
didConsume = adaptor.handleKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 8);
didConsume = adaptor.sendKeyEvent(downKeyDown);
didConsume = adaptor.handleKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 12);
// When there is only one region indicator left with no pair, the cursor
// moves over that single region indicator.
didConsume = adaptor.sendKeyEvent(downKeyDown);
didConsume = adaptor.handleKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 14);
// If the cursor is placed in the middle of a region indicator pair, it
// moves over only the second half of the pair.
adaptor.setSelection(6, 6);
didConsume = adaptor.sendKeyEvent(downKeyDown);
didConsume = adaptor.handleKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 8);
}
@ -726,71 +779,71 @@ public class InputConnectionAdaptorTest {
boolean didConsume;
// First CodePoint
didConsume = adaptor.sendKeyEvent(downKeyDown);
didConsume = adaptor.handleKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 1);
// Simple Emoji
didConsume = adaptor.sendKeyEvent(downKeyDown);
didConsume = adaptor.handleKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 3);
// Regional Indicator Symbol even
didConsume = adaptor.sendKeyEvent(downKeyDown);
didConsume = adaptor.handleKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 7);
// Regional Indicator Symbol odd
didConsume = adaptor.sendKeyEvent(downKeyDown);
didConsume = adaptor.handleKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 9);
// Carriage Return
didConsume = adaptor.sendKeyEvent(downKeyDown);
didConsume = adaptor.handleKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 10);
// Line Feed and Carriage Return
didConsume = adaptor.sendKeyEvent(downKeyDown);
didConsume = adaptor.handleKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 12);
// Line Feed
didConsume = adaptor.sendKeyEvent(downKeyDown);
didConsume = adaptor.handleKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 13);
// Modified Emoji
didConsume = adaptor.sendKeyEvent(downKeyDown);
didConsume = adaptor.handleKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 16);
// Emoji Modifier
adaptor.setSelection(14, 14);
didConsume = adaptor.sendKeyEvent(downKeyDown);
didConsume = adaptor.handleKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 16);
// Emoji Modifier with invalid base
adaptor.setSelection(18, 18);
didConsume = adaptor.sendKeyEvent(downKeyDown);
didConsume = adaptor.handleKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 19);
// Variation Selector
didConsume = adaptor.sendKeyEvent(downKeyDown);
didConsume = adaptor.handleKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 21);
// Variation Selector with invalid base
adaptor.setSelection(22, 22);
didConsume = adaptor.sendKeyEvent(downKeyDown);
didConsume = adaptor.handleKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 23);
// Emoji Tag Sequence
for (int i = 0; i < 7; i++) {
didConsume = adaptor.sendKeyEvent(downKeyDown);
didConsume = adaptor.handleKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 25 + 2 * i);
}
@ -800,7 +853,7 @@ public class InputConnectionAdaptorTest {
// Pass the sequence
adaptor.setSelection(39, 39);
for (int i = 0; i < 6; i++) {
didConsume = adaptor.sendKeyEvent(downKeyDown);
didConsume = adaptor.handleKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 41 + 2 * i);
}
@ -808,45 +861,45 @@ public class InputConnectionAdaptorTest {
// ----- End Emoji Tag Sequence with invalid base testing ----
// Zero Width Joiner with invalid base
didConsume = adaptor.sendKeyEvent(downKeyDown);
didConsume = adaptor.handleKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 52);
didConsume = adaptor.sendKeyEvent(downKeyDown);
didConsume = adaptor.handleKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 53);
didConsume = adaptor.sendKeyEvent(downKeyDown);
didConsume = adaptor.handleKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 55);
// Zero Width Joiner
didConsume = adaptor.sendKeyEvent(downKeyDown);
didConsume = adaptor.handleKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 66);
// Keycap with invalid base
adaptor.setSelection(67, 67);
didConsume = adaptor.sendKeyEvent(downKeyDown);
didConsume = adaptor.handleKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 68);
didConsume = adaptor.sendKeyEvent(downKeyDown);
didConsume = adaptor.handleKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 69);
// Keycap
didConsume = adaptor.sendKeyEvent(downKeyDown);
didConsume = adaptor.handleKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 72);
// Non-Spacing Mark
didConsume = adaptor.sendKeyEvent(downKeyDown);
didConsume = adaptor.handleKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 73);
didConsume = adaptor.sendKeyEvent(downKeyDown);
didConsume = adaptor.handleKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 74);
// Normal Character
didConsume = adaptor.sendKeyEvent(downKeyDown);
didConsume = adaptor.handleKeyEvent(downKeyDown);
assertTrue(didConsume);
assertEquals(Selection.getSelectionStart(editable), 75);
}
@ -859,7 +912,7 @@ public class InputConnectionAdaptorTest {
InputConnectionAdaptor adaptor = sampleInputConnectionAdaptor(editable);
KeyEvent rightKeyDown = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_RIGHT);
boolean didConsume = adaptor.sendKeyEvent(rightKeyDown);
boolean didConsume = adaptor.handleKeyEvent(rightKeyDown);
assertTrue(didConsume);
assertEquals(selStart, Selection.getSelectionStart(editable));
@ -875,7 +928,7 @@ public class InputConnectionAdaptorTest {
KeyEvent shiftRightKeyDown =
new KeyEvent(
0, 0, KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_RIGHT, 0, KeyEvent.META_SHIFT_ON);
boolean didConsume = adaptor.sendKeyEvent(shiftRightKeyDown);
boolean didConsume = adaptor.handleKeyEvent(shiftRightKeyDown);
assertTrue(didConsume);
assertEquals(selStart, Selection.getSelectionStart(editable));
@ -889,7 +942,7 @@ public class InputConnectionAdaptorTest {
InputConnectionAdaptor adaptor = sampleInputConnectionAdaptor(editable);
KeyEvent upKeyDown = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_UP);
boolean didConsume = adaptor.sendKeyEvent(upKeyDown);
boolean didConsume = adaptor.handleKeyEvent(upKeyDown);
assertTrue(didConsume);
// Checks the caret moved left (to some previous character). Selection.moveUp() behaves
@ -904,7 +957,7 @@ public class InputConnectionAdaptorTest {
InputConnectionAdaptor adaptor = sampleInputConnectionAdaptor(editable);
KeyEvent downKeyDown = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_DOWN);
boolean didConsume = adaptor.sendKeyEvent(downKeyDown);
boolean didConsume = adaptor.handleKeyEvent(downKeyDown);
assertTrue(didConsume);
// Checks the caret moved right (to some following character). Selection.moveDown() behaves
@ -919,25 +972,25 @@ public class InputConnectionAdaptorTest {
InputConnectionAdaptor adaptor = sampleInputConnectionAdaptor(editable);
KeyEvent keyEvent = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_DOWN);
boolean didConsume = adaptor.sendKeyEvent(keyEvent);
boolean didConsume = adaptor.handleKeyEvent(keyEvent);
assertFalse(didConsume);
assertEquals(Selection.getSelectionStart(editable), -1);
assertEquals(Selection.getSelectionEnd(editable), -1);
keyEvent = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_UP);
didConsume = adaptor.sendKeyEvent(keyEvent);
didConsume = adaptor.handleKeyEvent(keyEvent);
assertFalse(didConsume);
assertEquals(Selection.getSelectionStart(editable), -1);
assertEquals(Selection.getSelectionEnd(editable), -1);
keyEvent = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_LEFT);
didConsume = adaptor.sendKeyEvent(keyEvent);
didConsume = adaptor.handleKeyEvent(keyEvent);
assertFalse(didConsume);
assertEquals(Selection.getSelectionStart(editable), -1);
assertEquals(Selection.getSelectionEnd(editable), -1);
keyEvent = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_RIGHT);
didConsume = adaptor.sendKeyEvent(keyEvent);
didConsume = adaptor.handleKeyEvent(keyEvent);
assertFalse(didConsume);
assertEquals(Selection.getSelectionStart(editable), -1);
assertEquals(Selection.getSelectionEnd(editable), -1);
@ -964,13 +1017,12 @@ public class InputConnectionAdaptorTest {
}
ListenableEditingState editable = sampleEditable(5, 5);
View testView = new View(RuntimeEnvironment.application);
AndroidKeyProcessor mockKeyProcessor = mock(AndroidKeyProcessor.class);
InputConnectionAdaptor adaptor =
new InputConnectionAdaptor(
testView,
1,
mock(TextInputChannel.class),
mockKeyProcessor,
mockKeyboardManager,
editable,
new EditorInfo());
TestImm testImm =
@ -1020,7 +1072,6 @@ public class InputConnectionAdaptorTest {
return;
}
AndroidKeyProcessor mockKeyProcessor = mock(AndroidKeyProcessor.class);
ListenableEditingState editable = sampleEditable(5, 5);
View testView = new View(RuntimeEnvironment.application);
InputConnectionAdaptor adaptor =
@ -1028,7 +1079,7 @@ public class InputConnectionAdaptorTest {
testView,
1,
mock(TextInputChannel.class),
mockKeyProcessor,
mockKeyboardManager,
editable,
new EditorInfo());
TestImm testImm =
@ -1064,30 +1115,27 @@ public class InputConnectionAdaptorTest {
@Test
public void testSendKeyEvent_sendSoftKeyEvents() {
ListenableEditingState editable = sampleEditable(5, 5);
AndroidKeyProcessor mockKeyProcessor = mock(AndroidKeyProcessor.class);
when(mockKeyProcessor.isPendingEvent(any())).thenReturn(true);
InputConnectionAdaptor adaptor = sampleInputConnectionAdaptor(editable, mockKeyProcessor);
InputConnectionAdaptor adaptor = sampleInputConnectionAdaptor(editable, mockKeyboardManager);
KeyEvent shiftKeyDown = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_SHIFT_LEFT);
boolean didConsume = adaptor.sendKeyEvent(shiftKeyDown);
boolean didConsume = adaptor.handleKeyEvent(shiftKeyDown);
assertFalse(didConsume);
verify(mockKeyProcessor, never()).onKeyEvent(shiftKeyDown);
verify(mockKeyboardManager, never()).handleEvent(shiftKeyDown);
}
@Test
public void testSendKeyEvent_sendHardwareKeyEvents() {
ListenableEditingState editable = sampleEditable(5, 5);
AndroidKeyProcessor mockKeyProcessor = mock(AndroidKeyProcessor.class);
when(mockKeyProcessor.isPendingEvent(any())).thenReturn(false);
when(mockKeyProcessor.onKeyEvent(any())).thenReturn(true);
InputConnectionAdaptor adaptor = sampleInputConnectionAdaptor(editable, mockKeyProcessor);
when(mockKeyboardManager.handleEvent(any())).thenReturn(true);
InputConnectionAdaptor adaptor = sampleInputConnectionAdaptor(editable, mockKeyboardManager);
KeyEvent shiftKeyDown = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_SHIFT_LEFT);
// Call sendKeyEvent instead of handleKeyEvent.
boolean didConsume = adaptor.sendKeyEvent(shiftKeyDown);
assertTrue(didConsume);
verify(mockKeyProcessor, times(1)).onKeyEvent(shiftKeyDown);
verify(mockKeyboardManager, times(1)).handleEvent(shiftKeyDown);
}
@Test
@ -1098,7 +1146,7 @@ public class InputConnectionAdaptorTest {
KeyEvent downKeyDown = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL);
for (int i = 0; i < 4; i++) {
boolean didConsume = adaptor.sendKeyEvent(downKeyDown);
boolean didConsume = adaptor.handleKeyEvent(downKeyDown);
assertFalse(didConsume);
}
assertEquals(5, Selection.getSelectionStart(editable));
@ -1110,7 +1158,7 @@ public class InputConnectionAdaptorTest {
InputConnectionAdaptor adaptor = sampleInputConnectionAdaptor(editable);
FakeKeyEvent keyEvent = new FakeKeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_BACK);
boolean didConsume = adaptor.sendKeyEvent(keyEvent);
boolean didConsume = adaptor.handleKeyEvent(keyEvent);
assertFalse(didConsume);
}
@ -1158,11 +1206,11 @@ public class InputConnectionAdaptorTest {
private static InputConnectionAdaptor sampleInputConnectionAdaptor(
ListenableEditingState editable) {
return sampleInputConnectionAdaptor(editable, mock(AndroidKeyProcessor.class));
return sampleInputConnectionAdaptor(editable, mock(KeyboardManager.class));
}
private static InputConnectionAdaptor sampleInputConnectionAdaptor(
ListenableEditingState editable, AndroidKeyProcessor mockKeyProcessor) {
ListenableEditingState editable, KeyboardManager mockKeyboardManager) {
View testView = new View(RuntimeEnvironment.application);
int client = 0;
TextInputChannel textInputChannel = mock(TextInputChannel.class);
@ -1183,7 +1231,7 @@ public class InputConnectionAdaptorTest {
.thenAnswer(
(invocation) -> Emoji.isRegionalIndicatorSymbol((int) invocation.getArguments()[0]));
return new InputConnectionAdaptor(
testView, client, textInputChannel, mockKeyProcessor, editable, null, mockFlutterJNI);
testView, client, textInputChannel, mockKeyboardManager, editable, null, mockFlutterJNI);
}
private class TestTextInputChannel extends TextInputChannel {

View File

@ -10,11 +10,14 @@ import android.text.Selection;
import android.view.View;
import android.view.inputmethod.BaseInputConnection;
import android.view.inputmethod.EditorInfo;
import io.flutter.embedding.android.AndroidKeyProcessor;
import io.flutter.embedding.android.KeyboardManager;
import io.flutter.embedding.engine.systemchannels.TextInputChannel;
import java.util.ArrayList;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
@ -22,6 +25,8 @@ import org.robolectric.annotation.Config;
@Config(manifest = Config.NONE)
@RunWith(RobolectricTestRunner.class)
public class ListenableEditingStateTest {
@Mock KeyboardManager mockKeyboardManager;
private BaseInputConnection getTestInputConnection(View view, Editable mEditable) {
new View(RuntimeEnvironment.application);
return new BaseInputConnection(view, true) {
@ -32,6 +37,11 @@ public class ListenableEditingStateTest {
};
}
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
}
// -------- Start: Test BatchEditing -------
@Test
public void testBatchEditing() {
@ -239,13 +249,12 @@ public class ListenableEditingStateTest {
final Listener listener = new Listener();
final View testView = new View(RuntimeEnvironment.application);
final AndroidKeyProcessor mockKeyProcessor = mock(AndroidKeyProcessor.class);
final InputConnectionAdaptor inputConnection =
new InputConnectionAdaptor(
testView,
0,
mock(TextInputChannel.class),
mockKeyProcessor,
mockKeyboardManager,
editingState,
new EditorInfo());
@ -266,13 +275,12 @@ public class ListenableEditingStateTest {
new ListenableEditingState(null, new View(RuntimeEnvironment.application));
final Listener listener = new Listener();
final View testView = new View(RuntimeEnvironment.application);
final AndroidKeyProcessor mockKeyProcessor = mock(AndroidKeyProcessor.class);
final InputConnectionAdaptor inputConnection =
new InputConnectionAdaptor(
testView,
0,
mock(TextInputChannel.class),
mockKeyProcessor,
mockKeyboardManager,
editingState,
new EditorInfo());
editingState.replace(0, editingState.length(), "initial text");
@ -302,13 +310,12 @@ public class ListenableEditingStateTest {
new ListenableEditingState(null, new View(RuntimeEnvironment.application));
final Listener listener = new Listener();
final View testView = new View(RuntimeEnvironment.application);
final AndroidKeyProcessor mockKeyProcessor = mock(AndroidKeyProcessor.class);
final InputConnectionAdaptor inputConnection =
new InputConnectionAdaptor(
testView,
0,
mock(TextInputChannel.class),
mockKeyProcessor,
mockKeyboardManager,
editingState,
new EditorInfo());
editingState.replace(0, editingState.length(), "initial text");
@ -364,13 +371,12 @@ public class ListenableEditingStateTest {
new ListenableEditingState(null, new View(RuntimeEnvironment.application));
final Listener listener = new Listener();
final View testView = new View(RuntimeEnvironment.application);
final AndroidKeyProcessor mockKeyProcessor = mock(AndroidKeyProcessor.class);
final InputConnectionAdaptor inputConnection =
new InputConnectionAdaptor(
testView,
0,
mock(TextInputChannel.class),
mockKeyProcessor,
mockKeyboardManager,
editingState,
new EditorInfo());
editingState.replace(0, editingState.length(), "initial text");

View File

@ -40,8 +40,8 @@ import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
import android.view.inputmethod.InputMethodManager;
import android.view.inputmethod.InputMethodSubtype;
import io.flutter.embedding.android.AndroidKeyProcessor;
import io.flutter.embedding.android.FlutterView;
import io.flutter.embedding.android.KeyboardManager;
import io.flutter.embedding.engine.FlutterEngine;
import io.flutter.embedding.engine.FlutterJNI;
import io.flutter.embedding.engine.dart.DartExecutor;
@ -210,7 +210,8 @@ public class TextInputPluginTest {
.updateEditingState(anyInt(), any(), anyInt(), anyInt(), anyInt(), anyInt());
InputConnectionAdaptor inputConnectionAdaptor =
(InputConnectionAdaptor) textInputPlugin.createInputConnection(testView, outAttrs);
(InputConnectionAdaptor)
textInputPlugin.createInputConnection(testView, mock(KeyboardManager.class), outAttrs);
inputConnectionAdaptor.beginBatchEdit();
verify(textInputChannel, times(0))
@ -376,7 +377,9 @@ public class TextInputPluginTest {
textInputPlugin.setTextInputEditingState(
testView, new TextInputChannel.TextEditState("", 0, 0, -1, -1));
assertEquals(1, testImm.getRestartCount(testView));
InputConnection connection = textInputPlugin.createInputConnection(testView, new EditorInfo());
InputConnection connection =
textInputPlugin.createInputConnection(
testView, mock(KeyboardManager.class), new EditorInfo());
connection.setComposingText("POWERRRRR", 1);
textInputPlugin.setTextInputEditingState(
@ -520,9 +523,12 @@ public class TextInputPluginTest {
any(BinaryMessenger.BinaryReply.class));
assertEquals("flutter/textinput", channelCaptor.getValue());
verifyMethodCall(bufferCaptor.getValue(), "TextInputClient.requestExistingInputState", null);
InputConnection connection = textInputPlugin.createInputConnection(testView, new EditorInfo());
InputConnectionAdaptor connection =
(InputConnectionAdaptor)
textInputPlugin.createInputConnection(
testView, mock(KeyboardManager.class), new EditorInfo());
connection.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER));
connection.handleKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER));
verify(dartExecutor, times(2))
.send(
channelCaptor.capture(),
@ -533,9 +539,9 @@ public class TextInputPluginTest {
bufferCaptor.getValue(),
"TextInputClient.performAction",
new String[] {"0", "TextInputAction.done"});
connection.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_ENTER));
connection.handleKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_ENTER));
connection.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_NUMPAD_ENTER));
connection.handleKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_NUMPAD_ENTER));
verify(dartExecutor, times(3))
.send(
channelCaptor.capture(),
@ -585,7 +591,9 @@ public class TextInputPluginTest {
// There's a pending restart since we initialized the text input client. Flush that now.
textInputPlugin.setTextInputEditingState(
testView, new TextInputChannel.TextEditState("text", 0, 0, -1, -1));
InputConnection connection = textInputPlugin.createInputConnection(testView, new EditorInfo());
InputConnection connection =
textInputPlugin.createInputConnection(
testView, mock(KeyboardManager.class), new EditorInfo());
connection.requestCursorUpdates(
InputConnection.CURSOR_UPDATE_MONITOR | InputConnection.CURSOR_UPDATE_IMMEDIATE);
@ -789,13 +797,13 @@ public class TextInputPluginTest {
// The input method updates the text, call notifyValueChanged.
testAfm.resetStates();
final AndroidKeyProcessor mockKeyProcessor = mock(AndroidKeyProcessor.class);
final KeyboardManager mockKeyboardManager = mock(KeyboardManager.class);
InputConnectionAdaptor adaptor =
new InputConnectionAdaptor(
testView,
0,
mock(TextInputChannel.class),
mockKeyProcessor,
mockKeyboardManager,
(ListenableEditingState) textInputPlugin.getEditable(),
new EditorInfo());
adaptor.commitText("input from IME ", 1);

View File

@ -4,81 +4,186 @@
<sdk dir="../../../third_party/android_tools/sdk" />
<module name="FlutterEngine" android="true" library="true" compile-sdk-version="android-P">
<manifest file="../../../flutter/shell/platform/android/AndroidManifest.xml" />
<src file="../../../flutter/shell/platform/android/io/flutter/app/FlutterPluginRegistry.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/app/FlutterFragmentActivity.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/app/FlutterActivity.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/app/FlutterActivityEvents.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/app/FlutterApplication.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/app/FlutterActivityDelegate.java" />
<src file="../../../flutter/shell/platform/android/gen/io/flutter/app/BuildConfig.java" />
<src file="../../../flutter/shell/platform/android/gen/io/flutter/app/Manifest.java" />
<src file="../../../flutter/shell/platform/android/gen/io/flutter/app/R.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/Log.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/util/Preconditions.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/util/Predicate.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/util/PathUtils.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/embedding/android/AndroidTouchProcessor.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/embedding/android/FlutterView.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/embedding/android/FlutterTextureView.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/embedding/android/FlutterFragment.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/embedding/android/FlutterSurfaceView.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/embedding/android/AndroidKeyProcessor.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/embedding/engine/renderer/FlutterRenderer.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/embedding/engine/renderer/OnFirstFrameRenderedListener.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/app/FlutterPluginRegistry.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/app/FlutterActivityDelegate.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/app/FlutterFragmentActivity.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/app/FlutterPlayStoreSplitApplication.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/app/FlutterApplication.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/app/FlutterActivity.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/app/FlutterActivityEvents.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/localization/LocalizationPlugin.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/editing/ListenableEditingState.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/editing/FlutterTextUtils.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/editing/ImeSyncDeferringInsetsCallback.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/platform/PlatformView.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewRegistryImpl.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/platform/SingleViewPresentation.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewsAccessibilityDelegate.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/platform/AccessibilityEventsDelegate.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewRegistry.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/platform/PlatformPlugin.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewFactory.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/platform/VirtualDisplayController.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/common/MethodChannel.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/common/JSONMethodCodec.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/common/ActivityLifecycleListener.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/common/FlutterException.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/common/JSONMessageCodec.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/common/MethodCodec.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/common/StringCodec.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/common/MessageCodec.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/common/EventChannel.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/common/BinaryMessenger.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/common/BasicMessageChannel.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/common/BinaryCodec.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/common/JSONUtil.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/common/MethodCall.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/common/ErrorLogResult.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/common/StandardMessageCodec.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/common/PluginRegistry.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/common/StandardMethodCodec.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/mouse/MouseCursorPlugin.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/view/AccessibilityBridge.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/view/VsyncWaiter.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/view/FlutterMain.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/view/FlutterCallbackInformation.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/view/AccessibilityViewEmbedder.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/view/FlutterRunArguments.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/view/FlutterView.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/view/FlutterNativeView.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/view/TextureRegistry.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/embedding/engine/loader/FlutterLoader.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/embedding/engine/loader/FlutterApplicationInfo.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/embedding/engine/loader/ApplicationInfoLoader.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/embedding/engine/loader/ResourceExtractor.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/embedding/engine/FlutterEngineCache.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/embedding/engine/dart/DartExecutor.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/embedding/engine/dart/PlatformMessageHandler.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/embedding/engine/dart/DartMessenger.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/embedding/engine/FlutterEngineCache.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/embedding/engine/FlutterEngineGroup.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/embedding/engine/FlutterShellArgs.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/SettingsChannel.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/NavigationChannel.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/LocalizationChannel.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/TextInputChannel.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/AccessibilityChannel.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/KeyEventChannel.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/PlatformChannel.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/embedding/engine/FlutterOverlaySurface.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/embedding/engine/renderer/SurfaceTextureWrapper.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/embedding/engine/renderer/FlutterRenderer.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/embedding/engine/renderer/RenderSurface.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/embedding/engine/renderer/FlutterUiDisplayListener.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/SystemChannel.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/PlatformViewsChannel.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/NavigationChannel.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/LifecycleChannel.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewRegistry.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/platform/VirtualDisplayController.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewRegistryImpl.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/platform/SingleViewPresentation.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewFactory.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/platform/AccessibilityEventsDelegate.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/platform/PlatformView.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewsAccessibilityDelegate.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/platform/PlatformPlugin.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/common/BasicMessageChannel.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/common/JSONMethodCodec.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/common/JSONUtil.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/common/PluginRegistry.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/common/MessageCodec.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/common/ErrorLogResult.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/common/JSONMessageCodec.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/common/ActivityLifecycleListener.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/common/BinaryCodec.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/common/FlutterException.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/common/StringCodec.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/common/StandardMessageCodec.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/common/StandardMethodCodec.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/common/MethodCodec.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/common/BinaryMessenger.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/common/MethodCall.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/common/EventChannel.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/common/MethodChannel.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/editing/FlutterTextUtils.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/view/FlutterNativeView.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/view/FlutterCallbackInformation.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/view/VsyncWaiter.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/view/FlutterView.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/view/FlutterMain.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/view/ResourceExtractor.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/view/TextureRegistry.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/view/FlutterRunArguments.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/view/AccessibilityBridge.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/view/AccessibilityViewEmbedder.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/MouseCursorChannel.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/SettingsChannel.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/LocalizationChannel.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/KeyEventChannel.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/TextInputChannel.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/RestorationChannel.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/PlatformChannel.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/AccessibilityChannel.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/DeferredComponentChannel.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/embedding/engine/FlutterEngineConnectionRegistry.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/embedding/engine/deferredcomponents/PlayStoreDeferredComponentManager.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/embedding/engine/deferredcomponents/DeferredComponentManager.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/embedding/engine/FlutterEngine.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/embedding/engine/FlutterEngineGroup.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/embedding/engine/plugins/service/ServicePluginBinding.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/embedding/engine/plugins/service/ServiceControlSurface.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/embedding/engine/plugins/service/ServiceAware.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/embedding/engine/plugins/broadcastreceiver/BroadcastReceiverControlSurface.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/embedding/engine/plugins/broadcastreceiver/BroadcastReceiverPluginBinding.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/embedding/engine/plugins/broadcastreceiver/BroadcastReceiverAware.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/embedding/engine/plugins/lifecycle/HiddenLifecycleReference.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/embedding/engine/plugins/util/GeneratedPluginRegister.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/embedding/engine/plugins/contentprovider/ContentProviderAware.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/embedding/engine/plugins/contentprovider/ContentProviderPluginBinding.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/embedding/engine/plugins/contentprovider/ContentProviderControlSurface.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/embedding/engine/plugins/activity/ActivityControlSurface.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/embedding/engine/plugins/activity/ActivityAware.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/embedding/engine/plugins/activity/ActivityPluginBinding.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/embedding/engine/plugins/shim/ShimPluginRegistry.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/embedding/engine/plugins/shim/ShimRegistrar.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/embedding/engine/plugins/PluginRegistry.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/embedding/engine/plugins/FlutterPlugin.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/embedding/engine/mutatorsstack/FlutterMutatorView.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/embedding/engine/mutatorsstack/FlutterMutatorsStack.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/embedding/android/FlutterActivityLaunchConfigs.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/embedding/android/KeyboardManager.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/embedding/android/ExclusiveAppComponent.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/embedding/android/SplashScreenProvider.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/embedding/android/FlutterTextureView.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/embedding/android/FlutterSurfaceView.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/embedding/android/FlutterSplashView.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/embedding/android/FlutterFragmentActivity.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/embedding/android/DrawableSplashScreen.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/embedding/android/FlutterEngineProvider.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/embedding/android/MotionEventTracker.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/embedding/android/TransparencyMode.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/embedding/android/SplashScreen.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/embedding/android/FlutterEngineConfigurator.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/embedding/android/RenderMode.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/embedding/android/KeyChannelResponder.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/embedding/android/FlutterView.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/embedding/android/AndroidTouchProcessor.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/embedding/android/FlutterImageView.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/embedding/android/FlutterFragment.java" />
<src file="../../../flutter/shell/platform/android/io/flutter/FlutterInjector.java" />
<src file="../../../flutter/shell/platform/android/test/io/flutter/util/PreconditionsTest.java" />
<src file="../../../flutter/shell/platform/android/test/io/flutter/util/FakeKeyEvent.java" />
<src file="../../../flutter/shell/platform/android/test/io/flutter/FlutterTestSuite.java" />
<src file="../../../flutter/shell/platform/android/test/io/flutter/FlutterInjectorTest.java" />
<src file="../../../flutter/shell/platform/android/test/io/flutter/plugin/localization/LocalizationPluginTest.java" />
<src file="../../../flutter/shell/platform/android/test/io/flutter/plugin/editing/InputConnectionAdaptorTest.java" />
<src file="../../../flutter/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java" />
<src file="../../../flutter/shell/platform/android/test/io/flutter/plugin/editing/ListenableEditingStateTest.java" />
<src file="../../../flutter/shell/platform/android/test/io/flutter/plugin/platform/PlatformPluginTest.java" />
<src file="../../../flutter/shell/platform/android/test/io/flutter/plugin/platform/SingleViewPresentationTest.java" />
<src file="../../../flutter/shell/platform/android/test/io/flutter/plugin/platform/PlatformViewsControllerTest.java" />
<src file="../../../flutter/shell/platform/android/test/io/flutter/plugin/common/StandardMethodCodecTest.java" />
<src file="../../../flutter/shell/platform/android/test/io/flutter/plugin/common/StandardMessageCodecTest.java" />
<src file="../../../flutter/shell/platform/android/test/io/flutter/plugin/mouse/MouseCursorPluginTest.java" />
<src file="../../../flutter/shell/platform/android/test/io/flutter/view/AccessibilityBridgeTest.java" />
<src file="../../../flutter/shell/platform/android/test/io/flutter/SmokeTest.java" />
<src file="../../../flutter/shell/platform/android/test/io/flutter/external/FlutterLaunchTests.java" />
<src file="../../../flutter/shell/platform/android/test/io/flutter/embedding/engine/loader/FlutterLoaderTest.java" />
<src file="../../../flutter/shell/platform/android/test/io/flutter/embedding/engine/loader/ApplicationInfoLoaderTest.java" />
<src file="../../../flutter/shell/platform/android/test/io/flutter/embedding/engine/RenderingComponentTest.java" />
<src file="../../../flutter/shell/platform/android/test/io/flutter/embedding/engine/dart/DartExecutorTest.java" />
<src file="../../../flutter/shell/platform/android/test/io/flutter/embedding/engine/dart/DartMessengerTest.java" />
<src file="../../../flutter/shell/platform/android/test/io/flutter/embedding/engine/FlutterEngineGroupComponentTest.java" />
<src file="../../../flutter/shell/platform/android/test/io/flutter/embedding/engine/FlutterJNITest.java" />
<src file="../../../flutter/shell/platform/android/test/io/flutter/embedding/engine/FlutterEngineConnectionRegistryTest.java" />
<src file="../../../flutter/shell/platform/android/test/io/flutter/embedding/engine/FlutterEngineCacheTest.java" />
<src file="../../../flutter/shell/platform/android/test/io/flutter/embedding/engine/renderer/FlutterRendererTest.java" />
<src file="../../../flutter/shell/platform/android/test/io/flutter/embedding/engine/systemchannels/RestorationChannelTest.java" />
<src file="../../../flutter/shell/platform/android/test/io/flutter/embedding/engine/systemchannels/KeyEventChannelTest.java" />
<src file="../../../flutter/shell/platform/android/test/io/flutter/embedding/engine/systemchannels/PlatformChannelTest.java" />
<src file="../../../flutter/shell/platform/android/test/io/flutter/embedding/engine/systemchannels/DeferredComponentChannelTest.java" />
<src file="../../../flutter/shell/platform/android/test/io/flutter/embedding/engine/deferredcomponents/PlayStoreDeferredComponentManagerTest.java" />
<src file="../../../flutter/shell/platform/android/test/io/flutter/embedding/engine/FlutterEngineTest.java" />
<src file="../../../flutter/shell/platform/android/test/io/flutter/embedding/engine/plugins/shim/ShimPluginRegistryTest.java" />
<src file="../../../flutter/shell/platform/android/test/io/flutter/embedding/engine/FlutterShellArgsTest.java" />
<src file="../../../flutter/shell/platform/android/test/io/flutter/embedding/engine/PluginComponentTest.java" />
<src file="../../../flutter/shell/platform/android/test/io/flutter/embedding/engine/mutatorsstack/FlutterMutatorViewTest.java" />
<src file="../../../flutter/shell/platform/android/test/io/flutter/embedding/android/KeyChannelResponderTest.java" />
<src file="../../../flutter/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java" />
<src file="../../../flutter/shell/platform/android/test/io/flutter/embedding/android/FlutterAndroidComponentTest.java" />
<src file="../../../flutter/shell/platform/android/test/io/flutter/embedding/android/FlutterViewTest.java" />
<src file="../../../flutter/shell/platform/android/test/io/flutter/embedding/android/KeyboardManagerTest.java" />
<src file="../../../flutter/shell/platform/android/test/io/flutter/embedding/android/RobolectricFlutterActivity.java" />
<src file="../../../flutter/shell/platform/android/test/io/flutter/embedding/android/FlutterFragmentActivityTest.java" />
<src file="../../../flutter/shell/platform/android/test/io/flutter/embedding/android/FlutterFragmentTest.java" />
<src file="../../../flutter/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityTest.java" />
<src file="../../../flutter/shell/platform/android/test/io/flutter/plugins/GeneratedPluginRegistrant.java" />
</module>
</project>