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

Commit 96736f6

Browse files
0xZOneMAEDA Naohito
authored andcommitted
[Android] Fix the issue of blank or frozen pages in shared engine scenarios (#50947)
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. After #47358, no new surface is created when the Flutter view is attached again. This results in the Flutter view having no underlying surface, which causes the page to appear blank or freeze without responding. [C++, Objective-C, Java style guides]: https://github.com/flutter/engine/blob/main/CONTRIBUTING.md#style
1 parent c4cd48e commit 96736f6

File tree

4 files changed

+136
-7
lines changed

4 files changed

+136
-7
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: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,29 @@
11
package io.flutter.embedding.android;
22

3+
import static io.flutter.Build.API_LEVELS;
4+
import static org.mockito.ArgumentMatchers.any;
35
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;
49
import static org.mockito.Mockito.verify;
10+
import static org.mockito.Mockito.when;
511

612
import android.annotation.TargetApi;
713
import android.graphics.SurfaceTexture;
814
import android.view.Surface;
915
import android.view.TextureView;
1016
import androidx.test.core.app.ApplicationProvider;
1117
import androidx.test.ext.junit.runners.AndroidJUnit4;
18+
import io.flutter.embedding.engine.FlutterJNI;
19+
import io.flutter.embedding.engine.renderer.FlutterRenderer;
1220
import org.junit.Test;
1321
import org.junit.runner.RunWith;
1422
import org.robolectric.annotation.Config;
1523

1624
@Config(manifest = Config.NONE)
1725
@RunWith(AndroidJUnit4.class)
18-
@TargetApi(30)
26+
@TargetApi(API_LEVELS.API_30)
1927
public class FlutterTextureViewTest {
2028
@Test
2129
public void surfaceTextureListenerReleasesRenderer() {
@@ -30,4 +38,47 @@ public void surfaceTextureListenerReleasesRenderer() {
3038

3139
verify(mockRenderSurface).release();
3240
}
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+
}
3384
}

0 commit comments

Comments
 (0)