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

Commit 77ba89e

Browse files
committed
[Android] Fix the issue of blank or frozen pages in shared engine scenarios
1 parent 4addb6a commit 77ba89e

File tree

4 files changed

+133
-6
lines changed

4 files changed

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

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,14 +1,21 @@
11
package io.flutter.embedding.android;
22

3+
import static org.mockito.ArgumentMatchers.any;
34
import static org.mockito.Mockito.mock;
5+
import static org.mockito.Mockito.never;
6+
import static org.mockito.Mockito.spy;
7+
import static org.mockito.Mockito.times;
48
import static org.mockito.Mockito.verify;
9+
import static org.mockito.Mockito.when;
510

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

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

0 commit comments

Comments
 (0)