diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java b/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java index 060221650243f..16497674240de 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterActivity.java @@ -542,7 +542,8 @@ private View createFlutterView() { /* inflater=*/ null, /* container=*/ null, /* savedInstanceState=*/ null, - /*flutterViewId=*/ FLUTTER_VIEW_ID); + /*flutterViewId=*/ FLUTTER_VIEW_ID, + /*shouldDelayFirstAndroidViewDraw=*/ getRenderMode() == RenderMode.surface); } private void configureStatusBarForFullscreenFlutterExperience() { diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java b/shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java index b6f5eb465a03d..d153a26d7cfc8 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java @@ -15,6 +15,7 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.view.ViewTreeObserver.OnPreDrawListener; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; @@ -67,15 +68,17 @@ private static final String TAG = "FlutterActivityAndFragmentDelegate"; private static final String FRAMEWORK_RESTORATION_BUNDLE_KEY = "framework"; private static final String PLUGINS_RESTORATION_BUNDLE_KEY = "plugins"; + private static final int FLUTTER_SPLASH_VIEW_FALLBACK_ID = 486947586; // The FlutterActivity or FlutterFragment that is delegating most of its calls // to this FlutterActivityAndFragmentDelegate. @NonNull private Host host; @Nullable private FlutterEngine flutterEngine; - @Nullable private FlutterSplashView flutterSplashView; @Nullable private FlutterView flutterView; @Nullable private PlatformPlugin platformPlugin; + @VisibleForTesting @Nullable OnPreDrawListener activePreDrawListener; private boolean isFlutterEngineFromHost; + private boolean isFlutterUiDisplayed; @NonNull private final FlutterUiDisplayListener flutterUiDisplayListener = @@ -83,11 +86,13 @@ @Override public void onFlutterUiDisplayed() { host.onFlutterUiDisplayed(); + isFlutterUiDisplayed = true; } @Override public void onFlutterUiNoLongerDisplayed() { host.onFlutterUiNoLongerDisplayed(); + isFlutterUiDisplayed = false; } }; @@ -254,6 +259,16 @@ void onAttach(@NonNull Context context) { * *

{@code inflater} and {@code container} may be null when invoked from an {@code Activity}. * + *

{@code shouldDelayFirstAndroidViewDraw} determines whether to set up an {@link + * android.view.ViewTreeObserver.OnPreDrawListener}, which will defer the current drawing pass + * till after the Flutter UI has been displayed. This results in more accurate timings reported + * with Android tools, such as "Displayed" timing printed with `am start`. + * + *

Note that it should only be set to true when {@code Host#getRenderMode()} is {@code + * RenderMode.surface}. This parameter is also ignored, disabling the delay should the legacy + * {@code Host#provideSplashScreen()} be non-null. See Android Splash Migration. + * *

This method: * *

    @@ -269,7 +284,8 @@ View onCreateView( LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState, - int flutterViewId) { + int flutterViewId, + boolean shouldDelayFirstAndroidViewDraw) { Log.v(TAG, "Creating FlutterView."); ensureAlive(); @@ -298,15 +314,28 @@ View onCreateView( // Add listener to be notified when Flutter renders its first frame. flutterView.addOnFirstFrameRenderedListener(flutterUiDisplayListener); - flutterSplashView = new FlutterSplashView(host.getContext()); - flutterSplashView.setId(ViewUtils.generateViewId(486947586)); - flutterSplashView.displayFlutterViewWithSplash(flutterView, host.provideSplashScreen()); - Log.v(TAG, "Attaching FlutterEngine to FlutterView."); flutterView.attachToFlutterEngine(flutterEngine); flutterView.setId(flutterViewId); - return flutterSplashView; + SplashScreen splashScreen = host.provideSplashScreen(); + + if (splashScreen != null) { + Log.w( + TAG, + "A splash screen was provided to Flutter, but this is deprecated. See" + + " flutter.dev/go/android-splash-migration for migration steps."); + FlutterSplashView flutterSplashView = new FlutterSplashView(host.getContext()); + flutterSplashView.setId(ViewUtils.generateViewId(FLUTTER_SPLASH_VIEW_FALLBACK_ID)); + flutterSplashView.displayFlutterViewWithSplash(flutterView, splashScreen); + + return flutterSplashView; + } + + if (shouldDelayFirstAndroidViewDraw) { + delayFirstAndroidViewDraw(flutterView); + } + return flutterView; } void onRestoreInstanceState(@Nullable Bundle bundle) { @@ -415,6 +444,38 @@ private String maybeGetInitialRouteFromIntent(Intent intent) { return null; } + /** + * Delays the first drawing of the {@code flutterView} until the Flutter first has been displayed. + */ + private void delayFirstAndroidViewDraw(FlutterView flutterView) { + if (host.getRenderMode() != RenderMode.surface) { + // Using a TextureView will cause a deadlock, where the underlying SurfaceTexture is never + // available since it will wait for drawing to be completed first. At the same time, the + // preDraw listener keeps returning false since the Flutter Engine waits for the + // SurfaceTexture to be available. + throw new IllegalArgumentException( + "Cannot delay the first Android view draw when the render mode is not set to" + + " `RenderMode.surface`."); + } + + if (activePreDrawListener != null) { + flutterView.getViewTreeObserver().removeOnPreDrawListener(activePreDrawListener); + } + + activePreDrawListener = + new OnPreDrawListener() { + @Override + public boolean onPreDraw() { + if (isFlutterUiDisplayed && activePreDrawListener != null) { + flutterView.getViewTreeObserver().removeOnPreDrawListener(this); + activePreDrawListener = null; + } + return isFlutterUiDisplayed; + } + }; + flutterView.getViewTreeObserver().addOnPreDrawListener(activePreDrawListener); + } + /** * Invoke this from {@code Activity#onResume()} or {@code Fragment#onResume()}. * @@ -496,6 +557,10 @@ void onDestroyView() { Log.v(TAG, "onDestroyView()"); ensureAlive(); + if (activePreDrawListener != null) { + flutterView.getViewTreeObserver().removeOnPreDrawListener(activePreDrawListener); + activePreDrawListener = null; + } flutterView.detachFromFlutterEngine(); flutterView.removeOnFirstFrameRenderedListener(flutterUiDisplayListener); } diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterFragment.java b/shell/platform/android/io/flutter/embedding/android/FlutterFragment.java index fec62df74a73e..bb886ec8d8957 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterFragment.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterFragment.java @@ -113,6 +113,10 @@ public class FlutterFragment extends Fragment protected static final String ARG_HANDLE_DEEPLINKING = "handle_deeplinking"; /** Path to Flutter's Dart code. */ protected static final String ARG_APP_BUNDLE_PATH = "app_bundle_path"; + /** Whether to delay the Android drawing pass till after the Flutter UI has been displayed. */ + protected static final String ARG_SHOULD_DELAY_FIRST_ANDROID_VIEW_DRAW = + "should_delay_first_android_view_draw"; + /** Flutter shell arguments. */ protected static final String ARG_FLUTTER_INITIALIZATION_ARGS = "initialization_args"; /** @@ -229,6 +233,7 @@ public static class NewEngineFragmentBuilder { private TransparencyMode transparencyMode = TransparencyMode.transparent; private boolean shouldAttachEngineToActivity = true; private boolean shouldAutomaticallyHandleOnBackPressed = false; + private boolean shouldDelayFirstAndroidViewDraw = false; /** * Constructs a {@code NewEngineFragmentBuilder} that is configured to construct an instance of @@ -382,6 +387,18 @@ public NewEngineFragmentBuilder shouldAutomaticallyHandleOnBackPressed( return this; } + /** + * Whether to delay the Android drawing pass till after the Flutter UI has been displayed. + * + *

    See {#link FlutterActivityAndFragmentDelegate#onCreateView} for more details. + */ + @NonNull + public NewEngineFragmentBuilder shouldDelayFirstAndroidViewDraw( + boolean shouldDelayFirstAndroidViewDraw) { + this.shouldDelayFirstAndroidViewDraw = shouldDelayFirstAndroidViewDraw; + return this; + } + /** * Creates a {@link Bundle} of arguments that are assigned to the new {@code FlutterFragment}. * @@ -410,6 +427,7 @@ protected Bundle createArgs() { args.putBoolean(ARG_DESTROY_ENGINE_WITH_FRAGMENT, true); args.putBoolean( ARG_SHOULD_AUTOMATICALLY_HANDLE_ON_BACK_PRESSED, shouldAutomaticallyHandleOnBackPressed); + args.putBoolean(ARG_SHOULD_DELAY_FIRST_ANDROID_VIEW_DRAW, shouldDelayFirstAndroidViewDraw); return args; } @@ -496,6 +514,7 @@ public static class CachedEngineFragmentBuilder { private TransparencyMode transparencyMode = TransparencyMode.transparent; private boolean shouldAttachEngineToActivity = true; private boolean shouldAutomaticallyHandleOnBackPressed = false; + private boolean shouldDelayFirstAndroidViewDraw = false; private CachedEngineFragmentBuilder(@NonNull String engineId) { this(FlutterFragment.class, engineId); @@ -621,6 +640,18 @@ public CachedEngineFragmentBuilder shouldAutomaticallyHandleOnBackPressed( return this; } + /** + * Whether to delay the Android drawing pass till after the Flutter UI has been displayed. + * + *

    See {#link FlutterActivityAndFragmentDelegate#onCreateView} for more details. + */ + @NonNull + public CachedEngineFragmentBuilder shouldDelayFirstAndroidViewDraw( + @NonNull boolean shouldDelayFirstAndroidViewDraw) { + this.shouldDelayFirstAndroidViewDraw = shouldDelayFirstAndroidViewDraw; + return this; + } + /** * Creates a {@link Bundle} of arguments that are assigned to the new {@code FlutterFragment}. * @@ -642,6 +673,7 @@ protected Bundle createArgs() { args.putBoolean(ARG_SHOULD_ATTACH_ENGINE_TO_ACTIVITY, shouldAttachEngineToActivity); args.putBoolean( ARG_SHOULD_AUTOMATICALLY_HANDLE_ON_BACK_PRESSED, shouldAutomaticallyHandleOnBackPressed); + args.putBoolean(ARG_SHOULD_DELAY_FIRST_ANDROID_VIEW_DRAW, shouldDelayFirstAndroidViewDraw); return args; } @@ -727,7 +759,11 @@ public void onCreate(@Nullable Bundle savedInstanceState) { public View onCreateView( LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { return delegate.onCreateView( - inflater, container, savedInstanceState, /*flutterViewId=*/ FLUTTER_VIEW_ID); + inflater, + container, + savedInstanceState, + /*flutterViewId=*/ FLUTTER_VIEW_ID, + shouldDelayFirstAndroidViewDraw()); } @Override @@ -1267,6 +1303,12 @@ public boolean popSystemNavigator() { return false; } + @VisibleForTesting + @NonNull + boolean shouldDelayFirstAndroidViewDraw() { + return getArguments().getBoolean(ARG_SHOULD_DELAY_FIRST_ANDROID_VIEW_DRAW); + } + private boolean stillAttachedForEvent(String event) { if (delegate == null) { Log.w(TAG, "FlutterFragment " + hashCode() + " " + event + " called after release."); diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterFragmentActivity.java b/shell/platform/android/io/flutter/embedding/android/FlutterFragmentActivity.java index 74e008448872f..52c65ca855d45 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterFragmentActivity.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterFragmentActivity.java @@ -424,6 +424,7 @@ protected FlutterFragment createFlutterFragment() { backgroundMode == BackgroundMode.opaque ? TransparencyMode.opaque : TransparencyMode.transparent; + final boolean shouldDelayFirstAndroidViewDraw = renderMode == RenderMode.surface; if (getCachedEngineId() != null) { Log.v( @@ -447,6 +448,7 @@ protected FlutterFragment createFlutterFragment() { .handleDeeplinking(shouldHandleDeeplinking()) .shouldAttachEngineToActivity(shouldAttachEngineToActivity()) .destroyEngineWithFragment(shouldDestroyEngineWithHost()) + .shouldDelayFirstAndroidViewDraw(shouldDelayFirstAndroidViewDraw) .build(); } else { Log.v( @@ -476,6 +478,7 @@ protected FlutterFragment createFlutterFragment() { .renderMode(renderMode) .transparencyMode(transparencyMode) .shouldAttachEngineToActivity(shouldAttachEngineToActivity()) + .shouldDelayFirstAndroidViewDraw(shouldDelayFirstAndroidViewDraw) .build(); } } diff --git a/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java b/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java index 5dfd509230e49..112b3736a3080 100644 --- a/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java +++ b/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java @@ -2,7 +2,9 @@ import static android.content.ComponentCallbacks2.*; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; import static org.mockito.Matchers.any; import static org.mockito.Matchers.eq; import static org.mockito.Matchers.notNull; @@ -15,6 +17,8 @@ import android.app.Activity; import android.content.Context; import android.content.Intent; +import android.graphics.Color; +import android.graphics.drawable.ColorDrawable; import android.net.Uri; import androidx.annotation.NonNull; import androidx.lifecycle.Lifecycle; @@ -86,7 +90,7 @@ public void itSendsLifecycleEventsToFlutter() { // We're testing lifecycle behaviors, which require/expect that certain methods have already // been executed by the time they run. Therefore, we run those expected methods first. delegate.onAttach(RuntimeEnvironment.application); - delegate.onCreateView(null, null, null, 0); + delegate.onCreateView(null, null, null, 0, true); // --- Execute the behavior under test --- // By the time an Activity/Fragment is started, we don't expect any lifecycle messages @@ -164,7 +168,7 @@ public void itUsesCachedEngineWhenProvided() { // --- Execute the behavior under test --- // The FlutterEngine is obtained in onAttach(). delegate.onAttach(RuntimeEnvironment.application); - delegate.onCreateView(null, null, null, 0); + delegate.onCreateView(null, null, null, 0, true); delegate.onStart(); delegate.onResume(); @@ -220,7 +224,7 @@ public void itGivesHostAnOpportunityToConfigureFlutterSurfaceView() { // --- Execute the behavior under test --- delegate.onAttach(RuntimeEnvironment.application); - delegate.onCreateView(null, null, null, 0); + delegate.onCreateView(null, null, null, 0, true); // Verify that the host was asked to configure a FlutterSurfaceView. verify(mockHost, times(1)).onFlutterSurfaceViewCreated(notNull(FlutterSurfaceView.class)); @@ -249,7 +253,7 @@ public void itGivesHostAnOpportunityToConfigureFlutterTextureView() { // --- Execute the behavior under test --- delegate.onAttach(RuntimeEnvironment.application); - delegate.onCreateView(null, null, null, 0); + delegate.onCreateView(null, null, null, 0, false); // Verify that the host was asked to configure a FlutterTextureView. verify(customMockHost, times(1)).onFlutterTextureViewCreated(notNull(FlutterTextureView.class)); @@ -282,7 +286,7 @@ public void itSendsInitialRouteToFlutter() { // --- Execute the behavior under test --- // The initial route is sent in onStart(). delegate.onAttach(RuntimeEnvironment.application); - delegate.onCreateView(null, null, null, 0); + delegate.onCreateView(null, null, null, 0, true); delegate.onStart(); // Verify that the navigation channel was given our initial route. @@ -306,7 +310,7 @@ public void itExecutesDartEntrypointProvidedByHost() { // --- Execute the behavior under test --- // Dart is executed in onStart(). delegate.onAttach(RuntimeEnvironment.application); - delegate.onCreateView(null, null, null, 0); + delegate.onCreateView(null, null, null, 0, true); delegate.onStart(); // Verify that the host's Dart entrypoint was used. @@ -335,7 +339,7 @@ public void itUsesDefaultFlutterLoaderAppBundlePathWhenUnspecified() { // --- Execute the behavior under test --- // Dart is executed in onStart(). delegate.onAttach(RuntimeEnvironment.application); - delegate.onCreateView(null, null, null, 0); + delegate.onCreateView(null, null, null, 0, true); delegate.onStart(); // Verify that the host's Dart entrypoint was used. @@ -390,7 +394,7 @@ public void itDoesNotAttachFlutterToTheActivityIfNotDesired() { // Make sure all of the other lifecycle methods can run safely as well // without a valid Activity - delegate.onCreateView(null, null, null, 0); + delegate.onCreateView(null, null, null, 0, true); delegate.onStart(); delegate.onResume(); delegate.onPause(); @@ -751,7 +755,7 @@ public void itDestroysItsOwnEngineIfHostRequestsIt() { // --- Execute the behavior under test --- // Push the delegate through all lifecycle methods all the way to destruction. delegate.onAttach(RuntimeEnvironment.application); - delegate.onCreateView(null, null, null, 0); + delegate.onCreateView(null, null, null, 0, true); delegate.onStart(); delegate.onResume(); delegate.onPause(); @@ -775,7 +779,7 @@ public void itDoesNotDestroyItsOwnEngineWhenHostSaysNotTo() { // --- Execute the behavior under test --- // Push the delegate through all lifecycle methods all the way to destruction. delegate.onAttach(RuntimeEnvironment.application); - delegate.onCreateView(null, null, null, 0); + delegate.onCreateView(null, null, null, 0, true); delegate.onStart(); delegate.onResume(); delegate.onPause(); @@ -806,7 +810,7 @@ public void itDestroysCachedEngineWhenHostRequestsIt() { // --- Execute the behavior under test --- // Push the delegate through all lifecycle methods all the way to destruction. delegate.onAttach(RuntimeEnvironment.application); - delegate.onCreateView(null, null, null, 0); + delegate.onCreateView(null, null, null, 0, true); delegate.onStart(); delegate.onResume(); delegate.onPause(); @@ -838,7 +842,7 @@ public void itDoesNotDestroyCachedEngineWhenHostSaysNotTo() { // --- Execute the behavior under test --- // Push the delegate through all lifecycle methods all the way to destruction. delegate.onAttach(RuntimeEnvironment.application); - delegate.onCreateView(null, null, null, 0); + delegate.onCreateView(null, null, null, 0, true); delegate.onStart(); delegate.onResume(); delegate.onPause(); @@ -850,6 +854,77 @@ public void itDoesNotDestroyCachedEngineWhenHostSaysNotTo() { verify(cachedEngine, never()).destroy(); } + @Test + public void itDelaysFirstDrawWhenRequested() { + // ---- Test setup ---- + FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(mockHost); + + // We're testing lifecycle behaviors, which require/expect that certain methods have already + // been executed by the time they run. Therefore, we run those expected methods first. + delegate.onAttach(RuntimeEnvironment.application); + + // --- Execute the behavior under test --- + boolean shouldDelayFirstAndroidViewDraw = true; + delegate.onCreateView(null, null, null, 0, shouldDelayFirstAndroidViewDraw); + + assertNotNull(delegate.activePreDrawListener); + } + + @Test + public void itDoesNotDelayFirstDrawWhenNotRequested() { + // ---- Test setup ---- + FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(mockHost); + + // We're testing lifecycle behaviors, which require/expect that certain methods have already + // been executed by the time they run. Therefore, we run those expected methods first. + delegate.onAttach(RuntimeEnvironment.application); + + // --- Execute the behavior under test --- + boolean shouldDelayFirstAndroidViewDraw = false; + delegate.onCreateView(null, null, null, 0, shouldDelayFirstAndroidViewDraw); + + assertNull(delegate.activePreDrawListener); + } + + @Test + public void itThrowsWhenDelayingTheFirstDrawAndUsingATextureView() { + // ---- Test setup ---- + when(mockHost.getRenderMode()).thenReturn(RenderMode.texture); + FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(mockHost); + + // We're testing lifecycle behaviors, which require/expect that certain methods have already + // been executed by the time they run. Therefore, we run those expected methods first. + delegate.onAttach(RuntimeEnvironment.application); + + // --- Execute the behavior under test --- + boolean shouldDelayFirstAndroidViewDraw = true; + assertThrows( + IllegalArgumentException.class, + () -> { + delegate.onCreateView(null, null, null, 0, shouldDelayFirstAndroidViewDraw); + }); + } + + @Test + public void itDoesNotDelayTheFirstDrawWhenRequestedAndWithAProvidedSplashScreen() { + when(mockHost.provideSplashScreen()) + .thenReturn(new DrawableSplashScreen(new ColorDrawable(Color.GRAY))); + + // ---- Test setup ---- + // Create the real object that we're testing. + FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(mockHost); + + // We're testing lifecycle behaviors, which require/expect that certain methods have already + // been executed by the time they run. Therefore, we run those expected methods first. + delegate.onAttach(RuntimeEnvironment.application); + + // --- Execute the behavior under test --- + boolean shouldDelayFirstAndroidViewDraw = true; + delegate.onCreateView(null, null, null, 0, shouldDelayFirstAndroidViewDraw); + + assertNull(delegate.activePreDrawListener); + } + /** * Creates a mock {@link io.flutter.embedding.engine.FlutterEngine}. * diff --git a/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityTest.java b/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityTest.java index ae42af77c5c38..b88460b3cb423 100644 --- a/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityTest.java +++ b/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityTest.java @@ -291,6 +291,31 @@ public void itCanBeDetachedFromTheEngineAndStopSendingFurtherEvents() { verify(mockDelegate, times(1)).onDetach(); } + @Test + public void itDelaysDrawing() { + Intent intent = FlutterActivity.createDefaultIntent(RuntimeEnvironment.application); + ActivityController activityController = + Robolectric.buildActivity(FlutterActivity.class, intent); + FlutterActivity flutterActivity = activityController.get(); + + flutterActivity.onCreate(null); + + assertNotNull(flutterActivity.delegate.activePreDrawListener); + } + + @Test + public void itDoesNotDelayDrawingwhenUsingTextureRendering() { + Intent intent = + FlutterActivityWithTextureRendering.createDefaultIntent(RuntimeEnvironment.application); + ActivityController activityController = + Robolectric.buildActivity(FlutterActivityWithTextureRendering.class, intent); + FlutterActivityWithTextureRendering flutterActivity = activityController.get(); + + flutterActivity.onCreate(null); + + assertNull(flutterActivity.delegate.activePreDrawListener); + } + @Test public void itRestoresPluginStateBeforePluginOnCreate() { FlutterLoader mockFlutterLoader = mock(FlutterLoader.class); @@ -443,6 +468,13 @@ public static CachedEngineIntentBuilder withCachedEngine(@NonNull String cachedE } } + private static class FlutterActivityWithTextureRendering extends FlutterActivity { + @Override + public RenderMode getRenderMode() { + return RenderMode.texture; + } + } + private static final class FakeFlutterPlugin implements FlutterPlugin, ActivityAware, diff --git a/shell/platform/android/test/io/flutter/embedding/android/FlutterAndroidComponentTest.java b/shell/platform/android/test/io/flutter/embedding/android/FlutterAndroidComponentTest.java index 82392038ab72d..d051fa797c9ca 100644 --- a/shell/platform/android/test/io/flutter/embedding/android/FlutterAndroidComponentTest.java +++ b/shell/platform/android/test/io/flutter/embedding/android/FlutterAndroidComponentTest.java @@ -82,7 +82,7 @@ public void pluginsReceiveFlutterPluginBinding() { assertNotNull(binding.getPlatformViewRegistry()); delegate.onRestoreInstanceState(null); - delegate.onCreateView(null, null, null, 0); + delegate.onCreateView(null, null, null, 0, true); delegate.onStart(); delegate.onResume(); delegate.onPause(); @@ -156,7 +156,7 @@ public Object answer(InvocationOnMock invocation) throws Throwable { // Verify that after Activity creation, the plugin was allowed to restore state. verify(mockSaveStateListener, times(1)).onRestoreInstanceState(any(Bundle.class)); - delegate.onCreateView(null, null, null, 0); + delegate.onCreateView(null, null, null, 0, true); delegate.onStart(); delegate.onResume(); delegate.onPause(); @@ -195,7 +195,7 @@ public void normalLifecycleStepsDoNotTriggerADetachFromFlutterEngine() { // Push the delegate through all lifecycle methods all the way to destruction. delegate.onAttach(RuntimeEnvironment.application); delegate.onRestoreInstanceState(null); - delegate.onCreateView(null, null, null, 0); + delegate.onCreateView(null, null, null, 0, true); delegate.onStart(); delegate.onResume(); delegate.onPause(); diff --git a/shell/platform/android/test/io/flutter/embedding/android/FlutterFragmentTest.java b/shell/platform/android/test/io/flutter/embedding/android/FlutterFragmentTest.java index d9ff237c50d6e..d0fbf671f95de 100644 --- a/shell/platform/android/test/io/flutter/embedding/android/FlutterFragmentTest.java +++ b/shell/platform/android/test/io/flutter/embedding/android/FlutterFragmentTest.java @@ -3,6 +3,7 @@ import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; @@ -41,6 +42,7 @@ public void itCreatesDefaultFragmentWithExpectedDefaults() { assertTrue(fragment.shouldDestroyEngineWithHost()); assertEquals(RenderMode.surface, fragment.getRenderMode()); assertEquals(TransparencyMode.transparent, fragment.getTransparencyMode()); + assertFalse(fragment.shouldDelayFirstAndroidViewDraw()); } @Test @@ -68,12 +70,21 @@ public void itCreatesNewEngineFragmentWithRequestedSettings() { } @Test - public void itCreatesCachedEngineFragmentThatDoesNotDestroyTheEngine() { + public void itCreatesNewEngineFragmentThatDelaysFirstDrawWhenRequested() { + FlutterFragment fragment = + FlutterFragment.withNewEngine().shouldDelayFirstAndroidViewDraw(true).build(); + + assertNotNull(fragment.shouldDelayFirstAndroidViewDraw()); + } + + @Test + public void itCreatesCachedEngineFragmentWithExpectedDefaults() { FlutterFragment fragment = FlutterFragment.withCachedEngine("my_cached_engine").build(); assertTrue(fragment.shouldAttachEngineToActivity()); assertEquals("my_cached_engine", fragment.getCachedEngineId()); assertFalse(fragment.shouldDestroyEngineWithHost()); + assertFalse(fragment.shouldDelayFirstAndroidViewDraw()); } @Test @@ -88,6 +99,16 @@ public void itCreatesCachedEngineFragmentThatDestroysTheEngine() { assertTrue(fragment.shouldDestroyEngineWithHost()); } + @Test + public void itCreatesCachedEngineFragmentThatDelaysFirstDrawWhenRequested() { + FlutterFragment fragment = + FlutterFragment.withCachedEngine("my_cached_engine") + .shouldDelayFirstAndroidViewDraw(true) + .build(); + + assertNotNull(fragment.shouldDelayFirstAndroidViewDraw()); + } + @Test public void itCanBeDetachedFromTheEngineAndStopSendingFurtherEvents() { FlutterActivityAndFragmentDelegate mockDelegate =