From 970376c5f9395be44c977fc69f7e85e019043919 Mon Sep 17 00:00:00 2001 From: John McCutchan Date: Thu, 26 Oct 2023 13:57:28 -0700 Subject: [PATCH] Don't re-initialize the default RenderSurface when returning from hybrid composition mode When we enter hybrid composition mode we 'pause' the default RenderSurface (implemented by SurfaceView or TextureView) and swap to an ImageReader based RenderSurface. When we return from hybrid composition mode we recreate and re-initialize the real RenderSurface as if it was being used for the first time. This broke Platform Views in an internal app b/306122497 because we would incorrectly tell the texture to attach when it was never detached. This CL changes the protocol so that when we return from hybrid composition mode we only swap the RenderSurface and do not re-create it. This avoids doing a bunch of unnecessary work and fixes the logic error of re-attaching textures that were never detached. --- shell/common/shell.cc | 3 +- .../embedding/android/FlutterImageView.java | 4 ++ .../embedding/android/FlutterSurfaceView.java | 69 +++++++++++-------- .../embedding/android/FlutterTextureView.java | 61 +++++++++------- .../embedding/android/FlutterView.java | 4 +- .../engine/renderer/FlutterRenderer.java | 22 +++--- .../engine/renderer/RenderSurface.java | 7 ++ .../platform/android/platform_view_android.cc | 2 + .../engine/renderer/FlutterRendererTest.java | 66 ++++++++++++++---- 9 files changed, 157 insertions(+), 81 deletions(-) diff --git a/shell/common/shell.cc b/shell/common/shell.cc index b6318c45d9943..88948eb9d7d62 100644 --- a/shell/common/shell.cc +++ b/shell/common/shell.cc @@ -814,7 +814,6 @@ void Shell::OnPlatformViewCreated(std::unique_ptr surface) { const bool should_post_raster_task = !task_runners_.GetRasterTaskRunner()->RunsTasksOnCurrentThread(); - fml::AutoResetWaitableEvent latch; auto raster_task = fml::MakeCopyable( [&waiting_for_first_frame = waiting_for_first_frame_, // rasterizer = rasterizer_->GetWeakPtr(), // @@ -841,8 +840,8 @@ void Shell::OnPlatformViewCreated(std::unique_ptr surface) { // weak pointer. However, we are preventing the platform view from being // collected by using a latch. auto* platform_view = platform_view_.get(); - FML_DCHECK(platform_view); + fml::AutoResetWaitableEvent latch; auto io_task = [io_manager = io_manager_->GetWeakPtr(), platform_view, ui_task_runner = task_runners_.GetUITaskRunner(), ui_task, diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterImageView.java b/shell/platform/android/io/flutter/embedding/android/FlutterImageView.java index 1379b483043a4..65bf6936cc88b 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterImageView.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterImageView.java @@ -186,6 +186,10 @@ public void pause() { // Not supported. } + public void resume() { + // Not supported. + } + /** * Acquires the next image to be drawn to the {@link android.graphics.Canvas}. Returns true if * there's an image available in the queue. diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterSurfaceView.java b/shell/platform/android/io/flutter/embedding/android/FlutterSurfaceView.java index 5944aa0d635ee..9fdaa4c08bb1f 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterSurfaceView.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterSurfaceView.java @@ -38,9 +38,12 @@ public class FlutterSurfaceView extends SurfaceView implements RenderSurface { private final boolean renderTransparently; private boolean isSurfaceAvailableForRendering = false; private boolean isPaused = false; - private boolean isAttachedToFlutterRenderer = false; @Nullable private FlutterRenderer flutterRenderer; + private boolean shouldNotify() { + return flutterRenderer != null && !isPaused; + } + // Connects the {@code Surface} beneath this {@code SurfaceView} with Flutter's native code. // Callbacks are received by this Object and then those messages are forwarded to our // FlutterRenderer, and then on to the JNI bridge over to native Flutter code. @@ -51,7 +54,7 @@ public void surfaceCreated(@NonNull SurfaceHolder holder) { Log.v(TAG, "SurfaceHolder.Callback.startRenderingToSurface()"); isSurfaceAvailableForRendering = true; - if (isAttachedToFlutterRenderer) { + if (shouldNotify()) { connectSurfaceToRenderer(); } } @@ -60,7 +63,7 @@ public void surfaceCreated(@NonNull SurfaceHolder holder) { public void surfaceChanged( @NonNull SurfaceHolder holder, int format, int width, int height) { Log.v(TAG, "SurfaceHolder.Callback.surfaceChanged()"); - if (isAttachedToFlutterRenderer) { + if (shouldNotify()) { changeSurfaceSize(width, height); } } @@ -70,7 +73,7 @@ public void surfaceDestroyed(@NonNull SurfaceHolder holder) { Log.v(TAG, "SurfaceHolder.Callback.stopRenderingToSurface()"); isSurfaceAvailableForRendering = false; - if (isAttachedToFlutterRenderer) { + if (shouldNotify()) { disconnectSurfaceFromRenderer(); } } @@ -183,25 +186,15 @@ public void attachToRenderer(@NonNull FlutterRenderer flutterRenderer) { if (this.flutterRenderer != null) { Log.v( TAG, - "Already connected to a FlutterRenderer. Detaching from old one and attaching to new one."); + "Already connected to a FlutterRenderer. Detaching from old one and attaching to new" + + " one."); this.flutterRenderer.stopRenderingToSurface(); this.flutterRenderer.removeIsDisplayingFlutterUiListener(flutterUiDisplayListener); } this.flutterRenderer = flutterRenderer; - isAttachedToFlutterRenderer = true; - this.flutterRenderer.addIsDisplayingFlutterUiListener(flutterUiDisplayListener); - - // If we're already attached to an Android window then we're now attached to both a renderer - // and the Android window. We can begin rendering now. - if (isSurfaceAvailableForRendering) { - Log.v( - TAG, - "Surface is available for rendering. Connecting FlutterRenderer to Android surface."); - connectSurfaceToRenderer(); - } - isPaused = false; + resume(); } /** @@ -222,13 +215,13 @@ public void detachFromRenderer() { disconnectSurfaceFromRenderer(); } + pause(); + // Make the SurfaceView invisible to avoid showing a black rectangle. setAlpha(0.0f); - flutterRenderer.removeIsDisplayingFlutterUiListener(flutterUiDisplayListener); - flutterRenderer = null; - isAttachedToFlutterRenderer = false; + } else { Log.w(TAG, "detachFromRenderer() invoked when no FlutterRenderer was attached."); } @@ -239,22 +232,37 @@ public void detachFromRenderer() { * UI to this {@code FlutterSurfaceView}. */ public void pause() { - if (flutterRenderer != null) { - // Don't remove the `flutterUiDisplayListener` as `onFlutterUiDisplayed()` will make - // the `FlutterSurfaceView` visible. - flutterRenderer = null; - isPaused = true; - isAttachedToFlutterRenderer = false; - } else { + if (flutterRenderer == null) { Log.w(TAG, "pause() invoked when no FlutterRenderer was attached."); + return; + } + isPaused = true; + } + + public void resume() { + if (flutterRenderer == null) { + Log.w(TAG, "resume() invoked when no FlutterRenderer was attached."); + return; + } + this.flutterRenderer.addIsDisplayingFlutterUiListener(flutterUiDisplayListener); + + // If we're already attached to an Android window then we're now attached to both a renderer + // and the Android window. We can begin rendering now. + if (isSurfaceAvailableForRendering) { + Log.v( + TAG, + "Surface is available for rendering. Connecting FlutterRenderer to Android surface."); + connectSurfaceToRenderer(); } + isPaused = false; } // FlutterRenderer and getSurfaceTexture() must both be non-null. private void connectSurfaceToRenderer() { if (flutterRenderer == null || getHolder() == null) { throw new IllegalStateException( - "connectSurfaceToRenderer() should only be called when flutterRenderer and getHolder() are non-null."); + "connectSurfaceToRenderer() should only be called when flutterRenderer and getHolder()" + + " are non-null."); } // When connecting the surface to the renderer, it's possible that the surface is currently // paused. For instance, when a platform view is displayed, the current FlutterSurfaceView @@ -285,9 +293,10 @@ private void changeSurfaceSize(int width, int height) { private void disconnectSurfaceFromRenderer() { if (flutterRenderer == null) { throw new IllegalStateException( - "disconnectSurfaceFromRenderer() should only be called when flutterRenderer is non-null."); + "disconnectSurfaceFromRenderer() should only be called when flutterRenderer is" + + " non-null."); } flutterRenderer.stopRenderingToSurface(); } -} +} \ No newline at end of file diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterTextureView.java b/shell/platform/android/io/flutter/embedding/android/FlutterTextureView.java index 5e2d8e03dfb67..11237a1a70348 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterTextureView.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterTextureView.java @@ -35,11 +35,14 @@ public class FlutterTextureView extends TextureView implements RenderSurface { private static final String TAG = "FlutterTextureView"; private boolean isSurfaceAvailableForRendering = false; - private boolean isAttachedToFlutterRenderer = false; private boolean isPaused = false; @Nullable private FlutterRenderer flutterRenderer; @Nullable private Surface renderSurface; + private boolean shouldNotify() { + return flutterRenderer != null && !isPaused; + } + // Connects the {@code SurfaceTexture} beneath this {@code TextureView} with Flutter's native // code. // Callbacks are received by this Object and then those messages are forwarded to our @@ -55,7 +58,7 @@ public void onSurfaceTextureAvailable( // If we're already attached to a FlutterRenderer then we're now attached to both a // renderer // and the Android window, so we can begin rendering now. - if (isAttachedToFlutterRenderer) { + if (shouldNotify()) { connectSurfaceToRenderer(); } } @@ -64,7 +67,7 @@ public void onSurfaceTextureAvailable( public void onSurfaceTextureSizeChanged( @NonNull SurfaceTexture surface, int width, int height) { Log.v(TAG, "SurfaceTextureListener.onSurfaceTextureSizeChanged()"); - if (isAttachedToFlutterRenderer) { + if (shouldNotify()) { changeSurfaceSize(width, height); } } @@ -82,7 +85,7 @@ public boolean onSurfaceTextureDestroyed(@NonNull SurfaceTexture surface) { // If we're attached to a FlutterRenderer then we need to notify it that our // SurfaceTexture // has been destroyed. - if (isAttachedToFlutterRenderer) { + if (shouldNotify()) { disconnectSurfaceFromRenderer(); } @@ -139,21 +142,14 @@ public void attachToRenderer(@NonNull FlutterRenderer flutterRenderer) { if (this.flutterRenderer != null) { Log.v( TAG, - "Already connected to a FlutterRenderer. Detaching from old one and attaching to new one."); + "Already connected to a FlutterRenderer. Detaching from old one and attaching to new" + + " one."); this.flutterRenderer.stopRenderingToSurface(); } this.flutterRenderer = flutterRenderer; - isAttachedToFlutterRenderer = true; - // If we're already attached to an Android window then we're now attached to both a renderer - // and the Android window. We can begin rendering now. - if (isSurfaceAvailableForRendering) { - Log.v( - TAG, - "Surface is available for rendering. Connecting FlutterRenderer to Android surface."); - connectSurfaceToRenderer(); - } + resume(); } /** @@ -174,8 +170,9 @@ public void detachFromRenderer() { disconnectSurfaceFromRenderer(); } + pause(); + flutterRenderer = null; - isAttachedToFlutterRenderer = false; } else { Log.w(TAG, "detachFromRenderer() invoked when no FlutterRenderer was attached."); } @@ -186,13 +183,28 @@ public void detachFromRenderer() { * UI to this {@code FlutterTextureView}. */ public void pause() { - if (flutterRenderer != null) { - flutterRenderer = null; - isPaused = true; - isAttachedToFlutterRenderer = false; - } else { + if (flutterRenderer == null) { Log.w(TAG, "pause() invoked when no FlutterRenderer was attached."); + return; } + isPaused = true; + } + + public void resume() { + if (flutterRenderer == null) { + Log.w(TAG, "resume() invoked when no FlutterRenderer was attached."); + return; + } + + // If we're already attached to an Android window then we're now attached to both a renderer + // and the Android window. We can begin rendering now. + if (isSurfaceAvailableForRendering) { + Log.v( + TAG, + "Surface is available for rendering. Connecting FlutterRenderer to Android surface."); + connectSurfaceToRenderer(); + } + isPaused = false; } /** @@ -209,7 +221,8 @@ public void setRenderSurface(Surface renderSurface) { private void connectSurfaceToRenderer() { if (flutterRenderer == null || getSurfaceTexture() == null) { throw new IllegalStateException( - "connectSurfaceToRenderer() should only be called when flutterRenderer and getSurfaceTexture() are non-null."); + "connectSurfaceToRenderer() should only be called when flutterRenderer and" + + " getSurfaceTexture() are non-null."); } // Definitively release the surface to avoid leaked closeables, just in case @@ -220,7 +233,6 @@ private void connectSurfaceToRenderer() { renderSurface = new Surface(getSurfaceTexture()); flutterRenderer.startRenderingToSurface(renderSurface, isPaused); - isPaused = false; } // FlutterRenderer must be non-null. @@ -243,7 +255,8 @@ private void changeSurfaceSize(int width, int height) { private void disconnectSurfaceFromRenderer() { if (flutterRenderer == null) { throw new IllegalStateException( - "disconnectSurfaceFromRenderer() should only be called when flutterRenderer is non-null."); + "disconnectSurfaceFromRenderer() should only be called when flutterRenderer is" + + " non-null."); } flutterRenderer.stopRenderingToSurface(); @@ -252,4 +265,4 @@ private void disconnectSurfaceFromRenderer() { renderSurface = null; } } -} +} \ No newline at end of file diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterView.java b/shell/platform/android/io/flutter/embedding/android/FlutterView.java index 5e4f1d44f4d21..f5393bbaa43a0 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterView.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterView.java @@ -1369,9 +1369,9 @@ public void revertImageView(@NonNull Runnable onDone) { onDone.run(); return; } - // Start rendering on the previous surface. + // Resume rendering to the previous surface. // This surface is typically `FlutterSurfaceView` or `FlutterTextureView`. - renderSurface.attachToRenderer(renderer); + renderSurface.resume(); // Install a Flutter UI listener to wait until the first frame is rendered // in the new surface to call the `onDone` callback. 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 8b77bc7378cb8..81eea4ea2ac3c 100644 --- a/shell/platform/android/io/flutter/embedding/engine/renderer/FlutterRenderer.java +++ b/shell/platform/android/io/flutter/embedding/engine/renderer/FlutterRenderer.java @@ -467,20 +467,26 @@ protected void finalize() throws Throwable { * android.view.TextureView.SurfaceTextureListener} * * @param surface The render surface. - * @param keepCurrentSurface True if the current active surface should not be released. + * @param onlySwap True if the current active surface should not be detached. */ - public void startRenderingToSurface(@NonNull Surface surface, boolean keepCurrentSurface) { - // Don't stop rendering the surface if it's currently paused. - // Stop rendering to the surface releases the associated native resources, which - // causes a glitch when showing platform views. - // For more, https://github.com/flutter/flutter/issues/95343 - if (this.surface != null && !keepCurrentSurface) { + public void startRenderingToSurface(@NonNull Surface surface, boolean onlySwap) { + if (!onlySwap) { + // Stop rendering to the surface releases the associated native resources, which + // causes a glitch when toggling between rendering to an image view (hybrid composition) and + // rendering directly to a Surface or Texture view. For more, + // https://github.com/flutter/flutter/issues/95343 stopRenderingToSurface(); } this.surface = surface; - flutterJNI.onSurfaceCreated(surface); + if (onlySwap) { + // In the swap case we are just swapping the surface that we render to. + flutterJNI.onSurfaceWindowChanged(surface); + } else { + // In the non-swap case we are creating a new surface to render to. + flutterJNI.onSurfaceCreated(surface); + } } /** diff --git a/shell/platform/android/io/flutter/embedding/engine/renderer/RenderSurface.java b/shell/platform/android/io/flutter/embedding/engine/renderer/RenderSurface.java index 58af9a49fd418..9051359dbb458 100644 --- a/shell/platform/android/io/flutter/embedding/engine/renderer/RenderSurface.java +++ b/shell/platform/android/io/flutter/embedding/engine/renderer/RenderSurface.java @@ -60,4 +60,11 @@ public interface RenderSurface { * #attachToRenderer(FlutterRenderer)}. */ void pause(); + + /** + * Instructs this {@code RenderSurface} to resume forwarding {@code Surface} notifications to the + * {@code FlutterRenderer} that was previously connected with {@link + * #attachToRenderer(FlutterRenderer)}. + */ + void resume(); } diff --git a/shell/platform/android/platform_view_android.cc b/shell/platform/android/platform_view_android.cc index c4617c4b99cea..d5854c530f02b 100644 --- a/shell/platform/android/platform_view_android.cc +++ b/shell/platform/android/platform_view_android.cc @@ -197,6 +197,8 @@ void PlatformViewAndroid::NotifySurfaceWindowChanged( }); latch.Wait(); } + + PlatformView::ScheduleFrame(); } void PlatformViewAndroid::NotifyDestroyed() { diff --git a/shell/platform/android/test/io/flutter/embedding/engine/renderer/FlutterRendererTest.java b/shell/platform/android/test/io/flutter/embedding/engine/renderer/FlutterRendererTest.java index b1f4d3f76fd5f..172b685d676dd 100644 --- a/shell/platform/android/test/io/flutter/embedding/engine/renderer/FlutterRendererTest.java +++ b/shell/platform/android/test/io/flutter/embedding/engine/renderer/FlutterRendererTest.java @@ -36,11 +36,13 @@ public class FlutterRendererTest { private FlutterJNI fakeFlutterJNI; private Surface fakeSurface; + private Surface fakeSurface2; @Before public void setup() { fakeFlutterJNI = mock(FlutterJNI.class); fakeSurface = mock(Surface.class); + fakeSurface2 = mock(Surface.class); } @Test @@ -50,7 +52,7 @@ public void itForwardsSurfaceCreationNotificationToFlutterJNI() { FlutterRenderer flutterRenderer = new FlutterRenderer(fakeFlutterJNI); // Execute the behavior under test. - flutterRenderer.startRenderingToSurface(fakeSurface, /*keepCurrentSurface=*/ false); + flutterRenderer.startRenderingToSurface(fakeSurface, false); // Verify the behavior under test. verify(fakeFlutterJNI, times(1)).onSurfaceCreated(eq(fakeSurface)); @@ -62,7 +64,7 @@ public void itForwardsSurfaceChangeNotificationToFlutterJNI() { Surface fakeSurface = mock(Surface.class); FlutterRenderer flutterRenderer = new FlutterRenderer(fakeFlutterJNI); - flutterRenderer.startRenderingToSurface(fakeSurface, /*keepCurrentSurface=*/ false); + flutterRenderer.startRenderingToSurface(fakeSurface, false); // Execute the behavior under test. flutterRenderer.surfaceChanged(100, 50); @@ -77,7 +79,7 @@ public void itForwardsSurfaceDestructionNotificationToFlutterJNI() { Surface fakeSurface = mock(Surface.class); FlutterRenderer flutterRenderer = new FlutterRenderer(fakeFlutterJNI); - flutterRenderer.startRenderingToSurface(fakeSurface, /*keepCurrentSurface=*/ false); + flutterRenderer.startRenderingToSurface(fakeSurface, false); // Execute the behavior under test. flutterRenderer.stopRenderingToSurface(); @@ -92,10 +94,10 @@ public void itStopsRenderingToOneSurfaceBeforeRenderingToANewSurface() { Surface fakeSurface2 = mock(Surface.class); FlutterRenderer flutterRenderer = new FlutterRenderer(fakeFlutterJNI); - flutterRenderer.startRenderingToSurface(fakeSurface, /*keepCurrentSurface=*/ false); + flutterRenderer.startRenderingToSurface(fakeSurface, false); // Execute behavior under test. - flutterRenderer.startRenderingToSurface(fakeSurface2, /*keepCurrentSurface=*/ false); + flutterRenderer.startRenderingToSurface(fakeSurface2, false); // Verify behavior under test. verify(fakeFlutterJNI, times(1)).onSurfaceDestroyed(); // notification of 1st surface's removal. @@ -106,7 +108,7 @@ public void itStopsRenderingToSurfaceWhenRequested() { // Setup the test. FlutterRenderer flutterRenderer = new FlutterRenderer(fakeFlutterJNI); - flutterRenderer.startRenderingToSurface(fakeSurface, /*keepCurrentSurface=*/ false); + flutterRenderer.startRenderingToSurface(fakeSurface, false); // Execute the behavior under test. flutterRenderer.stopRenderingToSurface(); @@ -120,9 +122,9 @@ public void iStopsRenderingToSurfaceWhenSurfaceAlreadySet() { // Setup the test. FlutterRenderer flutterRenderer = new FlutterRenderer(fakeFlutterJNI); - flutterRenderer.startRenderingToSurface(fakeSurface, /*keepCurrentSurface=*/ false); + flutterRenderer.startRenderingToSurface(fakeSurface, false); - flutterRenderer.startRenderingToSurface(fakeSurface, /*keepCurrentSurface=*/ false); + flutterRenderer.startRenderingToSurface(fakeSurface, false); // Verify behavior under test. verify(fakeFlutterJNI, times(1)).onSurfaceDestroyed(); @@ -133,9 +135,9 @@ public void itNeverStopsRenderingToSurfaceWhenRequested() { // Setup the test. FlutterRenderer flutterRenderer = new FlutterRenderer(fakeFlutterJNI); - flutterRenderer.startRenderingToSurface(fakeSurface, /*keepCurrentSurface=*/ false); + flutterRenderer.startRenderingToSurface(fakeSurface, false); - flutterRenderer.startRenderingToSurface(fakeSurface, /*keepCurrentSurface=*/ true); + flutterRenderer.startRenderingToSurface(fakeSurface, true); // Verify behavior under test. verify(fakeFlutterJNI, never()).onSurfaceDestroyed(); @@ -151,7 +153,7 @@ public void itStopsSurfaceTextureCallbackWhenDetached() { FlutterRenderer.SurfaceTextureRegistryEntry entry = (FlutterRenderer.SurfaceTextureRegistryEntry) flutterRenderer.createSurfaceTexture(); - flutterRenderer.startRenderingToSurface(fakeSurface, /*keepCurrentSurface=*/ false); + flutterRenderer.startRenderingToSurface(fakeSurface, false); // Execute the behavior under test. flutterRenderer.stopRenderingToSurface(); @@ -174,7 +176,7 @@ public void itRegistersExistingSurfaceTexture() { (FlutterRenderer.SurfaceTextureRegistryEntry) flutterRenderer.registerSurfaceTexture(surfaceTexture); - flutterRenderer.startRenderingToSurface(fakeSurface, /*keepCurrentSurface=*/ false); + flutterRenderer.startRenderingToSurface(fakeSurface, false); // Verify behavior under test. assertEquals(surfaceTexture, entry.surfaceTexture()); @@ -195,7 +197,7 @@ public void itUnregistersTextureWhenSurfaceTextureFinalized() { (FlutterRenderer.SurfaceTextureRegistryEntry) flutterRenderer.createSurfaceTexture(); long id = entry.id(); - flutterRenderer.startRenderingToSurface(fakeSurface, /*keepCurrentSurface=*/ false); + flutterRenderer.startRenderingToSurface(fakeSurface, false); // Execute the behavior under test. runFinalization(entry); @@ -221,7 +223,7 @@ public void itStopsUnregisteringTextureWhenDetached() { (FlutterRenderer.SurfaceTextureRegistryEntry) flutterRenderer.createSurfaceTexture(); long id = entry.id(); - flutterRenderer.startRenderingToSurface(fakeSurface, /*keepCurrentSurface=*/ false); + flutterRenderer.startRenderingToSurface(fakeSurface, false); flutterRenderer.stopRenderingToSurface(); @@ -399,7 +401,7 @@ public void itDoesDispatchSurfaceDestructionNotificationOnlyOnce() { // Setup the test. FlutterRenderer flutterRenderer = new FlutterRenderer(fakeFlutterJNI); - flutterRenderer.startRenderingToSurface(fakeSurface, /*keepCurrentSurface=*/ false); + flutterRenderer.startRenderingToSurface(fakeSurface, false); // Execute the behavior under test. // Simulate calling |FlutterRenderer#stopRenderingToSurface| twice with different code paths. @@ -409,4 +411,38 @@ public void itDoesDispatchSurfaceDestructionNotificationOnlyOnce() { // Verify behavior under test. verify(fakeFlutterJNI, times(1)).onSurfaceDestroyed(); } + + @Test + public void itInvokesCreatesSurfaceWhenStartingRendering() { + FlutterRenderer flutterRenderer = new FlutterRenderer(fakeFlutterJNI); + + flutterRenderer.startRenderingToSurface(fakeSurface, false); + verify(fakeFlutterJNI, times(1)).onSurfaceCreated(eq(fakeSurface)); + } + + @Test + public void itDoesNotInvokeCreatesSurfaceWhenResumingRendering() { + FlutterRenderer flutterRenderer = new FlutterRenderer(fakeFlutterJNI); + + // The following call sequence mimics the behaviour of FlutterView when it exits from hybrid + // composition mode. + + // Install initial rendering surface. + flutterRenderer.startRenderingToSurface(fakeSurface, false); + verify(fakeFlutterJNI, times(1)).onSurfaceCreated(eq(fakeSurface)); + verify(fakeFlutterJNI, times(0)).onSurfaceWindowChanged(eq(fakeSurface)); + + // Install the image view. + flutterRenderer.startRenderingToSurface(fakeSurface2, true); + verify(fakeFlutterJNI, times(1)).onSurfaceCreated(eq(fakeSurface)); + verify(fakeFlutterJNI, times(0)).onSurfaceWindowChanged(eq(fakeSurface)); + verify(fakeFlutterJNI, times(0)).onSurfaceCreated(eq(fakeSurface2)); + verify(fakeFlutterJNI, times(1)).onSurfaceWindowChanged(eq(fakeSurface2)); + + flutterRenderer.startRenderingToSurface(fakeSurface, true); + verify(fakeFlutterJNI, times(1)).onSurfaceCreated(eq(fakeSurface)); + verify(fakeFlutterJNI, times(1)).onSurfaceWindowChanged(eq(fakeSurface)); + verify(fakeFlutterJNI, times(0)).onSurfaceCreated(eq(fakeSurface2)); + verify(fakeFlutterJNI, times(1)).onSurfaceWindowChanged(eq(fakeSurface2)); + } }