diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterSurfaceView.java b/shell/platform/android/io/flutter/embedding/android/FlutterSurfaceView.java index 9fdaa4c08bb1f..c5c6493494043 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterSurfaceView.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterSurfaceView.java @@ -12,6 +12,7 @@ import android.view.SurfaceView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import io.flutter.Log; import io.flutter.embedding.engine.renderer.FlutterRenderer; import io.flutter.embedding.engine.renderer.FlutterUiDisplayListener; @@ -168,6 +169,10 @@ public FlutterRenderer getAttachedRenderer() { return flutterRenderer; } + @VisibleForTesting + /* package */ boolean isSurfaceAvailableForRendering() { + return isSurfaceAvailableForRendering; + } /** * Invoked by the owner of this {@code FlutterSurfaceView} when it wants to begin rendering a * Flutter UI to this {@code FlutterSurfaceView}. @@ -215,8 +220,6 @@ public void detachFromRenderer() { disconnectSurfaceFromRenderer(); } - pause(); - // Make the SurfaceView invisible to avoid showing a black rectangle. setAlpha(0.0f); flutterRenderer.removeIsDisplayingFlutterUiListener(flutterUiDisplayListener); @@ -248,7 +251,7 @@ public void resume() { // 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) { + if (isSurfaceAvailableForRendering()) { Log.v( TAG, "Surface is available for rendering. Connecting FlutterRenderer to Android surface."); diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterTextureView.java b/shell/platform/android/io/flutter/embedding/android/FlutterTextureView.java index 11237a1a70348..704509aae1e87 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterTextureView.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterTextureView.java @@ -124,6 +124,11 @@ public FlutterRenderer getAttachedRenderer() { return flutterRenderer; } + @VisibleForTesting + /* package */ boolean isSurfaceAvailableForRendering() { + return isSurfaceAvailableForRendering; + } + /** * Invoked by the owner of this {@code FlutterTextureView} when it wants to begin rendering a * Flutter UI to this {@code FlutterTextureView}. @@ -170,8 +175,6 @@ public void detachFromRenderer() { disconnectSurfaceFromRenderer(); } - pause(); - flutterRenderer = null; } else { Log.w(TAG, "detachFromRenderer() invoked when no FlutterRenderer was attached."); @@ -198,7 +201,7 @@ public void resume() { // 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) { + if (isSurfaceAvailableForRendering()) { Log.v( TAG, "Surface is available for rendering. Connecting FlutterRenderer to Android surface."); diff --git a/shell/platform/android/test/io/flutter/embedding/android/FlutterSurfaceViewTest.java b/shell/platform/android/test/io/flutter/embedding/android/FlutterSurfaceViewTest.java new file mode 100644 index 0000000000000..33312dc7bf0c2 --- /dev/null +++ b/shell/platform/android/test/io/flutter/embedding/android/FlutterSurfaceViewTest.java @@ -0,0 +1,72 @@ +package io.flutter.embedding.android; + +import static io.flutter.Build.API_LEVELS; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.annotation.TargetApi; +import android.view.Surface; +import android.view.SurfaceHolder; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import io.flutter.embedding.engine.FlutterJNI; +import io.flutter.embedding.engine.renderer.FlutterRenderer; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.annotation.Config; + +@Config(manifest = Config.NONE) +@RunWith(AndroidJUnit4.class) +@TargetApi(API_LEVELS.API_30) +public class FlutterSurfaceViewTest { + @Test + public void itShouldCreateANewSurfaceWhenReattachedAfterDetachingFromRenderer() { + // Consider this scenario: In an add-to-app context, where multiple Flutter activities share the + // same engine, a situation occurs. When navigating from FlutterActivity1 to FlutterActivity2, + // the Flutter view associated with FlutterActivity1 is detached from the engine. Then, the + // Flutter view of FlutterActivity2 is attached. Upon navigating back to FlutterActivity1, its + // Flutter view is re-attached to the shared engine. + // + // The expected behavior is: When a Flutter view detaches from the shared engine, the associated + // surface should be released. When the Flutter view re-attaches, a new surface should be + // created. + + // Setup the test. + final FlutterSurfaceView surfaceView = + spy(new FlutterSurfaceView(ApplicationProvider.getApplicationContext())); + + FlutterJNI fakeFlutterJNI = mock(FlutterJNI.class); + FlutterRenderer flutterRenderer = new FlutterRenderer(fakeFlutterJNI); + + SurfaceHolder fakeSurfaceHolder = mock(SurfaceHolder.class); + Surface fakeSurface = mock(Surface.class); + when(surfaceView.getHolder()).thenReturn(fakeSurfaceHolder); + when(fakeSurfaceHolder.getSurface()).thenReturn(fakeSurface); + when(surfaceView.isSurfaceAvailableForRendering()).thenReturn(true); + when(surfaceView.getWindowToken()).thenReturn(mock(android.os.IBinder.class)); + + // Execute the behavior under test. + surfaceView.attachToRenderer(flutterRenderer); + + // Verify the behavior under test. + verify(fakeFlutterJNI, times(1)).onSurfaceCreated(any(Surface.class)); + + // Execute the behavior under test. + surfaceView.detachFromRenderer(); + + // Verify the behavior under test. + verify(fakeFlutterJNI, times(1)).onSurfaceDestroyed(); + + // Execute the behavior under test. + surfaceView.attachToRenderer(flutterRenderer); + + // Verify the behavior under test. + verify(fakeFlutterJNI, never()).onSurfaceWindowChanged(any(Surface.class)); + verify(fakeFlutterJNI, times(2)).onSurfaceCreated(any(Surface.class)); + } +} diff --git a/shell/platform/android/test/io/flutter/embedding/android/FlutterTextureViewTest.java b/shell/platform/android/test/io/flutter/embedding/android/FlutterTextureViewTest.java index 216c8ea161bb2..7711b1568c553 100644 --- a/shell/platform/android/test/io/flutter/embedding/android/FlutterTextureViewTest.java +++ b/shell/platform/android/test/io/flutter/embedding/android/FlutterTextureViewTest.java @@ -1,8 +1,13 @@ package io.flutter.embedding.android; import static io.flutter.Build.API_LEVELS; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import android.annotation.TargetApi; import android.graphics.SurfaceTexture; @@ -10,6 +15,8 @@ import android.view.TextureView; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; +import io.flutter.embedding.engine.FlutterJNI; +import io.flutter.embedding.engine.renderer.FlutterRenderer; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.annotation.Config; @@ -31,4 +38,47 @@ public void surfaceTextureListenerReleasesRenderer() { verify(mockRenderSurface).release(); } + + @Test + public void itShouldCreateANewSurfaceWhenReattachedAfterDetachingFromRenderer() { + // Consider this scenario: In an add-to-app context, where multiple Flutter activities share the + // same engine, a situation occurs. When navigating from FlutterActivity1 to FlutterActivity2, + // the Flutter view associated with FlutterActivity1 is detached from the engine. Then, the + // Flutter view of FlutterActivity2 is attached. Upon navigating back to FlutterActivity1, its + // Flutter view is re-attached to the shared engine. + // + // The expected behavior is: When a Flutter view detaches from the shared engine, the associated + // surface should be released. When the Flutter view re-attaches, a new surface should be + // created. + + // Setup the test. + final FlutterTextureView textureView = + spy(new FlutterTextureView(ApplicationProvider.getApplicationContext())); + + FlutterJNI fakeFlutterJNI = mock(FlutterJNI.class); + FlutterRenderer flutterRenderer = new FlutterRenderer(fakeFlutterJNI); + + when(textureView.isSurfaceAvailableForRendering()).thenReturn(true); + when(textureView.getSurfaceTexture()).thenReturn(mock(SurfaceTexture.class)); + when(textureView.getWindowToken()).thenReturn(mock(android.os.IBinder.class)); + + // Execute the behavior under test. + textureView.attachToRenderer(flutterRenderer); + + // Verify the behavior under test. + verify(fakeFlutterJNI, times(1)).onSurfaceCreated(any(Surface.class)); + + // Execute the behavior under test. + textureView.detachFromRenderer(); + + // Verify the behavior under test. + verify(fakeFlutterJNI, times(1)).onSurfaceDestroyed(); + + // Execute the behavior under test. + textureView.attachToRenderer(flutterRenderer); + + // Verify the behavior under test. + verify(fakeFlutterJNI, never()).onSurfaceWindowChanged(any(Surface.class)); + verify(fakeFlutterJNI, times(2)).onSurfaceCreated(any(Surface.class)); + } }