From 06872e30d8e744dcdeb435d71302ef2920bdd140 Mon Sep 17 00:00:00 2001 From: Emmanuel Garcia Date: Mon, 14 Feb 2022 23:36:28 -0800 Subject: [PATCH] Revert "Revert "Improve platform views performance (#31198)" (#31431)" This reverts commit 80b7a8cd9ac74209b2f7a4eb0fcc2ac19dae427e. --- ci/licenses_golden/licenses_flutter | 3 +- shell/platform/android/BUILD.gn | 3 +- .../android/AndroidTouchProcessor.java | 2 +- .../embedding/android/FlutterView.java | 16 - .../mutatorsstack/FlutterMutatorView.java | 29 +- .../systemchannels/PlatformViewsChannel.java | 165 +++--- .../systemchannels/TextInputChannel.java | 8 +- .../plugin/editing/TextInputPlugin.java | 109 +--- .../flutter/plugin/platform/PlatformView.java | 14 +- .../plugin/platform/PlatformViewWrapper.java | 263 ++++++++++ .../PlatformViewsAccessibilityDelegate.java | 7 +- .../platform/PlatformViewsController.java | 440 +++++++--------- .../platform/SingleViewPresentation.java | 478 ------------------ .../platform/VirtualDisplayController.java | 249 --------- .../android/io/flutter/util/ViewUtils.java | 26 + .../io/flutter/view/AccessibilityBridge.java | 37 +- .../android/io/flutter/view/FlutterView.java | 9 - .../mutatorsstack/FlutterMutatorViewTest.java | 45 -- .../platform/PlatformViewWrapperTest.java | 265 ++++++++++ .../platform/PlatformViewsControllerTest.java | 188 +------ .../platform/SingleViewPresentationTest.java | 82 --- .../test/io/flutter/util/ViewUtilsTest.java | 48 ++ .../flutter/view/AccessibilityBridgeTest.java | 31 -- tools/android_lint/baseline.xml | 11 - tools/android_lint/project.xml | 11 +- 25 files changed, 927 insertions(+), 1612 deletions(-) create mode 100644 shell/platform/android/io/flutter/plugin/platform/PlatformViewWrapper.java delete mode 100644 shell/platform/android/io/flutter/plugin/platform/SingleViewPresentation.java delete mode 100644 shell/platform/android/io/flutter/plugin/platform/VirtualDisplayController.java create mode 100644 shell/platform/android/test/io/flutter/plugin/platform/PlatformViewWrapperTest.java delete mode 100644 shell/platform/android/test/io/flutter/plugin/platform/SingleViewPresentationTest.java diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index 8740ab19c6545..43bcb9aa830c0 100755 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -1191,10 +1191,9 @@ FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/Platfor FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewFactory.java FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewRegistry.java FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewRegistryImpl.java +FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewWrapper.java FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewsAccessibilityDelegate.java FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java -FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/SingleViewPresentation.java -FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/VirtualDisplayController.java FILE: ../../../flutter/shell/platform/android/io/flutter/util/PathUtils.java FILE: ../../../flutter/shell/platform/android/io/flutter/util/Preconditions.java FILE: ../../../flutter/shell/platform/android/io/flutter/util/Predicate.java diff --git a/shell/platform/android/BUILD.gn b/shell/platform/android/BUILD.gn index ac44391e0d85a..41f9e3aed0ec7 100644 --- a/shell/platform/android/BUILD.gn +++ b/shell/platform/android/BUILD.gn @@ -268,10 +268,9 @@ android_java_sources = [ "io/flutter/plugin/platform/PlatformViewFactory.java", "io/flutter/plugin/platform/PlatformViewRegistry.java", "io/flutter/plugin/platform/PlatformViewRegistryImpl.java", + "io/flutter/plugin/platform/PlatformViewWrapper.java", "io/flutter/plugin/platform/PlatformViewsAccessibilityDelegate.java", "io/flutter/plugin/platform/PlatformViewsController.java", - "io/flutter/plugin/platform/SingleViewPresentation.java", - "io/flutter/plugin/platform/VirtualDisplayController.java", "io/flutter/util/PathUtils.java", "io/flutter/util/Preconditions.java", "io/flutter/util/Predicate.java", diff --git a/shell/platform/android/io/flutter/embedding/android/AndroidTouchProcessor.java b/shell/platform/android/io/flutter/embedding/android/AndroidTouchProcessor.java index 9e712e28043a4..32c6d954d5414 100644 --- a/shell/platform/android/io/flutter/embedding/android/AndroidTouchProcessor.java +++ b/shell/platform/android/io/flutter/embedding/android/AndroidTouchProcessor.java @@ -107,7 +107,7 @@ public boolean onTouchEvent(@NonNull MotionEvent event) { * the gesture pointers into screen coordinates. * @return True if the event was handled. */ - public boolean onTouchEvent(@NonNull MotionEvent event, Matrix transformMatrix) { + public boolean onTouchEvent(@NonNull MotionEvent event, @NonNull Matrix transformMatrix) { int pointerCount = event.getPointerCount(); // Prepare a data packet of the appropriate size and order. diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterView.java b/shell/platform/android/io/flutter/embedding/android/FlutterView.java index 39b4ebd23c704..cb6269756a9d6 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterView.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterView.java @@ -791,7 +791,6 @@ navigationBarVisible && guessBottomKeyboardInset(insets) == 0 + viewportMetrics.viewInsetBottom); sendViewportMetricsToFlutter(); - return newInsets; } @@ -867,21 +866,6 @@ public InputConnection onCreateInputConnection(@NonNull EditorInfo outAttrs) { return textInputPlugin.createInputConnection(this, keyboardManager, outAttrs); } - /** - * Allows a {@code View} that is not currently the input connection target to invoke commands on - * the {@link android.view.inputmethod.InputMethodManager}, which is otherwise disallowed. - * - *

Returns true to allow non-input-connection-targets to invoke methods on {@code - * InputMethodManager}, or false to exclusively allow the input connection target to invoke such - * methods. - */ - @Override - public boolean checkInputConnectionProxy(View view) { - return flutterEngine != null - ? flutterEngine.getPlatformViewsController().checkInputConnectionProxy(view) - : super.checkInputConnectionProxy(view); - } - /** * Invoked when a hardware key is pressed or released. * diff --git a/shell/platform/android/io/flutter/embedding/engine/mutatorsstack/FlutterMutatorView.java b/shell/platform/android/io/flutter/embedding/engine/mutatorsstack/FlutterMutatorView.java index d46f5346d7ab3..9be312cc53baf 100644 --- a/shell/platform/android/io/flutter/embedding/engine/mutatorsstack/FlutterMutatorView.java +++ b/shell/platform/android/io/flutter/embedding/engine/mutatorsstack/FlutterMutatorView.java @@ -9,13 +9,13 @@ import android.graphics.Path; import android.view.MotionEvent; import android.view.View; -import android.view.ViewGroup; import android.view.ViewTreeObserver; import android.widget.FrameLayout; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import io.flutter.embedding.android.AndroidTouchProcessor; +import io.flutter.util.ViewUtils; /** * A view that applies the {@link io.flutter.embedding.engine.mutatorsstack.FlutterMutatorsStack} to @@ -49,31 +49,6 @@ public FlutterMutatorView(@NonNull Context context) { this(context, 1, /* androidTouchProcessor=*/ null); } - /** - * Determines if the current view or any descendant view has focus. - * - * @param root The root view. - * @return True if the current view or any descendant view has focus. - */ - @VisibleForTesting - public static boolean childHasFocus(@Nullable View root) { - if (root == null) { - return false; - } - if (root.hasFocus()) { - return true; - } - if (root instanceof ViewGroup) { - final ViewGroup viewGroup = (ViewGroup) root; - for (int idx = 0; idx < viewGroup.getChildCount(); idx++) { - if (childHasFocus(viewGroup.getChildAt(idx))) { - return true; - } - } - } - return false; - } - @Nullable @VisibleForTesting ViewTreeObserver.OnGlobalFocusChangeListener activeFocusListener; /** @@ -95,7 +70,7 @@ public void setOnDescendantFocusChangeListener(@NonNull OnFocusChangeListener us new ViewTreeObserver.OnGlobalFocusChangeListener() { @Override public void onGlobalFocusChanged(View oldFocus, View newFocus) { - userFocusListener.onFocusChange(mutatorView, childHasFocus(mutatorView)); + userFocusListener.onFocusChange(mutatorView, ViewUtils.childHasFocus(mutatorView)); } }; observer.addOnGlobalFocusChangeListener(activeFocusListener); diff --git a/shell/platform/android/io/flutter/embedding/engine/systemchannels/PlatformViewsChannel.java b/shell/platform/android/io/flutter/embedding/engine/systemchannels/PlatformViewsChannel.java index 87891710bd784..4b50d01e856d9 100644 --- a/shell/platform/android/io/flutter/embedding/engine/systemchannels/PlatformViewsChannel.java +++ b/shell/platform/android/io/flutter/embedding/engine/systemchannels/PlatformViewsChannel.java @@ -14,6 +14,7 @@ import java.io.PrintWriter; import java.io.StringWriter; import java.nio.ByteBuffer; +import java.util.HashMap; import java.util.List; import java.util.Map; @@ -64,6 +65,9 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result case "resize": resize(call, result); break; + case "offset": + offset(call, result); + break; case "touch": touch(call, result); break; @@ -82,29 +86,40 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result } private void create(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { - Map createArgs = call.arguments(); - boolean usesHybridComposition = + final Map createArgs = call.arguments(); + // TODO(egarciad): Remove the "hybrid" case. + final boolean usesPlatformViewLayer = createArgs.containsKey("hybrid") && (boolean) createArgs.get("hybrid"); - // In hybrid mode, the size of the view is determined by the size of the Flow layer. - double width = (usesHybridComposition) ? 0 : (double) createArgs.get("width"); - double height = (usesHybridComposition) ? 0 : (double) createArgs.get("height"); - - PlatformViewCreationRequest request = - new PlatformViewCreationRequest( - (int) createArgs.get("id"), - (String) createArgs.get("viewType"), - width, - height, - (int) createArgs.get("direction"), - createArgs.containsKey("params") - ? ByteBuffer.wrap((byte[]) createArgs.get("params")) - : null); + final ByteBuffer additionalParams = + createArgs.containsKey("params") + ? ByteBuffer.wrap((byte[]) createArgs.get("params")) + : null; try { - if (usesHybridComposition) { - handler.createAndroidViewForPlatformView(request); + if (usesPlatformViewLayer) { + final PlatformViewCreationRequest request = + new PlatformViewCreationRequest( + (int) createArgs.get("id"), + (String) createArgs.get("viewType"), + 0, + 0, + 0, + 0, + (int) createArgs.get("direction"), + additionalParams); + handler.createForPlatformViewLayer(request); result.success(null); } else { - long textureId = handler.createVirtualDisplayForPlatformView(request); + final PlatformViewCreationRequest request = + new PlatformViewCreationRequest( + (int) createArgs.get("id"), + (String) createArgs.get("viewType"), + createArgs.containsKey("top") ? (double) createArgs.get("top") : 0.0, + createArgs.containsKey("left") ? (double) createArgs.get("left") : 0.0, + (double) createArgs.get("width"), + (double) createArgs.get("height"), + (int) createArgs.get("direction"), + additionalParams); + long textureId = handler.createForTextureLayer(request); result.success(textureId); } } catch (IllegalStateException exception) { @@ -115,15 +130,9 @@ private void create(@NonNull MethodCall call, @NonNull MethodChannel.Result resu private void dispose(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { Map disposeArgs = call.arguments(); int viewId = (int) disposeArgs.get("id"); - boolean usesHybridComposition = - disposeArgs.containsKey("hybrid") && (boolean) disposeArgs.get("hybrid"); try { - if (usesHybridComposition) { - handler.disposeAndroidViewForPlatformView(viewId); - } else { - handler.disposeVirtualDisplayForPlatformView(viewId); - } + handler.dispose(viewId); result.success(null); } catch (IllegalStateException exception) { result.error("error", detailedExceptionString(exception), null); @@ -138,14 +147,28 @@ private void resize(@NonNull MethodCall call, @NonNull MethodChannel.Result resu (double) resizeArgs.get("width"), (double) resizeArgs.get("height")); try { - handler.resizePlatformView( - resizeRequest, - new Runnable() { - @Override - public void run() { - result.success(null); - } - }); + final PlatformViewBufferSize sz = handler.resize(resizeRequest); + if (sz == null) { + result.error("error", "Failed to resize the platform view", null); + } else { + final Map response = new HashMap<>(); + response.put("width", (double) sz.width); + response.put("height", (double) sz.height); + result.success(response); + } + } catch (IllegalStateException exception) { + result.error("error", detailedExceptionString(exception), null); + } + } + + private void offset(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { + Map offsetArgs = call.arguments(); + try { + handler.offset( + (int) offsetArgs.get("id"), + (double) offsetArgs.get("top"), + (double) offsetArgs.get("left")); + result.success(null); } catch (IllegalStateException exception) { result.error("error", detailedExceptionString(exception), null); } @@ -249,36 +272,40 @@ public interface PlatformViewsHandler { * The Flutter application would like to display a new Android {@code View}, i.e., platform * view. * - *

The Android {@code View} is added to the view hierarchy. - */ - void createAndroidViewForPlatformView(@NonNull PlatformViewCreationRequest request); - - /** - * The Flutter application would like to dispose of an existing Android {@code View} rendered in - * the view hierarchy. + *

The Android View is added to the view hierarchy. This view is rendered in the Flutter + * framework by a PlatformViewLayer. + * + * @param request The metadata sent from the framework. */ - void disposeAndroidViewForPlatformView(int viewId); + void createForPlatformViewLayer(@NonNull PlatformViewCreationRequest request); /** - * The Flutter application would like to display a new Android {@code View}. + * The Flutter application would like to display a new Android {@code View}, i.e., platform + * view. * - *

{@code View} is added to a {@code VirtualDisplay}. The framework uses id returned by this - * method to lookup the texture in the engine. + *

The Android View is added to the view hierarchy. This view is rendered in the Flutter + * framework by a TextureLayer. + * + * @param request The metadata sent from the framework. + * @return The texture ID. */ - long createVirtualDisplayForPlatformView(@NonNull PlatformViewCreationRequest request); + long createForTextureLayer(@NonNull PlatformViewCreationRequest request); + + /** The Flutter application would like to dispose of an existing Android {@code View}. */ + void dispose(int viewId); /** - * The Flutter application would like to dispose of an existing Android {@code View} rendered in - * a virtual display. + * The Flutter application would like to resize an existing Android {@code View}. + * + * @param request The request to resize the platform view. + * @return The buffer size where the platform view pixels are written to. */ - void disposeVirtualDisplayForPlatformView(int viewId); + PlatformViewBufferSize resize(@NonNull PlatformViewResizeRequest request); /** - * The Flutter application would like to resize an existing Android {@code View}, i.e., platform - * view. + * The Flutter application would like to change the offset of an existing Android {@code View}. */ - void resizePlatformView( - @NonNull PlatformViewResizeRequest request, @NonNull Runnable onComplete); + void offset(int viewId, double top, double left); /** * The user touched a platform view within Flutter. @@ -321,6 +348,12 @@ public static class PlatformViewCreationRequest { /** The density independent height to display the platform view. */ public final double logicalHeight; + /** The density independent top position to display the platform view. */ + public final double logicalTop; + + /** The density independent left position to display the platform view. */ + public final double logicalLeft; + /** * The layout direction of the new platform view. * @@ -332,16 +365,20 @@ public static class PlatformViewCreationRequest { /** Custom parameters that are unique to the desired platform view. */ @Nullable public final ByteBuffer params; - /** Creates a request to construct a platform view that uses a virtual display. */ + /** Creates a request to construct a platform view. */ public PlatformViewCreationRequest( int viewId, @NonNull String viewType, + double logicalTop, + double logicalLeft, double logicalWidth, double logicalHeight, int direction, @Nullable ByteBuffer params) { this.viewId = viewId; this.viewType = viewType; + this.logicalTop = logicalTop; + this.logicalLeft = logicalLeft; this.logicalWidth = logicalWidth; this.logicalHeight = logicalHeight; this.direction = direction; @@ -349,11 +386,7 @@ public PlatformViewCreationRequest( } } - /** - * Request sent from Flutter to resize a platform view. - * - *

This only applies to platform views that use virtual displays. - */ + /** Request sent from Flutter to resize a platform view. */ public static class PlatformViewResizeRequest { /** The ID of the platform view as seen by the Flutter side. */ public final int viewId; @@ -371,6 +404,20 @@ public PlatformViewResizeRequest(int viewId, double newLogicalWidth, double newL } } + /** The platform view buffer size. */ + public static class PlatformViewBufferSize { + /** The width of the screen buffer. */ + public final int width; + + /** The height of the screen buffer. */ + public final int height; + + public PlatformViewBufferSize(int width, int height) { + this.width = width; + this.height = height; + } + } + /** The state of a touch event in Flutter within a platform view. */ public static class PlatformViewTouch { /** The ID of the platform view as seen by the Flutter side. */ diff --git a/shell/platform/android/io/flutter/embedding/engine/systemchannels/TextInputChannel.java b/shell/platform/android/io/flutter/embedding/engine/systemchannels/TextInputChannel.java index ecdf86c71820e..4c497e05d092b 100644 --- a/shell/platform/android/io/flutter/embedding/engine/systemchannels/TextInputChannel.java +++ b/shell/platform/android/io/flutter/embedding/engine/systemchannels/TextInputChannel.java @@ -89,9 +89,7 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result try { final JSONObject arguments = (JSONObject) args; final int platformViewId = arguments.getInt("platformViewId"); - final boolean usesVirtualDisplay = - arguments.optBoolean("usesVirtualDisplay", false); - textInputMethodHandler.setPlatformViewClient(platformViewId, usesVirtualDisplay); + textInputMethodHandler.setPlatformViewClient(platformViewId); result.success(null); } catch (JSONException exception) { result.error("error", exception.getMessage(), null); @@ -402,10 +400,8 @@ public interface TextInputMethodHandler { * different client is set. * * @param id the ID of the platform view to be set as a text input client. - * @param usesVirtualDisplay True if the platform view uses a virtual display, false if it uses - * hybrid composition. */ - void setPlatformViewClient(int id, boolean usesVirtualDisplay); + void setPlatformViewClient(int id); /** * Sets the size and the transform matrix of the current text input client. diff --git a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java index 7ca0febb19c39..cfd57ef7a26ea 100644 --- a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java +++ b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java @@ -54,12 +54,6 @@ public class TextInputPlugin implements ListenableEditingState.EditingStateWatch // Initialize the "last seen" text editing values to a non-null value. private TextEditState mLastKnownFrameworkTextEditingState; - // 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; - @SuppressLint("NewApi") public TextInputPlugin( View view, @@ -105,7 +99,7 @@ public void show() { @Override public void hide() { - if (inputTarget.type == InputTarget.Type.HC_PLATFORM_VIEW) { + if (inputTarget.type == InputTarget.Type.PLATFORM_VIEW) { notifyViewExited(); } else { hideTextInput(mView); @@ -136,8 +130,8 @@ public void setClient( } @Override - public void setPlatformViewClient(int platformViewId, boolean usesVirtualDisplay) { - setPlatformViewTextInputClient(platformViewId, usesVirtualDisplay); + public void setPlatformViewClient(int platformViewId) { + setPlatformViewTextInputClient(platformViewId); } @Override @@ -182,34 +176,6 @@ ImeSyncDeferringInsetsCallback getImeSyncCallback() { return imeSyncCallback; } - /** - * 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.VD_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. * @@ -292,21 +258,10 @@ public InputConnection createInputConnection( return null; } - if (inputTarget.type == InputTarget.Type.HC_PLATFORM_VIEW) { + if (inputTarget.type == InputTarget.Type.PLATFORM_VIEW) { return null; } - if (inputTarget.type == InputTarget.Type.VD_PLATFORM_VIEW) { - if (isInputConnectionLocked) { - return lastInputConnection; - } - lastInputConnection = - platformViewsController - .getPlatformViewById(inputTarget.id) - .onCreateInputConnection(outAttrs); - return lastInputConnection; - } - outAttrs.inputType = inputTypeFromTextInputType( configuration.inputType, @@ -361,9 +316,7 @@ public InputConnection getLastInputConnection() { * input connection. */ public void clearPlatformViewClient(int platformViewId) { - if ((inputTarget.type == InputTarget.Type.VD_PLATFORM_VIEW - || inputTarget.type == InputTarget.Type.HC_PLATFORM_VIEW) - && inputTarget.id == platformViewId) { + if (inputTarget.type == InputTarget.Type.PLATFORM_VIEW && inputTarget.id == platformViewId) { inputTarget = new InputTarget(InputTarget.Type.NO_TARGET, 0); notifyViewExited(); mImm.hideSoftInputFromWindow(mView.getApplicationWindowToken(), 0); @@ -424,25 +377,13 @@ void setTextInputClient(int client, TextInputChannel.Configuration configuration // setTextInputClient will be followed by a call to setTextInputEditingState. // Do a restartInput at that time. mRestartInputPending = true; - unlockPlatformViewInputConnection(); lastClientRect = null; mEditable.addEditingStateListener(this); } - private void setPlatformViewTextInputClient(int platformViewId, boolean usesVirtualDisplay) { - if (usesVirtualDisplay) { - // We need to make sure that the Flutter view is focused so that no imm operations get short - // circuited. - // Not asking for focus here specifically manifested in a but on API 28 devices where the - // platform view's request to show a keyboard was ignored. - mView.requestFocus(); - inputTarget = new InputTarget(InputTarget.Type.VD_PLATFORM_VIEW, platformViewId); - mImm.restartInput(mView); - mRestartInputPending = false; - } else { - inputTarget = new InputTarget(InputTarget.Type.HC_PLATFORM_VIEW, platformViewId); - lastInputConnection = null; - } + private void setPlatformViewTextInputClient(int platformViewId) { + inputTarget = new InputTarget(InputTarget.Type.PLATFORM_VIEW, platformViewId); + lastInputConnection = null; } private static boolean composingChanged( @@ -533,35 +474,10 @@ public void inspect(double x, double y) { @VisibleForTesting void clearTextInputClient() { - if (inputTarget.type == InputTarget.Type.VD_PLATFORM_VIEW) { - // This only applies to platform views that use a virtual display. - // 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 view's - // 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; - } mEditable.removeEditingStateListener(this); notifyViewExited(); updateAutofillConfigurationIfNeeded(null); inputTarget = new InputTarget(InputTarget.Type.NO_TARGET, 0); - unlockPlatformViewInputConnection(); lastClientRect = null; } @@ -571,12 +487,9 @@ enum Type { // InputConnection is managed by the TextInputPlugin, and events are forwarded to the Flutter // framework. FRAMEWORK_CLIENT, - // InputConnection is managed by an embedded platform view that is backed by a virtual - // display (VD). - VD_PLATFORM_VIEW, - // InputConnection is managed by an embedded platform view that is embeded in the Android view - // hierarchy, and uses hybrid composition (HC). - HC_PLATFORM_VIEW, + // InputConnection is managed by a platform view that is embeded in the Android view + // hierarchy. + PLATFORM_VIEW, } public InputTarget(@NonNull Type type, int id) { diff --git a/shell/platform/android/io/flutter/plugin/platform/PlatformView.java b/shell/platform/android/io/flutter/plugin/platform/PlatformView.java index 92f034d840d11..85ed3c40e8c2e 100644 --- a/shell/platform/android/io/flutter/plugin/platform/PlatformView.java +++ b/shell/platform/android/io/flutter/plugin/platform/PlatformView.java @@ -60,24 +60,26 @@ default void onFlutterViewDetached() {} void dispose(); /** - * Callback fired when the platform's input connection is locked, or should be used. See also - * {@link io.flutter.plugin.editing.TextInputPlugin#lockPlatformViewInputConnection}. + * Callback fired when the platform's input connection is locked, or should be used. * *

This hook only exists for rare cases where the plugin relies on the state of the input * connection. This probably doesn't need to be implemented. + * + *

This method is deprecated, and will be removed in a future release. */ - // Default interface methods are supported on all min SDK versions of Android. @SuppressLint("NewApi") + @Deprecated default void onInputConnectionLocked() {} /** - * Callback fired when the platform input connection has been unlocked. See also {@link - * io.flutter.plugin.editing.TextInputPlugin#lockPlatformViewInputConnection}. + * Callback fired when the platform input connection has been unlocked. * *

This hook only exists for rare cases where the plugin relies on the state of the input * connection. This probably doesn't need to be implemented. + * + *

This method is deprecated, and will be removed in a future release. */ - // Default interface methods are supported on all min SDK versions of Android. @SuppressLint("NewApi") + @Deprecated default void onInputConnectionUnlocked() {} } diff --git a/shell/platform/android/io/flutter/plugin/platform/PlatformViewWrapper.java b/shell/platform/android/io/flutter/plugin/platform/PlatformViewWrapper.java new file mode 100644 index 0000000000000..d7ebe184a656b --- /dev/null +++ b/shell/platform/android/io/flutter/plugin/platform/PlatformViewWrapper.java @@ -0,0 +1,263 @@ +// 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.plugin.platform; + +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.content.Context; +import android.graphics.BlendMode; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Matrix; +import android.graphics.SurfaceTexture; +import android.os.Build; +import android.view.MotionEvent; +import android.view.Surface; +import android.view.View; +import android.view.ViewTreeObserver; +import android.widget.FrameLayout; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import io.flutter.Log; +import io.flutter.embedding.android.AndroidTouchProcessor; +import io.flutter.util.ViewUtils; + +/** + * Wraps a platform view to intercept gestures and project this view onto a {@link SurfaceTexture}. + * + *

An Android platform view is composed by the engine using a {@code TextureLayer}. The view is + * embeded to the Android view hierarchy like a normal view, but it's projected onto a {@link + * SurfaceTexture}, so it can be efficiently composed by the engine. + * + *

Since the view is in the Android view hierarchy, keyboard and accessibility interactions + * behave normally. + */ +@TargetApi(23) +class PlatformViewWrapper extends FrameLayout { + private static final String TAG = "PlatformViewWrapper"; + + private int prevLeft; + private int prevTop; + private int left; + private int top; + private int bufferWidth; + private int bufferHeight; + private SurfaceTexture tx; + private Surface surface; + private AndroidTouchProcessor touchProcessor; + + @Nullable @VisibleForTesting ViewTreeObserver.OnGlobalFocusChangeListener activeFocusListener; + + public PlatformViewWrapper(@NonNull Context context) { + super(context); + setWillNotDraw(false); + } + + /** + * Sets the touch processor that allows to intercept gestures. + * + * @param newTouchProcessor The touch processor. + */ + public void setTouchProcessor(@Nullable AndroidTouchProcessor newTouchProcessor) { + touchProcessor = newTouchProcessor; + } + + /** + * Sets the texture where the view is projected onto. + * + *

{@link PlatformViewWrapper} doesn't take ownership of the {@link SurfaceTexture}. As a + * result, the caller is responsible for releasing the texture. + * + *

{@link io.flutter.view.TextureRegistry} is responsible for creating and registering textures + * in the engine. Therefore, the engine is responsible for also releasing the texture. + * + * @param newTx The texture where the view is projected onto. + */ + @SuppressLint("NewApi") + public void setTexture(@Nullable SurfaceTexture newTx) { + if (Build.VERSION.SDK_INT < 23) { + Log.e( + TAG, + "Platform views cannot be displayed below API level 23. " + + "You can prevent this issue by setting `minSdkVersion: 23` in build.gradle."); + return; + } + + tx = newTx; + + if (bufferWidth > 0 && bufferHeight > 0) { + tx.setDefaultBufferSize(bufferWidth, bufferHeight); + } + + if (surface != null) { + surface.release(); + } + surface = createSurface(newTx); + + // Fill the entire canvas with a transparent color. + // As a result, the background color of the platform view container is displayed + // to the user until the platform view draws its first frame. + final Canvas canvas = surface.lockHardwareCanvas(); + try { + if (Build.VERSION.SDK_INT >= 29) { + canvas.drawColor(Color.TRANSPARENT, BlendMode.CLEAR); + } else { + canvas.drawColor(Color.TRANSPARENT); + } + } finally { + surface.unlockCanvasAndPost(canvas); + } + } + + @NonNull + @VisibleForTesting + protected Surface createSurface(@NonNull SurfaceTexture tx) { + return new Surface(tx); + } + + /** Returns the texture where the view is projected. */ + @Nullable + public SurfaceTexture getTexture() { + return tx; + } + + /** + * Sets the layout parameters for this view. + * + * @param params The new parameters. + */ + public void setLayoutParams(@NonNull FrameLayout.LayoutParams params) { + super.setLayoutParams(params); + + left = params.leftMargin; + top = params.topMargin; + } + + /** + * Sets the size of the image buffer. + * + * @param width The width of the screen buffer. + * @param height The height of the screen buffer. + */ + public void setBufferSize(int width, int height) { + bufferWidth = width; + bufferHeight = height; + if (tx != null) { + tx.setDefaultBufferSize(width, height); + } + } + + /** Returns the image buffer width. */ + public int getBufferWidth() { + return bufferWidth; + } + + /** Returns the image buffer height. */ + public int getBufferHeight() { + return bufferHeight; + } + + /** Releases the surface. */ + public void release() { + // Don't release the texture. + tx = null; + if (surface != null) { + surface.release(); + surface = null; + } + } + + @Override + public boolean onInterceptTouchEvent(@NonNull MotionEvent event) { + return true; + } + + @Override + public void onDescendantInvalidated(@NonNull View child, @NonNull View target) { + super.onDescendantInvalidated(child, target); + invalidate(); + } + + @Override + @SuppressLint("NewApi") + public void draw(Canvas canvas) { + if (surface == null || !surface.isValid()) { + Log.e(TAG, "Invalid surface. The platform view cannot be displayed."); + return; + } + if (tx == null || tx.isReleased()) { + Log.e(TAG, "Invalid texture. The platform view cannot be displayed."); + return; + } + // Override the canvas that this subtree of views will use to draw. + final Canvas surfaceCanvas = surface.lockHardwareCanvas(); + try { + // Clear the current pixels in the canvas. + // This helps when a WebView renders an HTML document with transparent background. + if (Build.VERSION.SDK_INT >= 29) { + surfaceCanvas.drawColor(Color.TRANSPARENT, BlendMode.CLEAR); + } else { + surfaceCanvas.drawColor(Color.TRANSPARENT); + } + super.draw(surfaceCanvas); + } finally { + surface.unlockCanvasAndPost(surfaceCanvas); + } + } + + @Override + @SuppressLint("ClickableViewAccessibility") + public boolean onTouchEvent(@NonNull MotionEvent event) { + if (touchProcessor == null) { + return super.onTouchEvent(event); + } + final Matrix screenMatrix = new Matrix(); + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + prevLeft = left; + prevTop = top; + screenMatrix.postTranslate(left, top); + break; + case MotionEvent.ACTION_MOVE: + // While the view is dragged, use the left and top positions as + // they were at the moment the touch event fired. + screenMatrix.postTranslate(prevLeft, prevTop); + prevLeft = left; + prevTop = top; + break; + case MotionEvent.ACTION_UP: + default: + screenMatrix.postTranslate(left, top); + break; + } + return touchProcessor.onTouchEvent(event, screenMatrix); + } + + public void setOnDescendantFocusChangeListener(@NonNull OnFocusChangeListener userFocusListener) { + unsetOnDescendantFocusChangeListener(); + final ViewTreeObserver observer = getViewTreeObserver(); + if (observer.isAlive() && activeFocusListener == null) { + activeFocusListener = + new ViewTreeObserver.OnGlobalFocusChangeListener() { + @Override + public void onGlobalFocusChanged(View oldFocus, View newFocus) { + userFocusListener.onFocusChange( + PlatformViewWrapper.this, ViewUtils.childHasFocus(PlatformViewWrapper.this)); + } + }; + observer.addOnGlobalFocusChangeListener(activeFocusListener); + } + } + + public void unsetOnDescendantFocusChangeListener() { + final ViewTreeObserver observer = getViewTreeObserver(); + if (observer.isAlive() && activeFocusListener != null) { + final ViewTreeObserver.OnGlobalFocusChangeListener currFocusListener = activeFocusListener; + activeFocusListener = null; + observer.removeOnGlobalFocusChangeListener(currFocusListener); + } + } +} diff --git a/shell/platform/android/io/flutter/plugin/platform/PlatformViewsAccessibilityDelegate.java b/shell/platform/android/io/flutter/plugin/platform/PlatformViewsAccessibilityDelegate.java index ad693a8724880..0ab99bfe1fc66 100644 --- a/shell/platform/android/io/flutter/plugin/platform/PlatformViewsAccessibilityDelegate.java +++ b/shell/platform/android/io/flutter/plugin/platform/PlatformViewsAccessibilityDelegate.java @@ -5,6 +5,7 @@ package io.flutter.plugin.platform; import android.view.View; +import androidx.annotation.Nullable; import io.flutter.view.AccessibilityBridge; /** Facilitates interaction between the accessibility bridge and embedded platform views. */ @@ -13,10 +14,8 @@ 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. */ - View getPlatformViewById(Integer id); - - /** Returns true if the platform view uses virtual displays. */ - boolean usesVirtualDisplay(Integer id); + @Nullable + View getPlatformViewById(int viewId); /** * Attaches an accessibility bridge for this platform views accessibility delegate. diff --git a/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java b/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java index 29ace9e502c2d..063882fced75f 100644 --- a/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java +++ b/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java @@ -10,7 +10,6 @@ import android.annotation.TargetApi; import android.content.Context; import android.os.Build; -import android.util.DisplayMetrics; import android.util.SparseArray; import android.view.MotionEvent; import android.view.View; @@ -34,7 +33,6 @@ import io.flutter.view.AccessibilityBridge; import io.flutter.view.TextureRegistry; import java.util.ArrayList; -import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -70,20 +68,7 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega // dispatched. private final AccessibilityEventsDelegate accessibilityEventsDelegate; - // TODO(mattcarroll): Refactor overall platform views to facilitate testing and then make - // this private. This is visible as a hack to facilitate testing. This was deemed the least - // bad option at the time of writing. - @VisibleForTesting /* package */ final HashMap vdControllers; - - // Maps a virtual display's context to the platform view hosted in this virtual display. - // Since each virtual display has it's unique context this allows associating any view with the - // platform view that - // it is associated with(e.g if a platform view creates other views in the same virtual display. - @VisibleForTesting /* package */ final HashMap contextToPlatformView; - - // The views returned by `PlatformView#getView()`. - // - // This only applies to hybrid composition. + // The platform views. private final SparseArray platformViews; // The platform view parents that are appended to `FlutterView`. @@ -93,12 +78,19 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega // This view provides a wrapper that applies scene builder operations to the platform view. // For example, a transform matrix, or setting opacity on the platform view layer. // - // This is only applies to hybrid composition. + // This is only applies to hybrid composition (PlatformViewLayer render). + // TODO(egarciad): Eliminate this. + // https://github.com/flutter/flutter/issues/96679 private final SparseArray platformViewParent; // Map of unique IDs to views that render overlay layers. private final SparseArray overlayLayerViews; + // View wrappers are FrameLayouts that contain a single child view. + // This child view is the platform view. + // This only applies to hybrid composition (TextureLayer render). + private final SparseArray viewWrappers; + // Next available unique ID for use in overlayLayerViews. private int nextOverlayLayerId = 0; @@ -124,7 +116,9 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega @TargetApi(Build.VERSION_CODES.KITKAT) @Override - public void createAndroidViewForPlatformView( + // TODO(egarciad): Remove the need for this. + // https://github.com/flutter/flutter/issues/96679 + public void createForPlatformViewLayer( @NonNull PlatformViewsChannel.PlatformViewCreationRequest request) { // API level 19 is required for `android.graphics.ImageReader`. ensureValidAndroidVersion(Build.VERSION_CODES.KITKAT); @@ -154,167 +148,182 @@ public void createAndroidViewForPlatformView( platformViews.put(request.viewId, platformView); } + @TargetApi(Build.VERSION_CODES.M) @Override - public void disposeAndroidViewForPlatformView(int viewId) { - // Hybrid view. - final PlatformView platformView = platformViews.get(viewId); - final FlutterMutatorView parentView = platformViewParent.get(viewId); - if (platformView != null) { - if (parentView != null) { - parentView.removeView(platformView.getView()); - } - platformViews.remove(viewId); - platformView.dispose(); - } - if (parentView != null) { - parentView.unsetOnDescendantFocusChangeListener(); - ((ViewGroup) parentView.getParent()).removeView(parentView); - platformViewParent.remove(viewId); - } - } - - @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) - @Override - public long createVirtualDisplayForPlatformView( + public long createForTextureLayer( @NonNull PlatformViewsChannel.PlatformViewCreationRequest request) { - // API level 20 is required for VirtualDisplay#setSurface which we use when resizing a - // platform view. - ensureValidAndroidVersion(Build.VERSION_CODES.KITKAT_WATCH); + final int viewId = request.viewId; + if (viewWrappers.get(viewId) != null) { + throw new IllegalStateException( + "Trying to create an already created platform view, view id: " + viewId); + } if (!validateDirection(request.direction)) { throw new IllegalStateException( "Trying to create a view with unknown direction value: " + request.direction + "(view id: " - + request.viewId + + viewId + ")"); } - - if (vdControllers.containsKey(request.viewId)) { + if (textureRegistry == null) { throw new IllegalStateException( - "Trying to create an already created platform view, view id: " + request.viewId); + "Texture registry is null. This means that platform views controller was detached, view id: " + + viewId); } - - PlatformViewFactory viewFactory = registry.getFactory(request.viewType); + if (flutterView == null) { + throw new IllegalStateException( + "Flutter view is null. This means the platform views controller doesn't have an attached view, view id: " + + viewId); + } + final PlatformViewFactory viewFactory = registry.getFactory(request.viewType); if (viewFactory == null) { throw new IllegalStateException( "Trying to create a platform view of unregistered type: " + request.viewType); } - Object createParams = null; if (request.params != null) { createParams = viewFactory.getCreateArgsCodec().decodeMessage(request.params); } - int physicalWidth = toPhysicalPixels(request.logicalWidth); - int physicalHeight = toPhysicalPixels(request.logicalHeight); - validateVirtualDisplayDimensions(physicalWidth, physicalHeight); - - TextureRegistry.SurfaceTextureEntry textureEntry = textureRegistry.createSurfaceTexture(); - VirtualDisplayController vdController = - VirtualDisplayController.create( - context, - accessibilityEventsDelegate, - viewFactory, - textureEntry, - physicalWidth, - physicalHeight, - request.viewId, - createParams, - (view, hasFocus) -> { - if (hasFocus) { - platformViewsChannel.invokeViewFocused(request.viewId); - } - }); - - if (vdController == null) { - throw new IllegalStateException( - "Failed creating virtual display for a " - + request.viewType - + " with id: " - + request.viewId); - } - - // If our FlutterEngine is already attached to a Flutter UI, provide that Android - // View to this new platform view. - if (flutterView != null) { - vdController.onFlutterViewAttached(flutterView); - } - - vdControllers.put(request.viewId, vdController); - View platformView = vdController.getView(); - platformView.setLayoutDirection(request.direction); - contextToPlatformView.put(platformView.getContext(), platformView); - - // TODO(amirh): copy accessibility nodes to the FlutterView's accessibility tree. + final PlatformView platformView = viewFactory.create(context, viewId, createParams); + platformViews.put(viewId, platformView); + + final PlatformViewWrapper wrapperView = new PlatformViewWrapper(context); + final TextureRegistry.SurfaceTextureEntry textureEntry = + textureRegistry.createSurfaceTexture(); + wrapperView.setTexture(textureEntry.surfaceTexture()); + wrapperView.setTouchProcessor(androidTouchProcessor); + + final int physicalWidth = toPhysicalPixels(request.logicalWidth); + final int physicalHeight = toPhysicalPixels(request.logicalHeight); + wrapperView.setBufferSize(physicalWidth, physicalHeight); + + final FrameLayout.LayoutParams layoutParams = + new FrameLayout.LayoutParams(physicalWidth, physicalHeight); + + final int physicalTop = toPhysicalPixels(request.logicalTop); + final int physicalLeft = toPhysicalPixels(request.logicalLeft); + layoutParams.topMargin = physicalTop; + layoutParams.leftMargin = physicalLeft; + wrapperView.setLayoutParams(layoutParams); + + wrapperView.setLayoutDirection(request.direction); + wrapperView.addView(platformView.getView()); + wrapperView.setOnDescendantFocusChangeListener( + (view, hasFocus) -> { + if (hasFocus) { + platformViewsChannel.invokeViewFocused(viewId); + } else if (textInputPlugin != null) { + textInputPlugin.clearPlatformViewClient(viewId); + } + }); + flutterView.addView(wrapperView); + viewWrappers.append(viewId, wrapperView); return textureEntry.id(); } @Override - public void disposeVirtualDisplayForPlatformView(int viewId) { - ensureValidAndroidVersion(Build.VERSION_CODES.KITKAT_WATCH); - VirtualDisplayController vdController = vdControllers.get(viewId); - if (vdController == null) { - throw new IllegalStateException( - "Trying to dispose a platform view with unknown id: " + viewId); + public void dispose(int viewId) { + final PlatformView platformView = platformViews.get(viewId); + if (platformView != null) { + final ViewGroup pvParent = (ViewGroup) platformView.getView().getParent(); + if (pvParent != null) { + pvParent.removeView(platformView.getView()); + } + platformViews.remove(viewId); + platformView.dispose(); } - - if (textInputPlugin != null) { - textInputPlugin.clearPlatformViewClient(viewId); + // The platform view is displayed using a TextureLayer. + final PlatformViewWrapper viewWrapper = viewWrappers.get(viewId); + if (viewWrapper != null) { + viewWrapper.release(); + viewWrapper.unsetOnDescendantFocusChangeListener(); + + final ViewGroup wrapperParent = (ViewGroup) viewWrapper.getParent(); + if (wrapperParent != null) { + wrapperParent.removeView(viewWrapper); + } + viewWrappers.remove(viewId); + return; } + // The platform view is displayed using a PlatformViewLayer. + // TODO(egarciad): Eliminate this case. + // https://github.com/flutter/flutter/issues/96679 + final FlutterMutatorView parentView = platformViewParent.get(viewId); + if (parentView != null) { + parentView.unsetOnDescendantFocusChangeListener(); - contextToPlatformView.remove(vdController.getView().getContext()); - vdController.dispose(); - vdControllers.remove(viewId); + final ViewGroup mutatorViewParent = (ViewGroup) parentView.getParent(); + if (mutatorViewParent != null) { + mutatorViewParent.removeView(parentView); + } + platformViewParent.remove(viewId); + } } @Override - public void resizePlatformView( - @NonNull PlatformViewsChannel.PlatformViewResizeRequest request, - @NonNull Runnable onComplete) { - ensureValidAndroidVersion(Build.VERSION_CODES.KITKAT_WATCH); + public void offset(int viewId, double top, double left) { + final PlatformViewWrapper wrapper = viewWrappers.get(viewId); + if (wrapper == null) { + Log.e(TAG, "Setting offset for unknown platform view with id: " + viewId); + return; + } + final int physicalTop = toPhysicalPixels(top); + final int physicalLeft = toPhysicalPixels(left); + final FrameLayout.LayoutParams layoutParams = + (FrameLayout.LayoutParams) wrapper.getLayoutParams(); + layoutParams.topMargin = physicalTop; + layoutParams.leftMargin = physicalLeft; + wrapper.setLayoutParams(layoutParams); + } - final VirtualDisplayController vdController = vdControllers.get(request.viewId); - if (vdController == null) { - throw new IllegalStateException( - "Trying to resize a platform view with unknown id: " + request.viewId); + @Override + public PlatformViewsChannel.PlatformViewBufferSize resize( + @NonNull PlatformViewsChannel.PlatformViewResizeRequest request) { + final int viewId = request.viewId; + final PlatformViewWrapper view = viewWrappers.get(viewId); + if (view == null) { + Log.e(TAG, "Resizing unknown platform view with id: " + viewId); + return null; + } + final int newWidth = toPhysicalPixels(request.newLogicalWidth); + final int newHeight = toPhysicalPixels(request.newLogicalHeight); + + // Resize the buffer only when the current buffer size is smaller than the new size. + // This is required to prevent a situation when smooth keyboard animation + // resizes the texture too often, such that the GPU and the platform thread don't agree on + // the + // timing of the new size. + // Resizing the texture causes pixel stretching since the size of the GL texture used in + // the engine + // is set by the framework, but the texture buffer size is set by the platform down below. + if (newWidth > view.getBufferWidth() || newHeight > view.getBufferHeight()) { + view.setBufferSize(newWidth, newHeight); } - int physicalWidth = toPhysicalPixels(request.newLogicalWidth); - int physicalHeight = toPhysicalPixels(request.newLogicalHeight); - validateVirtualDisplayDimensions(physicalWidth, physicalHeight); - - // 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. - lockInputConnection(vdController); - vdController.resize( - physicalWidth, - physicalHeight, - () -> { - unlockInputConnection(vdController); - onComplete.run(); - }); + final FrameLayout.LayoutParams layoutParams = + (FrameLayout.LayoutParams) view.getLayoutParams(); + layoutParams.width = newWidth; + layoutParams.height = newHeight; + view.setLayoutParams(layoutParams); + + return new PlatformViewsChannel.PlatformViewBufferSize( + toLogicalPixels(view.getBufferWidth()), toLogicalPixels(view.getBufferHeight())); } @Override public void onTouch(@NonNull PlatformViewsChannel.PlatformViewTouch touch) { final int viewId = touch.viewId; - float density = context.getResources().getDisplayMetrics().density; - ensureValidAndroidVersion(Build.VERSION_CODES.KITKAT_WATCH); - if (vdControllers.containsKey(viewId)) { - final MotionEvent event = toMotionEvent(density, touch, /*usingVirtualDiplays=*/ true); - vdControllers.get(touch.viewId).dispatchTouchEvent(event); - } else if (platformViews.get(viewId) != null) { - final MotionEvent event = toMotionEvent(density, touch, /*usingVirtualDiplays=*/ false); - View view = platformViews.get(touch.viewId).getView(); - if (view != null) { - view.dispatchTouchEvent(event); - } - } else { - throw new IllegalStateException("Sending touch to an unknown view with id: " + viewId); + final PlatformView platformView = platformViews.get(viewId); + if (platformView == null) { + Log.e(TAG, "Sending touch to an unknown view with id: " + viewId); + return; } + ensureValidAndroidVersion(Build.VERSION_CODES.KITKAT_WATCH); + final float density = context.getResources().getDisplayMetrics().density; + final MotionEvent event = toMotionEvent(density, touch); + platformView.getView().dispatchTouchEvent(event); } @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) @@ -328,34 +337,23 @@ public void setDirection(int viewId, int direction) { + viewId + ")"); } - - ensureValidAndroidVersion(Build.VERSION_CODES.KITKAT_WATCH); final PlatformView platformView = platformViews.get(viewId); - if (platformView != null) { - platformView.getView().setLayoutDirection(direction); + if (platformView == null) { + Log.e(TAG, "Setting direction to an unknown view with id: " + viewId); return; } - VirtualDisplayController controller = vdControllers.get(viewId); - if (controller == null) { - throw new IllegalStateException( - "Trying to set direction: " - + direction - + " to an unknown platform view with id: " - + viewId); - } - controller.getView().setLayoutDirection(direction); + ensureValidAndroidVersion(Build.VERSION_CODES.KITKAT_WATCH); + platformViews.get(viewId).getView().setLayoutDirection(direction); } @Override public void clearFocus(int viewId) { final PlatformView platformView = platformViews.get(viewId); - if (platformView != null) { - platformView.getView().clearFocus(); + if (platformView == null) { + Log.e(TAG, "Clearing focus on an unknown view with id: " + viewId); return; } - ensureValidAndroidVersion(Build.VERSION_CODES.KITKAT_WATCH); - View view = vdControllers.get(viewId).getView(); - view.clearFocus(); + platformView.getView().clearFocus(); } private void ensureValidAndroidVersion(int minSdkVersion) { @@ -375,8 +373,7 @@ public void synchronizeToNativeViewHierarchy(boolean yes) { }; @VisibleForTesting - public MotionEvent toMotionEvent( - float density, PlatformViewsChannel.PlatformViewTouch touch, boolean usingVirtualDiplays) { + public MotionEvent toMotionEvent(float density, PlatformViewsChannel.PlatformViewTouch touch) { MotionEventTracker.MotionEventId motionEventId = MotionEventTracker.MotionEventId.from(touch.motionEventId); MotionEvent trackedEvent = motionEventTracker.pop(motionEventId); @@ -392,7 +389,7 @@ public MotionEvent toMotionEvent( parsePointerCoordsList(touch.rawPointerCoords, density) .toArray(new PointerCoords[touch.pointerCount]); - if (!usingVirtualDiplays && trackedEvent != null) { + if (trackedEvent != null) { return MotionEvent.obtain( trackedEvent.getDownTime(), trackedEvent.getEventTime(), @@ -431,13 +428,11 @@ public MotionEvent toMotionEvent( public PlatformViewsController() { registry = new PlatformViewRegistryImpl(); - vdControllers = new HashMap<>(); accessibilityEventsDelegate = new AccessibilityEventsDelegate(); - contextToPlatformView = new HashMap<>(); overlayLayerViews = new SparseArray<>(); currentFrameUsedOverlayLayerIds = new HashSet<>(); currentFrameUsedPlatformViewIds = new HashSet<>(); - + viewWrappers = new SparseArray<>(); platformViews = new SparseArray<>(); platformViewParent = new SparseArray<>(); @@ -489,13 +484,14 @@ public void detach() { * This {@code PlatformViewsController} and its {@code FlutterEngine} is now attached to an * Android {@code View} that renders a Flutter UI. */ - public void attachToView(@NonNull FlutterView flutterView) { - this.flutterView = flutterView; + public void attachToView(@NonNull FlutterView newFlutterView) { + flutterView = newFlutterView; // Inform all existing platform views that they are now associated with // a Flutter View. - for (VirtualDisplayController controller : vdControllers.values()) { - controller.onFlutterViewAttached(flutterView); + for (int i = 0; i < platformViews.size(); i++) { + final PlatformView view = platformViews.valueAt(i); + view.onFlutterViewAttached(flutterView); } } @@ -507,16 +503,16 @@ public void attachToView(@NonNull FlutterView flutterView) { * the previously attached {@code View}. */ public void detachFromView() { + for (int i = 0; i < platformViews.size(); i++) { + final PlatformView view = platformViews.valueAt(i); + view.onFlutterViewDetached(); + } + // TODO(egarciad): Remove this. + // https://github.com/flutter/flutter/issues/96679 destroyOverlaySurfaces(); removeOverlaySurfaces(); - this.flutterView = null; + flutterView = null; flutterViewConvertedToImageView = false; - - // Inform all existing platform views that they are no longer associated with - // a Flutter View. - for (VirtualDisplayController controller : vdControllers.values()) { - controller.onFlutterViewDetached(); - } } @Override @@ -547,29 +543,6 @@ public void detachTextInputPlugin() { textInputPlugin = null; } - /** - * Returns true if Flutter should perform input connection proxying for the view. - * - *

If the view is a platform view managed by this platform views controller returns true. Else - * if the view was created in a platform view's VD, delegates the decision to the platform view's - * {@link View#checkInputConnectionProxy(View)} method. Else returns false. - */ - public boolean checkInputConnectionProxy(@Nullable View view) { - // View can be null on some devices - // See: https://github.com/flutter/flutter/issues/36517 - if (view == null) { - return false; - } - if (!contextToPlatformView.containsKey(view.getContext())) { - return false; - } - View platformView = contextToPlatformView.get(view.getContext()); - if (platformView == view) { - return true; - } - return platformView.checkInputConnectionProxy(view); - } - public PlatformViewRegistry getRegistry() { return registry; } @@ -587,8 +560,6 @@ public void onAttachedToJNI() { * PlatformViewsController} detaches from JNI. */ public void onDetachedFromJNI() { - // Dispose all virtual displays so that any future updates to textures will not be - // propagated to the native peer. flushAllViews(); } @@ -597,37 +568,12 @@ public void onPreEngineRestart() { } @Override - public View getPlatformViewById(Integer id) { - // Hybrid composition. - if (platformViews.get(id) != null) { - return platformViews.get(id).getView(); - } - VirtualDisplayController controller = vdControllers.get(id); - if (controller == null) { + public View getPlatformViewById(int viewId) { + final PlatformView platformView = platformViews.get(viewId); + if (platformView == null) { return null; } - return controller.getView(); - } - - @Override - public boolean usesVirtualDisplay(Integer id) { - return vdControllers.containsKey(id); - } - - private void lockInputConnection(@NonNull VirtualDisplayController controller) { - if (textInputPlugin == null) { - return; - } - textInputPlugin.lockPlatformViewInputConnection(); - controller.onInputConnectionLocked(); - } - - private void unlockInputConnection(@NonNull VirtualDisplayController controller) { - if (textInputPlugin == null) { - return; - } - textInputPlugin.unlockPlatformViewInputConnection(); - controller.onInputConnectionUnlocked(); + return platformView.getView(); } private static boolean validateDirection(int direction) { @@ -679,29 +625,6 @@ private static PointerCoords parsePointerCoords(Object rawCoords, float density) return coords; } - // Creating a VirtualDisplay larger than the size of the device screen size - // could cause the device to restart: https://github.com/flutter/flutter/issues/28978 - private void validateVirtualDisplayDimensions(int width, int height) { - DisplayMetrics metrics = context.getResources().getDisplayMetrics(); - if (height > metrics.heightPixels || width > metrics.widthPixels) { - String message = - "Creating a virtual display of size: " - + "[" - + width - + ", " - + height - + "] may result in problems" - + "(https://github.com/flutter/flutter/issues/2897)." - + "It is larger than the device screen size: " - + "[" - + metrics.widthPixels - + ", " - + metrics.heightPixels - + "]."; - Log.w(TAG, message); - } - } - private float getDisplayDensity() { return context.getResources().getDisplayMetrics().density; } @@ -710,18 +633,13 @@ private int toPhysicalPixels(double logicalPixels) { return (int) Math.round(logicalPixels * getDisplayDensity()); } - private void flushAllViews() { - for (VirtualDisplayController controller : vdControllers.values()) { - controller.dispose(); - } - vdControllers.clear(); + private int toLogicalPixels(double physicalPixels) { + return (int) Math.round(physicalPixels / getDisplayDensity()); + } + private void flushAllViews() { while (platformViews.size() > 0) { - channelHandler.disposeAndroidViewForPlatformView(platformViews.keyAt(0)); - } - - if (contextToPlatformView.size() > 0) { - contextToPlatformView.clear(); + channelHandler.dispose(platformViews.keyAt(0)); } } diff --git a/shell/platform/android/io/flutter/plugin/platform/SingleViewPresentation.java b/shell/platform/android/io/flutter/plugin/platform/SingleViewPresentation.java deleted file mode 100644 index a561f73868d86..0000000000000 --- a/shell/platform/android/io/flutter/plugin/platform/SingleViewPresentation.java +++ /dev/null @@ -1,478 +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.plugin.platform; - -import static android.content.Context.WINDOW_SERVICE; -import static android.view.View.OnFocusChangeListener; - -import android.annotation.TargetApi; -import android.app.AlertDialog; -import android.app.Presentation; -import android.content.Context; -import android.content.ContextWrapper; -import android.graphics.Rect; -import android.graphics.drawable.ColorDrawable; -import android.os.Build; -import android.os.Bundle; -import android.view.Display; -import android.view.Gravity; -import android.view.View; -import android.view.ViewGroup; -import android.view.WindowManager; -import android.view.accessibility.AccessibilityEvent; -import android.view.inputmethod.InputMethodManager; -import android.widget.FrameLayout; -import androidx.annotation.Keep; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import io.flutter.Log; -import java.lang.reflect.InvocationHandler; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.lang.reflect.Proxy; - -/* - * A presentation used for hosting a single Android view in a virtual display. - * - * This presentation overrides the WindowManager's addView/removeView/updateViewLayout methods, such that views added - * directly to the WindowManager are added as part of the presentation's view hierarchy (to fakeWindowViewGroup). - * - * The view hierarchy for the presentation is as following: - * - * rootView - * / \ - * / \ - * / \ - * container state.fakeWindowViewGroup - * | - * EmbeddedView - */ -@Keep -@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) -class SingleViewPresentation extends Presentation { - - /* - * When an embedded view is resized in Flutterverse we move the Android view to a new virtual display - * that has the new size. This class keeps the presentation state that moves with the view to the presentation of - * the new virtual display. - */ - static class PresentationState { - // The Android view we are embedding in the Flutter app. - private PlatformView platformView; - - // The InvocationHandler for a WindowManager proxy. This is essentially the custom window - // manager for the - // presentation. - private WindowManagerHandler windowManagerHandler; - - // Contains views that were added directly to the window manager (e.g - // android.widget.PopupWindow). - private FakeWindowViewGroup fakeWindowViewGroup; - } - - private final PlatformViewFactory viewFactory; - - // 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; - - // This is the creation parameters for the platform view, we keep it here - // so when we create the platform view we can tell it its view id. - private Object createParams; - - // The root view for the presentation, it has 2 childs: container which contains the embedded - // view, and - // fakeWindowViewGroup which contains views that were added directly to the presentation's window - // manager. - private AccessibilityDelegatingFrameLayout rootView; - - // Contains the embedded platform view (platformView.getView()) when it is attached to the - // presentation. - private FrameLayout container; - - private final PresentationState state; - - private boolean startFocused = false; - - // The context for the application window that hosts FlutterView. - private final Context outerContext; - - /** - * Creates a presentation that will use the view factory to create a new platform view in the - * presentation's onCreate, and attach it. - */ - public SingleViewPresentation( - Context outerContext, - Display display, - PlatformViewFactory viewFactory, - AccessibilityEventsDelegate accessibilityEventsDelegate, - int viewId, - Object createParams, - OnFocusChangeListener focusChangeListener) { - super(new ImmContext(outerContext), display); - this.viewFactory = viewFactory; - this.accessibilityEventsDelegate = accessibilityEventsDelegate; - this.viewId = viewId; - this.createParams = createParams; - this.focusChangeListener = focusChangeListener; - this.outerContext = outerContext; - state = new PresentationState(); - getWindow() - .setFlags( - WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, - WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - getWindow().setType(WindowManager.LayoutParams.TYPE_PRIVATE_PRESENTATION); - } - } - - /** - * Creates a presentation that will attach an already existing view as its root view. - * - *

The display's density must match the density of the context used when the view was created. - */ - public SingleViewPresentation( - Context outerContext, - Display display, - AccessibilityEventsDelegate accessibilityEventsDelegate, - PresentationState state, - OnFocusChangeListener focusChangeListener, - boolean startFocused) { - super(new ImmContext(outerContext), display); - this.accessibilityEventsDelegate = accessibilityEventsDelegate; - viewFactory = null; - this.state = state; - this.focusChangeListener = focusChangeListener; - this.outerContext = outerContext; - getWindow() - .setFlags( - WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, - WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE); - this.startFocused = startFocused; - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - // This makes sure we preserve alpha for the VD's content. - getWindow().setBackgroundDrawable(new ColorDrawable(android.graphics.Color.TRANSPARENT)); - if (state.fakeWindowViewGroup == null) { - state.fakeWindowViewGroup = new FakeWindowViewGroup(getContext()); - } - if (state.windowManagerHandler == null) { - WindowManager windowManagerDelegate = - (WindowManager) getContext().getSystemService(WINDOW_SERVICE); - state.windowManagerHandler = - new WindowManagerHandler(windowManagerDelegate, state.fakeWindowViewGroup); - } - - container = new FrameLayout(getContext()); - - // Our base mContext has already been wrapped with an IMM cache at instantiation time, but - // we want to wrap it again here to also return state.windowManagerHandler. - Context context = - new PresentationContext(getContext(), state.windowManagerHandler, outerContext); - - if (state.platformView == null) { - state.platformView = viewFactory.create(context, viewId, createParams); - } - - View embeddedView = state.platformView.getView(); - container.addView(embeddedView); - 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); - } - - public PresentationState detachState() { - container.removeAllViews(); - rootView.removeAllViews(); - return state; - } - - public PlatformView getView() { - if (state.platformView == null) return null; - return state.platformView; - } - - /* - * A view group that implements the same layout protocol that exist between the WindowManager and its direct - * children. - * - * Currently only a subset of the protocol is supported (gravity, x, and y). - */ - static class FakeWindowViewGroup extends ViewGroup { - // Used in onLayout to keep the bounds of the current view. - // We keep it as a member to avoid object allocations during onLayout which are discouraged. - private final Rect viewBounds; - - // Used in onLayout to keep the bounds of the child views. - // We keep it as a member to avoid object allocations during onLayout which are discouraged. - private final Rect childRect; - - public FakeWindowViewGroup(Context context) { - super(context); - viewBounds = new Rect(); - childRect = new Rect(); - } - - @Override - protected void onLayout(boolean changed, int l, int t, int r, int b) { - for (int i = 0; i < getChildCount(); i++) { - View child = getChildAt(i); - WindowManager.LayoutParams params = (WindowManager.LayoutParams) child.getLayoutParams(); - viewBounds.set(l, t, r, b); - Gravity.apply( - params.gravity, - child.getMeasuredWidth(), - child.getMeasuredHeight(), - viewBounds, - params.x, - params.y, - childRect); - child.layout(childRect.left, childRect.top, childRect.right, childRect.bottom); - } - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - for (int i = 0; i < getChildCount(); i++) { - View child = getChildAt(i); - child.measure(atMost(widthMeasureSpec), atMost(heightMeasureSpec)); - } - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - } - - private static int atMost(int measureSpec) { - return MeasureSpec.makeMeasureSpec(MeasureSpec.getSize(measureSpec), MeasureSpec.AT_MOST); - } - } - - /** Answers calls for {@link InputMethodManager} with an instance cached at creation time. */ - // TODO(mklim): This caches the IMM at construction time and won't pick up any changes. In rare - // cases where the FlutterView changes windows this will return an outdated instance. This - // should be fixed to instead defer returning the IMM to something that know's FlutterView's - // true Context. - private static class ImmContext extends ContextWrapper { - private @NonNull final InputMethodManager inputMethodManager; - - ImmContext(Context base) { - this(base, /*inputMethodManager=*/ null); - } - - private ImmContext(Context base, @Nullable InputMethodManager inputMethodManager) { - super(base); - this.inputMethodManager = - inputMethodManager != null - ? inputMethodManager - : (InputMethodManager) base.getSystemService(INPUT_METHOD_SERVICE); - } - - @Override - public Object getSystemService(String name) { - if (INPUT_METHOD_SERVICE.equals(name)) { - return inputMethodManager; - } - return super.getSystemService(name); - } - - @Override - public Context createDisplayContext(Display display) { - Context displayContext = super.createDisplayContext(display); - return new ImmContext(displayContext, inputMethodManager); - } - } - - /** Proxies a Context replacing the WindowManager with our custom instance. */ - // TODO(mklim): This caches the IMM at construction time and won't pick up any changes. In rare - // cases where the FlutterView changes windows this will return an outdated instance. This - // should be fixed to instead defer returning the IMM to something that know's FlutterView's - // true Context. - private static class PresentationContext extends ContextWrapper { - private @NonNull final WindowManagerHandler windowManagerHandler; - private @Nullable WindowManager windowManager; - private final Context flutterAppWindowContext; - - PresentationContext( - Context base, - @NonNull WindowManagerHandler windowManagerHandler, - Context flutterAppWindowContext) { - super(base); - this.windowManagerHandler = windowManagerHandler; - this.flutterAppWindowContext = flutterAppWindowContext; - } - - @Override - public Object getSystemService(String name) { - if (WINDOW_SERVICE.equals(name)) { - if (isCalledFromAlertDialog()) { - // Alert dialogs are showing on top of the entire application and should not be limited to - // the virtual - // display. If we detect that an android.app.AlertDialog constructor is what's fetching - // the window manager - // we return the one for the application's window. - // - // Note that if we don't do this AlertDialog will throw a ClassCastException as down the - // line it tries - // to case this instance to a WindowManagerImpl which the object returned by - // getWindowManager is not - // a subclass of. - return flutterAppWindowContext.getSystemService(name); - } - return getWindowManager(); - } - return super.getSystemService(name); - } - - private WindowManager getWindowManager() { - if (windowManager == null) { - windowManager = windowManagerHandler.getWindowManager(); - } - return windowManager; - } - - private boolean isCalledFromAlertDialog() { - StackTraceElement[] stackTraceElements = Thread.currentThread().getStackTrace(); - for (int i = 0; i < stackTraceElements.length && i < 11; i++) { - if (stackTraceElements[i].getClassName().equals(AlertDialog.class.getCanonicalName()) - && stackTraceElements[i].getMethodName().equals("")) { - return true; - } - } - return false; - } - } - - /* - * A dynamic proxy handler for a WindowManager with custom overrides. - * - * The presentation's window manager delegates all calls to the default window manager. - * WindowManager#addView calls triggered by views that are attached to the virtual display are crashing - * (see: https://github.com/flutter/flutter/issues/20714). This was triggered when selecting text in an embedded - * WebView (as the selection handles are implemented as popup windows). - * - * This dynamic proxy overrides the addView, removeView, removeViewImmediate, and updateViewLayout methods - * to prevent these crashes. - * - * This will be more efficient as a static proxy that's not using reflection, but as the engine is currently - * not being built against the latest Android SDK we cannot override all relevant method. - * Tracking issue for upgrading the engine's Android sdk: https://github.com/flutter/flutter/issues/20717 - */ - static class WindowManagerHandler implements InvocationHandler { - private static final String TAG = "PlatformViewsController"; - - private final WindowManager delegate; - FakeWindowViewGroup fakeWindowRootView; - - WindowManagerHandler(WindowManager delegate, FakeWindowViewGroup fakeWindowViewGroup) { - this.delegate = delegate; - fakeWindowRootView = fakeWindowViewGroup; - } - - public WindowManager getWindowManager() { - return (WindowManager) - Proxy.newProxyInstance( - WindowManager.class.getClassLoader(), new Class[] {WindowManager.class}, this); - } - - @Override - public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { - switch (method.getName()) { - case "addView": - addView(args); - return null; - case "removeView": - removeView(args); - return null; - case "removeViewImmediate": - removeViewImmediate(args); - return null; - case "updateViewLayout": - updateViewLayout(args); - return null; - } - try { - return method.invoke(delegate, args); - } catch (InvocationTargetException e) { - throw e.getCause(); - } - } - - private void addView(Object[] args) { - if (fakeWindowRootView == null) { - Log.w(TAG, "Embedded view called addView while detached from presentation"); - return; - } - View view = (View) args[0]; - WindowManager.LayoutParams layoutParams = (WindowManager.LayoutParams) args[1]; - fakeWindowRootView.addView(view, layoutParams); - } - - private void removeView(Object[] args) { - if (fakeWindowRootView == null) { - Log.w(TAG, "Embedded view called removeView while detached from presentation"); - return; - } - View view = (View) args[0]; - fakeWindowRootView.removeView(view); - } - - private void removeViewImmediate(Object[] args) { - if (fakeWindowRootView == null) { - Log.w(TAG, "Embedded view called removeViewImmediate while detached from presentation"); - return; - } - View view = (View) args[0]; - view.clearAnimation(); - fakeWindowRootView.removeView(view); - } - - private void updateViewLayout(Object[] args) { - if (fakeWindowRootView == null) { - Log.w(TAG, "Embedded view called updateViewLayout while detached from presentation"); - return; - } - View view = (View) args[0]; - WindowManager.LayoutParams layoutParams = (WindowManager.LayoutParams) args[1]; - fakeWindowRootView.updateViewLayout(view, layoutParams); - } - } - - private static class AccessibilityDelegatingFrameLayout extends FrameLayout { - private final AccessibilityEventsDelegate accessibilityEventsDelegate; - private final View embeddedView; - - public AccessibilityDelegatingFrameLayout( - Context context, - AccessibilityEventsDelegate accessibilityEventsDelegate, - View embeddedView) { - super(context); - this.accessibilityEventsDelegate = accessibilityEventsDelegate; - this.embeddedView = embeddedView; - } - - @Override - public boolean requestSendAccessibilityEvent(View child, AccessibilityEvent event) { - return accessibilityEventsDelegate.requestSendAccessibilityEvent(embeddedView, child, event); - } - } -} diff --git a/shell/platform/android/io/flutter/plugin/platform/VirtualDisplayController.java b/shell/platform/android/io/flutter/plugin/platform/VirtualDisplayController.java deleted file mode 100644 index fec53e89a6d9b..0000000000000 --- a/shell/platform/android/io/flutter/plugin/platform/VirtualDisplayController.java +++ /dev/null @@ -1,249 +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.plugin.platform; - -import static android.view.View.OnFocusChangeListener; - -import android.annotation.TargetApi; -import android.content.Context; -import android.hardware.display.DisplayManager; -import android.hardware.display.VirtualDisplay; -import android.os.Build; -import android.view.MotionEvent; -import android.view.Surface; -import android.view.View; -import android.view.ViewTreeObserver; -import androidx.annotation.NonNull; -import androidx.annotation.VisibleForTesting; -import io.flutter.view.TextureRegistry; - -@TargetApi(Build.VERSION_CODES.KITKAT_WATCH) -class VirtualDisplayController { - - public static VirtualDisplayController create( - Context context, - AccessibilityEventsDelegate accessibilityEventsDelegate, - PlatformViewFactory viewFactory, - TextureRegistry.SurfaceTextureEntry textureEntry, - int width, - int height, - int viewId, - Object createParams, - OnFocusChangeListener focusChangeListener) { - textureEntry.surfaceTexture().setDefaultBufferSize(width, height); - Surface surface = new Surface(textureEntry.surfaceTexture()); - DisplayManager displayManager = - (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE); - - int densityDpi = context.getResources().getDisplayMetrics().densityDpi; - VirtualDisplay virtualDisplay = - displayManager.createVirtualDisplay("flutter-vd", width, height, densityDpi, surface, 0); - - if (virtualDisplay == null) { - return null; - } - - return new VirtualDisplayController( - 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; - @VisibleForTesting SingleViewPresentation presentation; - private final Surface surface; - - private VirtualDisplayController( - Context context, - AccessibilityEventsDelegate accessibilityEventsDelegate, - VirtualDisplay virtualDisplay, - 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, - 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. - // - // setSurface is only available starting API 20. We could support API 19 by re-creating a new - // SurfaceTexture here. This will require refactoring the TextureRegistry to allow recycling - // texture - // entry IDs. - virtualDisplay.setSurface(null); - virtualDisplay.release(); - - textureEntry.surfaceTexture().setDefaultBufferSize(width, height); - DisplayManager displayManager = - (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE); - virtualDisplay = - displayManager.createVirtualDisplay("flutter-vd", width, height, densityDpi, surface, 0); - - final View embeddedView = getView(); - // There's a bug in Android version older than O where view tree observer onDrawListeners don't - // get properly - // merged when attaching to window, as a workaround we register the on draw listener after the - // view is attached. - embeddedView.addOnAttachStateChangeListener( - new View.OnAttachStateChangeListener() { - @Override - public void onViewAttachedToWindow(View v) { - OneTimeOnDrawListener.schedule( - embeddedView, - new Runnable() { - @Override - public void run() { - // We need some delay here until the frame propagates through the vd surface to - // to the texture, - // 128ms was picked pretty arbitrarily based on trial and error. - // As long as we invoke the runnable after a new frame is available we avoid the - // scaling jank - // described in: https://github.com/flutter/flutter/issues/19572 - // We should ideally run onNewSizeFrameAvailable ASAP to make the embedded view - // more responsive - // following a resize. - embeddedView.postDelayed(onNewSizeFrameAvailable, 128); - } - }); - embeddedView.removeOnAttachStateChangeListener(this); - } - - @Override - public void onViewDetachedFromWindow(View v) {} - }); - - // Create a new SingleViewPresentation and show() it before we cancel() the existing - // presentation. Calling show() and cancel() in this order fixes - // https://github.com/flutter/flutter/issues/26345 and maintains seamless transition - // of the contents of the presentation. - SingleViewPresentation newPresentation = - new SingleViewPresentation( - context, - virtualDisplay.getDisplay(), - accessibilityEventsDelegate, - presentationState, - focusChangeListener, - isFocused); - newPresentation.show(); - presentation.cancel(); - presentation = newPresentation; - } - - public void dispose() { - PlatformView view = presentation.getView(); - // Fix rare crash on HuaWei device described in: https://github.com/flutter/engine/pull/9192 - presentation.cancel(); - presentation.detachState(); - view.dispose(); - virtualDisplay.release(); - textureEntry.release(); - } - - /** See {@link PlatformView#onFlutterViewAttached(View)} */ - /*package*/ void onFlutterViewAttached(@NonNull View flutterView) { - if (presentation == null || presentation.getView() == null) { - return; - } - presentation.getView().onFlutterViewAttached(flutterView); - } - - /** See {@link PlatformView#onFlutterViewDetached()} */ - /*package*/ void onFlutterViewDetached() { - if (presentation == null || presentation.getView() == null) { - return; - } - presentation.getView().onFlutterViewDetached(); - } - - /*package*/ void onInputConnectionLocked() { - if (presentation == null || presentation.getView() == null) { - return; - } - presentation.getView().onInputConnectionLocked(); - } - - /*package*/ void onInputConnectionUnlocked() { - if (presentation == null || presentation.getView() == null) { - return; - } - presentation.getView().onInputConnectionUnlocked(); - } - - public View getView() { - if (presentation == null) return null; - PlatformView platformView = presentation.getView(); - return platformView.getView(); - } - - /** Dispatches a motion event to the presentation for this controller. */ - public void dispatchTouchEvent(MotionEvent event) { - if (presentation == null) return; - presentation.dispatchTouchEvent(event); - } - - @TargetApi(Build.VERSION_CODES.JELLY_BEAN) - static class OneTimeOnDrawListener implements ViewTreeObserver.OnDrawListener { - static void schedule(View view, Runnable runnable) { - OneTimeOnDrawListener listener = new OneTimeOnDrawListener(view, runnable); - view.getViewTreeObserver().addOnDrawListener(listener); - } - - final View mView; - Runnable mOnDrawRunnable; - - OneTimeOnDrawListener(View view, Runnable onDrawRunnable) { - this.mView = view; - this.mOnDrawRunnable = onDrawRunnable; - } - - @Override - public void onDraw() { - if (mOnDrawRunnable == null) { - return; - } - mOnDrawRunnable.run(); - mOnDrawRunnable = null; - mView.post( - new Runnable() { - @Override - public void run() { - mView.getViewTreeObserver().removeOnDrawListener(OneTimeOnDrawListener.this); - } - }); - } - } -} diff --git a/shell/platform/android/io/flutter/util/ViewUtils.java b/shell/platform/android/io/flutter/util/ViewUtils.java index 6ebac49a00f89..bac41ac7c06ca 100644 --- a/shell/platform/android/io/flutter/util/ViewUtils.java +++ b/shell/platform/android/io/flutter/util/ViewUtils.java @@ -9,6 +9,8 @@ import android.content.ContextWrapper; import android.os.Build; import android.view.View; +import android.view.ViewGroup; +import androidx.annotation.Nullable; public final class ViewUtils { /** @@ -45,4 +47,28 @@ public static int generateViewId(int fallbackId) { } return fallbackId; } + + /** + * Determines if the current view or any descendant view has focus. + * + * @param root The root view. + * @return True if the current view or any descendant view has focus. + */ + public static boolean childHasFocus(@Nullable View root) { + if (root == null) { + return false; + } + if (root.hasFocus()) { + return true; + } + if (root instanceof ViewGroup) { + final ViewGroup viewGroup = (ViewGroup) root; + for (int idx = 0; idx < viewGroup.getChildCount(); idx++) { + if (childHasFocus(viewGroup.getChildAt(idx))) { + return true; + } + } + } + return false; + } } diff --git a/shell/platform/android/io/flutter/view/AccessibilityBridge.java b/shell/platform/android/io/flutter/view/AccessibilityBridge.java index 2381d879898e5..b79191bd09970 100644 --- a/shell/platform/android/io/flutter/view/AccessibilityBridge.java +++ b/shell/platform/android/io/flutter/view/AccessibilityBridge.java @@ -573,23 +573,6 @@ public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) { return null; } - // Generate accessibility node for platform views using a virtual display. - // - // In this case, register the accessibility node in the view embedder, - // so the accessibility tree can be mirrored as a subtree of the Flutter accessibility tree. - // This is in constrast to hybrid composition where the embedded view is in the view hiearchy, - // so it doesn't need to be mirrored. - // - // See the case down below for how hybrid composition is handled. - if (semanticsNode.platformViewId != -1) { - View embeddedView = - platformViewsAccessibilityDelegate.getPlatformViewById(semanticsNode.platformViewId); - if (platformViewsAccessibilityDelegate.usesVirtualDisplay(semanticsNode.platformViewId)) { - Rect bounds = semanticsNode.getGlobalRect(); - return accessibilityViewEmbedder.getRootNode(embeddedView, semanticsNode.id, bounds); - } - } - AccessibilityNodeInfo result = obtainAccessibilityNodeInfo(rootAccessibilityView, virtualViewId); // Work around for https://github.com/flutter/flutter/issues/2101 @@ -904,17 +887,10 @@ && shouldSetCollectionInfo(semanticsNode)) { // Add the embedded view as a child of the current accessibility node if it's using // hybrid composition. - // - // In this case, the view is in the Activity's view hierarchy, so it doesn't need to be - // mirrored. - // - // See the case above for how virtual displays are handled. - if (!platformViewsAccessibilityDelegate.usesVirtualDisplay(child.platformViewId)) { - result.addChild(embeddedView); - continue; - } + result.addChild(embeddedView); + } else { + result.addChild(rootAccessibilityView, child.id); } - result.addChild(rootAccessibilityView, child.id); } return result; } @@ -1545,8 +1521,7 @@ void updateSemantics( if (semanticsNode.hadPreviousConfig) { updated.add(semanticsNode); } - if (semanticsNode.platformViewId != -1 - && !platformViewsAccessibilityDelegate.usesVirtualDisplay(semanticsNode.platformViewId)) { + if (semanticsNode.platformViewId != -1) { View embeddedView = platformViewsAccessibilityDelegate.getPlatformViewById(semanticsNode.platformViewId); if (embeddedView != null) { @@ -1958,9 +1933,7 @@ private void willRemoveSemanticsNode(SemanticsNode semanticsNodeToBeRemoved) { embeddedAccessibilityFocusedNodeId = null; } - if (semanticsNodeToBeRemoved.platformViewId != -1 - && !platformViewsAccessibilityDelegate.usesVirtualDisplay( - semanticsNodeToBeRemoved.platformViewId)) { + if (semanticsNodeToBeRemoved.platformViewId != -1) { View embeddedView = platformViewsAccessibilityDelegate.getPlatformViewById( semanticsNodeToBeRemoved.platformViewId); diff --git a/shell/platform/android/io/flutter/view/FlutterView.java b/shell/platform/android/io/flutter/view/FlutterView.java index 9b86ed08193fe..beb15c38c0a8f 100644 --- a/shell/platform/android/io/flutter/view/FlutterView.java +++ b/shell/platform/android/io/flutter/view/FlutterView.java @@ -26,7 +26,6 @@ import android.view.Surface; import android.view.SurfaceHolder; import android.view.SurfaceView; -import android.view.View; import android.view.ViewConfiguration; import android.view.ViewStructure; import android.view.WindowInsets; @@ -427,14 +426,6 @@ public InputConnection onCreateInputConnection(EditorInfo outAttrs) { return mTextInputPlugin.createInputConnection(this, mKeyboardManager, outAttrs); } - @Override - public boolean checkInputConnectionProxy(View view) { - return mNativeView - .getPluginRegistry() - .getPlatformViewsController() - .checkInputConnectionProxy(view); - } - @Override public void onProvideAutofillVirtualStructure(ViewStructure structure, int flags) { super.onProvideAutofillVirtualStructure(structure, flags); diff --git a/shell/platform/android/test/io/flutter/embedding/engine/mutatorsstack/FlutterMutatorViewTest.java b/shell/platform/android/test/io/flutter/embedding/engine/mutatorsstack/FlutterMutatorViewTest.java index 16e5496ef9a50..d4bdf50dc873a 100644 --- a/shell/platform/android/test/io/flutter/embedding/engine/mutatorsstack/FlutterMutatorViewTest.java +++ b/shell/platform/android/test/io/flutter/embedding/engine/mutatorsstack/FlutterMutatorViewTest.java @@ -6,8 +6,6 @@ import android.graphics.Matrix; import android.view.MotionEvent; -import android.view.View; -import android.view.ViewGroup; import android.view.ViewTreeObserver; import androidx.test.ext.junit.runners.AndroidJUnit4; import io.flutter.embedding.android.AndroidTouchProcessor; @@ -81,49 +79,6 @@ public void canDragViews() { } } - @Test - public void childHasFocus_rootHasFocus() { - final View rootView = mock(View.class); - when(rootView.hasFocus()).thenReturn(true); - assertTrue(FlutterMutatorView.childHasFocus(rootView)); - } - - @Test - public void childHasFocus_rootDoesNotHaveFocus() { - final View rootView = mock(View.class); - when(rootView.hasFocus()).thenReturn(false); - assertFalse(FlutterMutatorView.childHasFocus(rootView)); - } - - @Test - public void childHasFocus_rootIsNull() { - assertFalse(FlutterMutatorView.childHasFocus(null)); - } - - @Test - public void childHasFocus_childHasFocus() { - final View childView = mock(View.class); - when(childView.hasFocus()).thenReturn(true); - - final ViewGroup rootView = mock(ViewGroup.class); - when(rootView.getChildCount()).thenReturn(1); - when(rootView.getChildAt(0)).thenReturn(childView); - - assertTrue(FlutterMutatorView.childHasFocus(rootView)); - } - - @Test - public void childHasFocus_childDoesNotHaveFocus() { - final View childView = mock(View.class); - when(childView.hasFocus()).thenReturn(false); - - final ViewGroup rootView = mock(ViewGroup.class); - when(rootView.getChildCount()).thenReturn(1); - when(rootView.getChildAt(0)).thenReturn(childView); - - assertFalse(FlutterMutatorView.childHasFocus(rootView)); - } - @Test public void focusChangeListener_hasFocus() { final ViewTreeObserver viewTreeObserver = mock(ViewTreeObserver.class); diff --git a/shell/platform/android/test/io/flutter/plugin/platform/PlatformViewWrapperTest.java b/shell/platform/android/test/io/flutter/plugin/platform/PlatformViewWrapperTest.java new file mode 100644 index 0000000000000..1c2d4f5168028 --- /dev/null +++ b/shell/platform/android/test/io/flutter/plugin/platform/PlatformViewWrapperTest.java @@ -0,0 +1,265 @@ +package io.flutter.plugin.platform; + +import static android.view.View.OnFocusChangeListener; +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import android.annotation.TargetApi; +import android.content.Context; +import android.graphics.BlendMode; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.SurfaceTexture; +import android.view.Surface; +import android.view.View; +import android.view.ViewTreeObserver; +import androidx.annotation.NonNull; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.robolectric.RuntimeEnvironment; + +@TargetApi(31) +@RunWith(AndroidJUnit4.class) +public class PlatformViewWrapperTest { + @Test + public void setTexture_writesToBuffer() { + final Surface surface = mock(Surface.class); + final Context ctx = ApplicationProvider.getApplicationContext(); + final PlatformViewWrapper wrapper = + new PlatformViewWrapper(ctx) { + @Override + protected Surface createSurface(@NonNull SurfaceTexture tx) { + return surface; + } + }; + + final SurfaceTexture tx = mock(SurfaceTexture.class); + when(tx.isReleased()).thenReturn(false); + + final Canvas canvas = mock(Canvas.class); + when(surface.lockHardwareCanvas()).thenReturn(canvas); + + // Test. + wrapper.setTexture(tx); + + // Verify. + verify(surface, times(1)).lockHardwareCanvas(); + verify(surface, times(1)).unlockCanvasAndPost(canvas); + verify(canvas, times(1)).drawColor(Color.TRANSPARENT, BlendMode.CLEAR); + verifyNoMoreInteractions(surface); + verifyNoMoreInteractions(canvas); + } + + @Test + public void draw_writesToBuffer() { + final Surface surface = mock(Surface.class); + final Context ctx = ApplicationProvider.getApplicationContext(); + final PlatformViewWrapper wrapper = + new PlatformViewWrapper(ctx) { + @Override + protected Surface createSurface(@NonNull SurfaceTexture tx) { + return surface; + } + }; + + wrapper.addView( + new View(ctx) { + @Override + public void draw(Canvas canvas) { + super.draw(canvas); + canvas.drawColor(Color.RED); + } + }); + + final int size = 100; + wrapper.measure(size, size); + wrapper.layout(0, 0, size, size); + + final SurfaceTexture tx = mock(SurfaceTexture.class); + when(tx.isReleased()).thenReturn(false); + + when(surface.lockHardwareCanvas()).thenReturn(mock(Canvas.class)); + + wrapper.setTexture(tx); + + reset(surface); + + final Canvas canvas = mock(Canvas.class); + when(surface.lockHardwareCanvas()).thenReturn(canvas); + when(surface.isValid()).thenReturn(true); + + // Test. + wrapper.invalidate(); + wrapper.draw(new Canvas()); + + // Verify. + verify(canvas, times(1)).drawColor(Color.TRANSPARENT, BlendMode.CLEAR); + verify(surface, times(1)).isValid(); + verify(surface, times(1)).lockHardwareCanvas(); + verify(surface, times(1)).unlockCanvasAndPost(canvas); + verifyNoMoreInteractions(surface); + verifyNoMoreInteractions(canvas); + } + + @Test + public void release() { + final Surface surface = mock(Surface.class); + final Context ctx = ApplicationProvider.getApplicationContext(); + final PlatformViewWrapper wrapper = + new PlatformViewWrapper(ctx) { + @Override + protected Surface createSurface(@NonNull SurfaceTexture tx) { + return surface; + } + }; + + final SurfaceTexture tx = mock(SurfaceTexture.class); + when(tx.isReleased()).thenReturn(false); + + final Canvas canvas = mock(Canvas.class); + when(surface.lockHardwareCanvas()).thenReturn(canvas); + + wrapper.setTexture(tx); + reset(surface); + reset(tx); + + // Test. + wrapper.release(); + + // Verify. + verify(surface, times(1)).release(); + verifyNoMoreInteractions(surface); + verifyNoMoreInteractions(tx); + } + + @Test + public void focusChangeListener_hasFocus() { + final ViewTreeObserver viewTreeObserver = mock(ViewTreeObserver.class); + when(viewTreeObserver.isAlive()).thenReturn(true); + + final PlatformViewWrapper view = + new PlatformViewWrapper(RuntimeEnvironment.application) { + @Override + public ViewTreeObserver getViewTreeObserver() { + return viewTreeObserver; + } + + @Override + public boolean hasFocus() { + return true; + } + }; + + final OnFocusChangeListener focusListener = mock(OnFocusChangeListener.class); + view.setOnDescendantFocusChangeListener(focusListener); + + final ArgumentCaptor focusListenerCaptor = + ArgumentCaptor.forClass(ViewTreeObserver.OnGlobalFocusChangeListener.class); + verify(viewTreeObserver).addOnGlobalFocusChangeListener(focusListenerCaptor.capture()); + + focusListenerCaptor.getValue().onGlobalFocusChanged(null, null); + verify(focusListener).onFocusChange(view, true); + } + + @Test + public void focusChangeListener_doesNotHaveFocus() { + final ViewTreeObserver viewTreeObserver = mock(ViewTreeObserver.class); + when(viewTreeObserver.isAlive()).thenReturn(true); + + final PlatformViewWrapper view = + new PlatformViewWrapper(RuntimeEnvironment.application) { + @Override + public ViewTreeObserver getViewTreeObserver() { + return viewTreeObserver; + } + + @Override + public boolean hasFocus() { + return false; + } + }; + + final OnFocusChangeListener focusListener = mock(OnFocusChangeListener.class); + view.setOnDescendantFocusChangeListener(focusListener); + + final ArgumentCaptor focusListenerCaptor = + ArgumentCaptor.forClass(ViewTreeObserver.OnGlobalFocusChangeListener.class); + verify(viewTreeObserver).addOnGlobalFocusChangeListener(focusListenerCaptor.capture()); + + focusListenerCaptor.getValue().onGlobalFocusChanged(null, null); + verify(focusListener).onFocusChange(view, false); + } + + @Test + public void focusChangeListener_viewTreeObserverIsAliveFalseDoesNotThrow() { + final PlatformViewWrapper view = + new PlatformViewWrapper(RuntimeEnvironment.application) { + @Override + public ViewTreeObserver getViewTreeObserver() { + final ViewTreeObserver viewTreeObserver = mock(ViewTreeObserver.class); + when(viewTreeObserver.isAlive()).thenReturn(false); + return viewTreeObserver; + } + }; + view.setOnDescendantFocusChangeListener(mock(OnFocusChangeListener.class)); + } + + @Test + public void setOnDescendantFocusChangeListener_keepsSingleListener() { + final ViewTreeObserver viewTreeObserver = mock(ViewTreeObserver.class); + when(viewTreeObserver.isAlive()).thenReturn(true); + + final PlatformViewWrapper view = + new PlatformViewWrapper(RuntimeEnvironment.application) { + @Override + public ViewTreeObserver getViewTreeObserver() { + return viewTreeObserver; + } + }; + + assertNull(view.activeFocusListener); + + view.setOnDescendantFocusChangeListener(mock(OnFocusChangeListener.class)); + assertNotNull(view.activeFocusListener); + + final ViewTreeObserver.OnGlobalFocusChangeListener activeFocusListener = + view.activeFocusListener; + + view.setOnDescendantFocusChangeListener(mock(OnFocusChangeListener.class)); + assertNotNull(view.activeFocusListener); + + verify(viewTreeObserver, times(1)).removeOnGlobalFocusChangeListener(activeFocusListener); + } + + @Test + public void unsetOnDescendantFocusChangeListener_removesActiveListener() { + final ViewTreeObserver viewTreeObserver = mock(ViewTreeObserver.class); + when(viewTreeObserver.isAlive()).thenReturn(true); + + final PlatformViewWrapper view = + new PlatformViewWrapper(RuntimeEnvironment.application) { + @Override + public ViewTreeObserver getViewTreeObserver() { + return viewTreeObserver; + } + }; + + assertNull(view.activeFocusListener); + + view.setOnDescendantFocusChangeListener(mock(OnFocusChangeListener.class)); + assertNotNull(view.activeFocusListener); + + final ViewTreeObserver.OnGlobalFocusChangeListener activeFocusListener = + view.activeFocusListener; + + view.unsetOnDescendantFocusChangeListener(); + assertNull(view.activeFocusListener); + + view.unsetOnDescendantFocusChangeListener(); + verify(viewTreeObserver, times(1)).removeOnGlobalFocusChangeListener(activeFocusListener); + } +} diff --git a/shell/platform/android/test/io/flutter/plugin/platform/PlatformViewsControllerTest.java b/shell/platform/android/test/io/flutter/plugin/platform/PlatformViewsControllerTest.java index 66669638e910e..0398f88003630 100644 --- a/shell/platform/android/test/io/flutter/plugin/platform/PlatformViewsControllerTest.java +++ b/shell/platform/android/test/io/flutter/plugin/platform/PlatformViewsControllerTest.java @@ -17,7 +17,6 @@ import android.view.SurfaceView; import android.view.View; import android.view.ViewParent; -import android.widget.FrameLayout.LayoutParams; import androidx.test.ext.junit.runners.AndroidJUnit4; import io.flutter.embedding.android.FlutterImageView; import io.flutter.embedding.android.FlutterView; @@ -56,120 +55,6 @@ @RunWith(AndroidJUnit4.class) public class PlatformViewsControllerTest { - @Ignore - @Test - public void itNotifiesVirtualDisplayControllersOfViewAttachmentAndDetachment() { - // Setup test structure. - // Create a fake View that represents the View that renders a Flutter UI. - FlutterView fakeFlutterView = new FlutterView(RuntimeEnvironment.systemContext); - - // Create fake VirtualDisplayControllers. This requires internal knowledge of - // PlatformViewsController. We know that all PlatformViewsController does is - // forward view attachment/detachment calls to it's VirtualDisplayControllers. - // - // TODO(mattcarroll): once PlatformViewsController is refactored into testable - // pieces, remove this test and avoid verifying private behavior. - VirtualDisplayController fakeVdController1 = mock(VirtualDisplayController.class); - VirtualDisplayController fakeVdController2 = mock(VirtualDisplayController.class); - - // Create the PlatformViewsController that is under test. - PlatformViewsController platformViewsController = new PlatformViewsController(); - - // Manually inject fake VirtualDisplayControllers into the PlatformViewsController. - platformViewsController.vdControllers.put(0, fakeVdController1); - platformViewsController.vdControllers.put(1, fakeVdController1); - - // Execute test & verify results. - // Attach PlatformViewsController to the fake Flutter View. - platformViewsController.attachToView(fakeFlutterView); - - // Verify that all virtual display controllers were notified of View attachment. - verify(fakeVdController1, times(1)).onFlutterViewAttached(eq(fakeFlutterView)); - verify(fakeVdController1, never()).onFlutterViewDetached(); - verify(fakeVdController2, times(1)).onFlutterViewAttached(eq(fakeFlutterView)); - verify(fakeVdController2, never()).onFlutterViewDetached(); - - // Detach PlatformViewsController from the fake Flutter View. - platformViewsController.detachFromView(); - - // Verify that all virtual display controllers were notified of the View detachment. - verify(fakeVdController1, times(1)).onFlutterViewAttached(eq(fakeFlutterView)); - verify(fakeVdController1, times(1)).onFlutterViewDetached(); - verify(fakeVdController2, times(1)).onFlutterViewAttached(eq(fakeFlutterView)); - verify(fakeVdController2, times(1)).onFlutterViewDetached(); - } - - @Ignore - @Test - public void itCancelsOldPresentationOnResize() { - // Setup test structure. - // Create a fake View that represents the View that renders a Flutter UI. - View fakeFlutterView = new View(RuntimeEnvironment.systemContext); - - // Create fake VirtualDisplayControllers. This requires internal knowledge of - // PlatformViewsController. We know that all PlatformViewsController does is - // forward view attachment/detachment calls to it's VirtualDisplayControllers. - // - // TODO(mattcarroll): once PlatformViewsController is refactored into testable - // pieces, remove this test and avoid verifying private behavior. - VirtualDisplayController fakeVdController1 = mock(VirtualDisplayController.class); - - SingleViewPresentation presentation = fakeVdController1.presentation; - - fakeVdController1.resize(10, 10, null); - - assertEquals(fakeVdController1.presentation != presentation, true); - assertEquals(presentation.isShowing(), false); - } - - @Test - public void itUsesActionEventTypeFromFrameworkEventForVirtualDisplays() { - MotionEventTracker motionEventTracker = MotionEventTracker.getInstance(); - PlatformViewsController platformViewsController = new PlatformViewsController(); - - MotionEvent original = - MotionEvent.obtain( - 100, // downTime - 100, // eventTime - 1, // action - 0, // x - 0, // y - 0 // metaState - ); - - // track an event that will later get passed to us from framework - MotionEventTracker.MotionEventId motionEventId = motionEventTracker.track(original); - - PlatformViewTouch frameWorkTouch = - new PlatformViewTouch( - 0, // viewId - original.getDownTime(), - original.getEventTime(), - 2, // action - 1, // pointerCount - Arrays.asList(Arrays.asList(0, 0)), // pointer properties - Arrays.asList(Arrays.asList(0., 1., 2., 3., 4., 5., 6., 7., 8.)), // pointer coords - original.getMetaState(), - original.getButtonState(), - original.getXPrecision(), - original.getYPrecision(), - original.getDeviceId(), - original.getEdgeFlags(), - original.getSource(), - original.getFlags(), - motionEventId.getId()); - - MotionEvent resolvedEvent = - platformViewsController.toMotionEvent( - 1, // density - frameWorkTouch, - true // usingVirtualDisplays - ); - - assertEquals(resolvedEvent.getAction(), frameWorkTouch.action); - assertNotEquals(resolvedEvent.getAction(), original.getAction()); - } - @Ignore @Test public void itUsesActionEventTypeFromMotionEventForHybridPlatformViews() { @@ -209,11 +94,7 @@ public void itUsesActionEventTypeFromMotionEventForHybridPlatformViews() { motionEventId.getId()); MotionEvent resolvedEvent = - platformViewsController.toMotionEvent( - 1, // density - frameWorkTouch, - false // usingVirtualDisplays - ); + platformViewsController.toMotionEvent(/*density=*/ 1, frameWorkTouch); assertNotEquals(resolvedEvent.getAction(), frameWorkTouch.action); assertEquals(resolvedEvent.getAction(), original.getAction()); @@ -297,66 +178,6 @@ public void createPlatformViewMessage__throwsIfViewIsNull() { }); } - @Test - @Config(shadows = {ShadowFlutterJNI.class, ShadowPlatformTaskQueue.class}) - public void onDetachedFromJNI_clearsPlatformViewContext() { - PlatformViewsController platformViewsController = new PlatformViewsController(); - - int platformViewId = 0; - assertNull(platformViewsController.getPlatformViewById(platformViewId)); - - PlatformViewFactory viewFactory = mock(PlatformViewFactory.class); - PlatformView platformView = mock(PlatformView.class); - - View pv = mock(View.class); - when(pv.getLayoutParams()).thenReturn(new LayoutParams(1, 1)); - - when(platformView.getView()).thenReturn(pv); - when(viewFactory.create(any(), eq(platformViewId), any())).thenReturn(platformView); - platformViewsController.getRegistry().registerViewFactory("testType", viewFactory); - - FlutterJNI jni = new FlutterJNI(); - attach(jni, platformViewsController); - - // Simulate create call from the framework. - createPlatformView( - jni, platformViewsController, platformViewId, "testType", /* hybrid=*/ false); - - assertFalse(platformViewsController.contextToPlatformView.isEmpty()); - platformViewsController.onDetachedFromJNI(); - assertTrue(platformViewsController.contextToPlatformView.isEmpty()); - } - - @Test - @Config(shadows = {ShadowFlutterJNI.class, ShadowPlatformTaskQueue.class}) - public void onPreEngineRestart_clearsPlatformViewContext() { - PlatformViewsController platformViewsController = new PlatformViewsController(); - - int platformViewId = 0; - assertNull(platformViewsController.getPlatformViewById(platformViewId)); - - PlatformViewFactory viewFactory = mock(PlatformViewFactory.class); - PlatformView platformView = mock(PlatformView.class); - - View pv = mock(View.class); - when(pv.getLayoutParams()).thenReturn(new LayoutParams(1, 1)); - - when(platformView.getView()).thenReturn(pv); - when(viewFactory.create(any(), eq(platformViewId), any())).thenReturn(platformView); - platformViewsController.getRegistry().registerViewFactory("testType", viewFactory); - - FlutterJNI jni = new FlutterJNI(); - attach(jni, platformViewsController); - - // Simulate create call from the framework. - createPlatformView( - jni, platformViewsController, platformViewId, "testType", /* hybrid=*/ false); - - assertFalse(platformViewsController.contextToPlatformView.isEmpty()); - platformViewsController.onDetachedFromJNI(); - assertTrue(platformViewsController.contextToPlatformView.isEmpty()); - } - @Test @Config(shadows = {ShadowFlutterJNI.class, ShadowPlatformTaskQueue.class}) public void createPlatformViewMessage__throwsIfViewHasParent() { @@ -772,13 +593,6 @@ public void destroyOverlaySurfaces__doesNotRemoveOverlayView() { verify(flutterView, never()).removeView(overlayImageView); } - @Test - public void checkInputConnectionProxy__falseIfViewIsNull() { - final PlatformViewsController platformViewsController = new PlatformViewsController(); - boolean shouldProxying = platformViewsController.checkInputConnectionProxy(null); - assertFalse(shouldProxying); - } - @Test @Config(shadows = {ShadowFlutterJNI.class, ShadowPlatformTaskQueue.class}) public void convertPlatformViewRenderSurfaceAsDefault() { diff --git a/shell/platform/android/test/io/flutter/plugin/platform/SingleViewPresentationTest.java b/shell/platform/android/test/io/flutter/plugin/platform/SingleViewPresentationTest.java deleted file mode 100644 index 2cc177fd86648..0000000000000 --- a/shell/platform/android/test/io/flutter/plugin/platform/SingleViewPresentationTest.java +++ /dev/null @@ -1,82 +0,0 @@ -package io.flutter.plugin.platform; - -import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR1; -import static android.os.Build.VERSION_CODES.P; -import static android.os.Build.VERSION_CODES.R; -import static junit.framework.Assert.assertEquals; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.when; - -import android.annotation.TargetApi; -import android.content.Context; -import android.hardware.display.DisplayManager; -import android.view.Display; -import android.view.inputmethod.InputMethodManager; -import androidx.test.ext.junit.runners.AndroidJUnit4; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.robolectric.RuntimeEnvironment; -import org.robolectric.annotation.Config; - -@Config(manifest = Config.NONE) -@RunWith(AndroidJUnit4.class) -@TargetApi(P) -public class SingleViewPresentationTest { - @Test - @Config(minSdk = JELLY_BEAN_MR1, maxSdk = R) - public void returnsOuterContextInputMethodManager() { - // There's a bug in Android Q caused by the IMM being instanced per display. - // https://github.com/flutter/flutter/issues/38375. We need the context returned by - // SingleViewPresentation to be consistent from its instantiation instead of defaulting to - // what the system would have returned at call time. - - // It's not possible to set up the exact same conditions as the unit test in the bug here, - // but we can make sure that we're wrapping the Context passed in at instantiation time and - // returning the same InputMethodManager from it. This test passes in a Spy context instance - // that initially returns a mock. Without the bugfix this test falls back to Robolectric's - // system service instead of the spy's and fails. - - // Create an SVP under test with a Context that returns a local IMM mock. - Context context = spy(RuntimeEnvironment.application); - InputMethodManager expected = mock(InputMethodManager.class); - when(context.getSystemService(Context.INPUT_METHOD_SERVICE)).thenReturn(expected); - DisplayManager dm = (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE); - SingleViewPresentation svp = - new SingleViewPresentation(context, dm.getDisplay(0), null, null, null, false); - - // Get the IMM from the SVP's context. - InputMethodManager actual = - (InputMethodManager) svp.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); - - // This should be the mocked instance from construction, not the IMM from the greater - // Android OS (or Robolectric's shadow, in this case). - assertEquals(expected, actual); - } - - @Test - @Config(minSdk = JELLY_BEAN_MR1, maxSdk = R) - public void returnsOuterContextInputMethodManager_createDisplayContext() { - // The IMM should also persist across display contexts created from the base context. - - // Create an SVP under test with a Context that returns a local IMM mock. - Context context = spy(RuntimeEnvironment.application); - InputMethodManager expected = mock(InputMethodManager.class); - when(context.getSystemService(Context.INPUT_METHOD_SERVICE)).thenReturn(expected); - Display display = - ((DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE)).getDisplay(0); - SingleViewPresentation svp = - new SingleViewPresentation(context, display, null, null, null, false); - - // Get the IMM from the SVP's context. - InputMethodManager actual = - (InputMethodManager) - svp.getContext() - .createDisplayContext(display) - .getSystemService(Context.INPUT_METHOD_SERVICE); - - // This should be the mocked instance from construction, not the IMM from the greater - // Android OS (or Robolectric's shadow, in this case). - assertEquals(expected, actual); - } -} diff --git a/shell/platform/android/test/io/flutter/util/ViewUtilsTest.java b/shell/platform/android/test/io/flutter/util/ViewUtilsTest.java index 937aa06dae408..2adbc6c2afaba 100644 --- a/shell/platform/android/test/io/flutter/util/ViewUtilsTest.java +++ b/shell/platform/android/test/io/flutter/util/ViewUtilsTest.java @@ -5,11 +5,16 @@ package io.flutter.util; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import android.app.Activity; import android.content.Context; import android.content.ContextWrapper; +import android.view.View; +import android.view.ViewGroup; import androidx.test.ext.junit.runners.AndroidJUnit4; import org.junit.Test; import org.junit.runner.RunWith; @@ -30,4 +35,47 @@ public void canGetActivity() { ContextWrapper wrapper = new ContextWrapper(new ContextWrapper(activity)); assertEquals(activity, ViewUtils.getActivity(wrapper)); } + + @Test + public void childHasFocus_rootHasFocus() { + final View rootView = mock(View.class); + when(rootView.hasFocus()).thenReturn(true); + assertTrue(ViewUtils.childHasFocus(rootView)); + } + + @Test + public void childHasFocus_rootDoesNotHaveFocus() { + final View rootView = mock(View.class); + when(rootView.hasFocus()).thenReturn(false); + assertFalse(ViewUtils.childHasFocus(rootView)); + } + + @Test + public void childHasFocus_rootIsNull() { + assertFalse(ViewUtils.childHasFocus(null)); + } + + @Test + public void childHasFocus_childHasFocus() { + final View childView = mock(View.class); + when(childView.hasFocus()).thenReturn(true); + + final ViewGroup rootView = mock(ViewGroup.class); + when(rootView.getChildCount()).thenReturn(1); + when(rootView.getChildAt(0)).thenReturn(childView); + + assertTrue(ViewUtils.childHasFocus(rootView)); + } + + @Test + public void childHasFocus_childDoesNotHaveFocus() { + final View childView = mock(View.class); + when(childView.hasFocus()).thenReturn(false); + + final ViewGroup rootView = mock(ViewGroup.class); + when(rootView.getChildCount()).thenReturn(1); + when(rootView.getChildAt(0)).thenReturn(childView); + + assertFalse(ViewUtils.childHasFocus(rootView)); + } } diff --git a/shell/platform/android/test/io/flutter/view/AccessibilityBridgeTest.java b/shell/platform/android/test/io/flutter/view/AccessibilityBridgeTest.java index a1135bb6873ff..194465e9a5570 100644 --- a/shell/platform/android/test/io/flutter/view/AccessibilityBridgeTest.java +++ b/shell/platform/android/test/io/flutter/view/AccessibilityBridgeTest.java @@ -1467,7 +1467,6 @@ public void itProducesPlatformViewNodeForHybridComposition() { View embeddedView = mock(View.class); when(accessibilityDelegate.getPlatformViewById(1)).thenReturn(embeddedView); - when(accessibilityDelegate.usesVirtualDisplay(1)).thenReturn(false); AccessibilityNodeInfo nodeInfo = mock(AccessibilityNodeInfo.class); when(embeddedView.createAccessibilityNodeInfo()).thenReturn(nodeInfo); @@ -1505,7 +1504,6 @@ public void itMakesPlatformViewImportantForAccessibility() { View embeddedView = mock(View.class); when(accessibilityDelegate.getPlatformViewById(1)).thenReturn(embeddedView); - when(accessibilityDelegate.usesVirtualDisplay(1)).thenReturn(false); TestSemanticsUpdate testSemanticsRootUpdate = root.toUpdate(); testSemanticsRootUpdate.sendUpdateToBridge(accessibilityBridge); @@ -1540,7 +1538,6 @@ public void itMakesPlatformViewNoImportantForAccessibility() { View embeddedView = mock(View.class); when(accessibilityDelegate.getPlatformViewById(1)).thenReturn(embeddedView); - when(accessibilityDelegate.usesVirtualDisplay(1)).thenReturn(false); TestSemanticsUpdate testSemanticsRootWithPlatformViewUpdate = rootWithPlatformView.toUpdate(); testSemanticsRootWithPlatformViewUpdate.sendUpdateToBridge(accessibilityBridge); @@ -1555,34 +1552,6 @@ public void itMakesPlatformViewNoImportantForAccessibility() { .setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS); } - @Test - public void itProducesPlatformViewNodeForVirtualDisplay() { - PlatformViewsAccessibilityDelegate accessibilityDelegate = - mock(PlatformViewsAccessibilityDelegate.class); - AccessibilityViewEmbedder accessibilityViewEmbedder = mock(AccessibilityViewEmbedder.class); - AccessibilityBridge accessibilityBridge = - setUpBridge( - /*rootAccessibilityView=*/ null, - /*accessibilityChannel=*/ null, - /*accessibilityManager=*/ null, - /*contentResolver=*/ null, - accessibilityViewEmbedder, - accessibilityDelegate); - - TestSemanticsNode platformView = new TestSemanticsNode(); - platformView.platformViewId = 1; - - TestSemanticsUpdate testSemanticsUpdate = platformView.toUpdate(); - testSemanticsUpdate.sendUpdateToBridge(accessibilityBridge); - - View embeddedView = mock(View.class); - when(accessibilityDelegate.getPlatformViewById(1)).thenReturn(embeddedView); - when(accessibilityDelegate.usesVirtualDisplay(1)).thenReturn(true); - - accessibilityBridge.createAccessibilityNodeInfo(0); - verify(accessibilityViewEmbedder).getRootNode(eq(embeddedView), eq(0), any(Rect.class)); - } - @Test public void releaseDropsChannelMessageHandler() { AccessibilityChannel mockChannel = mock(AccessibilityChannel.class); diff --git a/tools/android_lint/baseline.xml b/tools/android_lint/baseline.xml index 0e47fcf9f88ea..00aad19954db8 100644 --- a/tools/android_lint/baseline.xml +++ b/tools/android_lint/baseline.xml @@ -133,17 +133,6 @@ column="27"/> - - - - - @@ -40,20 +39,21 @@ + - + - + @@ -151,13 +151,12 @@ - - + @@ -178,8 +177,8 @@ - +