diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index 8d1d6795f8fd5..e39c787af335d 100644 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -2435,6 +2435,7 @@ ORIGIN: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/Platf ORIGIN: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewFactory.java + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewRegistry.java + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewRegistryImpl.java + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewRenderTarget.java + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewWrapper.java + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewsAccessibilityDelegate.java + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java + ../../../flutter/LICENSE @@ -5136,10 +5137,12 @@ FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/Platfor FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewFactory.java FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewRegistry.java FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewRegistryImpl.java +FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewRenderTarget.java FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewWrapper.java FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewsAccessibilityDelegate.java FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/SingleViewPresentation.java +FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/SurfaceTexturePlatformViewRenderTarget.java FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/VirtualDisplayController.java FILE: ../../../flutter/shell/platform/android/io/flutter/util/HandlerCompat.java FILE: ../../../flutter/shell/platform/android/io/flutter/util/PathUtils.java diff --git a/shell/platform/android/BUILD.gn b/shell/platform/android/BUILD.gn index 15bfe17524176..89871decbf6f5 100644 --- a/shell/platform/android/BUILD.gn +++ b/shell/platform/android/BUILD.gn @@ -297,10 +297,12 @@ android_java_sources = [ "io/flutter/plugin/platform/PlatformViewFactory.java", "io/flutter/plugin/platform/PlatformViewRegistry.java", "io/flutter/plugin/platform/PlatformViewRegistryImpl.java", + "io/flutter/plugin/platform/PlatformViewRenderTarget.java", "io/flutter/plugin/platform/PlatformViewWrapper.java", "io/flutter/plugin/platform/PlatformViewsAccessibilityDelegate.java", "io/flutter/plugin/platform/PlatformViewsController.java", "io/flutter/plugin/platform/SingleViewPresentation.java", + "io/flutter/plugin/platform/SurfaceTexturePlatformViewRenderTarget.java", "io/flutter/plugin/platform/VirtualDisplayController.java", "io/flutter/util/HandlerCompat.java", "io/flutter/util/PathUtils.java", diff --git a/shell/platform/android/io/flutter/plugin/platform/PlatformViewRenderTarget.java b/shell/platform/android/io/flutter/plugin/platform/PlatformViewRenderTarget.java new file mode 100644 index 0000000000000..da4d2b54bf5f1 --- /dev/null +++ b/shell/platform/android/io/flutter/plugin/platform/PlatformViewRenderTarget.java @@ -0,0 +1,45 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugin.platform; + +import android.graphics.Canvas; +import android.view.Surface; + +/** + * A PlatformViewRenderTarget interface allows an Android Platform View to be rendered into an + * offscreen buffer (usually a texture is involved) that the engine can compose into the + * FlutterView. + */ +public interface PlatformViewRenderTarget { + // Called when the render target should be resized. + public void resize(int width, int height); + + // Returns the currently specified width. + public int getWidth(); + + // Returns the currently specified height. + public int getHeight(); + + // Forwards call to Surface returned by getSurface. + // NOTE: If this returns null the RenderTarget is "full" and has no room for a + // new frame. + Canvas lockHardwareCanvas(); + + // Forwards call to Surface returned by getSurface. + // NOTE: Must be called if lockHardwareCanvas returns a non-null Canvas. + void unlockCanvasAndPost(Canvas canvas); + + // The id of this render target. + public long getId(); + + // Releases backing resources. + public void release(); + + // Returns true in the case that backing resource have been released. + public boolean isReleased(); + + // Returns the Surface to be rendered on to. + public Surface getSurface(); +} diff --git a/shell/platform/android/io/flutter/plugin/platform/PlatformViewWrapper.java b/shell/platform/android/io/flutter/plugin/platform/PlatformViewWrapper.java index d55b0a6b7ce7f..c5d2282b9dbb2 100644 --- a/shell/platform/android/io/flutter/plugin/platform/PlatformViewWrapper.java +++ b/shell/platform/android/io/flutter/plugin/platform/PlatformViewWrapper.java @@ -4,8 +4,6 @@ package io.flutter.plugin.platform; -import static android.content.ComponentCallbacks2.TRIM_MEMORY_COMPLETE; - import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.content.Context; @@ -14,10 +12,7 @@ import android.graphics.Matrix; import android.graphics.PorterDuff; import android.graphics.Rect; -import android.graphics.SurfaceTexture; -import android.os.Build; import android.view.MotionEvent; -import android.view.Surface; import android.view.View; import android.view.ViewParent; import android.view.ViewTreeObserver; @@ -30,85 +25,30 @@ import io.flutter.embedding.android.AndroidTouchProcessor; import io.flutter.util.ViewUtils; import io.flutter.view.TextureRegistry; -import java.util.concurrent.atomic.AtomicLong; /** - * Wraps a platform view to intercept gestures and project this view onto a {@link SurfaceTexture}. + * Wraps a platform view to intercept gestures and project this view onto a {@link + * PlatformViewRenderTarget}. * *

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

Since the view is in the Android view hierarchy, keyboard and accessibility interactions * behave normally. */ @TargetApi(23) -class PlatformViewWrapper extends FrameLayout { +public class PlatformViewWrapper extends FrameLayout { private static final String TAG = "PlatformViewWrapper"; private int prevLeft; private int prevTop; private int left; private int top; - private int bufferWidth; - private int bufferHeight; - private SurfaceTexture tx; - private Surface surface; private AndroidTouchProcessor touchProcessor; + private PlatformViewRenderTarget renderTarget; - @Nullable @VisibleForTesting ViewTreeObserver.OnGlobalFocusChangeListener activeFocusListener; - private final AtomicLong pendingFramesCount = new AtomicLong(0L); - - private final TextureRegistry.OnFrameConsumedListener frameConsumedListener = - new TextureRegistry.OnFrameConsumedListener() { - @Override - public void onFrameConsumed() { - if (Build.VERSION.SDK_INT == 29) { - pendingFramesCount.decrementAndGet(); - } - } - }; - - private boolean shouldRecreateSurfaceForLowMemory = false; - private final TextureRegistry.OnTrimMemoryListener trimMemoryListener = - new TextureRegistry.OnTrimMemoryListener() { - @Override - public void onTrimMemory(int level) { - // When a memory pressure warning is received and the level equal {@code - // ComponentCallbacks2.TRIM_MEMORY_COMPLETE}, the Android system releases the underlying - // surface. If we continue to use the surface (e.g., call lockHardwareCanvas), a crash - // occurs, and we found that this crash appeared on Android10 and above. - // See https://github.com/flutter/flutter/issues/103870 for more details. - // - // Here our workaround is to recreate the surface before using it. - if (level == TRIM_MEMORY_COMPLETE && Build.VERSION.SDK_INT >= 29) { - shouldRecreateSurfaceForLowMemory = true; - } - } - }; - - private void onFrameProduced() { - if (Build.VERSION.SDK_INT == 29) { - pendingFramesCount.incrementAndGet(); - } - } - - private void recreateSurfaceIfNeeded() { - if (shouldRecreateSurfaceForLowMemory) { - if (surface != null) { - surface.release(); - } - surface = createSurface(tx); - shouldRecreateSurfaceForLowMemory = false; - } - } - - private boolean shouldDrawToSurfaceNow() { - if (Build.VERSION.SDK_INT == 29) { - return pendingFramesCount.get() <= 0L; - } - return true; - } + private ViewTreeObserver.OnGlobalFocusChangeListener activeFocusListener; public PlatformViewWrapper(@NonNull Context context) { super(context); @@ -118,9 +58,13 @@ public PlatformViewWrapper(@NonNull Context context) { public PlatformViewWrapper( @NonNull Context context, @NonNull TextureRegistry.SurfaceTextureEntry textureEntry) { this(context); - textureEntry.setOnFrameConsumedListener(frameConsumedListener); - textureEntry.setOnTrimMemoryListener(trimMemoryListener); - setTexture(textureEntry.surfaceTexture()); + this.renderTarget = new SurfaceTexturePlatformViewRenderTarget(textureEntry); + } + + public PlatformViewWrapper( + @NonNull Context context, @NonNull PlatformViewRenderTarget renderTarget) { + this(context); + this.renderTarget = renderTarget; } /** @@ -132,62 +76,6 @@ public void setTouchProcessor(@Nullable AndroidTouchProcessor newTouchProcessor) touchProcessor = newTouchProcessor; } - /** - * Sets the texture where the view is projected onto. - * - *

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

{@link io.flutter.view.TextureRegistry} is responsible for creating and registering textures - * in the engine. Therefore, the engine is responsible for also releasing the texture. - * - * @param newTx The texture where the view is projected onto. - */ - @SuppressLint("NewApi") - public void setTexture(@Nullable SurfaceTexture newTx) { - if (Build.VERSION.SDK_INT < 23) { - Log.e( - TAG, - "Platform views cannot be displayed below API level 23. " - + "You can prevent this issue by setting `minSdkVersion: 23` in build.gradle."); - return; - } - - tx = newTx; - - if (bufferWidth > 0 && bufferHeight > 0) { - tx.setDefaultBufferSize(bufferWidth, bufferHeight); - } - - if (surface != null) { - surface.release(); - } - surface = createSurface(newTx); - - // Fill the entire canvas with a transparent color. - // As a result, the background color of the platform view container is displayed - // to the user until the platform view draws its first frame. - final Canvas canvas = surface.lockHardwareCanvas(); - try { - canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR); - onFrameProduced(); - } finally { - surface.unlockCanvasAndPost(canvas); - } - } - - @NonNull - @VisibleForTesting - protected Surface createSurface(@NonNull SurfaceTexture tx) { - return new Surface(tx); - } - - /** Returns the texture where the view is projected. */ - @Nullable - public SurfaceTexture getTexture() { - return tx; - } - /** * Sets the layout parameters for this view. * @@ -200,37 +88,31 @@ public void setLayoutParams(@NonNull FrameLayout.LayoutParams params) { top = params.topMargin; } - /** - * Sets the size of the image buffer. - * - * @param width The width of the screen buffer. - * @param height The height of the screen buffer. - */ - public void setBufferSize(int width, int height) { - bufferWidth = width; - bufferHeight = height; - if (tx != null) { - tx.setDefaultBufferSize(width, height); + public void resizeRenderTarget(int width, int height) { + if (renderTarget != null) { + renderTarget.resize(width, height); } } - /** Returns the image buffer width. */ - public int getBufferWidth() { - return bufferWidth; + public int getRenderTargetWidth() { + if (renderTarget != null) { + return renderTarget.getWidth(); + } + return 0; } - /** Returns the image buffer height. */ - public int getBufferHeight() { - return bufferHeight; + public int getRenderTargetHeight() { + if (renderTarget != null) { + return renderTarget.getHeight(); + } + return 0; } - /** Releases the surface. */ + /** Releases resources. */ public void release() { - // Don't release the texture. - tx = null; - if (surface != null) { - surface.release(); - surface = null; + if (renderTarget != null) { + renderTarget.release(); + renderTarget = null; } } @@ -271,42 +153,26 @@ public ViewParent invalidateChildInParent(int[] location, Rect dirty) { @Override @SuppressLint("NewApi") public void draw(Canvas canvas) { - if (surface == null) { + if (renderTarget == null) { super.draw(canvas); - Log.e(TAG, "Platform view cannot be composed without a surface."); + Log.e(TAG, "Platform view cannot be composed without a RenderTarget."); return; } - if (!surface.isValid()) { - Log.e(TAG, "Invalid surface. The platform view cannot be displayed."); - return; - } - if (tx == null || tx.isReleased()) { - Log.e(TAG, "Invalid texture. The platform view cannot be displayed."); + final Canvas targetCanvas = renderTarget.lockHardwareCanvas(); + if (targetCanvas == null) { + // Cannot render right now. + invalidate(); return; } - // We've observed on Android Q that we have to wait for the consumer of {@link SurfaceTexture} - // to consume the last image before continuing to draw, otherwise subsequent calls to - // {@code dequeueBuffer} to request a free buffer from the {@link BufferQueue} will fail. - // See https://github.com/flutter/flutter/issues/98722 - if (!shouldDrawToSurfaceNow()) { - // If there are still frames that are not consumed, we will draw them next time. - invalidate(); - } else { - // We try to recreate the surface before using it to avoid the crash: - // https://github.com/flutter/flutter/issues/103870 - recreateSurfaceIfNeeded(); + try { + // Fill the render target with transparent pixels. This is needed for platform views that + // expect a transparent background. + targetCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR); // Override the canvas that this subtree of views will use to draw. - final Canvas surfaceCanvas = surface.lockHardwareCanvas(); - try { - // Clear the current pixels in the canvas. - // This helps when a WebView renders an HTML document with transparent background. - surfaceCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR); - super.draw(surfaceCanvas); - onFrameProduced(); - } finally { - surface.unlockCanvasAndPost(surfaceCanvas); - } + super.draw(targetCanvas); + } finally { + renderTarget.unlockCanvasAndPost(targetCanvas); } } @@ -338,6 +204,11 @@ public boolean onTouchEvent(@NonNull MotionEvent event) { return touchProcessor.onTouchEvent(event, screenMatrix); } + @VisibleForTesting + public ViewTreeObserver.OnGlobalFocusChangeListener getActiveFocusListener() { + return this.activeFocusListener; + } + public void setOnDescendantFocusChangeListener(@NonNull OnFocusChangeListener userFocusListener) { unsetOnDescendantFocusChangeListener(); final ViewTreeObserver observer = getViewTreeObserver(); diff --git a/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java b/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java index 185e03a1129ec..832f00a83d017 100644 --- a/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java +++ b/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java @@ -341,8 +341,8 @@ public void resize( context == null ? originalDisplayDensity : getDisplayDensity(); onComplete.run( new PlatformViewsChannel.PlatformViewBufferSize( - toLogicalPixels(vdController.getBufferWidth(), displayDensity), - toLogicalPixels(vdController.getBufferHeight(), displayDensity))); + toLogicalPixels(vdController.getRenderTargetWidth(), displayDensity), + toLogicalPixels(vdController.getRenderTargetHeight(), displayDensity))); }); return; } @@ -361,9 +361,9 @@ 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 > viewWrapper.getBufferWidth() - || physicalHeight > viewWrapper.getBufferHeight()) { - viewWrapper.setBufferSize(physicalWidth, physicalHeight); + if (physicalWidth > viewWrapper.getRenderTargetWidth() + || physicalHeight > viewWrapper.getRenderTargetHeight()) { + viewWrapper.resizeRenderTarget(physicalWidth, physicalHeight); } final ViewGroup.LayoutParams viewWrapperLayoutParams = viewWrapper.getLayoutParams(); @@ -380,8 +380,8 @@ public void resize( } onComplete.run( new PlatformViewsChannel.PlatformViewBufferSize( - toLogicalPixels(viewWrapper.getBufferWidth()), - toLogicalPixels(viewWrapper.getBufferHeight()))); + toLogicalPixels(viewWrapper.getRenderTargetWidth()), + toLogicalPixels(viewWrapper.getRenderTargetHeight()))); } @Override @@ -615,7 +615,7 @@ public long configureForTextureLayerComposition( textureId = textureEntry.id(); } viewWrapper.setTouchProcessor(androidTouchProcessor); - viewWrapper.setBufferSize(physicalWidth, physicalHeight); + viewWrapper.resizeRenderTarget(physicalWidth, physicalHeight); final FrameLayout.LayoutParams viewWrapperLayoutParams = new FrameLayout.LayoutParams(physicalWidth, physicalHeight); diff --git a/shell/platform/android/io/flutter/plugin/platform/SurfaceTexturePlatformViewRenderTarget.java b/shell/platform/android/io/flutter/plugin/platform/SurfaceTexturePlatformViewRenderTarget.java new file mode 100644 index 0000000000000..4f2fb12beac22 --- /dev/null +++ b/shell/platform/android/io/flutter/plugin/platform/SurfaceTexturePlatformViewRenderTarget.java @@ -0,0 +1,179 @@ +package io.flutter.plugin.platform; + +import static android.content.ComponentCallbacks2.TRIM_MEMORY_COMPLETE; + +import android.annotation.TargetApi; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.PorterDuff; +import android.graphics.SurfaceTexture; +import android.os.Build; +import android.view.Surface; +import io.flutter.Log; +import io.flutter.view.TextureRegistry; +import io.flutter.view.TextureRegistry.SurfaceTextureEntry; +import java.util.concurrent.atomic.AtomicLong; + +@TargetApi(26) +public class SurfaceTexturePlatformViewRenderTarget implements PlatformViewRenderTarget { + private static final String TAG = "SurfaceTexturePlatformViewRenderTarget"; + + private final AtomicLong pendingFramesCount = new AtomicLong(0L); + + private void onFrameProduced() { + if (Build.VERSION.SDK_INT == 29) { + pendingFramesCount.incrementAndGet(); + } + } + + private final SurfaceTextureEntry surfaceTextureEntry; + + private SurfaceTexture surfaceTexture; + private Surface surface; + private int bufferWidth = 0; + private int bufferHeight = 0; + + private final TextureRegistry.OnFrameConsumedListener frameConsumedListener = + new TextureRegistry.OnFrameConsumedListener() { + @Override + public void onFrameConsumed() { + if (Build.VERSION.SDK_INT == 29) { + pendingFramesCount.decrementAndGet(); + } + } + }; + + private boolean shouldRecreateSurfaceForLowMemory = false; + private final TextureRegistry.OnTrimMemoryListener trimMemoryListener = + new TextureRegistry.OnTrimMemoryListener() { + @Override + public void onTrimMemory(int level) { + // When a memory pressure warning is received and the level equal {@code + // ComponentCallbacks2.TRIM_MEMORY_COMPLETE}, the Android system releases the + // underlying + // surface. If we continue to use the surface (e.g., call lockHardwareCanvas), a + // crash + // occurs, and we found that this crash appeared on Android10 and above. + // See https://github.com/flutter/flutter/issues/103870 for more details. + // + // Here our workaround is to recreate the surface before using it. + if (level == TRIM_MEMORY_COMPLETE && Build.VERSION.SDK_INT >= 29) { + shouldRecreateSurfaceForLowMemory = true; + } + } + }; + + private void recreateSurfaceIfNeeded() { + if (!shouldRecreateSurfaceForLowMemory) { + return; + } + if (surface != null) { + surface.release(); + surface = null; + } + surface = createSurface(); + shouldRecreateSurfaceForLowMemory = false; + } + + protected Surface createSurface() { + return new Surface(surfaceTexture); + } + + private void init() { + if (bufferWidth > 0 && bufferHeight > 0) { + surfaceTexture.setDefaultBufferSize(bufferWidth, bufferHeight); + } + if (surface != null) { + surface.release(); + surface = null; + } + surface = createSurface(); + + // Fill the entire canvas with a transparent color. + // As a result, the background color of the platform view container is displayed + // to the user until the platform view draws its first frame. + final Canvas canvas = lockHardwareCanvas(); + try { + canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR); + } finally { + unlockCanvasAndPost(canvas); + } + } + + /** Implementation of PlatformViewRenderTarget */ + public SurfaceTexturePlatformViewRenderTarget(SurfaceTextureEntry surfaceTextureEntry) { + if (Build.VERSION.SDK_INT < 23) { + throw new UnsupportedOperationException( + "Platform views cannot be displayed below API level 23" + + "You can prevent this issue by setting `minSdkVersion: 23` in build.gradle."); + } + this.surfaceTextureEntry = surfaceTextureEntry; + this.surfaceTexture = surfaceTextureEntry.surfaceTexture(); + surfaceTextureEntry.setOnFrameConsumedListener(frameConsumedListener); + surfaceTextureEntry.setOnTrimMemoryListener(trimMemoryListener); + init(); + } + + public Canvas lockHardwareCanvas() { + recreateSurfaceIfNeeded(); + + // We've observed on Android Q that we have to wait for the consumer of {@link + // SurfaceTexture} + // to consume the last image before continuing to draw, otherwise subsequent + // calls to + // {@code dequeueBuffer} to request a free buffer from the {@link BufferQueue} + // will fail. + // See https://github.com/flutter/flutter/issues/98722 + if (Build.VERSION.SDK_INT == 29 && pendingFramesCount.get() > 0L) { + return null; + } + if (surfaceTexture == null || surfaceTexture.isReleased()) { + Log.e(TAG, "Invalid RenderTarget: null or already released SurfaceTexture"); + return null; + } + onFrameProduced(); + return surface.lockHardwareCanvas(); + } + + public void unlockCanvasAndPost(Canvas canvas) { + surface.unlockCanvasAndPost(canvas); + } + + public void resize(int width, int height) { + bufferWidth = width; + bufferHeight = height; + if (surfaceTexture != null) { + surfaceTexture.setDefaultBufferSize(bufferWidth, bufferHeight); + } + } + + public int getWidth() { + return bufferWidth; + } + + public int getHeight() { + return bufferHeight; + } + + public long getId() { + return this.surfaceTextureEntry.id(); + } + + public boolean isReleased() { + return surfaceTexture == null; + } + + public void release() { + // Don't release the texture. + surfaceTexture = null; + if (surface != null) { + surface.release(); + surface = null; + } + } + + public Surface getSurface() { + recreateSurfaceIfNeeded(); + return surface; + } +} diff --git a/shell/platform/android/io/flutter/plugin/platform/VirtualDisplayController.java b/shell/platform/android/io/flutter/plugin/platform/VirtualDisplayController.java index 3438db5dd648c..20886358086be 100644 --- a/shell/platform/android/io/flutter/plugin/platform/VirtualDisplayController.java +++ b/shell/platform/android/io/flutter/plugin/platform/VirtualDisplayController.java @@ -12,8 +12,8 @@ import android.hardware.display.VirtualDisplay; import android.util.DisplayMetrics; import android.view.MotionEvent; -import android.view.Surface; import android.view.View; +import android.view.View.OnFocusChangeListener; import android.view.ViewTreeObserver; import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; @@ -33,12 +33,16 @@ public static VirtualDisplayController create( int viewId, Object createParams, OnFocusChangeListener focusChangeListener) { - - DisplayMetrics metrics = context.getResources().getDisplayMetrics(); if (width == 0 || height == 0) { return null; } + DisplayManager displayManager = + (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE); + final DisplayMetrics metrics = context.getResources().getDisplayMetrics(); + final PlatformViewRenderTarget renderTarget = + new SurfaceTexturePlatformViewRenderTarget(textureEntry); + // Virtual Display crashes for some PlatformViews if the width or height is bigger // than the physical screen size. We have tried to clamp or scale down the size to prevent // the crash, but both solutions lead to unwanted behavior because the @@ -49,14 +53,15 @@ public static VirtualDisplayController create( // TODO(cyanglaz): find a way to prevent the crash without introducing size mistach betewen // virtual display and AndroidPlatformView widget. // https://github.com/flutter/flutter/issues/93115 - 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; + renderTarget.resize(width, height); VirtualDisplay virtualDisplay = - displayManager.createVirtualDisplay("flutter-vd", width, height, densityDpi, surface, 0); + displayManager.createVirtualDisplay( + "flutter-vd#" + viewId, + width, + height, + metrics.densityDpi, + renderTarget.getSurface(), + 0); if (virtualDisplay == null) { return null; @@ -67,13 +72,11 @@ public static VirtualDisplayController create( accessibilityEventsDelegate, virtualDisplay, view, - surface, + renderTarget, textureEntry, focusChangeListener, viewId, createParams); - controller.bufferWidth = width; - controller.bufferHeight = height; return controller; } @@ -82,31 +85,31 @@ public static VirtualDisplayController create( private final Context context; private final AccessibilityEventsDelegate accessibilityEventsDelegate; private final int densityDpi; + private final int viewId; private final TextureRegistry.SurfaceTextureEntry textureEntry; + private final PlatformViewRenderTarget renderTarget; private final OnFocusChangeListener focusChangeListener; - private final Surface surface; private VirtualDisplay virtualDisplay; - private int bufferWidth; - private int bufferHeight; private VirtualDisplayController( Context context, AccessibilityEventsDelegate accessibilityEventsDelegate, VirtualDisplay virtualDisplay, PlatformView view, - Surface surface, + PlatformViewRenderTarget renderTarget, TextureRegistry.SurfaceTextureEntry textureEntry, OnFocusChangeListener focusChangeListener, int viewId, Object createParams) { this.context = context; this.accessibilityEventsDelegate = accessibilityEventsDelegate; + this.renderTarget = renderTarget; this.textureEntry = textureEntry; this.focusChangeListener = focusChangeListener; - this.surface = surface; + this.viewId = viewId; this.virtualDisplay = virtualDisplay; - densityDpi = context.getResources().getDisplayMetrics().densityDpi; + this.densityDpi = context.getResources().getDisplayMetrics().densityDpi; presentation = new SingleViewPresentation( context, @@ -118,33 +121,33 @@ private VirtualDisplayController( presentation.show(); } - public int getBufferWidth() { - return bufferWidth; + public int getRenderTargetWidth() { + if (renderTarget != null) { + return renderTarget.getWidth(); + } + return 0; } - public int getBufferHeight() { - return bufferHeight; + public int getRenderTargetHeight() { + if (renderTarget != null) { + return renderTarget.getHeight(); + } + return 0; } 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(); - bufferWidth = width; - bufferHeight = height; - textureEntry.surfaceTexture().setDefaultBufferSize(width, height); - DisplayManager displayManager = + final DisplayManager displayManager = (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE); + renderTarget.resize(width, height); virtualDisplay = - displayManager.createVirtualDisplay("flutter-vd", width, height, densityDpi, surface, 0); + displayManager.createVirtualDisplay( + "flutter-vd#" + viewId, width, height, densityDpi, renderTarget.getSurface(), 0); final View embeddedView = getView(); // There's a bug in Android version older than O where view tree observer onDrawListeners don't diff --git a/shell/platform/android/test/io/flutter/plugin/platform/PlatformViewWrapperTest.java b/shell/platform/android/test/io/flutter/plugin/platform/PlatformViewWrapperTest.java index 8c1cc74a13ad1..ffb50fb65b510 100644 --- a/shell/platform/android/test/io/flutter/plugin/platform/PlatformViewWrapperTest.java +++ b/shell/platform/android/test/io/flutter/plugin/platform/PlatformViewWrapperTest.java @@ -4,19 +4,17 @@ import static org.junit.Assert.*; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; +import static org.mockito.Mockito.spy; import android.annotation.TargetApi; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; -import android.graphics.PorterDuff; -import android.graphics.SurfaceTexture; -import android.view.Surface; import android.view.View; +import android.view.View.OnFocusChangeListener; import android.view.ViewGroup; import android.view.ViewTreeObserver; import android.view.accessibility.AccessibilityEvent; -import androidx.annotation.NonNull; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import org.junit.Test; @@ -42,83 +40,6 @@ public void invalidateChildInParent_callsInvalidate() { verify(wrapper, times(1)).invalidate(); } - @Test - public void setTexture_writesToBuffer() { - final Surface surface = mock(Surface.class); - final PlatformViewWrapper wrapper = - new PlatformViewWrapper(ctx) { - @Override - protected Surface createSurface(@NonNull SurfaceTexture tx) { - return surface; - } - }; - - final SurfaceTexture tx = mock(SurfaceTexture.class); - when(tx.isReleased()).thenReturn(false); - - final Canvas canvas = mock(Canvas.class); - when(surface.lockHardwareCanvas()).thenReturn(canvas); - - // Test. - wrapper.setTexture(tx); - - // Verify. - verify(surface, times(1)).lockHardwareCanvas(); - verify(surface, times(1)).unlockCanvasAndPost(canvas); - verify(canvas, times(1)).drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR); - verifyNoMoreInteractions(surface); - verifyNoMoreInteractions(canvas); - } - - @Test - public void draw_writesToBuffer() { - final Surface surface = mock(Surface.class); - final PlatformViewWrapper wrapper = - new PlatformViewWrapper(ctx) { - @Override - protected Surface createSurface(@NonNull SurfaceTexture tx) { - return surface; - } - }; - - wrapper.addView( - new View(ctx) { - @Override - public void draw(Canvas canvas) { - super.draw(canvas); - canvas.drawColor(Color.RED); - } - }); - - final int size = 100; - wrapper.measure(size, size); - wrapper.layout(0, 0, size, size); - - final SurfaceTexture tx = mock(SurfaceTexture.class); - when(tx.isReleased()).thenReturn(false); - - when(surface.lockHardwareCanvas()).thenReturn(mock(Canvas.class)); - - wrapper.setTexture(tx); - - reset(surface); - - final Canvas canvas = mock(Canvas.class); - when(surface.lockHardwareCanvas()).thenReturn(canvas); - when(surface.isValid()).thenReturn(true); - - // Test. - wrapper.invalidate(); - wrapper.draw(new Canvas()); - - // Verify. - verify(canvas, times(1)).drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR); - verify(surface, times(1)).isValid(); - verify(surface, times(1)).lockHardwareCanvas(); - verify(surface, times(1)).unlockCanvasAndPost(canvas); - verifyNoMoreInteractions(surface); - } - @Test @Config( shadows = { @@ -140,36 +61,6 @@ public void onDraw(Canvas canvas) { verify(canvas, times(1)).drawColor(Color.RED); } - @Test - public void release() { - final Surface surface = mock(Surface.class); - final PlatformViewWrapper wrapper = - new PlatformViewWrapper(ctx) { - @Override - protected Surface createSurface(@NonNull SurfaceTexture tx) { - return surface; - } - }; - - final SurfaceTexture tx = mock(SurfaceTexture.class); - when(tx.isReleased()).thenReturn(false); - - final Canvas canvas = mock(Canvas.class); - when(surface.lockHardwareCanvas()).thenReturn(canvas); - - wrapper.setTexture(tx); - reset(surface); - reset(tx); - - // Test. - wrapper.release(); - - // Verify. - verify(surface, times(1)).release(); - verifyNoMoreInteractions(surface); - verifyNoMoreInteractions(tx); - } - @Test public void focusChangeListener_hasFocus() { final ViewTreeObserver viewTreeObserver = mock(ViewTreeObserver.class); @@ -255,16 +146,16 @@ public ViewTreeObserver getViewTreeObserver() { } }; - assertNull(view.activeFocusListener); + assertNull(view.getActiveFocusListener()); view.setOnDescendantFocusChangeListener(mock(OnFocusChangeListener.class)); - assertNotNull(view.activeFocusListener); + assertNotNull(view.getActiveFocusListener()); final ViewTreeObserver.OnGlobalFocusChangeListener activeFocusListener = - view.activeFocusListener; + view.getActiveFocusListener(); view.setOnDescendantFocusChangeListener(mock(OnFocusChangeListener.class)); - assertNotNull(view.activeFocusListener); + assertNotNull(view.getActiveFocusListener()); verify(viewTreeObserver, times(1)).removeOnGlobalFocusChangeListener(activeFocusListener); } @@ -282,16 +173,16 @@ public ViewTreeObserver getViewTreeObserver() { } }; - assertNull(view.activeFocusListener); + assertNull(view.getActiveFocusListener()); view.setOnDescendantFocusChangeListener(mock(OnFocusChangeListener.class)); - assertNotNull(view.activeFocusListener); + assertNotNull(view.getActiveFocusListener()); final ViewTreeObserver.OnGlobalFocusChangeListener activeFocusListener = - view.activeFocusListener; + view.getActiveFocusListener(); view.unsetOnDescendantFocusChangeListener(); - assertNull(view.activeFocusListener); + assertNull(view.getActiveFocusListener()); view.unsetOnDescendantFocusChangeListener(); verify(viewTreeObserver, times(1)).removeOnGlobalFocusChangeListener(activeFocusListener); diff --git a/shell/platform/android/test/io/flutter/plugin/platform/SurfaceTexturePlatformViewRenderTargetTest.java b/shell/platform/android/test/io/flutter/plugin/platform/SurfaceTexturePlatformViewRenderTargetTest.java new file mode 100644 index 0000000000000..a1ab6d75e1f29 --- /dev/null +++ b/shell/platform/android/test/io/flutter/plugin/platform/SurfaceTexturePlatformViewRenderTargetTest.java @@ -0,0 +1,128 @@ +package io.flutter.plugin.platform; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import android.annotation.TargetApi; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.PorterDuff; +import android.graphics.SurfaceTexture; +import android.view.Surface; +import android.view.View; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import io.flutter.view.TextureRegistry.SurfaceTextureEntry; +import org.junit.Test; +import org.junit.runner.RunWith; + +@TargetApi(31) +@RunWith(AndroidJUnit4.class) +public class SurfaceTexturePlatformViewRenderTargetTest { + private final Context ctx = ApplicationProvider.getApplicationContext(); + + @Test + public void create_clearsTexture() { + final Canvas canvas = mock(Canvas.class); + final Surface surface = mock(Surface.class); + when(surface.lockHardwareCanvas()).thenReturn(canvas); + when(surface.isValid()).thenReturn(true); + final SurfaceTexture surfaceTexture = mock(SurfaceTexture.class); + final SurfaceTextureEntry surfaceTextureEntry = mock(SurfaceTextureEntry.class); + when(surfaceTextureEntry.surfaceTexture()).thenReturn(surfaceTexture); + when(surfaceTexture.isReleased()).thenReturn(false); + + // Test. + final SurfaceTexturePlatformViewRenderTarget renderTarget = + new SurfaceTexturePlatformViewRenderTarget(surfaceTextureEntry) { + @Override + protected Surface createSurface() { + return surface; + } + }; + + // Verify. + verify(surface, times(1)).lockHardwareCanvas(); + verify(surface, times(1)).unlockCanvasAndPost(canvas); + verify(canvas, times(1)).drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR); + verifyNoMoreInteractions(surface); + verifyNoMoreInteractions(canvas); + } + + @Test + public void viewDraw_writesToBuffer() { + final Canvas canvas = mock(Canvas.class); + final Surface surface = mock(Surface.class); + when(surface.lockHardwareCanvas()).thenReturn(canvas); + when(surface.isValid()).thenReturn(true); + final SurfaceTexture surfaceTexture = mock(SurfaceTexture.class); + final SurfaceTextureEntry surfaceTextureEntry = mock(SurfaceTextureEntry.class); + when(surfaceTextureEntry.surfaceTexture()).thenReturn(surfaceTexture); + when(surfaceTexture.isReleased()).thenReturn(false); + + final SurfaceTexturePlatformViewRenderTarget renderTarget = + new SurfaceTexturePlatformViewRenderTarget(surfaceTextureEntry) { + @Override + protected Surface createSurface() { + return surface; + } + }; + + // Custom view. + final View platformView = + new View(ctx) { + @Override + public void draw(Canvas canvas) { + super.draw(canvas); + canvas.drawColor(Color.RED); + } + }; + final int size = 100; + platformView.measure(size, size); + platformView.layout(0, 0, size, size); + + // Test. + final Canvas c = renderTarget.lockHardwareCanvas(); + platformView.draw(c); + renderTarget.unlockCanvasAndPost(c); + + // Verify. + verify(canvas, times(1)).drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR); + verify(canvas, times(1)).drawColor(Color.RED); + verify(surface, times(2)).lockHardwareCanvas(); + verify(surface, times(2)).unlockCanvasAndPost(canvas); + verifyNoMoreInteractions(surface); + } + + @Test + public void release() { + final Canvas canvas = mock(Canvas.class); + final Surface surface = mock(Surface.class); + when(surface.lockHardwareCanvas()).thenReturn(canvas); + when(surface.isValid()).thenReturn(true); + final SurfaceTexture surfaceTexture = mock(SurfaceTexture.class); + final SurfaceTextureEntry surfaceTextureEntry = mock(SurfaceTextureEntry.class); + when(surfaceTextureEntry.surfaceTexture()).thenReturn(surfaceTexture); + when(surfaceTexture.isReleased()).thenReturn(false); + final SurfaceTexturePlatformViewRenderTarget renderTarget = + new SurfaceTexturePlatformViewRenderTarget(surfaceTextureEntry) { + @Override + protected Surface createSurface() { + return surface; + } + }; + + reset(surface); + reset(surfaceTexture); + + // Test. + renderTarget.release(); + + // Verify. + verify(surface, times(1)).release(); + verifyNoMoreInteractions(surface); + verifyNoMoreInteractions(surfaceTexture); + } +}