mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
[Android KeyEvents] Split AndroidKeyProcessor into separate classes (flutter/engine#25628)
This commit is contained in:
parent
270cc23b0f
commit
f2d4eff14c
@ -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
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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. */
|
||||
|
||||
@ -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());
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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'));
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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");
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user