Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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}.
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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.");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}.
Expand Down Expand Up @@ -170,8 +175,6 @@ public void detachFromRenderer() {
disconnectSurfaceFromRenderer();
}

pause();

flutterRenderer = null;
} else {
Log.w(TAG, "detachFromRenderer() invoked when no FlutterRenderer was attached.");
Expand All @@ -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.");
Expand Down
Original file line number Diff line number Diff line change
@@ -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));
}
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
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;
import android.view.Surface;
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;
Expand All @@ -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));
}
}