Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit f73ba50

Browse files
committed
[Android] Fix the issue of blank or frozen pages in shared engine scenarios
1 parent 76f33eb commit f73ba50

File tree

4 files changed

+134
-6
lines changed

4 files changed

+134
-6
lines changed

shell/platform/android/io/flutter/embedding/android/FlutterSurfaceView.java

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import android.view.SurfaceView;
1313
import androidx.annotation.NonNull;
1414
import androidx.annotation.Nullable;
15+
import androidx.annotation.VisibleForTesting;
1516
import io.flutter.Log;
1617
import io.flutter.embedding.engine.renderer.FlutterRenderer;
1718
import io.flutter.embedding.engine.renderer.FlutterUiDisplayListener;
@@ -168,6 +169,10 @@ public FlutterRenderer getAttachedRenderer() {
168169
return flutterRenderer;
169170
}
170171

172+
@VisibleForTesting
173+
/* package */ boolean isSurfaceAvailableForRendering() {
174+
return isSurfaceAvailableForRendering;
175+
}
171176
/**
172177
* Invoked by the owner of this {@code FlutterSurfaceView} when it wants to begin rendering a
173178
* Flutter UI to this {@code FlutterSurfaceView}.
@@ -215,8 +220,6 @@ public void detachFromRenderer() {
215220
disconnectSurfaceFromRenderer();
216221
}
217222

218-
pause();
219-
220223
// Make the SurfaceView invisible to avoid showing a black rectangle.
221224
setAlpha(0.0f);
222225
flutterRenderer.removeIsDisplayingFlutterUiListener(flutterUiDisplayListener);
@@ -248,7 +251,7 @@ public void resume() {
248251

249252
// If we're already attached to an Android window then we're now attached to both a renderer
250253
// and the Android window. We can begin rendering now.
251-
if (isSurfaceAvailableForRendering) {
254+
if (isSurfaceAvailableForRendering()) {
252255
Log.v(
253256
TAG,
254257
"Surface is available for rendering. Connecting FlutterRenderer to Android surface.");

shell/platform/android/io/flutter/embedding/android/FlutterTextureView.java

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,11 @@ public FlutterRenderer getAttachedRenderer() {
124124
return flutterRenderer;
125125
}
126126

127+
@VisibleForTesting
128+
/* package */ boolean isSurfaceAvailableForRendering() {
129+
return isSurfaceAvailableForRendering;
130+
}
131+
127132
/**
128133
* Invoked by the owner of this {@code FlutterTextureView} when it wants to begin rendering a
129134
* Flutter UI to this {@code FlutterTextureView}.
@@ -170,8 +175,6 @@ public void detachFromRenderer() {
170175
disconnectSurfaceFromRenderer();
171176
}
172177

173-
pause();
174-
175178
flutterRenderer = null;
176179
} else {
177180
Log.w(TAG, "detachFromRenderer() invoked when no FlutterRenderer was attached.");
@@ -198,7 +201,7 @@ public void resume() {
198201

199202
// If we're already attached to an Android window then we're now attached to both a renderer
200203
// and the Android window. We can begin rendering now.
201-
if (isSurfaceAvailableForRendering) {
204+
if (isSurfaceAvailableForRendering()) {
202205
Log.v(
203206
TAG,
204207
"Surface is available for rendering. Connecting FlutterRenderer to Android surface.");
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package io.flutter.embedding.android;
2+
3+
import static io.flutter.Build.API_LEVELS;
4+
import static org.mockito.ArgumentMatchers.any;
5+
import static org.mockito.Mockito.mock;
6+
import static org.mockito.Mockito.never;
7+
import static org.mockito.Mockito.spy;
8+
import static org.mockito.Mockito.times;
9+
import static org.mockito.Mockito.verify;
10+
import static org.mockito.Mockito.when;
11+
12+
import android.annotation.TargetApi;
13+
import android.view.Surface;
14+
import android.view.SurfaceHolder;
15+
import androidx.test.core.app.ApplicationProvider;
16+
import androidx.test.ext.junit.runners.AndroidJUnit4;
17+
import io.flutter.embedding.engine.FlutterJNI;
18+
import io.flutter.embedding.engine.renderer.FlutterRenderer;
19+
import org.junit.Test;
20+
import org.junit.runner.RunWith;
21+
import org.robolectric.annotation.Config;
22+
23+
@Config(manifest = Config.NONE)
24+
@RunWith(AndroidJUnit4.class)
25+
@TargetApi(API_LEVELS.API_30)
26+
public class FlutterSurfaceViewTest {
27+
@Test
28+
public void itShouldCreateANewSurfaceWhenReattachedAfterDetachingFromRenderer() {
29+
// Consider this scenario: In an add-to-app context, where multiple Flutter activities share the
30+
// same engine, a situation occurs. When navigating from FlutterActivity1 to FlutterActivity2,
31+
// the Flutter view associated with FlutterActivity1 is detached from the engine. Then, the
32+
// Flutter view of FlutterActivity2 is attached. Upon navigating back to FlutterActivity1, its
33+
// Flutter view is re-attached to the shared engine.
34+
//
35+
// The expected behavior is: When a Flutter view detaches from the shared engine, the associated
36+
// surface should be released. When the Flutter view re-attaches, a new surface should be
37+
// created.
38+
39+
// Setup the test.
40+
final FlutterSurfaceView surfaceView =
41+
spy(new FlutterSurfaceView(ApplicationProvider.getApplicationContext()));
42+
43+
FlutterJNI fakeFlutterJNI = mock(FlutterJNI.class);
44+
FlutterRenderer flutterRenderer = new FlutterRenderer(fakeFlutterJNI);
45+
46+
SurfaceHolder fakeSurfaceHolder = mock(SurfaceHolder.class);
47+
Surface fakeSurface = mock(Surface.class);
48+
when(surfaceView.getHolder()).thenReturn(fakeSurfaceHolder);
49+
when(fakeSurfaceHolder.getSurface()).thenReturn(fakeSurface);
50+
when(surfaceView.isSurfaceAvailableForRendering()).thenReturn(true);
51+
when(surfaceView.getWindowToken()).thenReturn(mock(android.os.IBinder.class));
52+
53+
// Execute the behavior under test.
54+
surfaceView.attachToRenderer(flutterRenderer);
55+
56+
// Verify the behavior under test.
57+
verify(fakeFlutterJNI, times(1)).onSurfaceCreated(any(Surface.class));
58+
59+
// Execute the behavior under test.
60+
surfaceView.detachFromRenderer();
61+
62+
// Verify the behavior under test.
63+
verify(fakeFlutterJNI, times(1)).onSurfaceDestroyed();
64+
65+
// Execute the behavior under test.
66+
surfaceView.attachToRenderer(flutterRenderer);
67+
68+
// Verify the behavior under test.
69+
verify(fakeFlutterJNI, never()).onSurfaceWindowChanged(any(Surface.class));
70+
verify(fakeFlutterJNI, times(2)).onSurfaceCreated(any(Surface.class));
71+
}
72+
}

shell/platform/android/test/io/flutter/embedding/android/FlutterTextureViewTest.java

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,22 @@
11
package io.flutter.embedding.android;
22

33
import static io.flutter.Build.API_LEVELS;
4+
import static org.mockito.ArgumentMatchers.any;
45
import static org.mockito.Mockito.mock;
6+
import static org.mockito.Mockito.never;
7+
import static org.mockito.Mockito.spy;
8+
import static org.mockito.Mockito.times;
59
import static org.mockito.Mockito.verify;
10+
import static org.mockito.Mockito.when;
611

712
import android.annotation.TargetApi;
813
import android.graphics.SurfaceTexture;
914
import android.view.Surface;
1015
import android.view.TextureView;
1116
import androidx.test.core.app.ApplicationProvider;
1217
import androidx.test.ext.junit.runners.AndroidJUnit4;
18+
import io.flutter.embedding.engine.FlutterJNI;
19+
import io.flutter.embedding.engine.renderer.FlutterRenderer;
1320
import org.junit.Test;
1421
import org.junit.runner.RunWith;
1522
import org.robolectric.annotation.Config;
@@ -31,4 +38,47 @@ public void surfaceTextureListenerReleasesRenderer() {
3138

3239
verify(mockRenderSurface).release();
3340
}
41+
42+
@Test
43+
public void itShouldCreateANewSurfaceWhenReattachedAfterDetachingFromRenderer() {
44+
// Consider this scenario: In an add-to-app context, where multiple Flutter activities share the
45+
// same engine, a situation occurs. When navigating from FlutterActivity1 to FlutterActivity2,
46+
// the Flutter view associated with FlutterActivity1 is detached from the engine. Then, the
47+
// Flutter view of FlutterActivity2 is attached. Upon navigating back to FlutterActivity1, its
48+
// Flutter view is re-attached to the shared engine.
49+
//
50+
// The expected behavior is: When a Flutter view detaches from the shared engine, the associated
51+
// surface should be released. When the Flutter view re-attaches, a new surface should be
52+
// created.
53+
54+
// Setup the test.
55+
final FlutterTextureView textureView =
56+
spy(new FlutterTextureView(ApplicationProvider.getApplicationContext()));
57+
58+
FlutterJNI fakeFlutterJNI = mock(FlutterJNI.class);
59+
FlutterRenderer flutterRenderer = new FlutterRenderer(fakeFlutterJNI);
60+
61+
when(textureView.isSurfaceAvailableForRendering()).thenReturn(true);
62+
when(textureView.getSurfaceTexture()).thenReturn(mock(SurfaceTexture.class));
63+
when(textureView.getWindowToken()).thenReturn(mock(android.os.IBinder.class));
64+
65+
// Execute the behavior under test.
66+
textureView.attachToRenderer(flutterRenderer);
67+
68+
// Verify the behavior under test.
69+
verify(fakeFlutterJNI, times(1)).onSurfaceCreated(any(Surface.class));
70+
71+
// Execute the behavior under test.
72+
textureView.detachFromRenderer();
73+
74+
// Verify the behavior under test.
75+
verify(fakeFlutterJNI, times(1)).onSurfaceDestroyed();
76+
77+
// Execute the behavior under test.
78+
textureView.attachToRenderer(flutterRenderer);
79+
80+
// Verify the behavior under test.
81+
verify(fakeFlutterJNI, never()).onSurfaceWindowChanged(any(Surface.class));
82+
verify(fakeFlutterJNI, times(2)).onSurfaceCreated(any(Surface.class));
83+
}
3484
}

0 commit comments

Comments
 (0)