From 0832efccc536a2b00db6ca8553d827e82a9a37c6 Mon Sep 17 00:00:00 2001 From: Emmanuel Garcia Date: Tue, 24 May 2022 15:54:45 -0700 Subject: [PATCH 01/25] Handle SurfaceView and TextureView in a VirtualDisplay --- shell/platform/android/BUILD.gn | 2 + .../platform/SingleViewPresentation.java | 478 ++++++++++++++++++ .../platform/VirtualDisplayController.java | 249 +++++++++ 3 files changed, 729 insertions(+) create mode 100644 shell/platform/android/io/flutter/plugin/platform/SingleViewPresentation.java create mode 100644 shell/platform/android/io/flutter/plugin/platform/VirtualDisplayController.java diff --git a/shell/platform/android/BUILD.gn b/shell/platform/android/BUILD.gn index 7444efa71a0eb..84827f4610e77 100644 --- a/shell/platform/android/BUILD.gn +++ b/shell/platform/android/BUILD.gn @@ -286,6 +286,8 @@ android_java_sources = [ "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/plugin/platform/SingleViewPresentation.java b/shell/platform/android/io/flutter/plugin/platform/SingleViewPresentation.java new file mode 100644 index 0000000000000..a561f73868d86 --- /dev/null +++ b/shell/platform/android/io/flutter/plugin/platform/SingleViewPresentation.java @@ -0,0 +1,478 @@ +// 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 new file mode 100644 index 0000000000000..fec53e89a6d9b --- /dev/null +++ b/shell/platform/android/io/flutter/plugin/platform/VirtualDisplayController.java @@ -0,0 +1,249 @@ +// 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); + } + }); + } + } +} From e2b2b94571ec0e5c1c9a279ff181a2fde0390b66 Mon Sep 17 00:00:00 2001 From: Emmanuel Garcia Date: Tue, 24 May 2022 16:56:23 -0700 Subject: [PATCH 02/25] refactor --- .../systemchannels/PlatformViewsChannel.java | 32 ++- .../plugin/editing/TextInputPlugin.java | 34 +++ .../flutter/plugin/platform/PlatformView.java | 6 - .../platform/PlatformViewsController.java | 213 +++++++++++++++--- .../platform/SingleViewPresentation.java | 20 +- .../platform/VirtualDisplayController.java | 51 +++-- .../android/io/flutter/util/ViewUtils.java | 47 +++- 7 files changed, 331 insertions(+), 72 deletions(-) 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 4b50d01e856d9..d338b7cfcbce1 100644 --- a/shell/platform/android/io/flutter/embedding/engine/systemchannels/PlatformViewsChannel.java +++ b/shell/platform/android/io/flutter/embedding/engine/systemchannels/PlatformViewsChannel.java @@ -147,15 +147,18 @@ private void resize(@NonNull MethodCall call, @NonNull MethodChannel.Result resu (double) resizeArgs.get("width"), (double) resizeArgs.get("height")); try { - 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); - } + handler.resize( + resizeRequest, + (PlatformViewBufferSize bufferSize) -> { + if (bufferSize == null) { + result.error("error", "Failed to resize the platform view", null); + } else { + final Map response = new HashMap<>(); + response.put("width", (double) bufferSize.width); + response.put("height", (double) bufferSize.height); + result.success(response); + } + }); } catch (IllegalStateException exception) { result.error("error", detailedExceptionString(exception), null); } @@ -298,9 +301,11 @@ public interface PlatformViewsHandler { * 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. + * @param onComplete Once the resize is completed, this is the handler to notify the size of the + * platform view buffer. */ - PlatformViewBufferSize resize(@NonNull PlatformViewResizeRequest request); + void resize( + @NonNull PlatformViewResizeRequest request, @NonNull PlatformViewBufferResized onComplete); /** * The Flutter application would like to change the offset of an existing Android {@code View}. @@ -418,6 +423,11 @@ public PlatformViewBufferSize(int width, int height) { } } + /** Allows to notify when a platform view buffer has been resized. */ + public interface PlatformViewBufferResized { + void run(@Nullable PlatformViewBufferSize bufferSize); + } + /** 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/plugin/editing/TextInputPlugin.java b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java index 70658ee934493..6310c995f0c22 100644 --- a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java +++ b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java @@ -54,6 +54,12 @@ 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( @NonNull View view, @@ -176,6 +182,34 @@ 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.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. * diff --git a/shell/platform/android/io/flutter/plugin/platform/PlatformView.java b/shell/platform/android/io/flutter/plugin/platform/PlatformView.java index 2c563ccac937b..d7b3cf52aba91 100644 --- a/shell/platform/android/io/flutter/plugin/platform/PlatformView.java +++ b/shell/platform/android/io/flutter/plugin/platform/PlatformView.java @@ -66,11 +66,8 @@ default void onFlutterViewDetached() {} * *

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. */ @SuppressLint("NewApi") - @Deprecated default void onInputConnectionLocked() {} /** @@ -78,10 +75,7 @@ default void onInputConnectionLocked() {} * *

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. */ @SuppressLint("NewApi") - @Deprecated default void onInputConnectionUnlocked() {} } diff --git a/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java b/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java index 8008da1a77b28..8a312417c25a1 100644 --- a/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java +++ b/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java @@ -9,9 +9,12 @@ import android.annotation.TargetApi; import android.content.Context; +import android.content.MutableContextWrapper; import android.os.Build; import android.util.SparseArray; import android.view.MotionEvent; +import android.view.SurfaceView; +import android.view.TextureView; import android.view.View; import android.view.ViewGroup; import android.widget.FrameLayout; @@ -30,9 +33,11 @@ import io.flutter.embedding.engine.renderer.FlutterRenderer; import io.flutter.embedding.engine.systemchannels.PlatformViewsChannel; import io.flutter.plugin.editing.TextInputPlugin; +import io.flutter.util.ViewUtils; 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; @@ -45,6 +50,7 @@ */ public class PlatformViewsController implements PlatformViewsAccessibilityDelegate { private static final String TAG = "PlatformViewsController"; + private static Class[] VIEW_TYPES_REQUIRE_VD = {SurfaceView.class, TextureView.class}; private final PlatformViewRegistryImpl registry; @@ -68,6 +74,17 @@ 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 embedded 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 contextToEmbeddedView; + // The platform views. private final SparseArray platformViews; @@ -123,14 +140,14 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega private final PlatformViewsChannel.PlatformViewsHandler channelHandler = new PlatformViewsChannel.PlatformViewsHandler() { - @TargetApi(Build.VERSION_CODES.KITKAT) + @TargetApi(19) @Override // 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); + ensureValidAndroidVersion(19); if (!validateDirection(request.direction)) { throw new IllegalStateException( @@ -157,7 +174,7 @@ public void createForPlatformViewLayer( platformViews.put(request.viewId, platformView); } - @TargetApi(Build.VERSION_CODES.M) + @TargetApi(20) @Override public long createForTextureLayer( @NonNull PlatformViewsChannel.PlatformViewCreationRequest request) { @@ -194,9 +211,71 @@ public long createForTextureLayer( createParams = viewFactory.getCreateArgsCodec().decodeMessage(request.params); } - final PlatformView platformView = viewFactory.create(context, viewId, createParams); + // The virtual display controller will change the embedded view context. + final Context embeddedViewContext = new MutableContextWrapper(context); + final PlatformView platformView = + viewFactory.create(embeddedViewContext, viewId, createParams); platformViews.put(viewId, platformView); + final View embeddedView = platformView.getView(); + if (embeddedView == null) { + throw new IllegalStateException( + "PlatformView#getView() returned null, but an Android view reference was expected."); + } else if (embeddedView.getParent() != null) { + throw new IllegalStateException( + "The Android view returned from PlatformView#getView() was already added to a parent view."); + } + + embeddedView.setLayoutDirection(request.direction); + + final int physicalWidth = toPhysicalPixels(request.logicalWidth); + final int physicalHeight = toPhysicalPixels(request.logicalHeight); + final boolean shouldUseVirtualDisplay = + ViewUtils.hasChildViewOfType(embeddedView, VIEW_TYPES_REQUIRE_VD); + + if (!usesSoftwareRendering && shouldUseVirtualDisplay) { + // API level 20 is required to use VirtualDisplay#setSurface. + ensureValidAndroidVersion(20); + + final TextureRegistry.SurfaceTextureEntry textureEntry = + textureRegistry.createSurfaceTexture(); + VirtualDisplayController vdController = + VirtualDisplayController.create( + context, + accessibilityEventsDelegate, + platformView, + 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); + contextToEmbeddedView.put(embeddedView.getContext(), embeddedView); + return textureEntry.id(); + } + + // API level 23 is required to use Surface#lockHardwareCanvas(). + ensureValidAndroidVersion(23); + PlatformViewWrapper wrapperView; long txId; if (usesSoftwareRendering) { @@ -209,9 +288,6 @@ public long createForTextureLayer( txId = textureEntry.id(); } wrapperView.setTouchProcessor(androidTouchProcessor); - - final int physicalWidth = toPhysicalPixels(request.logicalWidth); - final int physicalHeight = toPhysicalPixels(request.logicalHeight); wrapperView.setBufferSize(physicalWidth, physicalHeight); final FrameLayout.LayoutParams layoutParams = @@ -265,7 +341,19 @@ public void dispose(int viewId) { platformViews.remove(viewId); platformView.dispose(); } - // The platform view is displayed using a TextureLayer. + final VirtualDisplayController vdController = vdControllers.get(viewId); + // The platform view is displayed using TextureLayer and a virtual display. + if (vdController != null) { + final View embeddedView = platformView.getView(); + if (embeddedView != null) { + contextToEmbeddedView.remove(embeddedView.getContext()); + } + vdController.dispose(); + vdControllers.remove(viewId); + return; + } + // The platform view is displayed using a TextureLayer and is inserted in the view + // hierarchy. final PlatformViewWrapper viewWrapper = viewWrappers.get(viewId); if (viewWrapper != null) { viewWrapper.removeAllViews(); @@ -295,6 +383,18 @@ public void dispose(int viewId) { @Override public void offset(int viewId, double top, double left) { + // Virtual displays don't need an accessibility offset. + if (vdControllers.containsKey(viewId)) { + return; + } + // For platform views that use TextureView and are in the view hierarchy, set + // an offset to the wrapper view. + // This ensures that the accessibility highlights are drawn in the expected position on + // screen. + // This offset doesn't affect the position of the embeded view by itself since the GL + // texture + // is positioned by the Flutter engine, which knows where to position different types of + // layers. final PlatformViewWrapper wrapper = viewWrappers.get(viewId); if (wrapper == null) { Log.e(TAG, "Setting offset for unknown platform view with id: " + viewId); @@ -310,18 +410,38 @@ public void offset(int viewId, double top, double left) { } @Override - public PlatformViewsChannel.PlatformViewBufferSize resize( - @NonNull PlatformViewsChannel.PlatformViewResizeRequest request) { + public void resize( + @NonNull PlatformViewsChannel.PlatformViewResizeRequest request, + @NonNull PlatformViewsChannel.PlatformViewBufferResized onComplete) { + final int physicalWidth = toPhysicalPixels(request.newLogicalWidth); + final int physicalHeight = toPhysicalPixels(request.newLogicalHeight); final int viewId = request.viewId; + final VirtualDisplayController vdController = vdControllers.get(viewId); + if (vdController != null) { + // Resizing involved moving the platform view to a new virtual display. Doing so + // potentially results in losing an active input connection. To make sure we preserve + // the input connection when resizing we lock it here and unlock after the resize is + // complete. + lockInputConnection(vdController); + vdController.resize( + physicalWidth, + physicalHeight, + () -> { + unlockInputConnection(vdController); + onComplete.run( + new PlatformViewsChannel.PlatformViewBufferSize( + toLogicalPixels(vdController.getBufferWidth()), + toLogicalPixels(vdController.getBufferHeight()))); + }); + return; + } + final PlatformView platformView = platformViews.get(viewId); final PlatformViewWrapper view = viewWrappers.get(viewId); if (platformView == null || view == null) { Log.e(TAG, "Resizing unknown platform view with id: " + viewId); - return null; + return; } - 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 @@ -330,8 +450,8 @@ public PlatformViewsChannel.PlatformViewBufferSize resize( // 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); + if (physicalWidth > view.getBufferWidth() || physicalHeight > view.getBufferHeight()) { + view.setBufferSize(physicalWidth, physicalHeight); } final ViewGroup.LayoutParams viewWrapperLayoutParams = view.getLayoutParams(); @@ -346,30 +466,36 @@ public PlatformViewsChannel.PlatformViewBufferSize resize( embeddedViewLayoutParams.height = newHeight; embeddedView.setLayoutParams(embeddedViewLayoutParams); } - return new PlatformViewsChannel.PlatformViewBufferSize( - toLogicalPixels(view.getBufferWidth()), toLogicalPixels(view.getBufferHeight())); + onComplete.run( + new PlatformViewsChannel.PlatformViewBufferSize( + toLogicalPixels(view.getBufferWidth()), toLogicalPixels(view.getBufferHeight()))); } @Override public void onTouch(@NonNull PlatformViewsChannel.PlatformViewTouch touch) { final int viewId = touch.viewId; + final VirtualDisplayController vdController = vdControllers.get(viewId); + final float density = context.getResources().getDisplayMetrics().density; + if (vdController != null) { + final MotionEvent event = toMotionEvent(density, touch, true); + vdController.dispatchTouchEvent(event); + return; + } 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); final View view = platformView.getView(); if (view == null) { Log.e(TAG, "Sending touch to a null view with id: " + viewId); return; } + final MotionEvent event = toMotionEvent(density, touch, false); view.dispatchTouchEvent(event); } - @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + @TargetApi(17) @Override public void setDirection(int viewId, int direction) { if (!validateDirection(direction)) { @@ -385,7 +511,6 @@ public void setDirection(int viewId, int direction) { Log.e(TAG, "Setting direction to an unknown view with id: " + viewId); return; } - ensureValidAndroidVersion(Build.VERSION_CODES.KITKAT_WATCH); final View view = platformView.getView(); if (view == null) { Log.e(TAG, "Setting direction to a null view with id: " + viewId); @@ -426,7 +551,8 @@ public void synchronizeToNativeViewHierarchy(boolean yes) { }; @VisibleForTesting - public MotionEvent toMotionEvent(float density, PlatformViewsChannel.PlatformViewTouch touch) { + public MotionEvent toMotionEvent( + float density, PlatformViewsChannel.PlatformViewTouch touch, boolean usingVirtualDiplay) { MotionEventTracker.MotionEventId motionEventId = MotionEventTracker.MotionEventId.from(touch.motionEventId); MotionEvent trackedEvent = motionEventTracker.pop(motionEventId); @@ -442,7 +568,7 @@ public MotionEvent toMotionEvent(float density, PlatformViewsChannel.PlatformVie parsePointerCoordsList(touch.rawPointerCoords, density) .toArray(new PointerCoords[touch.pointerCount]); - if (trackedEvent != null) { + if (!usingVirtualDiplay && trackedEvent != null) { return MotionEvent.obtain( trackedEvent.getDownTime(), trackedEvent.getEventTime(), @@ -481,7 +607,9 @@ public MotionEvent toMotionEvent(float density, PlatformViewsChannel.PlatformVie public PlatformViewsController() { registry = new PlatformViewRegistryImpl(); + vdControllers = new HashMap<>(); accessibilityEventsDelegate = new AccessibilityEventsDelegate(); + contextToEmbeddedView = new HashMap<>(); overlayLayerViews = new SparseArray<>(); currentFrameUsedOverlayLayerIds = new HashSet<>(); currentFrameUsedPlatformViewIds = new HashSet<>(); @@ -574,6 +702,9 @@ public void attachToView(@NonNull FlutterView newFlutterView) { final PlatformView view = platformViews.valueAt(index); view.onFlutterViewAttached(flutterView); } + for (VirtualDisplayController controller : vdControllers.values()) { + controller.onFlutterViewAttached(flutterView); + } } /** @@ -594,6 +725,12 @@ public void detachFromView() { final FlutterMutatorView view = platformViewParent.valueAt(index); flutterView.removeView(view); } + // Inform all existing platform views that they are no longer associated with + // a Flutter View. + for (VirtualDisplayController controller : vdControllers.values()) { + controller.onFlutterViewDetached(); + } + destroyOverlaySurfaces(); removeOverlaySurfaces(); flutterView = null; @@ -668,6 +805,22 @@ public View getPlatformViewById(int viewId) { return platformView.getView(); } + 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(); + } + private static boolean validateDirection(int direction) { return direction == View.LAYOUT_DIRECTION_LTR || direction == View.LAYOUT_DIRECTION_RTL; } @@ -730,8 +883,16 @@ private int toLogicalPixels(double physicalPixels) { } private void flushAllViews() { + for (VirtualDisplayController controller : vdControllers.values()) { + controller.dispose(); + } + vdControllers.clear(); + contextToEmbeddedView.clear(); + while (platformViews.size() > 0) { - channelHandler.dispose(platformViews.keyAt(0)); + final int viewId = platformViews.keyAt(0); + platformViews.remove(viewId); + channelHandler.dispose(viewId); } } diff --git a/shell/platform/android/io/flutter/plugin/platform/SingleViewPresentation.java b/shell/platform/android/io/flutter/plugin/platform/SingleViewPresentation.java index a561f73868d86..aee46e2a48573 100644 --- a/shell/platform/android/io/flutter/plugin/platform/SingleViewPresentation.java +++ b/shell/platform/android/io/flutter/plugin/platform/SingleViewPresentation.java @@ -12,6 +12,7 @@ import android.app.Presentation; import android.content.Context; import android.content.ContextWrapper; +import android.content.MutableContextWrapper; import android.graphics.Rect; import android.graphics.drawable.ColorDrawable; import android.os.Build; @@ -72,8 +73,6 @@ static class PresentationState { 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; @@ -112,19 +111,19 @@ static class PresentationState { public SingleViewPresentation( Context outerContext, Display display, - PlatformViewFactory viewFactory, + PlatformView view, 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(); + state.platformView = view; getWindow() .setFlags( WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, @@ -148,7 +147,6 @@ public SingleViewPresentation( boolean startFocused) { super(new ImmContext(outerContext), display); this.accessibilityEventsDelegate = accessibilityEventsDelegate; - viewFactory = null; this.state = state; this.focusChangeListener = focusChangeListener; this.outerContext = outerContext; @@ -178,14 +176,18 @@ protected void onCreate(Bundle savedInstanceState) { // 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 = + Context baseContext = new PresentationContext(getContext(), state.windowManagerHandler, outerContext); - if (state.platformView == null) { - state.platformView = viewFactory.create(context, viewId, createParams); + View embeddedView = state.platformView.getView(); + if (embeddedView.getContext() instanceof MutableContextWrapper) { + MutableContextWrapper currentContext = (MutableContextWrapper) embeddedView.getContext(); + currentContext.setBaseContext(baseContext); + } else { + throw new IllegalStateException( + "embedded view context must be a MutableContextWrapper: " + viewId); } - View embeddedView = state.platformView.getView(); container.addView(embeddedView); rootView = new AccessibilityDelegatingFrameLayout( diff --git a/shell/platform/android/io/flutter/plugin/platform/VirtualDisplayController.java b/shell/platform/android/io/flutter/plugin/platform/VirtualDisplayController.java index fec53e89a6d9b..5c85874fc206a 100644 --- a/shell/platform/android/io/flutter/plugin/platform/VirtualDisplayController.java +++ b/shell/platform/android/io/flutter/plugin/platform/VirtualDisplayController.java @@ -25,7 +25,7 @@ class VirtualDisplayController { public static VirtualDisplayController create( Context context, AccessibilityEventsDelegate accessibilityEventsDelegate, - PlatformViewFactory viewFactory, + PlatformView view, TextureRegistry.SurfaceTextureEntry textureEntry, int width, int height, @@ -44,33 +44,40 @@ public static VirtualDisplayController create( if (virtualDisplay == null) { return null; } - - return new VirtualDisplayController( - context, - accessibilityEventsDelegate, - virtualDisplay, - viewFactory, - surface, - textureEntry, - focusChangeListener, - viewId, - createParams); + VirtualDisplayController controller = + new VirtualDisplayController( + context, + accessibilityEventsDelegate, + virtualDisplay, + view, + surface, + textureEntry, + focusChangeListener, + viewId, + createParams); + controller.bufferWidth = width; + controller.bufferHeight = height; + return controller; } + @VisibleForTesting SingleViewPresentation presentation; + 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 VirtualDisplay virtualDisplay; + private int bufferWidth; + private int bufferHeight; + private VirtualDisplayController( Context context, AccessibilityEventsDelegate accessibilityEventsDelegate, VirtualDisplay virtualDisplay, - PlatformViewFactory viewFactory, + PlatformView view, Surface surface, TextureRegistry.SurfaceTextureEntry textureEntry, OnFocusChangeListener focusChangeListener, @@ -87,7 +94,7 @@ private VirtualDisplayController( new SingleViewPresentation( context, this.virtualDisplay.getDisplay(), - viewFactory, + view, accessibilityEventsDelegate, viewId, createParams, @@ -95,6 +102,14 @@ private VirtualDisplayController( presentation.show(); } + public int getBufferWidth() { + return bufferWidth; + } + + public int getBufferHeight() { + return bufferHeight; + } + public void resize(final int width, final int height, final Runnable onNewSizeFrameAvailable) { boolean isFocused = getView().isFocused(); final SingleViewPresentation.PresentationState presentationState = presentation.detachState(); @@ -107,6 +122,8 @@ public void resize(final int width, final int height, final Runnable onNewSizeFr virtualDisplay.setSurface(null); virtualDisplay.release(); + bufferWidth = width; + bufferHeight = height; textureEntry.surfaceTexture().setDefaultBufferSize(width, height); DisplayManager displayManager = (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE); @@ -164,11 +181,9 @@ public void onViewDetachedFromWindow(View v) {} } 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(); } diff --git a/shell/platform/android/io/flutter/util/ViewUtils.java b/shell/platform/android/io/flutter/util/ViewUtils.java index 95ea8fa855437..5baaca2ba549d 100644 --- a/shell/platform/android/io/flutter/util/ViewUtils.java +++ b/shell/platform/android/io/flutter/util/ViewUtils.java @@ -10,6 +10,7 @@ import android.os.Build; import android.view.View; import android.view.ViewGroup; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; public final class ViewUtils { @@ -56,16 +57,58 @@ public static int generateViewId(int fallbackId) { * @return True if the current view or any descendant view has focus. */ public static boolean childHasFocus(@Nullable View root) { + return traverseHierarchy(root, (View view) -> view.hasFocus()); + } + + /** + * Returns true if the root or any child view is an instance of the given types. + * + * @param root The root view. + * @param viewTypes The types of views. + * @return true if any child view is an instance of any of the given types. + */ + public static boolean hasChildViewOfType(@Nullable View root, Class[] viewTypes) { + return traverseHierarchy( + root, + (View view) -> { + for (int i = 0; i < viewTypes.length; i++) { + final Class viewType = viewTypes[i]; + if (viewType.isInstance(view)) { + return true; + } + } + return false; + }); + } + + /** Allows to visit a view. */ + public interface ViewVisitor { + boolean run(@NonNull View view); + } + + /** + * Traverses the view hierarchy in pre-order and runs the visitor for each child view including + * the root view. + * + *

If the visitor returns true, the traversal stops, and the method returns true. + * + *

If the visitor returns false, the traversal continues until all views are visited. + * + * @param root The root view. + * @param visitor The visitor. + * @return true if the visitor returned true for a given view. + */ + public static boolean traverseHierarchy(@Nullable View root, @NonNull ViewVisitor visitor) { if (root == null) { return false; } - if (root.hasFocus()) { + if (visitor.run(root)) { return true; } if (root instanceof ViewGroup) { final ViewGroup viewGroup = (ViewGroup) root; for (int idx = 0; idx < viewGroup.getChildCount(); idx++) { - if (childHasFocus(viewGroup.getChildAt(idx))) { + if (traverseHierarchy(viewGroup.getChildAt(idx), visitor)) { return true; } } From e2e2341cb2809e954c1d394ebb0c452e78080744 Mon Sep 17 00:00:00 2001 From: Emmanuel Garcia Date: Wed, 25 May 2022 16:55:07 -0700 Subject: [PATCH 03/25] Fix licenses --- ci/licenses_golden/licenses_flutter | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index b4162f0f439ea..7b8be44bebe60 100644 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -1482,6 +1482,8 @@ FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/Platfor 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 From a8675eca3e92d626f9bbd25d90b0387735372745 Mon Sep 17 00:00:00 2001 From: Emmanuel Garcia Date: Wed, 25 May 2022 17:08:31 -0700 Subject: [PATCH 04/25] Add logs --- .../io/flutter/plugin/platform/PlatformViewsController.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java b/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java index 8a312417c25a1..9a1a2d3a32ba3 100644 --- a/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java +++ b/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java @@ -172,6 +172,7 @@ public void createForPlatformViewLayer( final PlatformView platformView = factory.create(context, request.viewId, createParams); platformView.getView().setLayoutDirection(request.direction); platformViews.put(request.viewId, platformView); + Log.v(TAG, "Using hybrid composition for platform view: " + viewId); } @TargetApi(20) @@ -234,6 +235,7 @@ public long createForTextureLayer( ViewUtils.hasChildViewOfType(embeddedView, VIEW_TYPES_REQUIRE_VD); if (!usesSoftwareRendering && shouldUseVirtualDisplay) { + Log.v(TAG, "Hosting view in a virtual display for platform view: " + viewId); // API level 20 is required to use VirtualDisplay#setSurface. ensureValidAndroidVersion(20); @@ -275,6 +277,7 @@ public long createForTextureLayer( // API level 23 is required to use Surface#lockHardwareCanvas(). ensureValidAndroidVersion(23); + Log.v(TAG, "Hosting view in view hierarchy for platform view: " + viewId); PlatformViewWrapper wrapperView; long txId; From 9d30502c9ece35e2fe260b2ee741909435990ba8 Mon Sep 17 00:00:00 2001 From: Emmanuel Garcia Date: Wed, 25 May 2022 17:27:45 -0700 Subject: [PATCH 05/25] comment --- .../flutter/plugin/platform/PlatformViewsController.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java b/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java index 9a1a2d3a32ba3..11d2ed5cc5118 100644 --- a/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java +++ b/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java @@ -50,6 +50,12 @@ */ public class PlatformViewsController implements PlatformViewsAccessibilityDelegate { private static final String TAG = "PlatformViewsController"; + + // These view types allow to issue drawing commands directly without + // notifying the Android view hierarchy that a change was made. + // To support these cases, Flutter hosts the embedded view in a VirtualDisplay, + // and binds the VirtualDisplay to a GL texture that is then composed by the engine. + // Related issue: https://github.com/flutter/flutter/issues/103630 private static Class[] VIEW_TYPES_REQUIRE_VD = {SurfaceView.class, TextureView.class}; private final PlatformViewRegistryImpl registry; @@ -233,7 +239,6 @@ public long createForTextureLayer( final int physicalHeight = toPhysicalPixels(request.logicalHeight); final boolean shouldUseVirtualDisplay = ViewUtils.hasChildViewOfType(embeddedView, VIEW_TYPES_REQUIRE_VD); - if (!usesSoftwareRendering && shouldUseVirtualDisplay) { Log.v(TAG, "Hosting view in a virtual display for platform view: " + viewId); // API level 20 is required to use VirtualDisplay#setSurface. From 6981201d741ed6643c0123866d96123fe99f11ed Mon Sep 17 00:00:00 2001 From: Emmanuel Garcia Date: Wed, 25 May 2022 17:33:16 -0700 Subject: [PATCH 06/25] fix --- .../flutter/plugin/platform/PlatformViewsController.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java b/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java index 11d2ed5cc5118..61c5ca0b57fd1 100644 --- a/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java +++ b/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java @@ -155,12 +155,13 @@ public void createForPlatformViewLayer( // API level 19 is required for `android.graphics.ImageReader`. ensureValidAndroidVersion(19); + final int viewId = request.viewId; if (!validateDirection(request.direction)) { throw new IllegalStateException( "Trying to create a view with unknown direction value: " + request.direction + "(view id: " - + request.viewId + + viewId + ")"); } @@ -175,9 +176,9 @@ public void createForPlatformViewLayer( createParams = factory.getCreateArgsCodec().decodeMessage(request.params); } - final PlatformView platformView = factory.create(context, request.viewId, createParams); + final PlatformView platformView = factory.create(context, viewId, createParams); platformView.getView().setLayoutDirection(request.direction); - platformViews.put(request.viewId, platformView); + platformViews.put(viewId, platformView); Log.v(TAG, "Using hybrid composition for platform view: " + viewId); } From b8e49a0402d48a48af48d502a6e6046635a6549e Mon Sep 17 00:00:00 2001 From: Emmanuel Garcia Date: Wed, 25 May 2022 18:44:07 -0700 Subject: [PATCH 07/25] use info --- .../io/flutter/plugin/platform/PlatformViewsController.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java b/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java index 61c5ca0b57fd1..9cb21374d5af3 100644 --- a/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java +++ b/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java @@ -179,7 +179,7 @@ public void createForPlatformViewLayer( final PlatformView platformView = factory.create(context, viewId, createParams); platformView.getView().setLayoutDirection(request.direction); platformViews.put(viewId, platformView); - Log.v(TAG, "Using hybrid composition for platform view: " + viewId); + Log.i(TAG, "Using hybrid composition for platform view: " + viewId); } @TargetApi(20) @@ -241,7 +241,7 @@ public long createForTextureLayer( final boolean shouldUseVirtualDisplay = ViewUtils.hasChildViewOfType(embeddedView, VIEW_TYPES_REQUIRE_VD); if (!usesSoftwareRendering && shouldUseVirtualDisplay) { - Log.v(TAG, "Hosting view in a virtual display for platform view: " + viewId); + Log.i(TAG, "Hosting view in a virtual display for platform view: " + viewId); // API level 20 is required to use VirtualDisplay#setSurface. ensureValidAndroidVersion(20); @@ -283,7 +283,7 @@ public long createForTextureLayer( // API level 23 is required to use Surface#lockHardwareCanvas(). ensureValidAndroidVersion(23); - Log.v(TAG, "Hosting view in view hierarchy for platform view: " + viewId); + Log.i(TAG, "Hosting view in view hierarchy for platform view: " + viewId); PlatformViewWrapper wrapperView; long txId; From 583b5970cbd2a0f5596c4f9763545534546fc47e Mon Sep 17 00:00:00 2001 From: Emmanuel Garcia Date: Thu, 26 May 2022 12:04:21 -0700 Subject: [PATCH 08/25] remove nullable context --- .../android/io/flutter/plugin/platform/PlatformViewFactory.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shell/platform/android/io/flutter/plugin/platform/PlatformViewFactory.java b/shell/platform/android/io/flutter/plugin/platform/PlatformViewFactory.java index 936f9fabeca5b..e21ee3897f555 100644 --- a/shell/platform/android/io/flutter/plugin/platform/PlatformViewFactory.java +++ b/shell/platform/android/io/flutter/plugin/platform/PlatformViewFactory.java @@ -28,7 +28,7 @@ public PlatformViewFactory(@Nullable MessageCodec createArgsCodec) { * null, or no arguments were sent from the Flutter app. */ @NonNull - public abstract PlatformView create(@Nullable Context context, int viewId, @Nullable Object args); + public abstract PlatformView create(Context context, int viewId, @Nullable Object args); /** Returns the codec to be used for decoding the args parameter of {@link #create}. */ @Nullable From dde18d7554b7634baadfdab4041219be7743a516 Mon Sep 17 00:00:00 2001 From: Emmanuel Garcia Date: Wed, 15 Jun 2022 16:16:18 -0700 Subject: [PATCH 09/25] document code --- .../platform/PlatformViewsController.java | 104 ++++++++++-------- 1 file changed, 57 insertions(+), 47 deletions(-) diff --git a/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java b/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java index 9cb21374d5af3..f888689600064 100644 --- a/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java +++ b/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java @@ -51,8 +51,8 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelegate { private static final String TAG = "PlatformViewsController"; - // These view types allow to issue drawing commands directly without - // notifying the Android view hierarchy that a change was made. + // These view types allow out-of-band drawing operation that don't notify the Android view + // hierarchy. // To support these cases, Flutter hosts the embedded view in a VirtualDisplay, // and binds the VirtualDisplay to a GL texture that is then composed by the engine. // Related issue: https://github.com/flutter/flutter/issues/103630 @@ -238,8 +238,15 @@ public long createForTextureLayer( final int physicalWidth = toPhysicalPixels(request.logicalWidth); final int physicalHeight = toPhysicalPixels(request.logicalHeight); + + // Case 1. Add the view to a virtual display if the embedded view contains any of the + // VIEW_TYPES_REQUIRE_VD view types. + // These views allow out-of-band graphics operations that aren't notified to the Android + // view hierarchy via callbacks such as ViewParent#onDescendantInvalidated(). + // The virtual display is wired up to a GL texture that is composed by the Flutter engine. final boolean shouldUseVirtualDisplay = ViewUtils.hasChildViewOfType(embeddedView, VIEW_TYPES_REQUIRE_VD); + if (!usesSoftwareRendering && shouldUseVirtualDisplay) { Log.i(TAG, "Hosting view in a virtual display for platform view: " + viewId); // API level 20 is required to use VirtualDisplay#setSurface. @@ -281,43 +288,42 @@ public long createForTextureLayer( return textureEntry.id(); } + // Case 2. Attach the view to the Android view hierarchy and record their drawing + // operations, so they can be forwarded to a GL texture that is composed by the + // Flutter engine. + // API level 23 is required to use Surface#lockHardwareCanvas(). ensureValidAndroidVersion(23); Log.i(TAG, "Hosting view in view hierarchy for platform view: " + viewId); - PlatformViewWrapper wrapperView; + PlatformViewWrapper viewWrapper; long txId; if (usesSoftwareRendering) { - wrapperView = new PlatformViewWrapper(context); + viewWrapper = new PlatformViewWrapper(context); txId = -1; } else { final TextureRegistry.SurfaceTextureEntry textureEntry = textureRegistry.createSurfaceTexture(); - wrapperView = new PlatformViewWrapper(context, textureEntry); + viewWrapper = new PlatformViewWrapper(context, textureEntry); txId = textureEntry.id(); } - wrapperView.setTouchProcessor(androidTouchProcessor); - wrapperView.setBufferSize(physicalWidth, physicalHeight); + viewWrapper.setTouchProcessor(androidTouchProcessor); + viewWrapper.setBufferSize(physicalWidth, physicalHeight); - final FrameLayout.LayoutParams layoutParams = + final FrameLayout.LayoutParams viewWrapperLayoutParams = new FrameLayout.LayoutParams(physicalWidth, physicalHeight); + // Size and position the view wrapper. final int physicalTop = toPhysicalPixels(request.logicalTop); final int physicalLeft = toPhysicalPixels(request.logicalLeft); - layoutParams.topMargin = physicalTop; - layoutParams.leftMargin = physicalLeft; - wrapperView.setLayoutParams(layoutParams); + viewWrapperLayoutParams.topMargin = physicalTop; + viewWrapperLayoutParams.leftMargin = physicalLeft; + viewWrapper.setLayoutParams(viewWrapperLayoutParams); - final View embeddedView = platformView.getView(); - if (embeddedView == null) { - throw new IllegalStateException( - "PlatformView#getView() returned null, but an Android view reference was expected."); - } else if (embeddedView.getParent() != null) { - throw new IllegalStateException( - "The Android view returned from PlatformView#getView() was already added to a parent view."); - } + // Size the embedded view. + // This isn't needed when the virtual display is used because the virtual display itself + // is sized. embeddedView.setLayoutParams(new FrameLayout.LayoutParams(physicalWidth, physicalHeight)); - embeddedView.setLayoutDirection(request.direction); // Accessibility in the embedded view is initially disabled because if a Flutter app // disabled accessibility in the first frame, the embedding won't receive an update to @@ -329,8 +335,12 @@ public long createForTextureLayer( embeddedView.setImportantForAccessibility( View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS); - wrapperView.addView(embeddedView); - wrapperView.setOnDescendantFocusChangeListener( + // Add the embedded view to the wrapper. + viewWrapper.addView(embeddedView); + + // Listen for focus changed in any subview, so the framework is notified when the platform + // view is focused. + viewWrapper.setOnDescendantFocusChangeListener( (v, hasFocus) -> { if (hasFocus) { platformViewsChannel.invokeViewFocused(viewId); @@ -338,8 +348,8 @@ public long createForTextureLayer( textInputPlugin.clearPlatformViewClient(viewId); } }); - flutterView.addView(wrapperView); - viewWrappers.append(viewId, wrapperView); + flutterView.addView(viewWrapper); + viewWrappers.append(viewId, viewWrapper); return txId; } @@ -401,21 +411,20 @@ public void offset(int viewId, double top, double left) { // This ensures that the accessibility highlights are drawn in the expected position on // screen. // This offset doesn't affect the position of the embeded view by itself since the GL - // texture - // is positioned by the Flutter engine, which knows where to position different types of - // layers. - final PlatformViewWrapper wrapper = viewWrappers.get(viewId); - if (wrapper == null) { + // texture is positioned by the Flutter engine, which knows where to position different + // types of layers. + final PlatformViewWrapper viewWrapper = viewWrappers.get(viewId); + if (viewWrapper == 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(); + (FrameLayout.LayoutParams) viewWrapper.getLayoutParams(); layoutParams.topMargin = physicalTop; layoutParams.leftMargin = physicalLeft; - wrapper.setLayoutParams(layoutParams); + viewWrapper.setLayoutParams(layoutParams); } @Override @@ -446,8 +455,8 @@ public void resize( } final PlatformView platformView = platformViews.get(viewId); - final PlatformViewWrapper view = viewWrappers.get(viewId); - if (platformView == null || view == null) { + final PlatformViewWrapper viewWrapper = viewWrappers.get(viewId); + if (platformView == null || viewWrapper == null) { Log.e(TAG, "Resizing unknown platform view with id: " + viewId); return; } @@ -459,20 +468,21 @@ public void resize( // 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 (physicalWidth > view.getBufferWidth() || physicalHeight > view.getBufferHeight()) { - view.setBufferSize(physicalWidth, physicalHeight); + if (physicalWidth > viewWrapper.getBufferWidth() + || physicalHeight > viewWrapper.getBufferHeight()) { + viewWrapper.setBufferSize(physicalWidth, physicalHeight); } - final ViewGroup.LayoutParams viewWrapperLayoutParams = view.getLayoutParams(); - viewWrapperLayoutParams.width = newWidth; - viewWrapperLayoutParams.height = newHeight; - view.setLayoutParams(viewWrapperLayoutParams); + final ViewGroup.LayoutParams viewWrapperLayoutParams = viewWrapper.getLayoutParams(); + viewWrapperLayoutParams.width = physicalWidth; + viewWrapperLayoutParams.height = physicalHeight; + viewWrapper.setLayoutParams(viewWrapperLayoutParams); final View embeddedView = platformView.getView(); if (embeddedView != null) { final ViewGroup.LayoutParams embeddedViewLayoutParams = embeddedView.getLayoutParams(); - embeddedViewLayoutParams.width = newWidth; - embeddedViewLayoutParams.height = newHeight; + embeddedViewLayoutParams.width = physicalWidth; + embeddedViewLayoutParams.height = physicalHeight; embeddedView.setLayoutParams(embeddedViewLayoutParams); } onComplete.run( @@ -520,12 +530,12 @@ public void setDirection(int viewId, int direction) { Log.e(TAG, "Setting direction to an unknown view with id: " + viewId); return; } - final View view = platformView.getView(); - if (view == null) { + final View embeddedView = platformView.getView(); + if (embeddedView == null) { Log.e(TAG, "Setting direction to a null view with id: " + viewId); return; } - view.setLayoutDirection(direction); + embeddedView.setLayoutDirection(direction); } @Override @@ -535,12 +545,12 @@ public void clearFocus(int viewId) { Log.e(TAG, "Clearing focus on an unknown view with id: " + viewId); return; } - final View view = platformView.getView(); - if (view == null) { + final View embeddedView = platformView.getView(); + if (embeddedView == null) { Log.e(TAG, "Clearing focus on a null view with id: " + viewId); return; } - view.clearFocus(); + embeddedView.clearFocus(); } private void ensureValidAndroidVersion(int minSdkVersion) { From 7f87be7e60ba070d46bf286fe03b504639390cb6 Mon Sep 17 00:00:00 2001 From: Emmanuel Garcia Date: Wed, 15 Jun 2022 17:15:16 -0700 Subject: [PATCH 10/25] fixes --- .../io/flutter/plugin/platform/PlatformViewsController.java | 3 ++- .../io/flutter/plugin/platform/VirtualDisplayController.java | 4 +--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java b/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java index f888689600064..4834310ad620a 100644 --- a/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java +++ b/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java @@ -487,7 +487,8 @@ public void resize( } onComplete.run( new PlatformViewsChannel.PlatformViewBufferSize( - toLogicalPixels(view.getBufferWidth()), toLogicalPixels(view.getBufferHeight()))); + toLogicalPixels(viewWrapper.getBufferWidth()), + toLogicalPixels(viewWrapper.getBufferHeight()))); } @Override diff --git a/shell/platform/android/io/flutter/plugin/platform/VirtualDisplayController.java b/shell/platform/android/io/flutter/plugin/platform/VirtualDisplayController.java index 5c85874fc206a..0b1c3e9dae5b6 100644 --- a/shell/platform/android/io/flutter/plugin/platform/VirtualDisplayController.java +++ b/shell/platform/android/io/flutter/plugin/platform/VirtualDisplayController.java @@ -10,7 +10,6 @@ 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; @@ -19,7 +18,7 @@ import androidx.annotation.VisibleForTesting; import io.flutter.view.TextureRegistry; -@TargetApi(Build.VERSION_CODES.KITKAT_WATCH) +@TargetApi(20) class VirtualDisplayController { public static VirtualDisplayController create( @@ -230,7 +229,6 @@ public void dispatchTouchEvent(MotionEvent event) { 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); From 5e924abce53bd9ef38472b308bb598c2fc2b0923 Mon Sep 17 00:00:00 2001 From: Emmanuel Garcia Date: Wed, 15 Jun 2022 17:55:00 -0700 Subject: [PATCH 11/25] fix test --- .../flutter/plugin/platform/PlatformViewsControllerTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 d80e59186998b..3198c8512bfef 100644 --- a/shell/platform/android/test/io/flutter/plugin/platform/PlatformViewsControllerTest.java +++ b/shell/platform/android/test/io/flutter/plugin/platform/PlatformViewsControllerTest.java @@ -96,7 +96,8 @@ public void itUsesActionEventTypeFromMotionEventForHybridPlatformViews() { motionEventId.getId()); MotionEvent resolvedEvent = - platformViewsController.toMotionEvent(/*density=*/ 1, frameWorkTouch); + platformViewsController.toMotionEvent( + /*density=*/ 1, frameWorkTouch, /*usingVirtualDisplay=*/ false); assertNotEquals(resolvedEvent.getAction(), frameWorkTouch.action); assertEquals(resolvedEvent.getAction(), original.getAction()); From 7cb291964a66d66e5628b9477cd8d2ecdecc705c Mon Sep 17 00:00:00 2001 From: Emmanuel Garcia Date: Fri, 17 Jun 2022 14:25:44 -0700 Subject: [PATCH 12/25] fallback to VD if API level < 23 --- .../flutter/plugin/platform/PlatformViewsController.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java b/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java index 4834310ad620a..bab575b317e17 100644 --- a/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java +++ b/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java @@ -244,10 +244,13 @@ public long createForTextureLayer( // These views allow out-of-band graphics operations that aren't notified to the Android // view hierarchy via callbacks such as ViewParent#onDescendantInvalidated(). // The virtual display is wired up to a GL texture that is composed by the Flutter engine. - final boolean shouldUseVirtualDisplay = - ViewUtils.hasChildViewOfType(embeddedView, VIEW_TYPES_REQUIRE_VD); + // Also, use virtual display if the API level is 20, 21 or 22 since the Case 2. requires + // at least API level 23. + final boolean shouldUseVD = + ViewUtils.hasChildViewOfType(embeddedView, VIEW_TYPES_REQUIRE_VD) + || Build.VERSION.SDK_INT < 23; - if (!usesSoftwareRendering && shouldUseVirtualDisplay) { + if (!usesSoftwareRendering && shouldUseVD) { Log.i(TAG, "Hosting view in a virtual display for platform view: " + viewId); // API level 20 is required to use VirtualDisplay#setSurface. ensureValidAndroidVersion(20); From 42adbc45774233b1607d99a3f8157d7667842ab3 Mon Sep 17 00:00:00 2001 From: Emmanuel Garcia Date: Fri, 17 Jun 2022 17:18:05 -0700 Subject: [PATCH 13/25] revert last pieces --- .../embedding/android/FlutterView.java | 15 ++ .../systemchannels/TextInputChannel.java | 8 +- .../plugin/editing/TextInputPlugin.java | 81 ++++++-- .../PlatformViewsAccessibilityDelegate.java | 3 + .../platform/PlatformViewsController.java | 134 ++++++++++--- .../platform/SingleViewPresentation.java | 4 +- .../io/flutter/view/AccessibilityBridge.java | 31 ++- .../android/io/flutter/view/FlutterView.java | 9 + .../platform/PlatformViewsControllerTest.java | 184 ++++++++++++++++++ .../platform/SingleViewPresentationTest.java | 82 ++++++++ .../flutter/view/AccessibilityBridgeTest.java | 31 +++ tools/android_lint/baseline.xml | 11 ++ tools/android_lint/project.xml | 2 + 13 files changed, 550 insertions(+), 45 deletions(-) create mode 100644 shell/platform/android/test/io/flutter/plugin/platform/SingleViewPresentationTest.java diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterView.java b/shell/platform/android/io/flutter/embedding/android/FlutterView.java index d5fcac7d50e61..da32f3205b23c 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterView.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterView.java @@ -873,6 +873,21 @@ 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/systemchannels/TextInputChannel.java b/shell/platform/android/io/flutter/embedding/engine/systemchannels/TextInputChannel.java index a48e073dc5f60..094786f6d825e 100644 --- a/shell/platform/android/io/flutter/embedding/engine/systemchannels/TextInputChannel.java +++ b/shell/platform/android/io/flutter/embedding/engine/systemchannels/TextInputChannel.java @@ -89,7 +89,9 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result try { final JSONObject arguments = (JSONObject) args; final int platformViewId = arguments.getInt("platformViewId"); - textInputMethodHandler.setPlatformViewClient(platformViewId); + final boolean usesVirtualDisplay = + arguments.optBoolean("usesVirtualDisplay", false); + textInputMethodHandler.setPlatformViewClient(platformViewId, usesVirtualDisplay); result.success(null); } catch (JSONException exception) { result.error("error", exception.getMessage(), null); @@ -401,8 +403,10 @@ 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); + void setPlatformViewClient(int id, boolean usesVirtualDisplay); /** * 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 6310c995f0c22..5c82b2ffc1735 100644 --- a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java +++ b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java @@ -105,7 +105,7 @@ public void show() { @Override public void hide() { - if (inputTarget.type == InputTarget.Type.PLATFORM_VIEW) { + if (inputTarget.type == InputTarget.Type.HC_PLATFORM_VIEW) { notifyViewExited(); } else { hideTextInput(mView); @@ -136,8 +136,8 @@ public void setClient( } @Override - public void setPlatformViewClient(int platformViewId) { - setPlatformViewTextInputClient(platformViewId); + public void setPlatformViewClient(int platformViewId, boolean usesVirtualDisplay) { + setPlatformViewTextInputClient(platformViewId, usesVirtualDisplay); } @Override @@ -196,7 +196,7 @@ ImeSyncDeferringInsetsCallback getImeSyncCallback() { * display to another. */ public void lockPlatformViewInputConnection() { - if (inputTarget.type == InputTarget.Type.PLATFORM_VIEW) { + if (inputTarget.type == InputTarget.Type.VD_PLATFORM_VIEW) { isInputConnectionLocked = true; } } @@ -207,7 +207,9 @@ public void lockPlatformViewInputConnection() { *

See also: @{link lockPlatformViewInputConnection}. */ public void unlockPlatformViewInputConnection() { - isInputConnectionLocked = false; + if (inputTarget.type == InputTarget.Type.VD_PLATFORM_VIEW) { + isInputConnectionLocked = false; + } } /** @@ -293,10 +295,21 @@ public InputConnection createInputConnection( return null; } - if (inputTarget.type == InputTarget.Type.PLATFORM_VIEW) { + if (inputTarget.type == InputTarget.Type.HC_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, @@ -351,7 +364,9 @@ public InputConnection getLastInputConnection() { * input connection. */ public void clearPlatformViewClient(int platformViewId) { - if (inputTarget.type == InputTarget.Type.PLATFORM_VIEW && inputTarget.id == platformViewId) { + if ((inputTarget.type == InputTarget.Type.VD_PLATFORM_VIEW + || inputTarget.type == InputTarget.Type.HC_PLATFORM_VIEW) + && inputTarget.id == platformViewId) { inputTarget = new InputTarget(InputTarget.Type.NO_TARGET, 0); notifyViewExited(); mImm.hideSoftInputFromWindow(mView.getApplicationWindowToken(), 0); @@ -412,13 +427,25 @@ 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) { - inputTarget = new InputTarget(InputTarget.Type.PLATFORM_VIEW, platformViewId); - lastInputConnection = null; + 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 static boolean composingChanged( @@ -509,10 +536,35 @@ 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; } @@ -522,9 +574,12 @@ enum Type { // InputConnection is managed by the TextInputPlugin, and events are forwarded to the Flutter // framework. FRAMEWORK_CLIENT, - // InputConnection is managed by a platform view that is embeded in the Android view - // hierarchy. - PLATFORM_VIEW, + // 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, } public InputTarget(@NonNull Type type, int id) { diff --git a/shell/platform/android/io/flutter/plugin/platform/PlatformViewsAccessibilityDelegate.java b/shell/platform/android/io/flutter/plugin/platform/PlatformViewsAccessibilityDelegate.java index 968dda33f1ee8..f1f91ddaa3055 100644 --- a/shell/platform/android/io/flutter/plugin/platform/PlatformViewsAccessibilityDelegate.java +++ b/shell/platform/android/io/flutter/plugin/platform/PlatformViewsAccessibilityDelegate.java @@ -18,6 +18,9 @@ public interface PlatformViewsAccessibilityDelegate { @Nullable View getPlatformViewById(int viewId); + /** Returns true if the platform view uses virtual displays. */ + boolean usesVirtualDisplay(int id); + /** * 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 bab575b317e17..e1e66ff25712e 100644 --- a/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java +++ b/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java @@ -11,6 +11,7 @@ import android.content.Context; import android.content.MutableContextWrapper; import android.os.Build; +import android.util.DisplayMetrics; import android.util.SparseArray; import android.view.MotionEvent; import android.view.SurfaceView; @@ -251,13 +252,16 @@ public long createForTextureLayer( || Build.VERSION.SDK_INT < 23; if (!usesSoftwareRendering && shouldUseVD) { + validateVirtualDisplayDimensions(physicalWidth, physicalHeight); + Log.i(TAG, "Hosting view in a virtual display for platform view: " + viewId); // API level 20 is required to use VirtualDisplay#setSurface. ensureValidAndroidVersion(20); final TextureRegistry.SurfaceTextureEntry textureEntry = textureRegistry.createSurfaceTexture(); - VirtualDisplayController vdController = + + final VirtualDisplayController vdController = VirtualDisplayController.create( context, accessibilityEventsDelegate, @@ -272,6 +276,7 @@ public long createForTextureLayer( platformViewsChannel.invokeViewFocused(request.viewId); } }); + if (vdController == null) { throw new IllegalStateException( "Failed creating virtual display for a " @@ -358,22 +363,25 @@ public long createForTextureLayer( @Override public void dispose(int viewId) { - final PlatformView platformView = platformViews.get(viewId); - if (platformView != null) { - platformViews.remove(viewId); - platformView.dispose(); - } - final VirtualDisplayController vdController = vdControllers.get(viewId); - // The platform view is displayed using TextureLayer and a virtual display. - if (vdController != null) { - final View embeddedView = platformView.getView(); + if (usesVirtualDisplay(viewId)) { + final VirtualDisplayController vdController = vdControllers.get(viewId); + final View embeddedView = vdController.getView(); if (embeddedView != null) { contextToEmbeddedView.remove(embeddedView.getContext()); } + contextToEmbeddedView.remove(vdController.getView().getContext()); vdController.dispose(); vdControllers.remove(viewId); return; } + + final PlatformView platformView = platformViews.get(viewId); + if (platformView == null) { + Log.e(TAG, "Disposing unknown platform view with id: " + viewId); + return; + } + platformViews.remove(viewId); + platformView.dispose(); // The platform view is displayed using a TextureLayer and is inserted in the view // hierarchy. final PlatformViewWrapper viewWrapper = viewWrappers.get(viewId); @@ -405,8 +413,8 @@ public void dispose(int viewId) { @Override public void offset(int viewId, double top, double left) { - // Virtual displays don't need an accessibility offset. - if (vdControllers.containsKey(viewId)) { + if (usesVirtualDisplay(viewId)) { + // Virtual displays don't need an accessibility offset. return; } // For platform views that use TextureView and are in the view hierarchy, set @@ -437,8 +445,9 @@ public void resize( final int physicalWidth = toPhysicalPixels(request.newLogicalWidth); final int physicalHeight = toPhysicalPixels(request.newLogicalHeight); final int viewId = request.viewId; - final VirtualDisplayController vdController = vdControllers.get(viewId); - if (vdController != null) { + + if (usesVirtualDisplay(viewId)) { + final VirtualDisplayController vdController = vdControllers.get(viewId); // 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 @@ -497,13 +506,15 @@ public void resize( @Override public void onTouch(@NonNull PlatformViewsChannel.PlatformViewTouch touch) { final int viewId = touch.viewId; - final VirtualDisplayController vdController = vdControllers.get(viewId); final float density = context.getResources().getDisplayMetrics().density; - if (vdController != null) { + + if (usesVirtualDisplay(viewId)) { + final VirtualDisplayController vdController = vdControllers.get(viewId); final MotionEvent event = toMotionEvent(density, touch, true); vdController.dispatchTouchEvent(event); return; } + final PlatformView platformView = platformViews.get(viewId); if (platformView == null) { Log.e(TAG, "Sending touch to an unknown view with id: " + viewId); @@ -529,12 +540,20 @@ public void setDirection(int viewId, int direction) { + viewId + ")"); } - final PlatformView platformView = platformViews.get(viewId); - if (platformView == null) { - Log.e(TAG, "Setting direction to an unknown view with id: " + viewId); - return; + + View embeddedView; + + if (usesVirtualDisplay(viewId)) { + final VirtualDisplayController controller = vdControllers.get(viewId); + embeddedView = controller.getView(); + } else { + final PlatformView platformView = platformViews.get(viewId); + if (platformView == null) { + Log.e(TAG, "Setting direction to an unknown view with id: " + viewId); + return; + } + embeddedView = platformView.getView(); } - final View embeddedView = platformView.getView(); if (embeddedView == null) { Log.e(TAG, "Setting direction to a null view with id: " + viewId); return; @@ -544,12 +563,19 @@ public void setDirection(int viewId, int direction) { @Override public void clearFocus(int viewId) { - final PlatformView platformView = platformViews.get(viewId); - if (platformView == null) { - Log.e(TAG, "Clearing focus on an unknown view with id: " + viewId); - return; + View embeddedView; + + if (usesVirtualDisplay(viewId)) { + final VirtualDisplayController controller = vdControllers.get(viewId); + embeddedView = controller.getView(); + } else { + final PlatformView platformView = platformViews.get(viewId); + if (platformView == null) { + Log.e(TAG, "Clearing focus on an unknown view with id: " + viewId); + return; + } + embeddedView = platformView.getView(); } - final View embeddedView = platformView.getView(); if (embeddedView == null) { Log.e(TAG, "Clearing focus on a null view with id: " + viewId); return; @@ -794,6 +820,29 @@ 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 (!contextToEmbeddedView.containsKey(view.getContext())) { + return false; + } + View platformView = contextToEmbeddedView.get(view.getContext()); + if (platformView == view) { + return true; + } + return platformView.checkInputConnectionProxy(view); + } + public PlatformViewRegistry getRegistry() { return registry; } @@ -821,6 +870,11 @@ public void onPreEngineRestart() { @Override @Nullable public View getPlatformViewById(int viewId) { + if (usesVirtualDisplay(viewId)) { + final VirtualDisplayController controller = vdControllers.get(viewId); + return controller.getView(); + } + final PlatformView platformView = platformViews.get(viewId); if (platformView == null) { return null; @@ -828,6 +882,11 @@ public View getPlatformViewById(int viewId) { return platformView.getView(); } + @Override + public boolean usesVirtualDisplay(int id) { + return vdControllers.containsKey(id); + } + private void lockInputConnection(@NonNull VirtualDisplayController controller) { if (textInputPlugin == null) { return; @@ -893,6 +952,29 @@ 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; } diff --git a/shell/platform/android/io/flutter/plugin/platform/SingleViewPresentation.java b/shell/platform/android/io/flutter/plugin/platform/SingleViewPresentation.java index aee46e2a48573..188875d89d5af 100644 --- a/shell/platform/android/io/flutter/plugin/platform/SingleViewPresentation.java +++ b/shell/platform/android/io/flutter/plugin/platform/SingleViewPresentation.java @@ -185,7 +185,9 @@ protected void onCreate(Bundle savedInstanceState) { currentContext.setBaseContext(baseContext); } else { throw new IllegalStateException( - "embedded view context must be a MutableContextWrapper: " + viewId); + "Unexpected platform view context. " + + "When constructing a platform view in the factory, use the context from PlatformViewFactory#create, view id: " + + viewId); } container.addView(embeddedView); diff --git a/shell/platform/android/io/flutter/view/AccessibilityBridge.java b/shell/platform/android/io/flutter/view/AccessibilityBridge.java index 48f1ab700cc8a..aa6e38767947a 100644 --- a/shell/platform/android/io/flutter/view/AccessibilityBridge.java +++ b/shell/platform/android/io/flutter/view/AccessibilityBridge.java @@ -573,6 +573,23 @@ 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 @@ -887,8 +904,15 @@ && shouldSetCollectionInfo(semanticsNode)) { // Add the embedded view as a child of the current accessibility node if it's using // hybrid composition. - result.addChild(embeddedView); - } else { + // + // 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(rootAccessibilityView, child.id); } } @@ -1522,7 +1546,8 @@ void updateSemantics( if (semanticsNode.hadPreviousConfig) { updated.add(semanticsNode); } - if (semanticsNode.platformViewId != -1) { + if (semanticsNode.platformViewId != -1 + && !platformViewsAccessibilityDelegate.usesVirtualDisplay(semanticsNode.platformViewId)) { View embeddedView = platformViewsAccessibilityDelegate.getPlatformViewById(semanticsNode.platformViewId); if (embeddedView != null) { diff --git a/shell/platform/android/io/flutter/view/FlutterView.java b/shell/platform/android/io/flutter/view/FlutterView.java index 15245de3ef7db..eb498a2335e96 100644 --- a/shell/platform/android/io/flutter/view/FlutterView.java +++ b/shell/platform/android/io/flutter/view/FlutterView.java @@ -26,6 +26,7 @@ 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; @@ -421,6 +422,14 @@ 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/plugin/platform/PlatformViewsControllerTest.java b/shell/platform/android/test/io/flutter/plugin/platform/PlatformViewsControllerTest.java index 3198c8512bfef..d3fb9a8659efe 100644 --- a/shell/platform/android/test/io/flutter/plugin/platform/PlatformViewsControllerTest.java +++ b/shell/platform/android/test/io/flutter/plugin/platform/PlatformViewsControllerTest.java @@ -8,6 +8,7 @@ import static org.robolectric.Shadows.shadowOf; import android.content.Context; +import android.content.MutableContextWrapper; import android.content.res.AssetManager; import android.graphics.SurfaceTexture; import android.util.SparseArray; @@ -18,6 +19,7 @@ import android.view.View; import android.view.ViewParent; import android.widget.FrameLayout; +import android.widget.FrameLayout.LayoutParams; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import io.flutter.embedding.android.FlutterImageView; @@ -57,6 +59,119 @@ @RunWith(AndroidJUnit4.class) public class PlatformViewsControllerTest { + @Ignore + @Test + public void itNotifiesVirtualDisplayControllersOfViewAttachmentAndDetachment() { + // Setup test structure. + FlutterView fakeFlutterView = new FlutterView(ApplicationProvider.getApplicationContext()); + + // 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(ApplicationProvider.getApplicationContext()); + + // 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() { @@ -296,6 +411,68 @@ public void createHybridPlatformViewMessage__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); + + SurfaceView pv = mock(SurfaceView.class); + when(pv.getContext()).thenReturn(mock(MutableContextWrapper.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.contextToEmbeddedView.isEmpty()); + platformViewsController.onDetachedFromJNI(); + assertTrue(platformViewsController.contextToEmbeddedView.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); + + SurfaceView pv = mock(SurfaceView.class); + when(pv.getContext()).thenReturn(mock(MutableContextWrapper.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.contextToEmbeddedView.isEmpty()); + platformViewsController.onDetachedFromJNI(); + assertTrue(platformViewsController.contextToEmbeddedView.isEmpty()); + } + @Test @Config(shadows = {ShadowFlutterJNI.class, ShadowPlatformTaskQueue.class}) public void createPlatformViewMessage__throwsIfViewHasParent() { @@ -811,6 +988,13 @@ 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 new file mode 100644 index 0000000000000..2cc177fd86648 --- /dev/null +++ b/shell/platform/android/test/io/flutter/plugin/platform/SingleViewPresentationTest.java @@ -0,0 +1,82 @@ +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/view/AccessibilityBridgeTest.java b/shell/platform/android/test/io/flutter/view/AccessibilityBridgeTest.java index 86c714c0f3554..324331d9d1817 100644 --- a/shell/platform/android/test/io/flutter/view/AccessibilityBridgeTest.java +++ b/shell/platform/android/test/io/flutter/view/AccessibilityBridgeTest.java @@ -1527,6 +1527,7 @@ 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); @@ -1564,6 +1565,7 @@ 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); @@ -1598,6 +1600,7 @@ 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); @@ -1612,6 +1615,34 @@ 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 a3aef00b2236b..41e7e89b6a6f4 100644 --- a/tools/android_lint/baseline.xml +++ b/tools/android_lint/baseline.xml @@ -100,4 +100,15 @@ column="18"/> + + + + diff --git a/tools/android_lint/project.xml b/tools/android_lint/project.xml index 38b9f65eebcb8..583b66491985d 100644 --- a/tools/android_lint/project.xml +++ b/tools/android_lint/project.xml @@ -107,7 +107,9 @@ + + From cf3b220e07a032b03a0748804d4f823d0d13298a Mon Sep 17 00:00:00 2001 From: Emmanuel Garcia Date: Fri, 17 Jun 2022 18:12:58 -0700 Subject: [PATCH 14/25] add integration test --- testing/android_systrace_test.py | 2 +- testing/run_tests.py | 2 +- testing/scenario_app/android/BUILD.gn | 7 +- .../flutter/scenariosui/MemoryLeakTests.java | 6 +- .../scenariosui/PlatformTextureUiTests.java | 7 +- .../scenariosui/PlatformViewUiTests.java | 7 +- .../PlatformViewWithSurfaceViewUiTest.java | 130 ++++++++++++++++++ .../PlatformViewWithTextureViewUiTest.java | 128 +++++++++++++++++ .../android/app/src/main/AndroidManifest.xml | 2 +- ...tivity.java => PlatformViewsActivity.java} | 14 +- .../scenarios/SurfacePlatformViewFactory.java | 101 ++++++++++++++ .../flutter/scenarios/TextPlatformView.java | 46 ------- .../scenarios/TextPlatformViewFactory.java | 36 +++++ .../scenarios/TexturePlatformViewFactory.java | 102 ++++++++++++++ .../scenario_app/lib/src/platform_view.dart | 18 ++- 15 files changed, 540 insertions(+), 68 deletions(-) create mode 100644 testing/scenario_app/android/app/src/androidTest/java/dev/flutter/scenariosui/PlatformViewWithSurfaceViewUiTest.java create mode 100644 testing/scenario_app/android/app/src/androidTest/java/dev/flutter/scenariosui/PlatformViewWithTextureViewUiTest.java rename testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/{TextPlatformViewActivity.java => PlatformViewsActivity.java} (59%) create mode 100644 testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/SurfacePlatformViewFactory.java delete mode 100644 testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/TextPlatformView.java create mode 100644 testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/TexturePlatformViewFactory.java diff --git a/testing/android_systrace_test.py b/testing/android_systrace_test.py index 2367df9ae6138..e6c039db5ac59 100755 --- a/testing/android_systrace_test.py +++ b/testing/android_systrace_test.py @@ -127,7 +127,7 @@ def main(): dest='activity_name', action='store', help='The activity to launch as it appears in AndroidManifest.xml, ' - 'e.g. .TextPlatformViewActivity' + 'e.g. .PlatformViewsActivity' ) parser.add_argument( '--adb-path', diff --git a/testing/run_tests.py b/testing/run_tests.py index 5304afe3c379e..3f35ab2ee30dc 100755 --- a/testing/run_tests.py +++ b/testing/run_tests.py @@ -595,7 +595,7 @@ def RunAndroidTests(android_variant='android_debug_unopt', adb_path=None): RunCmd([ systrace_test, '--adb-path', adb_path, '--apk-path', scenario_apk, '--package-name', 'dev.flutter.scenarios', '--activity-name', - '.TextPlatformViewActivity' + '.PlatformViewsActivity' ]) diff --git a/testing/scenario_app/android/BUILD.gn b/testing/scenario_app/android/BUILD.gn index 67e53aab8b795..9e3b401f61882 100644 --- a/testing/scenario_app/android/BUILD.gn +++ b/testing/scenario_app/android/BUILD.gn @@ -13,16 +13,19 @@ _android_sources = [ "app/src/androidTest/java/dev/flutter/scenariosui/MemoryLeakTests.java", "app/src/androidTest/java/dev/flutter/scenariosui/PlatformTextureUiTests.java", "app/src/androidTest/java/dev/flutter/scenariosui/PlatformViewUiTests.java", + "app/src/androidTest/java/dev/flutter/scenariosui/PlatformViewWithSurfaceViewUiTest.java", + "app/src/androidTest/java/dev/flutter/scenariosui/PlatformViewWithTextureViewUiTest.java", "app/src/androidTest/java/dev/flutter/scenariosui/ScreenshotUtil.java", "app/src/androidTest/java/dev/flutter/scenariosui/SpawnEngineTests.java", "app/src/main/AndroidManifest.xml", + "app/src/main/java/dev/flutter/scenarios/PlatformViewsActivity.java", "app/src/main/java/dev/flutter/scenarios/SpawnedEngineActivity.java", "app/src/main/java/dev/flutter/scenarios/StrictModeFlutterActivity.java", + "app/src/main/java/dev/flutter/scenarios/SurfacePlatformViewFactory.java", "app/src/main/java/dev/flutter/scenarios/TestActivity.java", "app/src/main/java/dev/flutter/scenarios/TestableFlutterActivity.java", - "app/src/main/java/dev/flutter/scenarios/TextPlatformView.java", - "app/src/main/java/dev/flutter/scenarios/TextPlatformViewActivity.java", "app/src/main/java/dev/flutter/scenarios/TextPlatformViewFactory.java", + "app/src/main/java/dev/flutter/scenarios/TexturePlatformViewFactory.java", "build.gradle", ] diff --git a/testing/scenario_app/android/app/src/androidTest/java/dev/flutter/scenariosui/MemoryLeakTests.java b/testing/scenario_app/android/app/src/androidTest/java/dev/flutter/scenariosui/MemoryLeakTests.java index 52f270ad65cd0..7556e52419a4b 100644 --- a/testing/scenario_app/android/app/src/androidTest/java/dev/flutter/scenariosui/MemoryLeakTests.java +++ b/testing/scenario_app/android/app/src/androidTest/java/dev/flutter/scenariosui/MemoryLeakTests.java @@ -9,7 +9,7 @@ import androidx.test.filters.LargeTest; import androidx.test.rule.ActivityTestRule; import androidx.test.runner.AndroidJUnit4; -import dev.flutter.scenarios.TextPlatformViewActivity; +import dev.flutter.scenarios.PlatformViewsActivity; import leakcanary.FailTestOnLeak; import org.junit.Rule; import org.junit.Test; @@ -19,9 +19,9 @@ @LargeTest public class MemoryLeakTests { @Rule @NonNull - public ActivityTestRule activityRule = + public ActivityTestRule activityRule = new ActivityTestRule<>( - TextPlatformViewActivity.class, /*initialTouchMode=*/ false, /*launchActivity=*/ false); + PlatformViewsActivity.class, /*initialTouchMode=*/ false, /*launchActivity=*/ false); @Test @FailTestOnLeak diff --git a/testing/scenario_app/android/app/src/androidTest/java/dev/flutter/scenariosui/PlatformTextureUiTests.java b/testing/scenario_app/android/app/src/androidTest/java/dev/flutter/scenariosui/PlatformTextureUiTests.java index fa0747b9dccb0..2b3ff525e5f55 100644 --- a/testing/scenario_app/android/app/src/androidTest/java/dev/flutter/scenariosui/PlatformTextureUiTests.java +++ b/testing/scenario_app/android/app/src/androidTest/java/dev/flutter/scenariosui/PlatformTextureUiTests.java @@ -9,7 +9,7 @@ import androidx.test.filters.LargeTest; import androidx.test.rule.ActivityTestRule; import androidx.test.runner.AndroidJUnit4; -import dev.flutter.scenarios.TextPlatformViewActivity; +import dev.flutter.scenarios.PlatformViewsActivity; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -21,15 +21,16 @@ public class PlatformTextureUiTests { Intent intent; @Rule @NonNull - public ActivityTestRule activityRule = + public ActivityTestRule activityRule = new ActivityTestRule<>( - TextPlatformViewActivity.class, /*initialTouchMode=*/ false, /*launchActivity=*/ false); + PlatformViewsActivity.class, /*initialTouchMode=*/ false, /*launchActivity=*/ false); @Before public void setUp() { intent = new Intent(Intent.ACTION_MAIN); // Render a texture. intent.putExtra("use_android_view", false); + intent.putExtra("view_type", "scenarios/textPlatformView"); } @Test diff --git a/testing/scenario_app/android/app/src/androidTest/java/dev/flutter/scenariosui/PlatformViewUiTests.java b/testing/scenario_app/android/app/src/androidTest/java/dev/flutter/scenariosui/PlatformViewUiTests.java index c9ef50d18cede..8481c63e26abe 100644 --- a/testing/scenario_app/android/app/src/androidTest/java/dev/flutter/scenariosui/PlatformViewUiTests.java +++ b/testing/scenario_app/android/app/src/androidTest/java/dev/flutter/scenariosui/PlatformViewUiTests.java @@ -9,7 +9,7 @@ import androidx.test.filters.LargeTest; import androidx.test.rule.ActivityTestRule; import androidx.test.runner.AndroidJUnit4; -import dev.flutter.scenarios.TextPlatformViewActivity; +import dev.flutter.scenarios.PlatformViewsActivity; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -21,15 +21,16 @@ public class PlatformViewUiTests { Intent intent; @Rule @NonNull - public ActivityTestRule activityRule = + public ActivityTestRule activityRule = new ActivityTestRule<>( - TextPlatformViewActivity.class, /*initialTouchMode=*/ false, /*launchActivity=*/ false); + PlatformViewsActivity.class, /*initialTouchMode=*/ false, /*launchActivity=*/ false); @Before public void setUp() { intent = new Intent(Intent.ACTION_MAIN); // Render a native android view. intent.putExtra("use_android_view", true); + intent.putExtra("view_type", "scenarios/textPlatformView"); } @Test diff --git a/testing/scenario_app/android/app/src/androidTest/java/dev/flutter/scenariosui/PlatformViewWithSurfaceViewUiTest.java b/testing/scenario_app/android/app/src/androidTest/java/dev/flutter/scenariosui/PlatformViewWithSurfaceViewUiTest.java new file mode 100644 index 0000000000000..fcfd896eb786b --- /dev/null +++ b/testing/scenario_app/android/app/src/androidTest/java/dev/flutter/scenariosui/PlatformViewWithSurfaceViewUiTest.java @@ -0,0 +1,130 @@ +// 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 dev.flutter.scenariosui; + +import android.content.Intent; +import androidx.annotation.NonNull; +import androidx.test.filters.LargeTest; +import androidx.test.rule.ActivityTestRule; +import androidx.test.runner.AndroidJUnit4; +import dev.flutter.scenarios.PlatformViewsActivity; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +@LargeTest +public class PlatformViewWithSurfaceViewUiTest { + Intent intent; + + @Rule @NonNull + public ActivityTestRule activityRule = + new ActivityTestRule<>( + PlatformViewsActivity.class, /*initialTouchMode=*/ false, /*launchActivity=*/ false); + + @Before + public void setUp() { + intent = new Intent(Intent.ACTION_MAIN); + // Render a texture. + intent.putExtra("use_android_view", false); + intent.putExtra("view_type", "scenarios/surfacePlatformView"); + } + + @Test + public void testPlatformView() throws Exception { + intent.putExtra("scenario_name", "platform_view"); + ScreenshotUtil.capture( + activityRule.launchActivity(intent), "PlatformViewWithSurfaceViewUiTest_testPlatformView"); + } + + @Test + public void testPlatformViewMultiple() throws Exception { + intent.putExtra("scenario_name", "platform_view_multiple"); + ScreenshotUtil.capture( + activityRule.launchActivity(intent), + "PlatformViewWithSurfaceViewUiTest_testPlatformViewMultiple"); + } + + @Test + public void testPlatformViewMultipleBackgroundForeground() throws Exception { + intent.putExtra("scenario_name", "platform_view_multiple_background_foreground"); + ScreenshotUtil.capture( + activityRule.launchActivity(intent), + "PlatformViewWithSurfaceViewUiTest_testPlatformViewMultipleBackgroundForeground"); + } + + @Test + public void testPlatformViewCliprect() throws Exception { + intent.putExtra("scenario_name", "platform_view_cliprect"); + ScreenshotUtil.capture( + activityRule.launchActivity(intent), + "PlatformViewWithSurfaceViewUiTest_testPlatformViewCliprect"); + } + + @Test + public void testPlatformViewCliprrect() throws Exception { + intent.putExtra("scenario_name", "platform_view_cliprrect"); + ScreenshotUtil.capture( + activityRule.launchActivity(intent), + "PlatformViewWithSurfaceViewUiTest_testPlatformViewCliprrect"); + } + + @Test + public void testPlatformViewClippath() throws Exception { + intent.putExtra("scenario_name", "platform_view_clippath"); + ScreenshotUtil.capture( + activityRule.launchActivity(intent), + "PlatformViewWithSurfaceViewUiTest_testPlatformViewClippath"); + } + + @Test + public void testPlatformViewTransform() throws Exception { + intent.putExtra("scenario_name", "platform_view_transform"); + ScreenshotUtil.capture( + activityRule.launchActivity(intent), + "PlatformViewWithSurfaceViewUiTest_testPlatformViewTransform"); + } + + @Test + public void testPlatformViewOpacity() throws Exception { + intent.putExtra("scenario_name", "platform_view_opacity"); + ScreenshotUtil.capture( + activityRule.launchActivity(intent), + "PlatformViewWithSurfaceViewUiTest_testPlatformViewOpacity"); + } + + @Test + public void testPlatformViewRotate() throws Exception { + intent.putExtra("scenario_name", "platform_view_rotate"); + ScreenshotUtil.capture( + activityRule.launchActivity(intent), + "PlatformViewWithSurfaceViewUiTest_testPlatformViewRotate"); + } + + @Test + public void testPlatformViewMultipleWithoutOverlays() throws Exception { + intent.putExtra("scenario_name", "platform_view_multiple_without_overlays"); + ScreenshotUtil.capture( + activityRule.launchActivity(intent), + "PlatformViewWithSurfaceViewUiTest_testPlatformViewMultipleWithoutOverlays"); + } + + @Test + public void testPlatformViewTwoIntersectingOverlays() throws Exception { + intent.putExtra("scenario_name", "platform_view_two_intersecting_overlays"); + ScreenshotUtil.capture( + activityRule.launchActivity(intent), + "PlatformViewWithSurfaceViewUiTest_testPlatformViewTwoIntersectingOverlays"); + } + + @Test + public void testPlatformViewWithoutOverlayIntersection() throws Exception { + intent.putExtra("scenario_name", "platform_view_no_overlay_intersection"); + ScreenshotUtil.capture( + activityRule.launchActivity(intent), + "PlatformViewWithSurfaceViewUiTest_testPlatformViewWithoutOverlayIntersection"); + } +} diff --git a/testing/scenario_app/android/app/src/androidTest/java/dev/flutter/scenariosui/PlatformViewWithTextureViewUiTest.java b/testing/scenario_app/android/app/src/androidTest/java/dev/flutter/scenariosui/PlatformViewWithTextureViewUiTest.java new file mode 100644 index 0000000000000..30daa84839393 --- /dev/null +++ b/testing/scenario_app/android/app/src/androidTest/java/dev/flutter/scenariosui/PlatformViewWithTextureViewUiTest.java @@ -0,0 +1,128 @@ +// 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 dev.flutter.scenariosui; + +import android.content.Intent; +import androidx.annotation.NonNull; +import androidx.test.filters.LargeTest; +import androidx.test.rule.ActivityTestRule; +import androidx.test.runner.AndroidJUnit4; +import dev.flutter.scenarios.PlatformViewsActivity; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +@LargeTest +public class PlatformViewWithTextureViewUiTest { + Intent intent; + + @Rule @NonNull + public ActivityTestRule activityRule = + new ActivityTestRule<>( + PlatformViewsActivity.class, /*initialTouchMode=*/ false, /*launchActivity=*/ false); + + @Before + public void setUp() { + intent = new Intent(Intent.ACTION_MAIN); + intent.putExtra("view_type", "scenarios/texturePlatformView"); + } + + @Test + public void testPlatformView() throws Exception { + intent.putExtra("scenario_name", "platform_view"); + ScreenshotUtil.capture( + activityRule.launchActivity(intent), "PlatformViewWithTextureViewUiTest_testPlatformView"); + } + + @Test + public void testPlatformViewMultiple() throws Exception { + intent.putExtra("scenario_name", "platform_view_multiple"); + ScreenshotUtil.capture( + activityRule.launchActivity(intent), + "PlatformViewWithTextureViewUiTest_testPlatformViewMultiple"); + } + + @Test + public void testPlatformViewMultipleBackgroundForeground() throws Exception { + intent.putExtra("scenario_name", "platform_view_multiple_background_foreground"); + ScreenshotUtil.capture( + activityRule.launchActivity(intent), + "PlatformViewWithTextureViewUiTest_testPlatformViewMultipleBackgroundForeground"); + } + + @Test + public void testPlatformViewCliprect() throws Exception { + intent.putExtra("scenario_name", "platform_view_cliprect"); + ScreenshotUtil.capture( + activityRule.launchActivity(intent), + "PlatformViewWithTextureViewUiTest_testPlatformViewCliprect"); + } + + @Test + public void testPlatformViewCliprrect() throws Exception { + intent.putExtra("scenario_name", "platform_view_cliprrect"); + ScreenshotUtil.capture( + activityRule.launchActivity(intent), + "PlatformViewWithTextureViewUiTest_testPlatformViewCliprrect"); + } + + @Test + public void testPlatformViewClippath() throws Exception { + intent.putExtra("scenario_name", "platform_view_clippath"); + ScreenshotUtil.capture( + activityRule.launchActivity(intent), + "PlatformViewWithTextureViewUiTest_testPlatformViewClippath"); + } + + @Test + public void testPlatformViewTransform() throws Exception { + intent.putExtra("scenario_name", "platform_view_transform"); + ScreenshotUtil.capture( + activityRule.launchActivity(intent), + "PlatformViewWithTextureViewUiTest_testPlatformViewTransform"); + } + + @Test + public void testPlatformViewOpacity() throws Exception { + intent.putExtra("scenario_name", "platform_view_opacity"); + ScreenshotUtil.capture( + activityRule.launchActivity(intent), + "PlatformViewWithTextureViewUiTest_testPlatformViewOpacity"); + } + + @Test + public void testPlatformViewRotate() throws Exception { + intent.putExtra("scenario_name", "platform_view_rotate"); + ScreenshotUtil.capture( + activityRule.launchActivity(intent), + "PlatformViewWithTextureViewUiTest_testPlatformViewRotate"); + } + + @Test + public void testPlatformViewMultipleWithoutOverlays() throws Exception { + intent.putExtra("scenario_name", "platform_view_multiple_without_overlays"); + ScreenshotUtil.capture( + activityRule.launchActivity(intent), + "PlatformViewWithTextureViewUiTest_testPlatformViewMultipleWithoutOverlays"); + } + + @Test + public void testPlatformViewTwoIntersectingOverlays() throws Exception { + intent.putExtra("scenario_name", "platform_view_two_intersecting_overlays"); + ScreenshotUtil.capture( + activityRule.launchActivity(intent), + "PlatformViewWithTextureViewUiTest_testPlatformViewTwoIntersectingOverlays"); + } + + @Test + public void testPlatformViewWithoutOverlayIntersection() throws Exception { + intent.putExtra("scenario_name", "platform_view_no_overlay_intersection"); + ScreenshotUtil.capture( + activityRule.launchActivity(intent), + "PlatformViewWithTextureViewUiTest_testPlatformViewWithoutOverlayIntersection"); + } +} diff --git a/testing/scenario_app/android/app/src/main/AndroidManifest.xml b/testing/scenario_app/android/app/src/main/AndroidManifest.xml index bbd62a00015cc..09f440887c41d 100644 --- a/testing/scenario_app/android/app/src/main/AndroidManifest.xml +++ b/testing/scenario_app/android/app/src/main/AndroidManifest.xml @@ -9,7 +9,7 @@ android:supportsRtl="true" android:theme="@style/AppTheme"> () { + @Nullable + @Override + public ByteBuffer encodeMessage(@Nullable Object o) { + if (o instanceof String) { + return StringCodec.INSTANCE.encodeMessage((String) o); + } + return null; + } + + @Nullable + @Override + public Object decodeMessage(@Nullable ByteBuffer byteBuffer) { + return StringCodec.INSTANCE.decodeMessage(byteBuffer); + } + }); + } + + @SuppressWarnings("unchecked") + @Override + @NonNull + public PlatformView create(@NonNull Context context, int id, @Nullable Object args) { + return new SurfacePlatformView(context); + } + + private static class SurfacePlatformView implements PlatformView { + static String TAG = "SurfacePlatformView"; + + final SurfaceView surfaceView; + + @SuppressWarnings("unchecked") + SurfacePlatformView(@NonNull final Context context) { + surfaceView = new SurfaceView(context); + surfaceView + .getHolder() + .addCallback( + new SurfaceHolder.Callback() { + @Override + public void surfaceCreated(SurfaceHolder holder) { + Log.i(TAG, "surfaceCreated"); + final Surface surface = holder.getSurface(); + final Canvas canvas = surface.lockHardwareCanvas(); + canvas.drawColor(Color.WHITE); + + final Paint paint = new Paint(); + paint.setColor(Color.RED); + canvas.drawCircle(canvas.getWidth() / 2, canvas.getHeight() / 2, 20, paint); + surface.unlockCanvasAndPost(canvas); + } + + @Override + public void surfaceChanged( + SurfaceHolder holder, int format, int width, int height) { + Log.i(TAG, "surfaceChanged"); + } + + @Override + public void surfaceDestroyed(SurfaceHolder holder) { + Log.i(TAG, "surfaceDestroyed"); + } + }); + } + + @Override + @NonNull + public View getView() { + return surfaceView; + } + + @Override + public void dispose() {} + } +} diff --git a/testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/TextPlatformView.java b/testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/TextPlatformView.java deleted file mode 100644 index e834913d0eac4..0000000000000 --- a/testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/TextPlatformView.java +++ /dev/null @@ -1,46 +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 dev.flutter.scenarios; - -import android.content.Context; -import android.graphics.Color; -import android.view.Choreographer; -import android.view.View; -import android.widget.TextView; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import io.flutter.plugin.platform.PlatformView; - -public class TextPlatformView implements PlatformView { - final TextView textView; - - @SuppressWarnings("unchecked") - TextPlatformView(@NonNull final Context context, int id, @Nullable String params) { - textView = new TextView(context); - textView.setTextSize(72); - textView.setBackgroundColor(Color.rgb(255, 255, 255)); - textView.setText(params); - - // Investigate why this is needed to pass some gold tests. - Choreographer.getInstance() - .postFrameCallbackDelayed( - new Choreographer.FrameCallback() { - @Override - public void doFrame(long frameTimeNanos) { - textView.invalidate(); - } - }, - 500); - } - - @Override - @NonNull - public View getView() { - return textView; - } - - @Override - public void dispose() {} -} diff --git a/testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/TextPlatformViewFactory.java b/testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/TextPlatformViewFactory.java index 3b660a0089c05..93806dea105f5 100644 --- a/testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/TextPlatformViewFactory.java +++ b/testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/TextPlatformViewFactory.java @@ -5,6 +5,10 @@ package dev.flutter.scenarios; import android.content.Context; +import android.graphics.Color; +import android.view.Choreographer; +import android.view.View; +import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import io.flutter.plugin.common.MessageCodec; @@ -41,4 +45,36 @@ public PlatformView create(@NonNull Context context, int id, @Nullable Object ar String params = (String) args; return new TextPlatformView(context, id, params); } + + private static class TextPlatformView implements PlatformView { + final TextView textView; + + @SuppressWarnings("unchecked") + TextPlatformView(@NonNull final Context context, int id, @Nullable String params) { + textView = new TextView(context); + textView.setTextSize(72); + textView.setBackgroundColor(Color.WHITE); + textView.setText(params); + + // Investigate why this is needed to pass some gold tests. + Choreographer.getInstance() + .postFrameCallbackDelayed( + new Choreographer.FrameCallback() { + @Override + public void doFrame(long frameTimeNanos) { + textView.invalidate(); + } + }, + 500); + } + + @Override + @NonNull + public View getView() { + return textView; + } + + @Override + public void dispose() {} + } } diff --git a/testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/TexturePlatformViewFactory.java b/testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/TexturePlatformViewFactory.java new file mode 100644 index 0000000000000..191c7ad82f437 --- /dev/null +++ b/testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/TexturePlatformViewFactory.java @@ -0,0 +1,102 @@ +// 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 dev.flutter.scenarios; + +import android.annotation.TargetApi; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.SurfaceTexture; +import android.view.TextureView; +import android.view.View; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import io.flutter.Log; +import io.flutter.plugin.common.MessageCodec; +import io.flutter.plugin.common.StringCodec; +import io.flutter.plugin.platform.PlatformView; +import io.flutter.plugin.platform.PlatformViewFactory; +import java.nio.ByteBuffer; + +@TargetApi(23) +public final class TexturePlatformViewFactory extends PlatformViewFactory { + TexturePlatformViewFactory() { + super( + new MessageCodec() { + @Nullable + @Override + public ByteBuffer encodeMessage(@Nullable Object o) { + if (o instanceof String) { + return StringCodec.INSTANCE.encodeMessage((String) o); + } + return null; + } + + @Nullable + @Override + public Object decodeMessage(@Nullable ByteBuffer byteBuffer) { + return StringCodec.INSTANCE.decodeMessage(byteBuffer); + } + }); + } + + @SuppressWarnings("unchecked") + @Override + @NonNull + public PlatformView create(@NonNull Context context, int id, @Nullable Object args) { + return new TexturePlatformView(context); + } + + private static class TexturePlatformView implements PlatformView { + static String TAG = "TexturePlatformView"; + + final TextureView textureView; + + @SuppressWarnings("unchecked") + TexturePlatformView(@NonNull final Context context) { + textureView = new TextureView(context); + textureView.setSurfaceTextureListener( + new TextureView.SurfaceTextureListener() { + @Override + public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) { + Log.i(TAG, "onSurfaceTextureAvailable"); + final Canvas canvas = textureView.lockCanvas(); + canvas.drawColor(Color.WHITE); + + final Paint paint = new Paint(); + paint.setColor(Color.RED); + canvas.drawCircle(canvas.getWidth() / 2, canvas.getHeight() / 2, 20, paint); + textureView.unlockCanvasAndPost(canvas); + } + + @Override + public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) { + Log.i(TAG, "onSurfaceTextureDestroyed"); + return true; + } + + @Override + public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) { + Log.i(TAG, "onSurfaceTextureSizeChanged"); + } + + @Override + public void onSurfaceTextureUpdated(SurfaceTexture surface) { + Log.i(TAG, "onSurfaceTextureUpdated"); + } + }); + } + + @Override + @NonNull + public View getView() { + return textureView; + } + + @Override + public void dispose() {} + } +} diff --git a/testing/scenario_app/lib/src/platform_view.dart b/testing/scenario_app/lib/src/platform_view.dart index 4f99d01244933..cbc1f7c330d61 100644 --- a/testing/scenario_app/lib/src/platform_view.dart +++ b/testing/scenario_app/lib/src/platform_view.dart @@ -1047,9 +1047,16 @@ void addPlatformView( String text = 'platform view', double width = 500, double height = 500, - String viewType = 'scenarios/textPlatformView', + String? viewType, }) { + if (viewType == null && scenarioParams['view_type'] is String) { + viewType = scenarioParams['view_type']; + } + + assert(viewType != null, 'view type not set'); + final String platformViewKey = '$viewType-$id'; + if (_createdPlatformViews.containsKey(platformViewKey)) { addPlatformViewToSceneBuilder( id, @@ -1060,9 +1067,10 @@ void addPlatformView( ); return; } + bool usesAndroidHybridComposition = false; - if (scenarioParams['use_android_view'] != null) { - usesAndroidHybridComposition = scenarioParams['use_android_view'] as bool; + if (scenarioParams['use_android_view'] is bool) { + usesAndroidHybridComposition = scenarioParams['use_android_view']; } const int _valueTrue = 1; @@ -1091,8 +1099,8 @@ void addPlatformView( 'viewType'.length, ...utf8.encode('viewType'), _valueString, - viewType.length, - ...utf8.encode(viewType), + viewType!.length, + ...utf8.encode(viewType!), if (Platform.isAndroid && !usesAndroidHybridComposition) ...[ _valueString, 'width'.length, From 225f943889f28fc9670b44fb538c46d066a66150 Mon Sep 17 00:00:00 2001 From: Emmanuel Garcia Date: Fri, 17 Jun 2022 18:43:53 -0700 Subject: [PATCH 15/25] clean up --- .../scenariosui/PlatformTextureUiTests.java | 34 +++++++-------- .../scenariosui/PlatformViewUiTests.java | 34 +++++++-------- .../PlatformViewWithSurfaceViewUiTest.java | 41 ++++++++----------- .../PlatformViewWithTextureViewUiTest.java | 41 ++++++++----------- .../scenarios/PlatformViewsActivity.java | 10 +++-- 5 files changed, 75 insertions(+), 85 deletions(-) diff --git a/testing/scenario_app/android/app/src/androidTest/java/dev/flutter/scenariosui/PlatformTextureUiTests.java b/testing/scenario_app/android/app/src/androidTest/java/dev/flutter/scenariosui/PlatformTextureUiTests.java index 2b3ff525e5f55..420d2a3341f52 100644 --- a/testing/scenario_app/android/app/src/androidTest/java/dev/flutter/scenariosui/PlatformTextureUiTests.java +++ b/testing/scenario_app/android/app/src/androidTest/java/dev/flutter/scenariosui/PlatformTextureUiTests.java @@ -25,26 +25,29 @@ public class PlatformTextureUiTests { new ActivityTestRule<>( PlatformViewsActivity.class, /*initialTouchMode=*/ false, /*launchActivity=*/ false); + private static String goldName(String suffix) { + return "PlatformTextureUiTests_" + suffix; + } + @Before public void setUp() { intent = new Intent(Intent.ACTION_MAIN); // Render a texture. intent.putExtra("use_android_view", false); - intent.putExtra("view_type", "scenarios/textPlatformView"); + intent.putExtra("view_type", PlatformViewsActivity.TEXT_VIEW_PV); } @Test public void testPlatformView() throws Exception { intent.putExtra("scenario_name", "platform_view"); - ScreenshotUtil.capture( - activityRule.launchActivity(intent), "PlatformTextureUiTests_testPlatformView"); + ScreenshotUtil.capture(activityRule.launchActivity(intent), goldName("testPlatformView")); } @Test public void testPlatformViewMultiple() throws Exception { intent.putExtra("scenario_name", "platform_view_multiple"); ScreenshotUtil.capture( - activityRule.launchActivity(intent), "PlatformTextureUiTests_testPlatformViewMultiple"); + activityRule.launchActivity(intent), goldName("testPlatformViewMultiple")); } @Test @@ -52,65 +55,62 @@ public void testPlatformViewMultipleBackgroundForeground() throws Exception { intent.putExtra("scenario_name", "platform_view_multiple_background_foreground"); ScreenshotUtil.capture( activityRule.launchActivity(intent), - "PlatformTextureUiTests_testPlatformViewMultipleBackgroundForeground"); + goldName("testPlatformViewMultipleBackgroundForeground")); } @Test public void testPlatformViewCliprect() throws Exception { intent.putExtra("scenario_name", "platform_view_cliprect"); ScreenshotUtil.capture( - activityRule.launchActivity(intent), "PlatformTextureUiTests_testPlatformViewCliprect"); + activityRule.launchActivity(intent), goldName("testPlatformViewCliprect")); } @Test public void testPlatformViewCliprrect() throws Exception { intent.putExtra("scenario_name", "platform_view_cliprrect"); ScreenshotUtil.capture( - activityRule.launchActivity(intent), "PlatformTextureUiTests_testPlatformViewCliprrect"); + activityRule.launchActivity(intent), goldName("testPlatformViewCliprrect")); } @Test public void testPlatformViewClippath() throws Exception { intent.putExtra("scenario_name", "platform_view_clippath"); ScreenshotUtil.capture( - activityRule.launchActivity(intent), "PlatformTextureUiTests_testPlatformViewClippath"); + activityRule.launchActivity(intent), goldName("testPlatformViewClippath")); } @Test public void testPlatformViewTransform() throws Exception { intent.putExtra("scenario_name", "platform_view_transform"); ScreenshotUtil.capture( - activityRule.launchActivity(intent), "PlatformTextureUiTests_testPlatformViewTransform"); + activityRule.launchActivity(intent), goldName("testPlatformViewTransform")); } @Test public void testPlatformViewOpacity() throws Exception { intent.putExtra("scenario_name", "platform_view_opacity"); ScreenshotUtil.capture( - activityRule.launchActivity(intent), "PlatformTextureUiTests_testPlatformViewOpacity"); + activityRule.launchActivity(intent), goldName("testPlatformViewOpacity")); } @Test public void testPlatformViewRotate() throws Exception { intent.putExtra("scenario_name", "platform_view_rotate"); - ScreenshotUtil.capture( - activityRule.launchActivity(intent), "PlatformTextureUiTests_testPlatformViewRotate"); + ScreenshotUtil.capture(activityRule.launchActivity(intent), goldName("testPlatformViewRotate")); } @Test public void testPlatformViewMultipleWithoutOverlays() throws Exception { intent.putExtra("scenario_name", "platform_view_multiple_without_overlays"); ScreenshotUtil.capture( - activityRule.launchActivity(intent), - "PlatformTextureUiTests_testPlatformViewMultipleWithoutOverlays"); + activityRule.launchActivity(intent), goldName("testPlatformViewMultipleWithoutOverlays")); } @Test public void testPlatformViewTwoIntersectingOverlays() throws Exception { intent.putExtra("scenario_name", "platform_view_two_intersecting_overlays"); ScreenshotUtil.capture( - activityRule.launchActivity(intent), - "PlatformTextureUiTests_testPlatformViewTwoIntersectingOverlays"); + activityRule.launchActivity(intent), goldName("testPlatformViewTwoIntersectingOverlays")); } @Test @@ -118,6 +118,6 @@ public void testPlatformViewWithoutOverlayIntersection() throws Exception { intent.putExtra("scenario_name", "platform_view_no_overlay_intersection"); ScreenshotUtil.capture( activityRule.launchActivity(intent), - "PlatformTextureUiTests_testPlatformViewWithoutOverlayIntersection"); + goldName("testPlatformViewWithoutOverlayIntersection")); } } diff --git a/testing/scenario_app/android/app/src/androidTest/java/dev/flutter/scenariosui/PlatformViewUiTests.java b/testing/scenario_app/android/app/src/androidTest/java/dev/flutter/scenariosui/PlatformViewUiTests.java index 8481c63e26abe..a612dfdb6b441 100644 --- a/testing/scenario_app/android/app/src/androidTest/java/dev/flutter/scenariosui/PlatformViewUiTests.java +++ b/testing/scenario_app/android/app/src/androidTest/java/dev/flutter/scenariosui/PlatformViewUiTests.java @@ -25,26 +25,29 @@ public class PlatformViewUiTests { new ActivityTestRule<>( PlatformViewsActivity.class, /*initialTouchMode=*/ false, /*launchActivity=*/ false); + private static String goldName(String suffix) { + return "PlatformViewUiTests_" + suffix; + } + @Before public void setUp() { intent = new Intent(Intent.ACTION_MAIN); // Render a native android view. intent.putExtra("use_android_view", true); - intent.putExtra("view_type", "scenarios/textPlatformView"); + intent.putExtra("view_type", PlatformViewsActivity.TEXT_VIEW_PV); } @Test public void testPlatformView() throws Exception { intent.putExtra("scenario_name", "platform_view"); - ScreenshotUtil.capture( - activityRule.launchActivity(intent), "PlatformViewUiTests_testPlatformView"); + ScreenshotUtil.capture(activityRule.launchActivity(intent), goldName("testPlatformView")); } @Test public void testPlatformViewMultiple() throws Exception { intent.putExtra("scenario_name", "platform_view_multiple"); ScreenshotUtil.capture( - activityRule.launchActivity(intent), "PlatformViewUiTests_testPlatformViewMultiple"); + activityRule.launchActivity(intent), goldName("testPlatformViewMultiple")); } @Test @@ -52,65 +55,62 @@ public void testPlatformViewMultipleBackgroundForeground() throws Exception { intent.putExtra("scenario_name", "platform_view_multiple_background_foreground"); ScreenshotUtil.capture( activityRule.launchActivity(intent), - "PlatformViewUiTests_testPlatformViewMultipleBackgroundForeground"); + goldName("testPlatformViewMultipleBackgroundForeground")); } @Test public void testPlatformViewCliprect() throws Exception { intent.putExtra("scenario_name", "platform_view_cliprect"); ScreenshotUtil.capture( - activityRule.launchActivity(intent), "PlatformViewUiTests_testPlatformViewCliprect"); + activityRule.launchActivity(intent), goldName("testPlatformViewCliprect")); } @Test public void testPlatformViewCliprrect() throws Exception { intent.putExtra("scenario_name", "platform_view_cliprrect"); ScreenshotUtil.capture( - activityRule.launchActivity(intent), "PlatformViewUiTests_testPlatformViewCliprrect"); + activityRule.launchActivity(intent), goldName("testPlatformViewCliprrect")); } @Test public void testPlatformViewClippath() throws Exception { intent.putExtra("scenario_name", "platform_view_clippath"); ScreenshotUtil.capture( - activityRule.launchActivity(intent), "PlatformViewUiTests_testPlatformViewClippath"); + activityRule.launchActivity(intent), goldName("testPlatformViewClippath")); } @Test public void testPlatformViewTransform() throws Exception { intent.putExtra("scenario_name", "platform_view_transform"); ScreenshotUtil.capture( - activityRule.launchActivity(intent), "PlatformViewUiTests_testPlatformViewTransform"); + activityRule.launchActivity(intent), goldName("testPlatformViewTransform")); } @Test public void testPlatformViewOpacity() throws Exception { intent.putExtra("scenario_name", "platform_view_opacity"); ScreenshotUtil.capture( - activityRule.launchActivity(intent), "PlatformViewUiTests_testPlatformViewOpacity"); + activityRule.launchActivity(intent), goldName("testPlatformViewOpacity")); } @Test public void testPlatformViewRotate() throws Exception { intent.putExtra("scenario_name", "platform_view_rotate"); - ScreenshotUtil.capture( - activityRule.launchActivity(intent), "PlatformViewUiTests_testPlatformViewRotate"); + ScreenshotUtil.capture(activityRule.launchActivity(intent), goldName("testPlatformViewRotate")); } @Test public void testPlatformViewMultipleWithoutOverlays() throws Exception { intent.putExtra("scenario_name", "platform_view_multiple_without_overlays"); ScreenshotUtil.capture( - activityRule.launchActivity(intent), - "PlatformViewUiTests_testPlatformViewMultipleWithoutOverlays"); + activityRule.launchActivity(intent), goldName("testPlatformViewMultipleWithoutOverlays")); } @Test public void testPlatformViewTwoIntersectingOverlays() throws Exception { intent.putExtra("scenario_name", "platform_view_two_intersecting_overlays"); ScreenshotUtil.capture( - activityRule.launchActivity(intent), - "PlatformViewUiTests_testPlatformViewTwoIntersectingOverlays"); + activityRule.launchActivity(intent), goldName("testPlatformViewTwoIntersectingOverlays")); } @Test @@ -118,6 +118,6 @@ public void testPlatformViewWithoutOverlayIntersection() throws Exception { intent.putExtra("scenario_name", "platform_view_no_overlay_intersection"); ScreenshotUtil.capture( activityRule.launchActivity(intent), - "PlatformViewUiTests_testPlatformViewWithoutOverlayIntersection"); + goldName("testPlatformViewWithoutOverlayIntersection")); } } diff --git a/testing/scenario_app/android/app/src/androidTest/java/dev/flutter/scenariosui/PlatformViewWithSurfaceViewUiTest.java b/testing/scenario_app/android/app/src/androidTest/java/dev/flutter/scenariosui/PlatformViewWithSurfaceViewUiTest.java index fcfd896eb786b..9695c848bda61 100644 --- a/testing/scenario_app/android/app/src/androidTest/java/dev/flutter/scenariosui/PlatformViewWithSurfaceViewUiTest.java +++ b/testing/scenario_app/android/app/src/androidTest/java/dev/flutter/scenariosui/PlatformViewWithSurfaceViewUiTest.java @@ -25,27 +25,29 @@ public class PlatformViewWithSurfaceViewUiTest { new ActivityTestRule<>( PlatformViewsActivity.class, /*initialTouchMode=*/ false, /*launchActivity=*/ false); + private static String goldName(String suffix) { + return "PlatformViewWithSurfaceViewUiTest_" + suffix; + } + @Before public void setUp() { intent = new Intent(Intent.ACTION_MAIN); // Render a texture. intent.putExtra("use_android_view", false); - intent.putExtra("view_type", "scenarios/surfacePlatformView"); + intent.putExtra("view_type", PlatformViewsActivity.SURFACE_VIEW_PV); } @Test public void testPlatformView() throws Exception { intent.putExtra("scenario_name", "platform_view"); - ScreenshotUtil.capture( - activityRule.launchActivity(intent), "PlatformViewWithSurfaceViewUiTest_testPlatformView"); + ScreenshotUtil.capture(activityRule.launchActivity(intent), goldName("testPlatformView")); } @Test public void testPlatformViewMultiple() throws Exception { intent.putExtra("scenario_name", "platform_view_multiple"); ScreenshotUtil.capture( - activityRule.launchActivity(intent), - "PlatformViewWithSurfaceViewUiTest_testPlatformViewMultiple"); + activityRule.launchActivity(intent), goldName("testPlatformViewMultiple")); } @Test @@ -53,71 +55,62 @@ public void testPlatformViewMultipleBackgroundForeground() throws Exception { intent.putExtra("scenario_name", "platform_view_multiple_background_foreground"); ScreenshotUtil.capture( activityRule.launchActivity(intent), - "PlatformViewWithSurfaceViewUiTest_testPlatformViewMultipleBackgroundForeground"); + goldName("testPlatformViewMultipleBackgroundForeground")); } @Test public void testPlatformViewCliprect() throws Exception { intent.putExtra("scenario_name", "platform_view_cliprect"); ScreenshotUtil.capture( - activityRule.launchActivity(intent), - "PlatformViewWithSurfaceViewUiTest_testPlatformViewCliprect"); + activityRule.launchActivity(intent), goldName("testPlatformViewCliprect")); } @Test public void testPlatformViewCliprrect() throws Exception { intent.putExtra("scenario_name", "platform_view_cliprrect"); ScreenshotUtil.capture( - activityRule.launchActivity(intent), - "PlatformViewWithSurfaceViewUiTest_testPlatformViewCliprrect"); + activityRule.launchActivity(intent), goldName("testPlatformViewCliprrect")); } @Test public void testPlatformViewClippath() throws Exception { intent.putExtra("scenario_name", "platform_view_clippath"); ScreenshotUtil.capture( - activityRule.launchActivity(intent), - "PlatformViewWithSurfaceViewUiTest_testPlatformViewClippath"); + activityRule.launchActivity(intent), goldName("testPlatformViewClippath")); } @Test public void testPlatformViewTransform() throws Exception { intent.putExtra("scenario_name", "platform_view_transform"); ScreenshotUtil.capture( - activityRule.launchActivity(intent), - "PlatformViewWithSurfaceViewUiTest_testPlatformViewTransform"); + activityRule.launchActivity(intent), goldName("testPlatformViewTransform")); } @Test public void testPlatformViewOpacity() throws Exception { intent.putExtra("scenario_name", "platform_view_opacity"); ScreenshotUtil.capture( - activityRule.launchActivity(intent), - "PlatformViewWithSurfaceViewUiTest_testPlatformViewOpacity"); + activityRule.launchActivity(intent), goldName("testPlatformViewOpacity")); } @Test public void testPlatformViewRotate() throws Exception { intent.putExtra("scenario_name", "platform_view_rotate"); - ScreenshotUtil.capture( - activityRule.launchActivity(intent), - "PlatformViewWithSurfaceViewUiTest_testPlatformViewRotate"); + ScreenshotUtil.capture(activityRule.launchActivity(intent), goldName("testPlatformViewRotate")); } @Test public void testPlatformViewMultipleWithoutOverlays() throws Exception { intent.putExtra("scenario_name", "platform_view_multiple_without_overlays"); ScreenshotUtil.capture( - activityRule.launchActivity(intent), - "PlatformViewWithSurfaceViewUiTest_testPlatformViewMultipleWithoutOverlays"); + activityRule.launchActivity(intent), goldName("testPlatformViewMultipleWithoutOverlays")); } @Test public void testPlatformViewTwoIntersectingOverlays() throws Exception { intent.putExtra("scenario_name", "platform_view_two_intersecting_overlays"); ScreenshotUtil.capture( - activityRule.launchActivity(intent), - "PlatformViewWithSurfaceViewUiTest_testPlatformViewTwoIntersectingOverlays"); + activityRule.launchActivity(intent), goldName("testPlatformViewTwoIntersectingOverlays")); } @Test @@ -125,6 +118,6 @@ public void testPlatformViewWithoutOverlayIntersection() throws Exception { intent.putExtra("scenario_name", "platform_view_no_overlay_intersection"); ScreenshotUtil.capture( activityRule.launchActivity(intent), - "PlatformViewWithSurfaceViewUiTest_testPlatformViewWithoutOverlayIntersection"); + goldName("testPlatformViewWithoutOverlayIntersection")); } } diff --git a/testing/scenario_app/android/app/src/androidTest/java/dev/flutter/scenariosui/PlatformViewWithTextureViewUiTest.java b/testing/scenario_app/android/app/src/androidTest/java/dev/flutter/scenariosui/PlatformViewWithTextureViewUiTest.java index 30daa84839393..a72a97a61a681 100644 --- a/testing/scenario_app/android/app/src/androidTest/java/dev/flutter/scenariosui/PlatformViewWithTextureViewUiTest.java +++ b/testing/scenario_app/android/app/src/androidTest/java/dev/flutter/scenariosui/PlatformViewWithTextureViewUiTest.java @@ -25,25 +25,27 @@ public class PlatformViewWithTextureViewUiTest { new ActivityTestRule<>( PlatformViewsActivity.class, /*initialTouchMode=*/ false, /*launchActivity=*/ false); + private static String goldName(String suffix) { + return "PlatformViewWithTextureViewUiTest_" + suffix; + } + @Before public void setUp() { intent = new Intent(Intent.ACTION_MAIN); - intent.putExtra("view_type", "scenarios/texturePlatformView"); + intent.putExtra("view_type", PlatformViewsActivity.TEXTURE_VIEW_PV); } @Test public void testPlatformView() throws Exception { intent.putExtra("scenario_name", "platform_view"); - ScreenshotUtil.capture( - activityRule.launchActivity(intent), "PlatformViewWithTextureViewUiTest_testPlatformView"); + ScreenshotUtil.capture(activityRule.launchActivity(intent), goldName("testPlatformView")); } @Test public void testPlatformViewMultiple() throws Exception { intent.putExtra("scenario_name", "platform_view_multiple"); ScreenshotUtil.capture( - activityRule.launchActivity(intent), - "PlatformViewWithTextureViewUiTest_testPlatformViewMultiple"); + activityRule.launchActivity(intent), goldName("testPlatformViewMultiple")); } @Test @@ -51,71 +53,62 @@ public void testPlatformViewMultipleBackgroundForeground() throws Exception { intent.putExtra("scenario_name", "platform_view_multiple_background_foreground"); ScreenshotUtil.capture( activityRule.launchActivity(intent), - "PlatformViewWithTextureViewUiTest_testPlatformViewMultipleBackgroundForeground"); + goldName("testPlatformViewMultipleBackgroundForeground")); } @Test public void testPlatformViewCliprect() throws Exception { intent.putExtra("scenario_name", "platform_view_cliprect"); ScreenshotUtil.capture( - activityRule.launchActivity(intent), - "PlatformViewWithTextureViewUiTest_testPlatformViewCliprect"); + activityRule.launchActivity(intent), goldName("testPlatformViewCliprect")); } @Test public void testPlatformViewCliprrect() throws Exception { intent.putExtra("scenario_name", "platform_view_cliprrect"); ScreenshotUtil.capture( - activityRule.launchActivity(intent), - "PlatformViewWithTextureViewUiTest_testPlatformViewCliprrect"); + activityRule.launchActivity(intent), goldName("testPlatformViewCliprrect")); } @Test public void testPlatformViewClippath() throws Exception { intent.putExtra("scenario_name", "platform_view_clippath"); ScreenshotUtil.capture( - activityRule.launchActivity(intent), - "PlatformViewWithTextureViewUiTest_testPlatformViewClippath"); + activityRule.launchActivity(intent), goldName("testPlatformViewClippath")); } @Test public void testPlatformViewTransform() throws Exception { intent.putExtra("scenario_name", "platform_view_transform"); ScreenshotUtil.capture( - activityRule.launchActivity(intent), - "PlatformViewWithTextureViewUiTest_testPlatformViewTransform"); + activityRule.launchActivity(intent), goldName("testPlatformViewTransform")); } @Test public void testPlatformViewOpacity() throws Exception { intent.putExtra("scenario_name", "platform_view_opacity"); ScreenshotUtil.capture( - activityRule.launchActivity(intent), - "PlatformViewWithTextureViewUiTest_testPlatformViewOpacity"); + activityRule.launchActivity(intent), goldName("testPlatformViewOpacity")); } @Test public void testPlatformViewRotate() throws Exception { intent.putExtra("scenario_name", "platform_view_rotate"); - ScreenshotUtil.capture( - activityRule.launchActivity(intent), - "PlatformViewWithTextureViewUiTest_testPlatformViewRotate"); + ScreenshotUtil.capture(activityRule.launchActivity(intent), goldName("testPlatformViewRotate")); } @Test public void testPlatformViewMultipleWithoutOverlays() throws Exception { intent.putExtra("scenario_name", "platform_view_multiple_without_overlays"); ScreenshotUtil.capture( - activityRule.launchActivity(intent), - "PlatformViewWithTextureViewUiTest_testPlatformViewMultipleWithoutOverlays"); + activityRule.launchActivity(intent), goldName("testPlatformViewMultipleWithoutOverlays")); } @Test public void testPlatformViewTwoIntersectingOverlays() throws Exception { intent.putExtra("scenario_name", "platform_view_two_intersecting_overlays"); ScreenshotUtil.capture( - activityRule.launchActivity(intent), - "PlatformViewWithTextureViewUiTest_testPlatformViewTwoIntersectingOverlays"); + activityRule.launchActivity(intent), goldName("testPlatformViewTwoIntersectingOverlays")); } @Test @@ -123,6 +116,6 @@ public void testPlatformViewWithoutOverlayIntersection() throws Exception { intent.putExtra("scenario_name", "platform_view_no_overlay_intersection"); ScreenshotUtil.capture( activityRule.launchActivity(intent), - "PlatformViewWithTextureViewUiTest_testPlatformViewWithoutOverlayIntersection"); + goldName("testPlatformViewWithoutOverlayIntersection")); } } diff --git a/testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/PlatformViewsActivity.java b/testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/PlatformViewsActivity.java index 8a361dd031cec..67e01ad45222e 100644 --- a/testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/PlatformViewsActivity.java +++ b/testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/PlatformViewsActivity.java @@ -8,22 +8,26 @@ import io.flutter.embedding.engine.FlutterEngine; public class PlatformViewsActivity extends TestActivity { + static final String TEXT_VIEW_PV = "scenarios/textPlatformView"; + static final String SURFACE_VIEW_PV = "scenarios/surfacePlatformView"; + static final String TEXTURE_VIEW_PV = "scenarios/texturePlatformView"; + @Override public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) { super.configureFlutterEngine(flutterEngine); flutterEngine .getPlatformViewsController() .getRegistry() - .registerViewFactory("scenarios/textPlatformView", new TextPlatformViewFactory()); + .registerViewFactory(TEXT_VIEW_PV, new TextPlatformViewFactory()); flutterEngine .getPlatformViewsController() .getRegistry() - .registerViewFactory("scenarios/surfacePlatformView", new SurfacePlatformViewFactory()); + .registerViewFactory(SURFACE_VIEW_PV, new SurfacePlatformViewFactory()); flutterEngine .getPlatformViewsController() .getRegistry() - .registerViewFactory("scenarios/texturePlatformView", new TexturePlatformViewFactory()); + .registerViewFactory(TEXTURE_VIEW_PV, new TexturePlatformViewFactory()); } } From 58e54dfb40da19ad01573a1216407c3872b31a2c Mon Sep 17 00:00:00 2001 From: Emmanuel Garcia Date: Fri, 17 Jun 2022 18:58:21 -0700 Subject: [PATCH 16/25] add missing view_type --- .../java/dev/flutter/scenariosui/MemoryLeakTests.java | 1 + .../java/dev/flutter/scenarios/PlatformViewsActivity.java | 6 +++--- .../src/main/java/dev/flutter/scenarios/TestActivity.java | 1 + 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/testing/scenario_app/android/app/src/androidTest/java/dev/flutter/scenariosui/MemoryLeakTests.java b/testing/scenario_app/android/app/src/androidTest/java/dev/flutter/scenariosui/MemoryLeakTests.java index 7556e52419a4b..7fbf4bdb9b218 100644 --- a/testing/scenario_app/android/app/src/androidTest/java/dev/flutter/scenariosui/MemoryLeakTests.java +++ b/testing/scenario_app/android/app/src/androidTest/java/dev/flutter/scenariosui/MemoryLeakTests.java @@ -29,6 +29,7 @@ public void platformViewHybridComposition_launchActivityFinishAndLaunchAgain() t Intent intent = new Intent(Intent.ACTION_MAIN); intent.putExtra("scenario_name", "platform_view"); intent.putExtra("use_android_view", true); + intent.putExtra("view_type", PlatformViewsActivity.TEXT_VIEW_PV); activityRule.launchActivity(intent); } diff --git a/testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/PlatformViewsActivity.java b/testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/PlatformViewsActivity.java index 67e01ad45222e..153a269256b76 100644 --- a/testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/PlatformViewsActivity.java +++ b/testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/PlatformViewsActivity.java @@ -8,9 +8,9 @@ import io.flutter.embedding.engine.FlutterEngine; public class PlatformViewsActivity extends TestActivity { - static final String TEXT_VIEW_PV = "scenarios/textPlatformView"; - static final String SURFACE_VIEW_PV = "scenarios/surfacePlatformView"; - static final String TEXTURE_VIEW_PV = "scenarios/texturePlatformView"; + public static final String TEXT_VIEW_PV = "scenarios/textPlatformView"; + public static final String SURFACE_VIEW_PV = "scenarios/surfacePlatformView"; + public static final String TEXTURE_VIEW_PV = "scenarios/texturePlatformView"; @Override public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) { diff --git a/testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/TestActivity.java b/testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/TestActivity.java index 80323a9a41944..8837035e0ebe0 100644 --- a/testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/TestActivity.java +++ b/testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/TestActivity.java @@ -84,6 +84,7 @@ public void onFlutterUiDisplayed() { test.put("name", "animated_color_square"); } test.put("use_android_view", launchIntent.getBooleanExtra("use_android_view", false)); + test.put("view_type", launchIntent.getStringExtra("view_type")); getScenarioParams(test); channel.invokeMethod("set_scenario", test); } From cffe771decacd25d9e89e7b15b18ef008725df7f Mon Sep 17 00:00:00 2001 From: Emmanuel Garcia Date: Sat, 18 Jun 2022 10:24:34 -0700 Subject: [PATCH 17/25] revert --- testing/scenario_app/lib/src/platform_view.dart | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/testing/scenario_app/lib/src/platform_view.dart b/testing/scenario_app/lib/src/platform_view.dart index cbc1f7c330d61..b1b39c27d801b 100644 --- a/testing/scenario_app/lib/src/platform_view.dart +++ b/testing/scenario_app/lib/src/platform_view.dart @@ -1047,14 +1047,12 @@ void addPlatformView( String text = 'platform view', double width = 500, double height = 500, - String? viewType, + String viewType = 'scenarios/textPlatformView', }) { - if (viewType == null && scenarioParams['view_type'] is String) { + if (scenarioParams['view_type'] is String) { viewType = scenarioParams['view_type']; } - assert(viewType != null, 'view type not set'); - final String platformViewKey = '$viewType-$id'; if (_createdPlatformViews.containsKey(platformViewKey)) { @@ -1099,8 +1097,8 @@ void addPlatformView( 'viewType'.length, ...utf8.encode('viewType'), _valueString, - viewType!.length, - ...utf8.encode(viewType!), + viewType.length, + ...utf8.encode(viewType), if (Platform.isAndroid && !usesAndroidHybridComposition) ...[ _valueString, 'width'.length, From 33877fabf732a90b27a9e766f3e8c0ee09723465 Mon Sep 17 00:00:00 2001 From: Emmanuel Garcia Date: Sat, 18 Jun 2022 19:00:24 -0700 Subject: [PATCH 18/25] fixes --- .../flutter/scenarios/PlatformViewsActivity.java | 4 ++-- .../scenarios/TexturePlatformViewFactory.java | 2 +- testing/scenario_app/lib/src/platform_view.dart | 4 ++-- testing/scenario_app/lib/src/scenario.dart | 14 +++++++------- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/PlatformViewsActivity.java b/testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/PlatformViewsActivity.java index 153a269256b76..a67be3ab2c76f 100644 --- a/testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/PlatformViewsActivity.java +++ b/testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/PlatformViewsActivity.java @@ -9,8 +9,8 @@ public class PlatformViewsActivity extends TestActivity { public static final String TEXT_VIEW_PV = "scenarios/textPlatformView"; - public static final String SURFACE_VIEW_PV = "scenarios/surfacePlatformView"; - public static final String TEXTURE_VIEW_PV = "scenarios/texturePlatformView"; + public static final String SURFACE_VIEW_PV = "scenarios/surfacePlatformV"; + public static final String TEXTURE_VIEW_PV = "scenarios/texturePlatformV"; @Override public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) { diff --git a/testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/TexturePlatformViewFactory.java b/testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/TexturePlatformViewFactory.java index 191c7ad82f437..6cf58eca04185 100644 --- a/testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/TexturePlatformViewFactory.java +++ b/testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/TexturePlatformViewFactory.java @@ -67,7 +67,7 @@ public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int hei canvas.drawColor(Color.WHITE); final Paint paint = new Paint(); - paint.setColor(Color.RED); + paint.setColor(Color.GREEN); canvas.drawCircle(canvas.getWidth() / 2, canvas.getHeight() / 2, 20, paint); textureView.unlockCanvasAndPost(canvas); } diff --git a/testing/scenario_app/lib/src/platform_view.dart b/testing/scenario_app/lib/src/platform_view.dart index b1b39c27d801b..f67fa66c84cf0 100644 --- a/testing/scenario_app/lib/src/platform_view.dart +++ b/testing/scenario_app/lib/src/platform_view.dart @@ -1085,9 +1085,9 @@ void addPlatformView( _valueMap, if (Platform.isIOS) 3, // 3 entries in map for iOS. if (Platform.isAndroid && !usesAndroidHybridComposition) - 6, // 6 entries in map for virtual displays on Android. + 6, // 6 entries in map for texture on Android. if (Platform.isAndroid && usesAndroidHybridComposition) - 5, // 5 entries in map for Android views. + 5, // 5 entries in map for hybrid composition on Android. _valueString, 'id'.length, ...utf8.encode('id'), diff --git a/testing/scenario_app/lib/src/scenario.dart b/testing/scenario_app/lib/src/scenario.dart index f0e9267dae1a1..6e66c862280e9 100644 --- a/testing/scenario_app/lib/src/scenario.dart +++ b/testing/scenario_app/lib/src/scenario.dart @@ -26,13 +26,13 @@ abstract class Scenario { /// /// See [PlatformDispatcher.onDrawFrame] for more details. void onDrawFrame() { - Future.delayed(const Duration(seconds: 1), () { - if (_didScheduleScreenshot) { - dispatcher.sendPlatformMessage('take_screenshot', null, null); - } else { - _didScheduleScreenshot = true; - dispatcher.scheduleFrame(); - } + if (_didScheduleScreenshot) { + dispatcher.sendPlatformMessage('take_screenshot', null, null); + return; + } + Future.delayed(const Duration(seconds: 2), () { + _didScheduleScreenshot = true; + dispatcher.scheduleFrame(); }); } From e44e6cdfe1c6db656471ad19b13984e6022b988b Mon Sep 17 00:00:00 2001 From: Emmanuel Garcia Date: Tue, 21 Jun 2022 10:11:49 -0700 Subject: [PATCH 19/25] talkback issue --- .../io/flutter/view/AccessibilityBridge.java | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/shell/platform/android/io/flutter/view/AccessibilityBridge.java b/shell/platform/android/io/flutter/view/AccessibilityBridge.java index aa6e38767947a..28b09507214b3 100644 --- a/shell/platform/android/io/flutter/view/AccessibilityBridge.java +++ b/shell/platform/android/io/flutter/view/AccessibilityBridge.java @@ -582,9 +582,12 @@ public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) { // // 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)) { + View embeddedView = + platformViewsAccessibilityDelegate.getPlatformViewById(semanticsNode.platformViewId); + if (embeddedView == null) { + return null; + } Rect bounds = semanticsNode.getGlobalRect(); return accessibilityViewEmbedder.getRootNode(embeddedView, semanticsNode.id, bounds); } @@ -902,8 +905,8 @@ && shouldSetCollectionInfo(semanticsNode)) { View embeddedView = platformViewsAccessibilityDelegate.getPlatformViewById(child.platformViewId); - // Add the embedded view as a child of the current accessibility node if it's using - // hybrid composition. + // Add the embedded view as a child of the current accessibility node if it's not + // using a virtual display. // // In this case, the view is in the Activity's view hierarchy, so it doesn't need to be // mirrored. @@ -913,8 +916,8 @@ && shouldSetCollectionInfo(semanticsNode)) { result.addChild(embeddedView); continue; } - result.addChild(rootAccessibilityView, child.id); } + result.addChild(rootAccessibilityView, child.id); } return result; } From 8f821f6f609e0d7c49a7ed3d0c12ee27d214edf9 Mon Sep 17 00:00:00 2001 From: Emmanuel Garcia Date: Tue, 21 Jun 2022 16:09:19 -0700 Subject: [PATCH 20/25] don't include TextureView since it breaks a11y in google maps --- .../plugin/platform/PlatformViewsController.java | 11 ++++++++--- .../flutter/scenarios/TexturePlatformViewFactory.java | 1 + 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java b/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java index e1e66ff25712e..2604cc4f9aa25 100644 --- a/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java +++ b/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java @@ -15,7 +15,6 @@ import android.util.SparseArray; import android.view.MotionEvent; import android.view.SurfaceView; -import android.view.TextureView; import android.view.View; import android.view.ViewGroup; import android.widget.FrameLayout; @@ -52,12 +51,18 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelegate { private static final String TAG = "PlatformViewsController"; - // These view types allow out-of-band drawing operation that don't notify the Android view + // These view types allow out-of-band drawing commands that don't notify the Android view // hierarchy. // To support these cases, Flutter hosts the embedded view in a VirtualDisplay, // and binds the VirtualDisplay to a GL texture that is then composed by the engine. + // However, there are a few issues with Virtual Displays. For example, they don't fully support + // accessibility due to https://github.com/flutter/flutter/issues/29717, + // and keyboard interactions may have non-derterministic behavior. + // Views that issue out-of-band drawing commands that aren't included in this array are + // required to call `View#invalidate()` to notify Flutter about the update. + // This isn't ideal, but given all the other limitations it's a reasonable tradeoff. // Related issue: https://github.com/flutter/flutter/issues/103630 - private static Class[] VIEW_TYPES_REQUIRE_VD = {SurfaceView.class, TextureView.class}; + private static Class[] VIEW_TYPES_REQUIRE_VD = {SurfaceView.class}; private final PlatformViewRegistryImpl registry; diff --git a/testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/TexturePlatformViewFactory.java b/testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/TexturePlatformViewFactory.java index 6cf58eca04185..5a65081353267 100644 --- a/testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/TexturePlatformViewFactory.java +++ b/testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/TexturePlatformViewFactory.java @@ -70,6 +70,7 @@ public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int hei paint.setColor(Color.GREEN); canvas.drawCircle(canvas.getWidth() / 2, canvas.getHeight() / 2, 20, paint); textureView.unlockCanvasAndPost(canvas); + textureView.invalidate(); } @Override From 1cde9af05dbe3cdc55dd5c4d0c7862883714862d Mon Sep 17 00:00:00 2001 From: Emmanuel Garcia Date: Tue, 21 Jun 2022 18:33:28 -0700 Subject: [PATCH 21/25] integration test (rotate activity + invalidate after frame) --- .../flutter/scenariosui/PlatformTextureUiTests.java | 5 ++++- .../dev/flutter/scenariosui/PlatformViewUiTests.java | 5 ++++- .../PlatformViewWithSurfaceViewUiTest.java | 5 ++++- .../PlatformViewWithTextureViewUiTest.java | 5 ++++- .../flutter/scenarios/TexturePlatformViewFactory.java | 11 ++++++++++- 5 files changed, 26 insertions(+), 5 deletions(-) diff --git a/testing/scenario_app/android/app/src/androidTest/java/dev/flutter/scenariosui/PlatformTextureUiTests.java b/testing/scenario_app/android/app/src/androidTest/java/dev/flutter/scenariosui/PlatformTextureUiTests.java index 420d2a3341f52..a4aa061f2a82c 100644 --- a/testing/scenario_app/android/app/src/androidTest/java/dev/flutter/scenariosui/PlatformTextureUiTests.java +++ b/testing/scenario_app/android/app/src/androidTest/java/dev/flutter/scenariosui/PlatformTextureUiTests.java @@ -5,6 +5,7 @@ package dev.flutter.scenariosui; import android.content.Intent; +import android.content.pm.ActivityInfo; import androidx.annotation.NonNull; import androidx.test.filters.LargeTest; import androidx.test.rule.ActivityTestRule; @@ -96,7 +97,9 @@ public void testPlatformViewOpacity() throws Exception { @Test public void testPlatformViewRotate() throws Exception { intent.putExtra("scenario_name", "platform_view_rotate"); - ScreenshotUtil.capture(activityRule.launchActivity(intent), goldName("testPlatformViewRotate")); + PlatformViewsActivity activity = activityRule.launchActivity(intent); + activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); + ScreenshotUtil.capture(activity, goldName("testPlatformViewRotate")); } @Test diff --git a/testing/scenario_app/android/app/src/androidTest/java/dev/flutter/scenariosui/PlatformViewUiTests.java b/testing/scenario_app/android/app/src/androidTest/java/dev/flutter/scenariosui/PlatformViewUiTests.java index a612dfdb6b441..50dc5dfef600b 100644 --- a/testing/scenario_app/android/app/src/androidTest/java/dev/flutter/scenariosui/PlatformViewUiTests.java +++ b/testing/scenario_app/android/app/src/androidTest/java/dev/flutter/scenariosui/PlatformViewUiTests.java @@ -5,6 +5,7 @@ package dev.flutter.scenariosui; import android.content.Intent; +import android.content.pm.ActivityInfo; import androidx.annotation.NonNull; import androidx.test.filters.LargeTest; import androidx.test.rule.ActivityTestRule; @@ -96,7 +97,9 @@ public void testPlatformViewOpacity() throws Exception { @Test public void testPlatformViewRotate() throws Exception { intent.putExtra("scenario_name", "platform_view_rotate"); - ScreenshotUtil.capture(activityRule.launchActivity(intent), goldName("testPlatformViewRotate")); + PlatformViewsActivity activity = activityRule.launchActivity(intent); + activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); + ScreenshotUtil.capture(activity, goldName("testPlatformViewRotate")); } @Test diff --git a/testing/scenario_app/android/app/src/androidTest/java/dev/flutter/scenariosui/PlatformViewWithSurfaceViewUiTest.java b/testing/scenario_app/android/app/src/androidTest/java/dev/flutter/scenariosui/PlatformViewWithSurfaceViewUiTest.java index 9695c848bda61..cbc629a792dde 100644 --- a/testing/scenario_app/android/app/src/androidTest/java/dev/flutter/scenariosui/PlatformViewWithSurfaceViewUiTest.java +++ b/testing/scenario_app/android/app/src/androidTest/java/dev/flutter/scenariosui/PlatformViewWithSurfaceViewUiTest.java @@ -5,6 +5,7 @@ package dev.flutter.scenariosui; import android.content.Intent; +import android.content.pm.ActivityInfo; import androidx.annotation.NonNull; import androidx.test.filters.LargeTest; import androidx.test.rule.ActivityTestRule; @@ -96,7 +97,9 @@ public void testPlatformViewOpacity() throws Exception { @Test public void testPlatformViewRotate() throws Exception { intent.putExtra("scenario_name", "platform_view_rotate"); - ScreenshotUtil.capture(activityRule.launchActivity(intent), goldName("testPlatformViewRotate")); + PlatformViewsActivity activity = activityRule.launchActivity(intent); + activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); + ScreenshotUtil.capture(activity, goldName("testPlatformViewRotate")); } @Test diff --git a/testing/scenario_app/android/app/src/androidTest/java/dev/flutter/scenariosui/PlatformViewWithTextureViewUiTest.java b/testing/scenario_app/android/app/src/androidTest/java/dev/flutter/scenariosui/PlatformViewWithTextureViewUiTest.java index a72a97a61a681..9a8dfd99878ee 100644 --- a/testing/scenario_app/android/app/src/androidTest/java/dev/flutter/scenariosui/PlatformViewWithTextureViewUiTest.java +++ b/testing/scenario_app/android/app/src/androidTest/java/dev/flutter/scenariosui/PlatformViewWithTextureViewUiTest.java @@ -5,6 +5,7 @@ package dev.flutter.scenariosui; import android.content.Intent; +import android.content.pm.ActivityInfo; import androidx.annotation.NonNull; import androidx.test.filters.LargeTest; import androidx.test.rule.ActivityTestRule; @@ -94,7 +95,9 @@ public void testPlatformViewOpacity() throws Exception { @Test public void testPlatformViewRotate() throws Exception { intent.putExtra("scenario_name", "platform_view_rotate"); - ScreenshotUtil.capture(activityRule.launchActivity(intent), goldName("testPlatformViewRotate")); + PlatformViewsActivity activity = activityRule.launchActivity(intent); + activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); + ScreenshotUtil.capture(activity, goldName("testPlatformViewRotate")); } @Test diff --git a/testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/TexturePlatformViewFactory.java b/testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/TexturePlatformViewFactory.java index 5a65081353267..e79dd16ccb4c5 100644 --- a/testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/TexturePlatformViewFactory.java +++ b/testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/TexturePlatformViewFactory.java @@ -10,6 +10,7 @@ import android.graphics.Color; import android.graphics.Paint; import android.graphics.SurfaceTexture; +import android.view.Choreographer; import android.view.TextureView; import android.view.View; import androidx.annotation.NonNull; @@ -70,7 +71,15 @@ public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int hei paint.setColor(Color.GREEN); canvas.drawCircle(canvas.getWidth() / 2, canvas.getHeight() / 2, 20, paint); textureView.unlockCanvasAndPost(canvas); - textureView.invalidate(); + Choreographer.getInstance() + .postFrameCallbackDelayed( + new Choreographer.FrameCallback() { + @Override + public void doFrame(long frameTimeNanos) { + textureView.invalidate(); + } + }, + 500); } @Override From 7b4708a2b40f7ab6d3372c0f0c486addc96b1de3 Mon Sep 17 00:00:00 2001 From: Emmanuel Garcia Date: Wed, 22 Jun 2022 14:04:48 -0700 Subject: [PATCH 22/25] feedback --- .../plugin/editing/TextInputPlugin.java | 35 ++++++++++--------- .../platform/PlatformViewsController.java | 30 ++++++---------- 2 files changed, 29 insertions(+), 36 deletions(-) diff --git a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java index 5c82b2ffc1735..a33a65b8420a7 100644 --- a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java +++ b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java @@ -105,7 +105,7 @@ public void show() { @Override public void hide() { - if (inputTarget.type == InputTarget.Type.HC_PLATFORM_VIEW) { + if (inputTarget.type == InputTarget.Type.PHYSICAL_DISPLAY_PLATFORM_VIEW) { notifyViewExited(); } else { hideTextInput(mView); @@ -196,7 +196,7 @@ ImeSyncDeferringInsetsCallback getImeSyncCallback() { * display to another. */ public void lockPlatformViewInputConnection() { - if (inputTarget.type == InputTarget.Type.VD_PLATFORM_VIEW) { + if (inputTarget.type == InputTarget.Type.VIRTUAL_DISPLAY_PLATFORM_VIEW) { isInputConnectionLocked = true; } } @@ -207,7 +207,7 @@ public void lockPlatformViewInputConnection() { *

See also: @{link lockPlatformViewInputConnection}. */ public void unlockPlatformViewInputConnection() { - if (inputTarget.type == InputTarget.Type.VD_PLATFORM_VIEW) { + if (inputTarget.type == InputTarget.Type.VIRTUAL_DISPLAY_PLATFORM_VIEW) { isInputConnectionLocked = false; } } @@ -295,11 +295,11 @@ public InputConnection createInputConnection( return null; } - if (inputTarget.type == InputTarget.Type.HC_PLATFORM_VIEW) { + if (inputTarget.type == InputTarget.Type.PHYSICAL_DISPLAY_PLATFORM_VIEW) { return null; } - if (inputTarget.type == InputTarget.Type.VD_PLATFORM_VIEW) { + if (inputTarget.type == InputTarget.Type.VIRTUAL_DISPLAY_PLATFORM_VIEW) { if (isInputConnectionLocked) { return lastInputConnection; } @@ -364,8 +364,8 @@ 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) + if ((inputTarget.type == InputTarget.Type.VIRTUAL_DISPLAY_PLATFORM_VIEW + || inputTarget.type == InputTarget.Type.PHYSICAL_DISPLAY_PLATFORM_VIEW) && inputTarget.id == platformViewId) { inputTarget = new InputTarget(InputTarget.Type.NO_TARGET, 0); notifyViewExited(); @@ -436,14 +436,15 @@ private void setPlatformViewTextInputClient(int platformViewId, boolean usesVirt 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 + // Not asking for focus here specifically manifested in a bug 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); + inputTarget = new InputTarget(InputTarget.Type.VIRTUAL_DISPLAY_PLATFORM_VIEW, platformViewId); mImm.restartInput(mView); mRestartInputPending = false; } else { - inputTarget = new InputTarget(InputTarget.Type.HC_PLATFORM_VIEW, platformViewId); + inputTarget = + new InputTarget(InputTarget.Type.PHYSICAL_DISPLAY_PLATFORM_VIEW, platformViewId); lastInputConnection = null; } } @@ -536,7 +537,7 @@ public void inspect(double x, double y) { @VisibleForTesting void clearTextInputClient() { - if (inputTarget.type == InputTarget.Type.VD_PLATFORM_VIEW) { + if (inputTarget.type == InputTarget.Type.VIRTUAL_DISPLAY_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 @@ -574,12 +575,12 @@ 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 presented on a virtual display. + VIRTUAL_DISPLAY_PLATFORM_VIEW, + // InputConnection is managed by a platform view that is embedded in the activity's view + // hierarchy. This view hierarchy is displayed in a physical display within the aplication + // display area. + PHYSICAL_DISPLAY_PLATFORM_VIEW, } public InputTarget(@NonNull Type type, int id) { diff --git a/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java b/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java index 2604cc4f9aa25..0de7294d6a83b 100644 --- a/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java +++ b/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java @@ -57,12 +57,12 @@ public class PlatformViewsController implements PlatformViewsAccessibilityDelega // and binds the VirtualDisplay to a GL texture that is then composed by the engine. // However, there are a few issues with Virtual Displays. For example, they don't fully support // accessibility due to https://github.com/flutter/flutter/issues/29717, - // and keyboard interactions may have non-derterministic behavior. + // and keyboard interactions may have non-deterministic behavior. // Views that issue out-of-band drawing commands that aren't included in this array are // required to call `View#invalidate()` to notify Flutter about the update. // This isn't ideal, but given all the other limitations it's a reasonable tradeoff. // Related issue: https://github.com/flutter/flutter/issues/103630 - private static Class[] VIEW_TYPES_REQUIRE_VD = {SurfaceView.class}; + private static Class[] VIEW_TYPES_REQUIRE_VIRTUAL_DISPLAY = {SurfaceView.class}; private final PlatformViewRegistryImpl registry; @@ -246,17 +246,17 @@ public long createForTextureLayer( final int physicalHeight = toPhysicalPixels(request.logicalHeight); // Case 1. Add the view to a virtual display if the embedded view contains any of the - // VIEW_TYPES_REQUIRE_VD view types. + // VIEW_TYPES_REQUIRE_VIRTUAL_DISPLAY view types. // These views allow out-of-band graphics operations that aren't notified to the Android // view hierarchy via callbacks such as ViewParent#onDescendantInvalidated(). // The virtual display is wired up to a GL texture that is composed by the Flutter engine. // Also, use virtual display if the API level is 20, 21 or 22 since the Case 2. requires // at least API level 23. - final boolean shouldUseVD = - ViewUtils.hasChildViewOfType(embeddedView, VIEW_TYPES_REQUIRE_VD) + final boolean shouldUseVirtualDisplay = + ViewUtils.hasChildViewOfType(embeddedView, VIEW_TYPES_REQUIRE_VIRTUAL_DISPLAY) || Build.VERSION.SDK_INT < 23; - if (!usesSoftwareRendering && shouldUseVD) { + if (!usesSoftwareRendering && shouldUseVirtualDisplay) { validateVirtualDisplayDimensions(physicalWidth, physicalHeight); Log.i(TAG, "Hosting view in a virtual display for platform view: " + viewId); @@ -963,19 +963,11 @@ 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 - + "]."; + String.format( + "Creating a virtual display of size: [%d, %d] " + + "may result in problems (https://github.com/flutter/flutter/issues/2897). " + + "It is larger than the device screen size: [%d, %d].", + width, height, metrics.widthPixels, metrics.heightPixels); Log.w(TAG, message); } } From f0d1029848a99378e59b6e3317b98f1bc53567eb Mon Sep 17 00:00:00 2001 From: Emmanuel Garcia Date: Wed, 22 Jun 2022 14:46:34 -0700 Subject: [PATCH 23/25] dispose bug --- .../platform/PlatformViewsController.java | 45 +++++++------------ 1 file changed, 17 insertions(+), 28 deletions(-) diff --git a/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java b/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java index 0de7294d6a83b..e175399dd9cb7 100644 --- a/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java +++ b/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java @@ -368,25 +368,28 @@ public long createForTextureLayer( @Override public void dispose(int viewId) { + final PlatformView platformView = platformViews.get(viewId); + if (platformView == null) { + Log.e(TAG, "Disposing unknown platform view with id: " + viewId); + return; + } + platformViews.remove(viewId); + + try { + platformView.dispose(); + } catch (RuntimeException exception) { + Log.e(TAG, "Disposing platform view threw an exception", exception); + } + if (usesVirtualDisplay(viewId)) { final VirtualDisplayController vdController = vdControllers.get(viewId); final View embeddedView = vdController.getView(); if (embeddedView != null) { contextToEmbeddedView.remove(embeddedView.getContext()); } - contextToEmbeddedView.remove(vdController.getView().getContext()); - vdController.dispose(); vdControllers.remove(viewId); return; } - - final PlatformView platformView = platformViews.get(viewId); - if (platformView == null) { - Log.e(TAG, "Disposing unknown platform view with id: " + viewId); - return; - } - platformViews.remove(viewId); - platformView.dispose(); // The platform view is displayed using a TextureLayer and is inserted in the view // hierarchy. final PlatformViewWrapper viewWrapper = viewWrappers.get(viewId); @@ -756,9 +759,6 @@ public void attachToView(@NonNull FlutterView newFlutterView) { final PlatformView view = platformViews.valueAt(index); view.onFlutterViewAttached(flutterView); } - for (VirtualDisplayController controller : vdControllers.values()) { - controller.onFlutterViewAttached(flutterView); - } } /** @@ -779,11 +779,6 @@ public void detachFromView() { final FlutterMutatorView view = platformViewParent.valueAt(index); flutterView.removeView(view); } - // Inform all existing platform views that they are no longer associated with - // a Flutter View. - for (VirtualDisplayController controller : vdControllers.values()) { - controller.onFlutterViewDetached(); - } destroyOverlaySurfaces(); removeOverlaySurfaces(); @@ -865,11 +860,11 @@ public void onAttachedToJNI() { * PlatformViewsController} detaches from JNI. */ public void onDetachedFromJNI() { - flushAllViews(); + diposeAllViews(); } public void onPreEngineRestart() { - flushAllViews(); + diposeAllViews(); } @Override @@ -984,16 +979,10 @@ private int toLogicalPixels(double physicalPixels) { return (int) Math.round(physicalPixels / getDisplayDensity()); } - private void flushAllViews() { - for (VirtualDisplayController controller : vdControllers.values()) { - controller.dispose(); - } - vdControllers.clear(); - contextToEmbeddedView.clear(); - + private void diposeAllViews() { while (platformViews.size() > 0) { final int viewId = platformViews.keyAt(0); - platformViews.remove(viewId); + // Dispose deletes the entry from platformViews and clears associated resources. channelHandler.dispose(viewId); } } From 110bdb50baffd0836ae16a7f278cdc625ab4d6f0 Mon Sep 17 00:00:00 2001 From: Emmanuel Garcia Date: Wed, 22 Jun 2022 15:03:50 -0700 Subject: [PATCH 24/25] fix comment line, so it fits in a line after formatting --- .../plugin/editing/TextInputPlugin.java | 25 +++++++------------ 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java index a33a65b8420a7..7382cd49e34a2 100644 --- a/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java +++ b/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java @@ -540,25 +540,18 @@ void clearTextInputClient() { if (inputTarget.type == InputTarget.Type.VIRTUAL_DISPLAY_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. + // 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. + // 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. + // 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. + // 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); From 5a2c81b3d4613c4410c4237d331aeba865cdbd56 Mon Sep 17 00:00:00 2001 From: Emmanuel Garcia Date: Wed, 22 Jun 2022 18:32:33 -0700 Subject: [PATCH 25/25] handle clamp --- .../platform/PlatformViewsController.java | 18 ------- .../platform/VirtualDisplayController.java | 51 +++++++++++++++++-- .../PlatformViewWithSurfaceViewUiTest.java | 8 +++ .../scenario_app/lib/src/platform_view.dart | 36 +++++++++++++ testing/scenario_app/lib/src/scenarios.dart | 1 + 5 files changed, 92 insertions(+), 22 deletions(-) diff --git a/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java b/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java index e175399dd9cb7..4f78a732d1fe1 100644 --- a/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java +++ b/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java @@ -11,7 +11,6 @@ import android.content.Context; import android.content.MutableContextWrapper; import android.os.Build; -import android.util.DisplayMetrics; import android.util.SparseArray; import android.view.MotionEvent; import android.view.SurfaceView; @@ -257,8 +256,6 @@ public long createForTextureLayer( || Build.VERSION.SDK_INT < 23; if (!usesSoftwareRendering && shouldUseVirtualDisplay) { - validateVirtualDisplayDimensions(physicalWidth, physicalHeight); - Log.i(TAG, "Hosting view in a virtual display for platform view: " + viewId); // API level 20 is required to use VirtualDisplay#setSurface. ensureValidAndroidVersion(20); @@ -952,21 +949,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 = - String.format( - "Creating a virtual display of size: [%d, %d] " - + "may result in problems (https://github.com/flutter/flutter/issues/2897). " - + "It is larger than the device screen size: [%d, %d].", - width, height, metrics.widthPixels, metrics.heightPixels); - Log.w(TAG, message); - } - } - private float getDisplayDensity() { return context.getResources().getDisplayMetrics().density; } diff --git a/shell/platform/android/io/flutter/plugin/platform/VirtualDisplayController.java b/shell/platform/android/io/flutter/plugin/platform/VirtualDisplayController.java index 0b1c3e9dae5b6..e049d5cccbd44 100644 --- a/shell/platform/android/io/flutter/plugin/platform/VirtualDisplayController.java +++ b/shell/platform/android/io/flutter/plugin/platform/VirtualDisplayController.java @@ -10,16 +10,20 @@ import android.content.Context; import android.hardware.display.DisplayManager; import android.hardware.display.VirtualDisplay; +import android.util.DisplayMetrics; 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.Log; import io.flutter.view.TextureRegistry; +import java.util.Locale; @TargetApi(20) class VirtualDisplayController { + private static String TAG = "VirtualDisplayController"; public static VirtualDisplayController create( Context context, @@ -31,14 +35,53 @@ public static VirtualDisplayController create( int viewId, Object createParams, OnFocusChangeListener focusChangeListener) { - textureEntry.surfaceTexture().setDefaultBufferSize(width, height); + + int selectedWidth = width; + int selectedHeight = height; + + DisplayMetrics metrics = context.getResources().getDisplayMetrics(); + if (selectedWidth == 0 || selectedHeight == 0) { + return null; + } + // Prevent https://github.com/flutter/flutter/issues/2897. + if (selectedWidth > metrics.widthPixels || selectedHeight > metrics.heightPixels) { + float aspectRatio = (float) selectedWidth / (float) selectedHeight; + int maybeWidth = (int) (metrics.heightPixels * aspectRatio); + int maybeHeight = (int) (metrics.widthPixels / aspectRatio); + + if (maybeHeight <= metrics.heightPixels) { + selectedWidth = metrics.widthPixels; + selectedHeight = maybeHeight; + } else if (maybeWidth <= metrics.widthPixels) { + selectedHeight = metrics.heightPixels; + selectedWidth = maybeWidth; + } else { + return null; + } + + String message = + String.format( + Locale.US, + "Resizing virtual display of size: [%d, %d] to size [%d, %d] " + + "since it's larger than the device display size [%d, %d].", + width, + height, + selectedWidth, + selectedHeight, + metrics.widthPixels, + metrics.heightPixels); + Log.w(TAG, message); + } + + textureEntry.surfaceTexture().setDefaultBufferSize(selectedWidth, selectedHeight); 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); + displayManager.createVirtualDisplay( + "flutter-vd", selectedWidth, selectedHeight, densityDpi, surface, 0); if (virtualDisplay == null) { return null; @@ -54,8 +97,8 @@ public static VirtualDisplayController create( focusChangeListener, viewId, createParams); - controller.bufferWidth = width; - controller.bufferHeight = height; + controller.bufferWidth = selectedWidth; + controller.bufferHeight = selectedHeight; return controller; } diff --git a/testing/scenario_app/android/app/src/androidTest/java/dev/flutter/scenariosui/PlatformViewWithSurfaceViewUiTest.java b/testing/scenario_app/android/app/src/androidTest/java/dev/flutter/scenariosui/PlatformViewWithSurfaceViewUiTest.java index cbc629a792dde..b1fcf35d7d55d 100644 --- a/testing/scenario_app/android/app/src/androidTest/java/dev/flutter/scenariosui/PlatformViewWithSurfaceViewUiTest.java +++ b/testing/scenario_app/android/app/src/androidTest/java/dev/flutter/scenariosui/PlatformViewWithSurfaceViewUiTest.java @@ -123,4 +123,12 @@ public void testPlatformViewWithoutOverlayIntersection() throws Exception { activityRule.launchActivity(intent), goldName("testPlatformViewWithoutOverlayIntersection")); } + + @Test + public void testPlatformViewLargerThanDisplaySize() throws Exception { + // Regression test for https://github.com/flutter/flutter/issues/2897. + intent.putExtra("scenario_name", "platform_view_larger_than_display_size"); + ScreenshotUtil.capture( + activityRule.launchActivity(intent), goldName("testPlatformViewLargerThanDisplaySize")); + } } diff --git a/testing/scenario_app/lib/src/platform_view.dart b/testing/scenario_app/lib/src/platform_view.dart index f67fa66c84cf0..8f1704e6ef6a2 100644 --- a/testing/scenario_app/lib/src/platform_view.dart +++ b/testing/scenario_app/lib/src/platform_view.dart @@ -114,6 +114,42 @@ class PlatformViewNoOverlayIntersectionScenario extends Scenario } } + +/// A platform view that is larger than the display size. +/// This is only applicable on Android while using virtual displays. +/// Related issue: https://github.com/flutter/flutter/issues/2897. +class PlatformViewLargerThanDisplaySize extends Scenario + with _BasePlatformViewScenarioMixin { + /// Creates the PlatformView scenario. + /// + /// The [dispatcher] parameter must not be null. + PlatformViewLargerThanDisplaySize( + PlatformDispatcher dispatcher, { + required this.id, + }) : assert(dispatcher != null), + super(dispatcher); + + /// The platform view identifier. + final int id; + + @override + void onBeginFrame(Duration duration) { + final SceneBuilder builder = SceneBuilder(); + + addPlatformView( + id, + dispatcher: dispatcher, + sceneBuilder: builder, + width: 15000, + height: 60000, + ); + + finishBuilder( + builder, + ); + } +} + /// A simple platform view with an overlay that partially intersects with the platform view. class PlatformViewPartialIntersectionScenario extends Scenario with _BasePlatformViewScenarioMixin { diff --git a/testing/scenario_app/lib/src/scenarios.dart b/testing/scenario_app/lib/src/scenarios.dart index 95554d8a796f9..99304222bd311 100644 --- a/testing/scenario_app/lib/src/scenarios.dart +++ b/testing/scenario_app/lib/src/scenarios.dart @@ -24,6 +24,7 @@ Map _scenarios = { 'locale_initialization': () => LocaleInitialization(PlatformDispatcher.instance), 'platform_view': () => PlatformViewScenario(PlatformDispatcher.instance, id: _viewId++), 'platform_view_no_overlay_intersection': () => PlatformViewNoOverlayIntersectionScenario(PlatformDispatcher.instance, id: _viewId++), + 'platform_view_larger_than_display_size': () => PlatformViewLargerThanDisplaySize(PlatformDispatcher.instance, id: _viewId++), 'platform_view_partial_intersection': () => PlatformViewPartialIntersectionScenario(PlatformDispatcher.instance, id: _viewId++), 'platform_view_two_intersecting_overlays': () => PlatformViewTwoIntersectingOverlaysScenario(PlatformDispatcher.instance, id: _viewId++), 'platform_view_one_overlay_two_intersecting_overlays': () => PlatformViewOneOverlayTwoIntersectingOverlaysScenario(PlatformDispatcher.instance, id: _viewId++),