diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterView.java b/shell/platform/android/io/flutter/embedding/android/FlutterView.java index 8cf69c24cf568..1cb273e32a009 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterView.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterView.java @@ -949,13 +949,22 @@ public void detachFromFlutterEngine() { flutterEngine = null; } + @VisibleForTesting + @NonNull + public FlutterImageView createImageView() { + return new FlutterImageView( + getContext(), getWidth(), getHeight(), FlutterImageView.SurfaceKind.background); + } + + /** + * Converts the current render surface to a {@link FlutterImageView} if it's not one already. + * Otherwise, it resizes the {@link FlutterImageView} based on the current view size. + */ public void convertToImageView() { renderSurface.pause(); if (flutterImageView == null) { - flutterImageView = - new FlutterImageView( - getContext(), getWidth(), getHeight(), FlutterImageView.SurfaceKind.background); + flutterImageView = createImageView(); addView(flutterImageView); } else { flutterImageView.resizeIfNeeded(getWidth(), getHeight()); @@ -969,10 +978,13 @@ public void convertToImageView() { } /** - * If the surface is rendered by a {@code FlutterImageView}. Then, calling this method will stop - * rendering to a {@code FlutterImageView}, and use the previous surface instead. + * If the surface is rendered by a {@link FlutterImageView}, then calling this method will stop + * rendering to a {@link FlutterImageView}, and render on the previous surface instead. + * + * @param onDone a callback called when Flutter UI is rendered on the previous surface. Use this + * callback to perform cleanups. For example, destroy overlay surfaces. */ - public void revertImageView() { + public void revertImageView(@NonNull Runnable onDone) { if (flutterImageView == null) { Log.v(TAG, "Tried to revert the image view, but no image view is used."); return; @@ -981,12 +993,39 @@ public void revertImageView() { Log.v(TAG, "Tried to revert the image view, but no previous surface was used."); return; } - flutterImageView.detachFromRenderer(); renderSurface = previousRenderSurface; previousRenderSurface = null; - if (flutterEngine != null) { - renderSurface.attachToRenderer(flutterEngine.getRenderer()); + if (flutterEngine == null) { + flutterImageView.detachFromRenderer(); + onDone.run(); + return; + } + final FlutterRenderer renderer = flutterEngine.getRenderer(); + if (renderer == null) { + flutterImageView.detachFromRenderer(); + onDone.run(); + return; } + // Start rendering on the previous surface. + // This surface is typically `FlutterSurfaceView` or `FlutterTextureView`. + renderSurface.attachToRenderer(renderer); + + // Install a Flutter UI listener to wait until the first frame is rendered + // in the new surface to call the `onDone` callback. + renderer.addIsDisplayingFlutterUiListener( + new FlutterUiDisplayListener() { + @Override + public void onFlutterUiDisplayed() { + renderer.removeIsDisplayingFlutterUiListener(this); + onDone.run(); + flutterImageView.detachFromRenderer(); + } + + @Override + public void onFlutterUiNoLongerDisplayed() { + // no-op + } + }); } public void attachOverlaySurfaceToRender(FlutterImageView view) { diff --git a/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java b/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java index 1291d5db05c96..59fa7132e3008 100644 --- a/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java +++ b/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java @@ -90,7 +90,7 @@ * *

To invoke a native method that is not associated with a platform view, invoke it statically: * - *

{@code bool enabled = FlutterJNI.nativeGetIsSoftwareRenderingEnabled(); } + *

{@code bool enabled = FlutterJNI.getIsSoftwareRenderingEnabled(); } */ @Keep public class FlutterJNI { @@ -120,9 +120,13 @@ public static native void nativeInit( */ public static native void nativePrefetchDefaultFontManager(); - // TODO(mattcarroll): add javadocs + private native boolean nativeGetIsSoftwareRenderingEnabled(); + @UiThread - public native boolean nativeGetIsSoftwareRenderingEnabled(); + // TODO(mattcarroll): add javadocs + public boolean getIsSoftwareRenderingEnabled() { + return nativeGetIsSoftwareRenderingEnabled(); + } @Nullable // TODO(mattcarroll): add javadocs @@ -212,7 +216,12 @@ public boolean isAttached() { public void attachToNative(boolean isBackgroundView) { ensureRunningOnMainThread(); ensureNotAttachedToNative(); - nativePlatformViewId = nativeAttach(this, isBackgroundView); + nativePlatformViewId = performNativeAttach(this, isBackgroundView); + } + + @VisibleForTesting + public long performNativeAttach(@NonNull FlutterJNI flutterJNI, boolean isBackgroundView) { + return nativeAttach(flutterJNI, isBackgroundView); } private native long nativeAttach(@NonNull FlutterJNI flutterJNI, boolean isBackgroundView); @@ -279,7 +288,7 @@ public void removeIsDisplayingFlutterUiListener(@NonNull FlutterUiDisplayListene @SuppressWarnings("unused") @VisibleForTesting @UiThread - void onFirstFrame() { + public void onFirstFrame() { ensureRunningOnMainThread(); for (FlutterUiDisplayListener listener : flutterUiDisplayListeners) { diff --git a/shell/platform/android/io/flutter/embedding/engine/renderer/FlutterRenderer.java b/shell/platform/android/io/flutter/embedding/engine/renderer/FlutterRenderer.java index f0cd24adea675..0aa11c4f2aacf 100644 --- a/shell/platform/android/io/flutter/embedding/engine/renderer/FlutterRenderer.java +++ b/shell/platform/android/io/flutter/embedding/engine/renderer/FlutterRenderer.java @@ -314,7 +314,7 @@ private void unregisterTexture(long textureId) { // TODO(mattcarroll): describe the native behavior that this invokes public boolean isSoftwareRenderingEnabled() { - return flutterJNI.nativeGetIsSoftwareRenderingEnabled(); + return flutterJNI.getIsSoftwareRenderingEnabled(); } // TODO(mattcarroll): describe the native behavior that this invokes diff --git a/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java b/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java index ee7d393320a2b..7c064a6f90818 100644 --- a/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java +++ b/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java @@ -713,7 +713,7 @@ public void onDisplayPlatformView( int width, int height, int viewWidth, - int ViewHeight, + int viewHeight, FlutterMutatorsStack mutatorsStack) { initializeRootImageViewIfNeeded(); initializePlatformViewIfNeeded(viewId); @@ -723,7 +723,7 @@ public void onDisplayPlatformView( mutatorView.setVisibility(View.VISIBLE); mutatorView.bringToFront(); - FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(viewWidth, ViewHeight); + FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(viewWidth, viewHeight); View platformView = platformViews.get(viewId); platformView.setLayoutParams(layoutParams); platformView.bringToFront(); @@ -753,6 +753,20 @@ public void onBeginFrame() { } public void onEndFrame() { + final FlutterView view = (FlutterView) flutterView; + // If there are no platform views in the current frame, + // then revert the image view surface and use the previous surface. + // + // Otherwise, acquire the latest image. + if (flutterViewConvertedToImageView && currentFrameUsedPlatformViewIds.isEmpty()) { + flutterViewConvertedToImageView = false; + view.revertImageView( + () -> { + // Destroy overlay surfaces once the surface reversion is completed. + finishFrame(false); + }); + return; + } // Whether the current frame was rendered using ImageReaders. // // Since the image readers may not have images available at this point, @@ -762,22 +776,12 @@ public void onEndFrame() { // If one of the surfaces doesn't have an image, the frame may be incomplete and must be // dropped. // For example, a toolbar widget painted by Flutter may not be rendered. - boolean isFrameRenderedUsingImageReaders = false; - - if (flutterViewConvertedToImageView) { - FlutterView view = (FlutterView) flutterView; - // If there are no platform views in the current frame, - // then revert the image view surface and use the previous surface. - // - // Otherwise, acquire the latest image. - if (currentFrameUsedPlatformViewIds.isEmpty()) { - view.revertImageView(); - flutterViewConvertedToImageView = false; - } else { - isFrameRenderedUsingImageReaders = view.acquireLatestImageViewFrame(); - } - } + boolean isFrameRenderedUsingImageReaders = + flutterViewConvertedToImageView && view.acquireLatestImageViewFrame(); + finishFrame(isFrameRenderedUsingImageReaders); + } + private void finishFrame(boolean isFrameRenderedUsingImageReaders) { for (int i = 0; i < overlayLayerViews.size(); i++) { int overlayId = overlayLayerViews.keyAt(i); FlutterImageView overlayView = overlayLayerViews.valueAt(i); @@ -818,6 +822,14 @@ public void onEndFrame() { } } + @VisibleForTesting + @TargetApi(19) + public FlutterOverlaySurface createOverlaySurface(@NonNull FlutterImageView imageView) { + final int id = nextOverlayLayerId++; + overlayLayerViews.put(id, imageView); + return new FlutterOverlaySurface(id, imageView.getSurface()); + } + @TargetApi(19) public FlutterOverlaySurface createOverlaySurface() { // Overlay surfaces have the same size as the background surface. @@ -826,17 +838,12 @@ public FlutterOverlaySurface createOverlaySurface() { // if the drawings they contain have a different tight bound. // // The final view size is determined when its frame is set. - FlutterImageView imageView = + return createOverlaySurface( new FlutterImageView( flutterView.getContext(), flutterView.getWidth(), flutterView.getHeight(), - FlutterImageView.SurfaceKind.overlay); - - int id = nextOverlayLayerId++; - overlayLayerViews.put(id, imageView); - - return new FlutterOverlaySurface(id, imageView.getSurface()); + FlutterImageView.SurfaceKind.overlay)); } public void destroyOverlaySurfaces() { diff --git a/shell/platform/android/io/flutter/view/FlutterView.java b/shell/platform/android/io/flutter/view/FlutterView.java index ed1499ced9641..f472b07c0753c 100644 --- a/shell/platform/android/io/flutter/view/FlutterView.java +++ b/shell/platform/android/io/flutter/view/FlutterView.java @@ -169,7 +169,7 @@ public FlutterView(Context context, AttributeSet attrs, FlutterNativeView native dartExecutor = mNativeView.getDartExecutor(); flutterRenderer = new FlutterRenderer(mNativeView.getFlutterJNI()); - mIsSoftwareRenderingEnabled = mNativeView.getFlutterJNI().nativeGetIsSoftwareRenderingEnabled(); + mIsSoftwareRenderingEnabled = mNativeView.getFlutterJNI().getIsSoftwareRenderingEnabled(); mMetrics = new ViewportMetrics(); mMetrics.devicePixelRatio = context.getResources().getDisplayMetrics().density; setFocusable(true); 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 fad8dca431591..659056afc5132 100644 --- a/shell/platform/android/test/io/flutter/plugin/platform/PlatformViewsControllerTest.java +++ b/shell/platform/android/test/io/flutter/plugin/platform/PlatformViewsControllerTest.java @@ -8,15 +8,29 @@ import android.content.Context; import android.content.res.AssetManager; import android.view.MotionEvent; +import android.view.Surface; +import android.view.SurfaceHolder; +import android.view.SurfaceView; import android.view.View; import android.view.ViewParent; +import io.flutter.embedding.android.FlutterImageView; import io.flutter.embedding.android.FlutterView; import io.flutter.embedding.android.MotionEventTracker; +import io.flutter.embedding.engine.FlutterEngine; import io.flutter.embedding.engine.FlutterJNI; +import io.flutter.embedding.engine.FlutterOverlaySurface; import io.flutter.embedding.engine.dart.DartExecutor; import io.flutter.embedding.engine.mutatorsstack.FlutterMutatorView; +import io.flutter.embedding.engine.mutatorsstack.FlutterMutatorsStack; +import io.flutter.embedding.engine.renderer.FlutterRenderer; +import io.flutter.embedding.engine.systemchannels.AccessibilityChannel; +import io.flutter.embedding.engine.systemchannels.KeyEventChannel; +import io.flutter.embedding.engine.systemchannels.MouseCursorChannel; +import io.flutter.embedding.engine.systemchannels.SettingsChannel; +import io.flutter.embedding.engine.systemchannels.TextInputChannel; import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.StandardMethodCodec; +import io.flutter.plugin.localization.LocalizationPlugin; import java.nio.ByteBuffer; import java.util.Arrays; import java.util.HashMap; @@ -27,6 +41,9 @@ import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; +import org.robolectric.annotation.Implementation; +import org.robolectric.annotation.Implements; +import org.robolectric.shadows.ShadowSurfaceView; @Config(manifest = Config.NONE) @RunWith(RobolectricTestRunner.class) @@ -196,6 +213,7 @@ public void itUsesActionEventTypeFromMotionEventForHybridPlatformViews() { } @Test + @Config(shadows = {ShadowFlutterJNI.class}) public void getPlatformViewById__hybridComposition() { PlatformViewsController platformViewsController = new PlatformViewsController(); @@ -223,6 +241,7 @@ public void getPlatformViewById__hybridComposition() { } @Test + @Config(shadows = {ShadowFlutterJNI.class}) public void initializePlatformViewIfNeeded__throwsIfViewIsNull() { PlatformViewsController platformViewsController = new PlatformViewsController(); @@ -254,6 +273,7 @@ public void initializePlatformViewIfNeeded__throwsIfViewIsNull() { } @Test + @Config(shadows = {ShadowFlutterJNI.class}) public void initializePlatformViewIfNeeded__throwsIfViewHasParent() { PlatformViewsController platformViewsController = new PlatformViewsController(); @@ -286,6 +306,7 @@ public void initializePlatformViewIfNeeded__throwsIfViewHasParent() { } @Test + @Config(shadows = {ShadowFlutterJNI.class}) public void disposeAndroidView__hybridComposition() { PlatformViewsController platformViewsController = new PlatformViewsController(); @@ -324,10 +345,72 @@ public void disposeAndroidView__hybridComposition() { assertTrue(androidView.getParent() instanceof FlutterMutatorView); } + @Test + @Config(shadows = {ShadowFlutterSurfaceView.class, ShadowFlutterJNI.class}) + public void onEndFrame__destroysOverlaySurfaceAfterFrameOnFlutterSurfaceView() { + final PlatformViewsController platformViewsController = new PlatformViewsController(); + + final int platformViewId = 0; + assertNull(platformViewsController.getPlatformViewById(platformViewId)); + + final PlatformViewFactory viewFactory = mock(PlatformViewFactory.class); + final PlatformView platformView = mock(PlatformView.class); + when(platformView.getView()).thenReturn(mock(View.class)); + when(viewFactory.create(any(), eq(platformViewId), any())).thenReturn(platformView); + + platformViewsController.getRegistry().registerViewFactory("testType", viewFactory); + + final FlutterJNI jni = new FlutterJNI(); + jni.attachToNative(false); + attach(jni, platformViewsController); + + jni.onFirstFrame(); + + // Simulate create call from the framework. + createPlatformView(jni, platformViewsController, platformViewId, "testType"); + + // Produce a frame that displays a platform view and an overlay surface. + platformViewsController.onBeginFrame(); + platformViewsController.onDisplayPlatformView( + platformViewId, + /* x=*/ 0, + /* y=*/ 0, + /* width=*/ 10, + /* height=*/ 10, + /* viewWidth=*/ 10, + /* viewHeight=*/ 10, + /* mutatorsStack=*/ new FlutterMutatorsStack()); + + final FlutterImageView overlayImageView = mock(FlutterImageView.class); + when(overlayImageView.acquireLatestImage()).thenReturn(true); + + final FlutterOverlaySurface overlaySurface = + platformViewsController.createOverlaySurface(overlayImageView); + platformViewsController.onDisplayOverlaySurface( + overlaySurface.getId(), /* x=*/ 0, /* y=*/ 0, /* width=*/ 10, /* height=*/ 10); + + platformViewsController.onEndFrame(); + + // Simulate first frame from the framework. + jni.onFirstFrame(); + + verify(overlayImageView, never()).detachFromRenderer(); + + // Produce a frame that doesn't display platform views. + platformViewsController.onBeginFrame(); + platformViewsController.onEndFrame(); + + verify(overlayImageView, never()).detachFromRenderer(); + + // Simulate first frame from the framework. + jni.onFirstFrame(); + verify(overlayImageView, times(1)).detachFromRenderer(); + } + private static byte[] encodeMethodCall(MethodCall call) { - ByteBuffer buffer = StandardMethodCodec.INSTANCE.encodeMethodCall(call); + final ByteBuffer buffer = StandardMethodCodec.INSTANCE.encodeMethodCall(call); buffer.rewind(); - byte[] dest = new byte[buffer.remaining()]; + final byte[] dest = new byte[buffer.remaining()]; buffer.get(dest); return dest; } @@ -337,12 +420,13 @@ private static void createPlatformView( PlatformViewsController platformViewsController, int platformViewId, String viewType) { - Map platformViewCreateArguments = new HashMap<>(); + final Map platformViewCreateArguments = new HashMap<>(); platformViewCreateArguments.put("hybrid", true); platformViewCreateArguments.put("id", platformViewId); platformViewCreateArguments.put("viewType", viewType); platformViewCreateArguments.put("direction", 0); - MethodCall platformCreateMethodCall = new MethodCall("create", platformViewCreateArguments); + final MethodCall platformCreateMethodCall = + new MethodCall("create", platformViewCreateArguments); jni.handlePlatformMessage( "flutter/platform_views", encodeMethodCall(platformCreateMethodCall), /*replyId=*/ 0); @@ -350,22 +434,124 @@ private static void createPlatformView( private static void disposePlatformView( FlutterJNI jni, PlatformViewsController platformViewsController, int platformViewId) { - Map platformViewDisposeArguments = new HashMap<>(); + + final Map platformViewDisposeArguments = new HashMap<>(); platformViewDisposeArguments.put("hybrid", true); platformViewDisposeArguments.put("id", platformViewId); - MethodCall platformDisposeMethodCall = new MethodCall("dispose", platformViewDisposeArguments); + + final MethodCall platformDisposeMethodCall = + new MethodCall("dispose", platformViewDisposeArguments); jni.handlePlatformMessage( "flutter/platform_views", encodeMethodCall(platformDisposeMethodCall), /*replyId=*/ 0); } - private void attach(FlutterJNI jni, PlatformViewsController platformViewsController) { - DartExecutor executor = new DartExecutor(jni, mock(AssetManager.class)); + private static void attach(FlutterJNI jni, PlatformViewsController platformViewsController) { + final DartExecutor executor = new DartExecutor(jni, mock(AssetManager.class)); executor.onAttachedToJNI(); - Context context = RuntimeEnvironment.application.getApplicationContext(); + final Context context = RuntimeEnvironment.application.getApplicationContext(); platformViewsController.attach(context, null, executor); - platformViewsController.attachToView(mock(FlutterView.class)); + final FlutterView view = + new FlutterView(context, FlutterView.RenderMode.surface) { + @Override + public FlutterImageView createImageView() { + final FlutterImageView view = mock(FlutterImageView.class); + when(view.acquireLatestImage()).thenReturn(true); + return mock(FlutterImageView.class); + } + }; + + view.layout(0, 0, 100, 100); + + final FlutterEngine engine = mock(FlutterEngine.class); + when(engine.getRenderer()).thenReturn(new FlutterRenderer(jni)); + when(engine.getMouseCursorChannel()).thenReturn(mock(MouseCursorChannel.class)); + when(engine.getTextInputChannel()).thenReturn(mock(TextInputChannel.class)); + when(engine.getSettingsChannel()).thenReturn(new SettingsChannel(executor)); + when(engine.getPlatformViewsController()).thenReturn(platformViewsController); + when(engine.getLocalizationPlugin()).thenReturn(mock(LocalizationPlugin.class)); + when(engine.getKeyEventChannel()).thenReturn(mock(KeyEventChannel.class)); + when(engine.getAccessibilityChannel()).thenReturn(mock(AccessibilityChannel.class)); + + view.attachToFlutterEngine(engine); + platformViewsController.attachToView(view); + } + + @Implements(FlutterJNI.class) + public static class ShadowFlutterJNI { + + public ShadowFlutterJNI() {} + + @Implementation + public boolean getIsSoftwareRenderingEnabled() { + return false; + } + + @Implementation + public long performNativeAttach(FlutterJNI flutterJNI, boolean isBackgroundView) { + return 1; + } + + @Implementation + public void dispatchPlatformMessage( + String channel, ByteBuffer message, int position, int responseId) {} + + @Implementation + public void onSurfaceCreated(Surface surface) {} + + @Implementation + public void onSurfaceDestroyed() {} + + @Implementation + public void onSurfaceWindowChanged(Surface surface) {} + + @Implementation + public void setViewportMetrics( + float devicePixelRatio, + int physicalWidth, + int physicalHeight, + int physicalPaddingTop, + int physicalPaddingRight, + int physicalPaddingBottom, + int physicalPaddingLeft, + int physicalViewInsetTop, + int physicalViewInsetRight, + int physicalViewInsetBottom, + int physicalViewInsetLeft, + int systemGestureInsetTop, + int systemGestureInsetRight, + int systemGestureInsetBottom, + int systemGestureInsetLeft) {} + + @Implementation + public void invokePlatformMessageResponseCallback( + int responseId, ByteBuffer message, int position) {} + } + + @Implements(SurfaceView.class) + public static class ShadowFlutterSurfaceView extends ShadowSurfaceView { + private final FakeSurfaceHolder holder = new FakeSurfaceHolder(); + + public static class FakeSurfaceHolder extends ShadowSurfaceView.FakeSurfaceHolder { + private final Surface surface = mock(Surface.class); + + public Surface getSurface() { + return surface; + } + + @Implementation + public void addCallback(SurfaceHolder.Callback callback) { + callback.surfaceCreated(this); + } + } + + public ShadowFlutterSurfaceView() {} + + @Implementation + public SurfaceHolder getHolder() { + return holder; + } } }