From 5f0df6eba86f12c8bc3cce11ad417272579e24ea Mon Sep 17 00:00:00 2001 From: Amir Hardon Date: Thu, 6 Jun 2019 21:58:04 -0700 Subject: [PATCH] Keyboard support for embedded Android views. (flutter/engine#9203) Generally what this PR is doing is setting up a delegation mechanism for Android's onCreateInputConnection. It works by letting the framework know when an embedded view gets loses focus(within the virtual display), the framework maintains a focus node for each Android view that is kept in sync with the focus state of the embedded view. The TextInputPlugin is extended to allow for 2 type of text clients a "framework client"(what we had before) and a "platform view client". When the AndroidView's focus node in the framework is focused the framework sets a "platform view text client" for the TextInputPlugin, which will result in the TextInputPlugin delegating createInputConnection to the platform view. When a platform view is resized, we are detaching it from a virtual display and attaching it to a new one, as a side affect a platform view might lose an active input connection, to workaround that we "lock" the connection when resizing(by caching it and forcing the cached copy until the resize is done). Additional things worth calling out in this PR: To properly answer which views are allowed for input connection proxying we compare a candidate view's root view to the set of root views of all virtual displays. We also preserve a view's focus state across resizes. Note that this PR only wires text for the io.flutter.view.FlutterView For the new Android embedding some additional plumbing is necessary. Corresponding framework PR: flutter/flutter#33901 flutter/flutter#19718 --- .../embedding/android/FlutterView.java | 3 +- .../systemchannels/PlatformViewsChannel.java | 29 +++++ .../systemchannels/TextInputChannel.java | 14 ++ .../plugin/editing/TextInputPlugin.java | 120 ++++++++++++++++-- .../PlatformViewsAccessibilityDelegate.java | 1 - .../platform/PlatformViewsController.java | 95 ++++++++++++-- .../platform/SingleViewPresentation.java | 23 +++- .../platform/VirtualDisplayController.java | 28 +++- .../android/io/flutter/view/FlutterView.java | 10 +- 9 files changed, 294 insertions(+), 29 deletions(-) diff --git a/engine/src/flutter/shell/platform/android/io/flutter/embedding/android/FlutterView.java b/engine/src/flutter/shell/platform/android/io/flutter/embedding/android/FlutterView.java index df3e9d9306..37986439e4 100644 --- a/engine/src/flutter/shell/platform/android/io/flutter/embedding/android/FlutterView.java +++ b/engine/src/flutter/shell/platform/android/io/flutter/embedding/android/FlutterView.java @@ -477,7 +477,8 @@ public class FlutterView extends FrameLayout { // in a way that Flutter understands. textInputPlugin = new TextInputPlugin( this, - this.flutterEngine.getDartExecutor() + this.flutterEngine.getDartExecutor(), + null ); androidKeyProcessor = new AndroidKeyProcessor( this.flutterEngine.getKeyEventChannel(), diff --git a/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/PlatformViewsChannel.java b/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/PlatformViewsChannel.java index 2a470011c8..21bc466991 100644 --- a/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/PlatformViewsChannel.java +++ b/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/PlatformViewsChannel.java @@ -26,6 +26,13 @@ public class PlatformViewsChannel { private final MethodChannel channel; private PlatformViewsHandler handler; + public void invokeViewFocused(int viewId) { + if (channel == null) { + return; + } + channel.invokeMethod("viewFocused", viewId); + } + private final MethodChannel.MethodCallHandler parsingHandler = new MethodChannel.MethodCallHandler() { @Override public void onMethodCall(MethodCall call, MethodChannel.Result result) { @@ -51,6 +58,9 @@ public class PlatformViewsChannel { case "setDirection": setDirection(call, result); break; + case "clearFocus": + clearFocus(call, result); + break; default: result.notImplemented(); } @@ -172,6 +182,20 @@ public class PlatformViewsChannel { ); } } + + private void clearFocus(MethodCall call, MethodChannel.Result result) { + int viewId = call.arguments(); + try { + handler.clearFocus(viewId); + result.success(null); + } catch (IllegalStateException exception) { + result.error( + "error", + exception.getMessage(), + null + ); + } + } }; /** @@ -241,6 +265,11 @@ public class PlatformViewsChannel { */ // TODO(mattcarroll): Introduce an annotation for @TextureId void setDirection(int viewId, int direction); + + /** + * Clears the focus from the platform view with a give id if it is currently focused. + */ + void clearFocus(int viewId); } /** diff --git a/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/TextInputChannel.java b/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/TextInputChannel.java index 728ae40797..ec43f402ea 100644 --- a/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/TextInputChannel.java +++ b/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/TextInputChannel.java @@ -70,6 +70,10 @@ public class TextInputChannel { result.error("error", exception.getMessage(), null); } break; + case "TextInput.setPlatformViewClient": + final int id = (int) args; + textInputMethodHandler.setPlatformViewClient(id); + break; case "TextInput.setEditingState": try { final JSONObject editingState = (JSONObject) args; @@ -218,6 +222,16 @@ public class TextInputChannel { // TODO(mattcarroll): javadoc void setClient(int textInputClientId, @NonNull Configuration configuration); + /** + * Sets a platform view as the text input client. + * + * Subsequent calls to createInputConnection will be delegated to the platform view until a + * different client is set. + * + * @param id the ID of the platform view to be set as a text input client. + */ + void setPlatformViewClient(int id); + // TODO(mattcarroll): javadoc void setEditingState(@NonNull TextEditState editingState); diff --git a/engine/src/flutter/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java b/engine/src/flutter/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java index 051befd6fa..15ddd66338 100644 --- a/engine/src/flutter/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java +++ b/engine/src/flutter/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java @@ -18,7 +18,7 @@ import android.view.inputmethod.InputMethodManager; import io.flutter.embedding.engine.dart.DartExecutor; import io.flutter.embedding.engine.systemchannels.TextInputChannel; -import io.flutter.view.FlutterView; +import io.flutter.plugin.platform.PlatformViewsController; /** * Android implementation of the text input plugin. @@ -30,7 +30,8 @@ public class TextInputPlugin { private final InputMethodManager mImm; @NonNull private final TextInputChannel textInputChannel; - private int mClient = 0; + @NonNull + private InputTarget inputTarget = new InputTarget(InputTarget.Type.NO_TARGET, 0); @Nullable private TextInputChannel.Configuration configuration; @Nullable @@ -39,7 +40,13 @@ public class TextInputPlugin { @Nullable private InputConnection lastInputConnection; - public TextInputPlugin(View view, @NonNull DartExecutor dartExecutor) { + private PlatformViewsController platformViewsController; + + // When true following calls to createInputConnection will return the cached lastInputConnection if the input + // target is a platform view. See the comments on lockPlatformViewInputConnection for more details. + private boolean isInputConnectionLocked; + + public TextInputPlugin(View view, @NonNull DartExecutor dartExecutor, PlatformViewsController platformViewsController) { mView = view; mImm = (InputMethodManager) view.getContext().getSystemService( Context.INPUT_METHOD_SERVICE); @@ -61,6 +68,11 @@ public class TextInputPlugin { setTextInputClient(textInputClientId, configuration); } + @Override + public void setPlatformViewClient(int platformViewId) { + setPlatformViewTextInputClient(platformViewId); + } + @Override public void setEditingState(TextInputChannel.TextEditState editingState) { setTextInputEditingState(mView, editingState); @@ -71,6 +83,8 @@ public class TextInputPlugin { clearTextInputClient(); } }); + this.platformViewsController = platformViewsController; + platformViewsController.attachTextInputPlugin(this); } @NonNull @@ -78,6 +92,40 @@ public class TextInputPlugin { return mImm; } + /*** + * Use the current platform view input connection until unlockPlatformViewInputConnection is called. + * + * The current input connection instance is cached and any following call to @{link createInputConnection} returns + * the cached connection until unlockPlatformViewInputConnection is called. + * + * This is a no-op if the current input target isn't a platform view. + * + * This is used to preserve an input connection when moving a platform view from one virtual display to another. + */ + public void lockPlatformViewInputConnection() { + if (inputTarget.type == InputTarget.Type.PLATFORM_VIEW) { + isInputConnectionLocked = true; + } + } + + /** + * Unlocks the input connection. + * + * See also: @{link lockPlatformViewInputConnection}. + */ + public void unlockPlatformViewInputConnection() { + isInputConnectionLocked = false; + } + + /** + * Detaches the text input plugin from the platform views controller. + * + * The TextInputPlugin instance should not be used after calling this. + */ + public void destroy() { + platformViewsController.detachTextInputPlugin(); + } + private static int inputTypeFromTextInputType( TextInputChannel.InputType type, boolean obscureText, @@ -128,8 +176,16 @@ public class TextInputPlugin { } public InputConnection createInputConnection(View view, EditorInfo outAttrs) { - if (mClient == 0) { + if (inputTarget.type == InputTarget.Type.NO_TARGET) { lastInputConnection = null; + return null; + } + + if (inputTarget.type == InputTarget.Type.PLATFORM_VIEW) { + if (isInputConnectionLocked) { + return lastInputConnection; + } + lastInputConnection = platformViewsController.getPlatformViewById(inputTarget.id).onCreateInputConnection(outAttrs); return lastInputConnection; } @@ -158,7 +214,7 @@ public class TextInputPlugin { InputConnectionAdaptor connection = new InputConnectionAdaptor( view, - mClient, + inputTarget.id, textInputChannel, mEditable ); @@ -180,17 +236,26 @@ public class TextInputPlugin { } private void hideTextInput(View view) { - mImm.hideSoftInputFromWindow(view.getApplicationWindowToken(), 0); + if (inputTarget.type == InputTarget.Type.FRAMEWORK_CLIENT) { + mImm.hideSoftInputFromWindow(view.getApplicationWindowToken(), 0); + } } private void setTextInputClient(int client, TextInputChannel.Configuration configuration) { - mClient = client; + inputTarget = new InputTarget(InputTarget.Type.FRAMEWORK_CLIENT, client); this.configuration = configuration; mEditable = Editable.Factory.getInstance().newEditable(""); // setTextInputClient will be followed by a call to setTextInputEditingState. // Do a restartInput at that time. mRestartInputPending = true; + unlockPlatformViewInputConnection(); + } + + private void setPlatformViewTextInputClient(int platformViewId) { + inputTarget = new InputTarget(InputTarget.Type.PLATFORM_VIEW, platformViewId); + mImm.restartInput(mView); + mRestartInputPending = false; } private void applyStateToSelection(TextInputChannel.TextEditState state) { @@ -220,6 +285,45 @@ public class TextInputPlugin { } private void clearTextInputClient() { - mClient = 0; + if (inputTarget.type == InputTarget.Type.PLATFORM_VIEW) { + // Focus changes in the framework tree have no guarantees on the order focus nodes are notified. A node + // that lost focus may be notified before or after a node that gained focus. + // When moving the focus from a Flutter text field to an AndroidView, it is possible that the Flutter text + // field's focus node will be notified that it lost focus after the AndroidView was notified that it gained + // focus. When this happens the text field will send a clearTextInput command which we ignore. + // By doing this we prevent the framework from clearing a platform view input client(the only way to do so + // is to set a new framework text client). I don't see an obvious use case for "clearing" a platform views + // text input client, and it may be error prone as we don't know how the platform view manages the input + // connection and we probably shouldn't interfere. + // If we ever want to allow the framework to clear a platform view text client we should probably consider + // changing the focus manager such that focus nodes that lost focus are notified before focus nodes that + // gained focus as part of the same focus event. + return; + } + inputTarget = new InputTarget(InputTarget.Type.NO_TARGET, 0); + unlockPlatformViewInputConnection(); + } + + static private class InputTarget { + enum Type { + NO_TARGET, + // InputConnection is managed by the TextInputPlugin, and events are forwarded to the Flutter framework. + FRAMEWORK_CLIENT, + // InputConnection is managed by an embedded platform view. + PLATFORM_VIEW + } + + public InputTarget(@NonNull Type type, int id) { + this.type = type; + this.id = id; + } + + @NonNull + Type type; + // The ID of the input target. + // + // For framework clients this is the framework input connection client ID. + // For platform views this is the platform view's ID. + int id; } } diff --git a/engine/src/flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewsAccessibilityDelegate.java b/engine/src/flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewsAccessibilityDelegate.java index 3b81a50555..b26c60ff6f 100644 --- a/engine/src/flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewsAccessibilityDelegate.java +++ b/engine/src/flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewsAccessibilityDelegate.java @@ -11,7 +11,6 @@ import io.flutter.view.AccessibilityBridge; * Facilitates interaction between the accessibility bridge and embedded platform views. */ public interface PlatformViewsAccessibilityDelegate { - /** * Returns the root of the view hierarchy for the platform view with the requested id, or null if there is no * corresponding view. diff --git a/engine/src/flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java b/engine/src/flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java index 2e2b621c7b..e6b8f387b3 100644 --- a/engine/src/flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java +++ b/engine/src/flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java @@ -23,12 +23,15 @@ import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodChannel; import io.flutter.plugin.common.StandardMethodCodec; +import io.flutter.plugin.editing.TextInputPlugin; import io.flutter.view.AccessibilityBridge; import io.flutter.view.TextureRegistry; +import java.lang.reflect.Method; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.List; /** @@ -51,6 +54,8 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega // The texture registry maintaining the textures into which the embedded views will be rendered. private TextureRegistry textureRegistry; + private TextInputPlugin textInputPlugin; + // The system channel used to communicate with the framework about platform views. private PlatformViewsChannel platformViewsChannel; @@ -59,6 +64,11 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega private final HashMap vdControllers; + // The set of root views for all active virtual displays managed by this controller. + // This allows an O(1) check whether a view is managed by this controller(by checking if it's root view is in this + // set). This is used by isPlatformView. + private final HashSet vdRootViews; + private final PlatformViewsChannel.PlatformViewsHandler channelHandler = new PlatformViewsChannel.PlatformViewsHandler() { @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) @Override @@ -92,14 +102,19 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega TextureRegistry.SurfaceTextureEntry textureEntry = textureRegistry.createSurfaceTexture(); VirtualDisplayController vdController = VirtualDisplayController.create( - context, - accessibilityEventsDelegate, - viewFactory, - textureEntry, - toPhysicalPixels(request.logicalWidth), - toPhysicalPixels(request.logicalHeight), - request.viewId, - createParams + context, + accessibilityEventsDelegate, + viewFactory, + textureEntry, + physicalWidth, + physicalHeight, + request.viewId, + createParams, + (view, hasFocus) -> { + if (hasFocus) { + platformViewsChannel.invokeViewFocused(request.viewId); + } + } ); if (vdController == null) { @@ -108,7 +123,9 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega } vdControllers.put(request.viewId, vdController); - vdController.getView().setLayoutDirection(request.direction); + View platformView = vdController.getView(); + platformView.setLayoutDirection(request.direction); + vdRootViews.add(platformView.getRootView()); // TODO(amirh): copy accessibility nodes to the FlutterView's accessibility tree. @@ -125,6 +142,9 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega + viewId); } + View rootView = vdController.getView().getRootView(); + vdRootViews.remove(rootView); + vdController.dispose(); vdControllers.remove(viewId); } @@ -143,11 +163,28 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega int physicalHeight = toPhysicalPixels(request.newLogicalHeight); validateVirtualDisplayDimensions(physicalWidth, physicalHeight); + if (textInputPlugin != null) { + // Resizing involved moving the platform view to a new virtual display. + // Doing so potentially results in losing an active input connection. + // To make sure we preserve the input connection when resizing we lock it here + // and unlock after the resize is complete. + textInputPlugin.lockPlatformViewInputConnection(); + } + vdRootViews.remove(vdController.getView().getRootView()); vdController.resize( - physicalWidth, - physicalHeight, - onComplete + physicalWidth, + physicalHeight, + new Runnable() { + @Override + public void run() { + if (textInputPlugin != null) { + textInputPlugin.unlockPlatformViewInputConnection(); + } + onComplete.run(); + } + } ); + vdRootViews.add(vdController.getView().getRootView()); } @Override @@ -207,6 +244,12 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega view.setLayoutDirection(direction); } + @Override + public void clearFocus(int viewId) { + View view = vdControllers.get(viewId).getView(); + view.clearFocus(); + } + private void ensureValidAndroidVersion() { if (Build.VERSION.SDK_INT < MINIMAL_SDK) { Log.e(TAG, "Trying to use platform views with API " + Build.VERSION.SDK_INT @@ -221,6 +264,7 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega registry = new PlatformViewRegistryImpl(); vdControllers = new HashMap<>(); accessibilityEventsDelegate = new AccessibilityEventsDelegate(); + vdRootViews = new HashSet<>(); } /** @@ -270,6 +314,33 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega accessibilityEventsDelegate.setAccessibilityBridge(null); } + /** + * Attaches this controller to a text input plugin. + * + * While a text input plugin is available, the platform views controller interacts with it to facilitate + * delegation of text input connections to platform views. + * + * A platform views controller should be attached to a text input plugin whenever it is possible for the Flutter + * framework to receive text input. + */ + public void attachTextInputPlugin(TextInputPlugin textInputPlugin) { + this.textInputPlugin = textInputPlugin; + } + + /** + * Detaches this controller from the currently attached text input plugin. + */ + public void detachTextInputPlugin() { + textInputPlugin = null; + } + + /** + * Returns true if the view is a platform view managed by this controller. + */ + public boolean isPlatformView(View view) { + return vdRootViews.contains(view.getRootView()); + } + public PlatformViewRegistry getRegistry() { return registry; } diff --git a/engine/src/flutter/shell/platform/android/io/flutter/plugin/platform/SingleViewPresentation.java b/engine/src/flutter/shell/platform/android/io/flutter/plugin/platform/SingleViewPresentation.java index baec72717d..d48343e273 100644 --- a/engine/src/flutter/shell/platform/android/io/flutter/plugin/platform/SingleViewPresentation.java +++ b/engine/src/flutter/shell/platform/android/io/flutter/plugin/platform/SingleViewPresentation.java @@ -19,6 +19,7 @@ import android.widget.FrameLayout; import java.lang.reflect.*; import static android.content.Context.WINDOW_SERVICE; +import static android.view.View.OnFocusChangeListener; /* * A presentation used for hosting a single Android view in a virtual display. @@ -61,6 +62,8 @@ class SingleViewPresentation extends Presentation { // A reference to the current accessibility bridge to which accessibility events will be delegated. private final AccessibilityEventsDelegate accessibilityEventsDelegate; + private final OnFocusChangeListener focusChangeListener; + // This is the view id assigned by the Flutter framework to the embedded view, we keep it here // so when we create the platform view we can tell it its view id. private int viewId; @@ -78,6 +81,8 @@ class SingleViewPresentation extends Presentation { private PresentationState state; + private boolean startFocused = false; + /** * Creates a presentation that will use the view factory to create a new * platform view in the presentation's onCreate, and attach it. @@ -88,13 +93,15 @@ class SingleViewPresentation extends Presentation { PlatformViewFactory viewFactory, AccessibilityEventsDelegate accessibilityEventsDelegate, int viewId, - Object createParams + Object createParams, + OnFocusChangeListener focusChangeListener ) { super(outerContext, display); this.viewFactory = viewFactory; this.accessibilityEventsDelegate = accessibilityEventsDelegate; this.viewId = viewId; this.createParams = createParams; + this.focusChangeListener = focusChangeListener; state = new PresentationState(); getWindow().setFlags( WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, @@ -113,16 +120,20 @@ class SingleViewPresentation extends Presentation { Context outerContext, Display display, AccessibilityEventsDelegate accessibilityEventsDelegate, - PresentationState state + PresentationState state, + OnFocusChangeListener focusChangeListener, + boolean startFocused ) { super(outerContext, display); this.accessibilityEventsDelegate = accessibilityEventsDelegate; viewFactory = null; this.state = state; + this.focusChangeListener = focusChangeListener; getWindow().setFlags( WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE ); + this.startFocused = startFocused; } @Override @@ -148,6 +159,14 @@ class SingleViewPresentation extends Presentation { rootView = new AccessibilityDelegatingFrameLayout(getContext(), accessibilityEventsDelegate, embeddedView); rootView.addView(container); rootView.addView(state.fakeWindowViewGroup); + + embeddedView.setOnFocusChangeListener(focusChangeListener); + rootView.setFocusableInTouchMode(true); + if (startFocused) { + embeddedView.requestFocus(); + } else { + rootView.requestFocus(); + } setContentView(rootView); } diff --git a/engine/src/flutter/shell/platform/android/io/flutter/plugin/platform/VirtualDisplayController.java b/engine/src/flutter/shell/platform/android/io/flutter/plugin/platform/VirtualDisplayController.java index 2f1ce898d8..eb59218ca6 100644 --- a/engine/src/flutter/shell/platform/android/io/flutter/plugin/platform/VirtualDisplayController.java +++ b/engine/src/flutter/shell/platform/android/io/flutter/plugin/platform/VirtualDisplayController.java @@ -9,11 +9,14 @@ import android.content.Context; import android.hardware.display.DisplayManager; import android.hardware.display.VirtualDisplay; import android.os.Build; +import android.util.Log; import android.view.Surface; import android.view.View; import android.view.ViewTreeObserver; import io.flutter.view.TextureRegistry; +import static android.view.View.OnFocusChangeListener; + @TargetApi(Build.VERSION_CODES.KITKAT_WATCH) class VirtualDisplayController { @@ -25,7 +28,8 @@ class VirtualDisplayController { int width, int height, int viewId, - Object createParams + Object createParams, + OnFocusChangeListener focusChangeListener ) { textureEntry.surfaceTexture().setDefaultBufferSize(width, height); Surface surface = new Surface(textureEntry.surfaceTexture()); @@ -46,13 +50,14 @@ class VirtualDisplayController { } return new VirtualDisplayController( - context, accessibilityEventsDelegate, virtualDisplay, viewFactory, surface, textureEntry, viewId, createParams); + context, accessibilityEventsDelegate, virtualDisplay, viewFactory, surface, textureEntry, focusChangeListener, viewId, createParams); } private final Context context; private final AccessibilityEventsDelegate accessibilityEventsDelegate; private final int densityDpi; private final TextureRegistry.SurfaceTextureEntry textureEntry; + private final OnFocusChangeListener focusChangeListener; private VirtualDisplay virtualDisplay; private SingleViewPresentation presentation; private Surface surface; @@ -65,21 +70,30 @@ class VirtualDisplayController { PlatformViewFactory viewFactory, Surface surface, TextureRegistry.SurfaceTextureEntry textureEntry, + OnFocusChangeListener focusChangeListener, int viewId, Object createParams ) { this.context = context; this.accessibilityEventsDelegate = accessibilityEventsDelegate; this.textureEntry = textureEntry; + this.focusChangeListener = focusChangeListener; this.surface = surface; this.virtualDisplay = virtualDisplay; densityDpi = context.getResources().getDisplayMetrics().densityDpi; presentation = new SingleViewPresentation( - context, this.virtualDisplay.getDisplay(), viewFactory, accessibilityEventsDelegate, viewId, createParams); + context, + this.virtualDisplay.getDisplay(), + viewFactory, + accessibilityEventsDelegate, + viewId, + createParams, + focusChangeListener); presentation.show(); } public void resize(final int width, final int height, final Runnable onNewSizeFrameAvailable) { + boolean isFocused = getView().isFocused(); final SingleViewPresentation.PresentationState presentationState = presentation.detachState(); // We detach the surface to prevent it being destroyed when releasing the vd. // @@ -125,7 +139,13 @@ class VirtualDisplayController { public void onViewDetachedFromWindow(View v) {} }); - presentation = new SingleViewPresentation(context, virtualDisplay.getDisplay(), accessibilityEventsDelegate, presentationState); + presentation = new SingleViewPresentation( + context, + virtualDisplay.getDisplay(), + accessibilityEventsDelegate, + presentationState, + focusChangeListener, + isFocused); presentation.show(); } diff --git a/engine/src/flutter/shell/platform/android/io/flutter/view/FlutterView.java b/engine/src/flutter/shell/platform/android/io/flutter/view/FlutterView.java index 1893a22e71..28e3338857 100644 --- a/engine/src/flutter/shell/platform/android/io/flutter/view/FlutterView.java +++ b/engine/src/flutter/shell/platform/android/io/flutter/view/FlutterView.java @@ -191,9 +191,11 @@ public class FlutterView extends SurfaceView implements BinaryMessenger, Texture PlatformPlugin platformPlugin = new PlatformPlugin(activity, platformChannel); addActivityLifecycleListener(platformPlugin); mImm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE); - mTextInputPlugin = new TextInputPlugin(this, dartExecutor); + PlatformViewsController platformViewsController = mNativeView.getPluginRegistry().getPlatformViewsController(); + mTextInputPlugin = new TextInputPlugin(this, dartExecutor, platformViewsController); androidKeyProcessor = new AndroidKeyProcessor(keyEventChannel, mTextInputPlugin); androidTouchProcessor = new AndroidTouchProcessor(flutterRenderer); + mNativeView.getPluginRegistry().getPlatformViewsController().attachTextInputPlugin(mTextInputPlugin); // Send initial platform information to Dart sendLocalesToDart(getResources().getConfiguration()); @@ -395,6 +397,12 @@ public class FlutterView extends SurfaceView implements BinaryMessenger, Texture return mTextInputPlugin.createInputConnection(this, outAttrs); } + @Override + public boolean checkInputConnectionProxy(View view) { + PlatformViewsController platformViewsController = mNativeView.getPluginRegistry().getPlatformViewsController(); + return platformViewsController.isPlatformView(view); + } + @Override public boolean onTouchEvent(MotionEvent event) { if (!isAttached()) {