diff --git a/engine/src/flutter/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java b/engine/src/flutter/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java index 6254032e7a1..3c3979d038c 100644 --- a/engine/src/flutter/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java +++ b/engine/src/flutter/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java @@ -29,6 +29,7 @@ import android.view.View; import android.window.BackEvent; import androidx.annotation.NonNull; import androidx.lifecycle.Lifecycle; +import androidx.test.core.app.ActivityScenario; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import io.flutter.FlutterInjector; @@ -73,9 +74,6 @@ public class FlutterActivityAndFragmentDelegateTest { private FlutterActivityAndFragmentDelegate.Host mockHost; private FlutterActivityAndFragmentDelegate.Host mockHost2; - @SuppressWarnings("deprecation") - // Robolectric.setupActivity - // TODO(reidbaker): https://github.com/flutter/flutter/issues/133151 @Before public void setup() { FlutterInjector.reset(); @@ -86,7 +84,6 @@ public class FlutterActivityAndFragmentDelegateTest { // Create a mocked Host, which is required by the delegate being tested. mockHost = mock(FlutterActivityAndFragmentDelegate.Host.class); when(mockHost.getContext()).thenReturn(ctx); - when(mockHost.getActivity()).thenReturn(Robolectric.setupActivity(Activity.class)); when(mockHost.getLifecycle()).thenReturn(mock(Lifecycle.class)); when(mockHost.getFlutterShellArgs()).thenReturn(new FlutterShellArgs(new String[] {})); when(mockHost.getDartEntrypointFunctionName()).thenReturn("main"); @@ -104,7 +101,6 @@ public class FlutterActivityAndFragmentDelegateTest { mockHost2 = mock(FlutterActivityAndFragmentDelegate.Host.class); when(mockHost2.getContext()).thenReturn(ctx); - when(mockHost2.getActivity()).thenReturn(Robolectric.setupActivity(Activity.class)); when(mockHost2.getLifecycle()).thenReturn(mock(Lifecycle.class)); when(mockHost2.getFlutterShellArgs()).thenReturn(new FlutterShellArgs(new String[] {})); when(mockHost2.getDartEntrypointFunctionName()).thenReturn("main"); @@ -126,79 +122,90 @@ public class FlutterActivityAndFragmentDelegateTest { // ---- Test setup ---- // Create the real object that we're testing. FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(mockHost); + try (ActivityScenario scenario = ActivityScenario.launch(Activity.class)) { + scenario.onActivity( + activity -> { + when(mockHost.getActivity()).thenReturn(activity); - // 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(ctx); - delegate.onCreateView(null, null, null, 0, true); + // 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(ctx); + 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 - // to have been sent to Flutter. - delegate.onStart(); - verify(mockFlutterEngine.getLifecycleChannel(), never()).aWindowIsFocused(); - verify(mockFlutterEngine.getLifecycleChannel(), never()).noWindowsAreFocused(); - verify(mockFlutterEngine.getLifecycleChannel(), never()).appIsResumed(); - verify(mockFlutterEngine.getLifecycleChannel(), never()).appIsInactive(); - verify(mockFlutterEngine.getLifecycleChannel(), never()).appIsPaused(); - verify(mockFlutterEngine.getLifecycleChannel(), never()).appIsDetached(); + // --- Execute the behavior under test --- + // By the time an Activity/Fragment is started, we don't expect any lifecycle messages + // to have been sent to Flutter. + delegate.onStart(); + verify(mockFlutterEngine.getLifecycleChannel(), never()).aWindowIsFocused(); + verify(mockFlutterEngine.getLifecycleChannel(), never()).noWindowsAreFocused(); + verify(mockFlutterEngine.getLifecycleChannel(), never()).appIsResumed(); + verify(mockFlutterEngine.getLifecycleChannel(), never()).appIsInactive(); + verify(mockFlutterEngine.getLifecycleChannel(), never()).appIsPaused(); + verify(mockFlutterEngine.getLifecycleChannel(), never()).appIsDetached(); - // When the Activity/Fragment is resumed, a resumed message should have been sent to Flutter. - delegate.onResume(); - verify(mockFlutterEngine.getLifecycleChannel(), never()).aWindowIsFocused(); - verify(mockFlutterEngine.getLifecycleChannel(), never()).noWindowsAreFocused(); - verify(mockFlutterEngine.getLifecycleChannel(), times(1)).appIsResumed(); - verify(mockFlutterEngine.getLifecycleChannel(), never()).appIsInactive(); - verify(mockFlutterEngine.getLifecycleChannel(), never()).appIsPaused(); - verify(mockFlutterEngine.getLifecycleChannel(), never()).appIsDetached(); + // When the Activity/Fragment is resumed, a resumed message should have been sent to + // Flutter. + delegate.onResume(); + verify(mockFlutterEngine.getLifecycleChannel(), never()).aWindowIsFocused(); + verify(mockFlutterEngine.getLifecycleChannel(), never()).noWindowsAreFocused(); + verify(mockFlutterEngine.getLifecycleChannel(), times(1)).appIsResumed(); + verify(mockFlutterEngine.getLifecycleChannel(), never()).appIsInactive(); + verify(mockFlutterEngine.getLifecycleChannel(), never()).appIsPaused(); + verify(mockFlutterEngine.getLifecycleChannel(), never()).appIsDetached(); - // When the app loses focus because something else has it (e.g. notification - // windowshade or app switcher), it should go to inactive. - delegate.onWindowFocusChanged(false); - verify(mockFlutterEngine.getLifecycleChannel(), never()).aWindowIsFocused(); - verify(mockFlutterEngine.getLifecycleChannel(), times(1)).noWindowsAreFocused(); - verify(mockFlutterEngine.getLifecycleChannel(), times(1)).appIsResumed(); - verify(mockFlutterEngine.getLifecycleChannel(), never()).appIsInactive(); - verify(mockFlutterEngine.getLifecycleChannel(), never()).appIsPaused(); - verify(mockFlutterEngine.getLifecycleChannel(), never()).appIsDetached(); + // When the app loses focus because something else has it (e.g. notification + // windowshade or app switcher), it should go to inactive. + delegate.onWindowFocusChanged(false); + verify(mockFlutterEngine.getLifecycleChannel(), never()).aWindowIsFocused(); + verify(mockFlutterEngine.getLifecycleChannel(), times(1)).noWindowsAreFocused(); + verify(mockFlutterEngine.getLifecycleChannel(), times(1)).appIsResumed(); + verify(mockFlutterEngine.getLifecycleChannel(), never()).appIsInactive(); + verify(mockFlutterEngine.getLifecycleChannel(), never()).appIsPaused(); + verify(mockFlutterEngine.getLifecycleChannel(), never()).appIsDetached(); - // When the app regains focus, it should go to resumed again. - delegate.onWindowFocusChanged(true); - verify(mockFlutterEngine.getLifecycleChannel(), times(1)).aWindowIsFocused(); - verify(mockFlutterEngine.getLifecycleChannel(), times(1)).noWindowsAreFocused(); - verify(mockFlutterEngine.getLifecycleChannel(), times(1)).appIsResumed(); - verify(mockFlutterEngine.getLifecycleChannel(), never()).appIsInactive(); - verify(mockFlutterEngine.getLifecycleChannel(), never()).appIsPaused(); - verify(mockFlutterEngine.getLifecycleChannel(), never()).appIsDetached(); + // When the app regains focus, it should go to resumed again. + delegate.onWindowFocusChanged(true); + verify(mockFlutterEngine.getLifecycleChannel(), times(1)).aWindowIsFocused(); + verify(mockFlutterEngine.getLifecycleChannel(), times(1)).noWindowsAreFocused(); + verify(mockFlutterEngine.getLifecycleChannel(), times(1)).appIsResumed(); + verify(mockFlutterEngine.getLifecycleChannel(), never()).appIsInactive(); + verify(mockFlutterEngine.getLifecycleChannel(), never()).appIsPaused(); + verify(mockFlutterEngine.getLifecycleChannel(), never()).appIsDetached(); - // When the Activity/Fragment is paused, an inactive message should have been sent to Flutter. - delegate.onPause(); - verify(mockFlutterEngine.getLifecycleChannel(), times(1)).aWindowIsFocused(); - verify(mockFlutterEngine.getLifecycleChannel(), times(1)).noWindowsAreFocused(); - verify(mockFlutterEngine.getLifecycleChannel(), times(1)).appIsResumed(); - verify(mockFlutterEngine.getLifecycleChannel(), times(1)).appIsInactive(); - verify(mockFlutterEngine.getLifecycleChannel(), never()).appIsPaused(); - verify(mockFlutterEngine.getLifecycleChannel(), never()).appIsDetached(); + // When the Activity/Fragment is paused, an inactive message should have been sent to + // Flutter. + delegate.onPause(); + verify(mockFlutterEngine.getLifecycleChannel(), times(1)).aWindowIsFocused(); + verify(mockFlutterEngine.getLifecycleChannel(), times(1)).noWindowsAreFocused(); + verify(mockFlutterEngine.getLifecycleChannel(), times(1)).appIsResumed(); + verify(mockFlutterEngine.getLifecycleChannel(), times(1)).appIsInactive(); + verify(mockFlutterEngine.getLifecycleChannel(), never()).appIsPaused(); + verify(mockFlutterEngine.getLifecycleChannel(), never()).appIsDetached(); - // When the Activity/Fragment is stopped, a paused message should have been sent to Flutter. - // Notice that Flutter uses the term "paused" in a different way, and at a different time - // than the Android OS. - delegate.onStop(); - verify(mockFlutterEngine.getLifecycleChannel(), times(1)).aWindowIsFocused(); - verify(mockFlutterEngine.getLifecycleChannel(), times(1)).noWindowsAreFocused(); - verify(mockFlutterEngine.getLifecycleChannel(), times(1)).appIsResumed(); - verify(mockFlutterEngine.getLifecycleChannel(), times(1)).appIsInactive(); - verify(mockFlutterEngine.getLifecycleChannel(), times(1)).appIsPaused(); - verify(mockFlutterEngine.getLifecycleChannel(), never()).appIsDetached(); + // When the Activity/Fragment is stopped, a paused message should have been sent to + // Flutter. + // Notice that Flutter uses the term "paused" in a different way, and at a different + // time + // than the Android OS. + delegate.onStop(); + verify(mockFlutterEngine.getLifecycleChannel(), times(1)).aWindowIsFocused(); + verify(mockFlutterEngine.getLifecycleChannel(), times(1)).noWindowsAreFocused(); + verify(mockFlutterEngine.getLifecycleChannel(), times(1)).appIsResumed(); + verify(mockFlutterEngine.getLifecycleChannel(), times(1)).appIsInactive(); + verify(mockFlutterEngine.getLifecycleChannel(), times(1)).appIsPaused(); + verify(mockFlutterEngine.getLifecycleChannel(), never()).appIsDetached(); - // When activity detaches, a detached message should have been sent to Flutter. - delegate.onDetach(); - verify(mockFlutterEngine.getLifecycleChannel(), times(1)).aWindowIsFocused(); - verify(mockFlutterEngine.getLifecycleChannel(), times(1)).noWindowsAreFocused(); - verify(mockFlutterEngine.getLifecycleChannel(), times(1)).appIsResumed(); - verify(mockFlutterEngine.getLifecycleChannel(), times(1)).appIsInactive(); - verify(mockFlutterEngine.getLifecycleChannel(), times(1)).appIsPaused(); - verify(mockFlutterEngine.getLifecycleChannel(), times(1)).appIsDetached(); + // When activity detaches, a detached message should have been sent to Flutter. + delegate.onDetach(); + verify(mockFlutterEngine.getLifecycleChannel(), times(1)).aWindowIsFocused(); + verify(mockFlutterEngine.getLifecycleChannel(), times(1)).noWindowsAreFocused(); + verify(mockFlutterEngine.getLifecycleChannel(), times(1)).appIsResumed(); + verify(mockFlutterEngine.getLifecycleChannel(), times(1)).appIsInactive(); + verify(mockFlutterEngine.getLifecycleChannel(), times(1)).appIsPaused(); + verify(mockFlutterEngine.getLifecycleChannel(), times(1)).appIsDetached(); + }); + } } @Test @@ -206,27 +213,34 @@ public class FlutterActivityAndFragmentDelegateTest { // ---- Test setup ---- // Create the real object that we're testing. FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(mockHost); + try (ActivityScenario scenario = ActivityScenario.launch(Activity.class)) { + scenario.onActivity( + activity -> { + when(mockHost.getActivity()).thenReturn(activity); - when(mockHost.shouldDispatchAppLifecycleState()).thenReturn(false); + when(mockHost.shouldDispatchAppLifecycleState()).thenReturn(false); - // 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(ctx); - delegate.onCreateView(null, null, null, 0, true); - delegate.onStart(); - delegate.onResume(); - delegate.onWindowFocusChanged(false); - delegate.onWindowFocusChanged(true); - delegate.onPause(); - delegate.onStop(); - delegate.onDetach(); + // 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(ctx); + delegate.onCreateView(null, null, null, 0, true); + delegate.onStart(); + delegate.onResume(); + delegate.onWindowFocusChanged(false); + delegate.onWindowFocusChanged(true); + delegate.onPause(); + delegate.onStop(); + delegate.onDetach(); - verify(mockFlutterEngine.getLifecycleChannel(), never()).aWindowIsFocused(); - verify(mockFlutterEngine.getLifecycleChannel(), never()).noWindowsAreFocused(); - verify(mockFlutterEngine.getLifecycleChannel(), never()).appIsResumed(); - verify(mockFlutterEngine.getLifecycleChannel(), never()).appIsPaused(); - verify(mockFlutterEngine.getLifecycleChannel(), never()).appIsInactive(); - verify(mockFlutterEngine.getLifecycleChannel(), never()).appIsDetached(); + verify(mockFlutterEngine.getLifecycleChannel(), never()).aWindowIsFocused(); + verify(mockFlutterEngine.getLifecycleChannel(), never()).noWindowsAreFocused(); + verify(mockFlutterEngine.getLifecycleChannel(), never()).appIsResumed(); + verify(mockFlutterEngine.getLifecycleChannel(), never()).appIsPaused(); + verify(mockFlutterEngine.getLifecycleChannel(), never()).appIsInactive(); + verify(mockFlutterEngine.getLifecycleChannel(), never()).appIsDetached(); + }); + } } @Test @@ -291,12 +305,19 @@ public class FlutterActivityAndFragmentDelegateTest { // Create the real object that we're testing. FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(mockHost); + try (ActivityScenario scenario = ActivityScenario.launch(Activity.class)) { + scenario.onActivity( + activity -> { + when(mockHost.getActivity()).thenReturn(activity); - // --- Execute the behavior under test --- - // The FlutterEngine existence is verified in onAttach() - delegate.onAttach(ctx); - - // Expect IllegalStateException. + // --- Execute the behavior under test --- + // The FlutterEngine existence is verified in onAttach() + delegate.onAttach(ctx); + }); + } catch (IllegalStateException e) { + // Expect IllegalStateException. + throw e; + } } // Bug: b/271100292 @@ -407,13 +428,19 @@ public class FlutterActivityAndFragmentDelegateTest { // ---- Test setup ---- // Create the real object that we're testing. FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(mockHost); + try (ActivityScenario scenario = ActivityScenario.launch(Activity.class)) { + scenario.onActivity( + activity -> { + when(mockHost.getActivity()).thenReturn(activity); - // --- Execute the behavior under test --- - // The FlutterEngine is created in onAttach(). - delegate.onAttach(ctx); + // --- Execute the behavior under test --- + // The FlutterEngine is created in onAttach(). + delegate.onAttach(ctx); - // Verify that the host was asked to configure our FlutterEngine. - verify(mockHost, times(1)).configureFlutterEngine(mockFlutterEngine); + // Verify that the host was asked to configure our FlutterEngine. + verify(mockHost, times(1)).configureFlutterEngine(mockFlutterEngine); + }); + } } @Test @@ -430,36 +457,40 @@ public class FlutterActivityAndFragmentDelegateTest { verify(mockHost, times(1)).onFlutterSurfaceViewCreated(isNotNull()); } - @SuppressWarnings("deprecation") - // Robolectric.setupActivity - // TODO(reidbaker): https://github.com/flutter/flutter/issues/133151 @Test public void itGivesHostAnOpportunityToConfigureFlutterTextureView() { // ---- Test setup ---- Host customMockHost = mock(Host.class); when(customMockHost.getContext()).thenReturn(ctx); - when(customMockHost.getActivity()).thenReturn(Robolectric.setupActivity(Activity.class)); - when(customMockHost.getLifecycle()).thenReturn(mock(Lifecycle.class)); - when(customMockHost.getFlutterShellArgs()).thenReturn(new FlutterShellArgs(new String[] {})); - when(customMockHost.getDartEntrypointFunctionName()).thenReturn("main"); - when(customMockHost.getAppBundlePath()).thenReturn("/fake/path"); - when(customMockHost.getInitialRoute()).thenReturn("/"); - when(customMockHost.getRenderMode()).thenReturn(RenderMode.texture); - when(customMockHost.getTransparencyMode()).thenReturn(TransparencyMode.transparent); - when(customMockHost.provideFlutterEngine(any(Context.class))).thenReturn(mockFlutterEngine); - when(customMockHost.shouldAttachEngineToActivity()).thenReturn(true); - when(customMockHost.shouldDestroyEngineWithHost()).thenReturn(true); + try (ActivityScenario scenario = ActivityScenario.launch(Activity.class)) { + scenario.onActivity( + activity -> { + when(customMockHost.getActivity()).thenReturn(activity); + when(customMockHost.getLifecycle()).thenReturn(mock(Lifecycle.class)); + when(customMockHost.getFlutterShellArgs()) + .thenReturn(new FlutterShellArgs(new String[] {})); + when(customMockHost.getDartEntrypointFunctionName()).thenReturn("main"); + when(customMockHost.getAppBundlePath()).thenReturn("/fake/path"); + when(customMockHost.getInitialRoute()).thenReturn("/"); + when(customMockHost.getRenderMode()).thenReturn(RenderMode.texture); + when(customMockHost.getTransparencyMode()).thenReturn(TransparencyMode.transparent); + when(customMockHost.provideFlutterEngine(any(Context.class))) + .thenReturn(mockFlutterEngine); + when(customMockHost.shouldAttachEngineToActivity()).thenReturn(true); + when(customMockHost.shouldDestroyEngineWithHost()).thenReturn(true); - // Create the real object that we're testing. - FlutterActivityAndFragmentDelegate delegate = - new FlutterActivityAndFragmentDelegate(customMockHost); + // Create the real object that we're testing. + FlutterActivityAndFragmentDelegate delegate = + new FlutterActivityAndFragmentDelegate(customMockHost); - // --- Execute the behavior under test --- - delegate.onAttach(ctx); - delegate.onCreateView(null, null, null, 0, false); + // --- Execute the behavior under test --- + delegate.onAttach(ctx); + delegate.onCreateView(null, null, null, 0, false); - // Verify that the host was asked to configure a FlutterTextureView. - verify(customMockHost, times(1)).onFlutterTextureViewCreated(isNotNull()); + // Verify that the host was asked to configure a FlutterTextureView. + verify(customMockHost, times(1)).onFlutterTextureViewCreated(isNotNull()); + }); + } } @Test @@ -467,14 +498,20 @@ public class FlutterActivityAndFragmentDelegateTest { // ---- Test setup ---- // Create the real object that we're testing. FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(mockHost); + try (ActivityScenario scenario = ActivityScenario.launch(Activity.class)) { + scenario.onActivity( + activity -> { + when(mockHost.getActivity()).thenReturn(activity); - // --- Execute the behavior under test --- - // The FlutterEngine is created in onAttach(). - delegate.onAttach(ctx); - delegate.onDetach(); + // --- Execute the behavior under test --- + // The FlutterEngine is created in onAttach(). + delegate.onAttach(ctx); + delegate.onDetach(); - // Verify that the host was asked to configure our FlutterEngine. - verify(mockHost, times(1)).cleanUpFlutterEngine(mockFlutterEngine); + // Verify that the host was asked to configure our FlutterEngine. + verify(mockHost, times(1)).cleanUpFlutterEngine(mockFlutterEngine); + }); + } } @Test @@ -608,20 +645,26 @@ public class FlutterActivityAndFragmentDelegateTest { // Create the real object that we're testing. FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(mockHost); + try (ActivityScenario scenario = ActivityScenario.launch(Activity.class)) { + scenario.onActivity( + activity -> { + when(mockHost.getActivity()).thenReturn(activity); - // --- Execute the behavior under test --- - // Flutter is attached to the surrounding Activity in onAttach. - delegate.onAttach(ctx); + // --- Execute the behavior under test --- + // Flutter is attached to the surrounding Activity in onAttach. + delegate.onAttach(ctx); - // Verify that the ActivityControlSurface was told to attach to an Activity. - verify(mockFlutterEngine.getActivityControlSurface(), times(1)) - .attachToActivity(any(ExclusiveAppComponent.class), any(Lifecycle.class)); + // Verify that the ActivityControlSurface was told to attach to an Activity. + verify(mockFlutterEngine.getActivityControlSurface(), times(1)) + .attachToActivity(any(ExclusiveAppComponent.class), any(Lifecycle.class)); - // Flutter is detached from the surrounding Activity in onDetach. - delegate.onDetach(); + // Flutter is detached from the surrounding Activity in onDetach. + delegate.onDetach(); - // Verify that the ActivityControlSurface was told to detach from the Activity. - verify(mockFlutterEngine.getActivityControlSurface(), times(1)).detachFromActivity(); + // Verify that the ActivityControlSurface was told to detach from the Activity. + verify(mockFlutterEngine.getActivityControlSurface(), times(1)).detachFromActivity(); + }); + } } // "Attaching" to the surrounding Activity refers to Flutter being able to control @@ -1114,20 +1157,26 @@ public class FlutterActivityAndFragmentDelegateTest { // Create the real object that we're testing. FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(mockHost); + try (ActivityScenario scenario = ActivityScenario.launch(Activity.class)) { + scenario.onActivity( + activity -> { + when(mockHost.getActivity()).thenReturn(activity); - // --- Execute the behavior under test --- - // Push the delegate through all lifecycle methods all the way to destruction. - delegate.onAttach(ctx); - delegate.onCreateView(null, null, null, 0, true); - delegate.onStart(); - delegate.onResume(); - delegate.onPause(); - delegate.onStop(); - delegate.onDestroyView(); - delegate.onDetach(); + // --- Execute the behavior under test --- + // Push the delegate through all lifecycle methods all the way to destruction. + delegate.onAttach(ctx); + delegate.onCreateView(null, null, null, 0, true); + delegate.onStart(); + delegate.onResume(); + delegate.onPause(); + delegate.onStop(); + delegate.onDestroyView(); + delegate.onDetach(); - // --- Verify that the cached engine was destroyed --- - verify(mockFlutterEngine, times(1)).destroy(); + // --- Verify that the cached engine was destroyed --- + verify(mockFlutterEngine, times(1)).destroy(); + }); + } } @Test @@ -1138,20 +1187,26 @@ public class FlutterActivityAndFragmentDelegateTest { // Create the real object that we're testing. FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(mockHost); + try (ActivityScenario scenario = ActivityScenario.launch(Activity.class)) { + scenario.onActivity( + activity -> { + when(mockHost.getActivity()).thenReturn(activity); - // --- Execute the behavior under test --- - // Push the delegate through all lifecycle methods all the way to destruction. - delegate.onAttach(ctx); - delegate.onCreateView(null, null, null, 0, true); - delegate.onStart(); - delegate.onResume(); - delegate.onPause(); - delegate.onStop(); - delegate.onDestroyView(); - delegate.onDetach(); + // --- Execute the behavior under test --- + // Push the delegate through all lifecycle methods all the way to destruction. + delegate.onAttach(ctx); + delegate.onCreateView(null, null, null, 0, true); + delegate.onStart(); + delegate.onResume(); + delegate.onPause(); + delegate.onStop(); + delegate.onDestroyView(); + delegate.onDetach(); - // --- Verify that the cached engine was destroyed --- - verify(mockFlutterEngine, never()).destroy(); + // --- Verify that the cached engine was destroyed --- + verify(mockFlutterEngine, never()).destroy(); + }); + } } @Test @@ -1169,21 +1224,27 @@ public class FlutterActivityAndFragmentDelegateTest { // Create the real object that we're testing. FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(mockHost); + try (ActivityScenario scenario = ActivityScenario.launch(Activity.class)) { + scenario.onActivity( + activity -> { + when(mockHost.getActivity()).thenReturn(activity); - // --- Execute the behavior under test --- - // Push the delegate through all lifecycle methods all the way to destruction. - delegate.onAttach(ctx); - delegate.onCreateView(null, null, null, 0, true); - delegate.onStart(); - delegate.onResume(); - delegate.onPause(); - delegate.onStop(); - delegate.onDestroyView(); - delegate.onDetach(); + // --- Execute the behavior under test --- + // Push the delegate through all lifecycle methods all the way to destruction. + delegate.onAttach(ctx); + delegate.onCreateView(null, null, null, 0, true); + delegate.onStart(); + delegate.onResume(); + delegate.onPause(); + delegate.onStop(); + delegate.onDestroyView(); + delegate.onDetach(); - // --- Verify that the cached engine was destroyed --- - verify(cachedEngine, times(1)).destroy(); - assertNull(FlutterEngineCache.getInstance().get("my_flutter_engine")); + // --- Verify that the cached engine was destroyed --- + verify(cachedEngine, times(1)).destroy(); + assertNull(FlutterEngineCache.getInstance().get("my_flutter_engine")); + }); + } } @Test @@ -1201,20 +1262,26 @@ public class FlutterActivityAndFragmentDelegateTest { // Create the real object that we're testing. FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(mockHost); + try (ActivityScenario scenario = ActivityScenario.launch(Activity.class)) { + scenario.onActivity( + activity -> { + when(mockHost.getActivity()).thenReturn(activity); - // --- Execute the behavior under test --- - // Push the delegate through all lifecycle methods all the way to destruction. - delegate.onAttach(ctx); - delegate.onCreateView(null, null, null, 0, true); - delegate.onStart(); - delegate.onResume(); - delegate.onPause(); - delegate.onStop(); - delegate.onDestroyView(); - delegate.onDetach(); + // --- Execute the behavior under test --- + // Push the delegate through all lifecycle methods all the way to destruction. + delegate.onAttach(ctx); + delegate.onCreateView(null, null, null, 0, true); + delegate.onStart(); + delegate.onResume(); + delegate.onPause(); + delegate.onStop(); + delegate.onDestroyView(); + delegate.onDetach(); - // --- Verify that the cached engine was NOT destroyed --- - verify(cachedEngine, never()).destroy(); + // --- Verify that the cached engine was NOT destroyed --- + verify(cachedEngine, never()).destroy(); + }); + } } @Test @@ -1385,55 +1452,75 @@ public class FlutterActivityAndFragmentDelegateTest { // Create the real objects that we're testing. FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(mockHost); - FlutterActivityAndFragmentDelegate delegate2 = - new FlutterActivityAndFragmentDelegate(mockHost2); + try (ActivityScenario scenario = ActivityScenario.launch(Activity.class)) { + scenario.onActivity( + activity -> { + when(mockHost.getActivity()).thenReturn(activity); - // This test is written to recreate the following scenario: - // 1. We have a FlutterFragment_A attached to a singleton cached engine. - // 2. An intent arrives that spawns FlutterFragment_B. - // 3. FlutterFragment_B starts and steals the engine from FlutterFragment_A while attaching. - // Via a call to FlutterActivityAndFragmentDelegate.detachFromFlutterEngine(). - // 4. FlutterFragment_A is forcibly detached from the engine. - // 5. FlutterFragment_B is attached to the engine. - // 6. FlutterFragment_A is detached from the engine. - // Note that the second detach for FlutterFragment_A is done unconditionally when the Fragment - // is being - // torn down. + FlutterActivityAndFragmentDelegate delegate2 = + new FlutterActivityAndFragmentDelegate(mockHost2); + try (ActivityScenario scenario2 = ActivityScenario.launch(Activity.class)) { + scenario2.onActivity( + activity2 -> { + when(mockHost2.getActivity()).thenReturn(activity2); - // At this point the engine's life cycle channel receives a message (triggered by - // FlutterFragment_A's second detach) - // that indicates the app is detached. This breaks FlutterFragment_B. + // This test is written to recreate the following scenario: + // 1. We have a FlutterFragment_A attached to a singleton cached engine. + // 2. An intent arrives that spawns FlutterFragment_B. + // 3. FlutterFragment_B starts and steals the engine from FlutterFragment_A + // while attaching. + // Via a call to + // FlutterActivityAndFragmentDelegate.detachFromFlutterEngine(). + // 4. FlutterFragment_A is forcibly detached from the engine. + // 5. FlutterFragment_B is attached to the engine. + // 6. FlutterFragment_A is detached from the engine. + // Note that the second detach for FlutterFragment_A is done unconditionally + // when the Fragment + // is being + // torn down. - // Below is a sequence of calls that mimicks the calls that the above scenario would trigger - // without - // relying on an intent to trigger the behaviour. + // At this point the engine's life cycle channel receives a message (triggered + // by + // FlutterFragment_A's second detach) + // that indicates the app is detached. This breaks FlutterFragment_B. - // FlutterFragment_A is attached to the engine. - delegate.onAttach(ctx); + // Below is a sequence of calls that mimicks the calls that the above scenario + // would trigger + // without + // relying on an intent to trigger the behaviour. - // NOTE: The following two calls happen in a slightly different order in reality. That is, via, - // a call to host.detachFromFlutterEngine, delegate2.onAttach ends up invoking - // delegate.onDetach. - // To keep this regression test simple, we call them directly. + // FlutterFragment_A is attached to the engine. + delegate.onAttach(ctx); - // Detach FlutterFragment_A. - delegate.onDetach(); + // NOTE: The following two calls happen in a slightly different order in + // reality. That is, via, + // a call to host.detachFromFlutterEngine, delegate2.onAttach ends up invoking + // delegate.onDetach. + // To keep this regression test simple, we call them directly. - verify(cachedEngine.getLifecycleChannel(), times(1)).appIsDetached(); + // Detach FlutterFragment_A. + delegate.onDetach(); - // Attaches to the engine FlutterFragment_B. - delegate2.onAttach(ctx); - delegate2.onResume(); + verify(cachedEngine.getLifecycleChannel(), times(1)).appIsDetached(); - verify(cachedEngine.getLifecycleChannel(), times(1)).appIsResumed(); - verify(cachedEngine.getLifecycleChannel(), times(1)).appIsDetached(); + // Attaches to the engine FlutterFragment_B. + delegate2.onAttach(ctx); + delegate2.onResume(); - // A second Detach of FlutterFragment_A happens when the Fragment is detached. - delegate.onDetach(); + verify(cachedEngine.getLifecycleChannel(), times(1)).appIsResumed(); + verify(cachedEngine.getLifecycleChannel(), times(1)).appIsDetached(); - // IMPORTANT: The bug we fixed would have resulted in the engine thinking the app - // is detached twice instead of once. - verify(cachedEngine.getLifecycleChannel(), times(1)).appIsDetached(); + // A second Detach of FlutterFragment_A happens when the Fragment is detached. + delegate.onDetach(); + + // IMPORTANT: The bug we fixed would have resulted in the engine thinking the + // app + // is detached twice instead of once. + verify(cachedEngine.getLifecycleChannel(), times(1)).appIsDetached(); + }); + } + }); + } } /** diff --git a/engine/src/flutter/shell/platform/android/test/io/flutter/embedding/android/FlutterAndroidComponentTest.java b/engine/src/flutter/shell/platform/android/test/io/flutter/embedding/android/FlutterAndroidComponentTest.java index 2db46ffbdc6..82b3b276ab3 100644 --- a/engine/src/flutter/shell/platform/android/test/io/flutter/embedding/android/FlutterAndroidComponentTest.java +++ b/engine/src/flutter/shell/platform/android/test/io/flutter/embedding/android/FlutterAndroidComponentTest.java @@ -25,6 +25,7 @@ import android.os.Bundle; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.lifecycle.Lifecycle; +import androidx.test.core.app.ActivityScenario; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import io.flutter.embedding.engine.FlutterEngine; @@ -66,43 +67,49 @@ public class FlutterAndroidComponentTest { cachedEngine.getPlugins().add(mockPlugin); // Create a fake Host, which is required by the delegate. - FakeHost fakeHost = new FakeHost(cachedEngine); - fakeHost.shouldDestroyEngineWithHost = true; + try (ActivityScenario scenario = ActivityScenario.launch(Activity.class)) { + scenario.onActivity( + activity -> { + FakeHost fakeHost = new FakeHost(cachedEngine, activity); + fakeHost.shouldDestroyEngineWithHost = true; - // Create the real object that we're testing. - FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(fakeHost); + // Create the real object that we're testing. + FlutterActivityAndFragmentDelegate delegate = + new FlutterActivityAndFragmentDelegate(fakeHost); - // --- Execute the behavior under test --- - // Push the delegate through all lifecycle methods all the way to destruction. - delegate.onAttach(ctx); + // --- Execute the behavior under test --- + // Push the delegate through all lifecycle methods all the way to destruction. + delegate.onAttach(ctx); - // Verify that the plugin is attached to the FlutterEngine. - ArgumentCaptor pluginBindingCaptor = - ArgumentCaptor.forClass(FlutterPlugin.FlutterPluginBinding.class); - verify(mockPlugin, times(1)).onAttachedToEngine(pluginBindingCaptor.capture()); - FlutterPlugin.FlutterPluginBinding binding = pluginBindingCaptor.getValue(); - assertNotNull(binding.getApplicationContext()); - assertNotNull(binding.getBinaryMessenger()); - assertNotNull(binding.getTextureRegistry()); - assertNotNull(binding.getPlatformViewRegistry()); + // Verify that the plugin is attached to the FlutterEngine. + ArgumentCaptor pluginBindingCaptor = + ArgumentCaptor.forClass(FlutterPlugin.FlutterPluginBinding.class); + verify(mockPlugin, times(1)).onAttachedToEngine(pluginBindingCaptor.capture()); + FlutterPlugin.FlutterPluginBinding binding = pluginBindingCaptor.getValue(); + assertNotNull(binding.getApplicationContext()); + assertNotNull(binding.getBinaryMessenger()); + assertNotNull(binding.getTextureRegistry()); + assertNotNull(binding.getPlatformViewRegistry()); - delegate.onRestoreInstanceState(null); - delegate.onCreateView(null, null, null, 0, true); - delegate.onStart(); - delegate.onResume(); - delegate.onPause(); - delegate.onStop(); - delegate.onDestroyView(); - delegate.onDetach(); + delegate.onRestoreInstanceState(null); + delegate.onCreateView(null, null, null, 0, true); + delegate.onStart(); + delegate.onResume(); + delegate.onPause(); + delegate.onStop(); + delegate.onDestroyView(); + delegate.onDetach(); - // Verify the plugin was detached from the FlutterEngine. - pluginBindingCaptor = ArgumentCaptor.forClass(FlutterPlugin.FlutterPluginBinding.class); - verify(mockPlugin, times(1)).onDetachedFromEngine(pluginBindingCaptor.capture()); - binding = pluginBindingCaptor.getValue(); - assertNotNull(binding.getApplicationContext()); - assertNotNull(binding.getBinaryMessenger()); - assertNotNull(binding.getTextureRegistry()); - assertNotNull(binding.getPlatformViewRegistry()); + // Verify the plugin was detached from the FlutterEngine. + pluginBindingCaptor = ArgumentCaptor.forClass(FlutterPlugin.FlutterPluginBinding.class); + verify(mockPlugin, times(1)).onDetachedFromEngine(pluginBindingCaptor.capture()); + binding = pluginBindingCaptor.getValue(); + assertNotNull(binding.getApplicationContext()); + assertNotNull(binding.getBinaryMessenger()); + assertNotNull(binding.getTextureRegistry()); + assertNotNull(binding.getPlatformViewRegistry()); + }); + } } @Test @@ -139,42 +146,49 @@ public class FlutterAndroidComponentTest { cachedEngine.getPlugins().add(mockPlugin); // Create a fake Host, which is required by the delegate. - FlutterActivityAndFragmentDelegate.Host fakeHost = new FakeHost(cachedEngine); + try (ActivityScenario scenario = ActivityScenario.launch(Activity.class)) { + scenario.onActivity( + activity -> { + FlutterActivityAndFragmentDelegate.Host fakeHost = new FakeHost(cachedEngine, activity); - FlutterActivityAndFragmentDelegate delegate = new FlutterActivityAndFragmentDelegate(fakeHost); + FlutterActivityAndFragmentDelegate delegate = + new FlutterActivityAndFragmentDelegate(fakeHost); - // --- Execute the behavior under test --- - // Push the delegate through all lifecycle methods all the way to destruction. - delegate.onAttach(ctx); + // --- Execute the behavior under test --- + // Push the delegate through all lifecycle methods all the way to destruction. + delegate.onAttach(ctx); - // Verify plugin was given an ActivityPluginBinding. - ArgumentCaptor pluginBindingCaptor = - ArgumentCaptor.forClass(ActivityPluginBinding.class); - verify(activityAwarePlugin, times(1)).onAttachedToActivity(pluginBindingCaptor.capture()); - ActivityPluginBinding binding = pluginBindingCaptor.getValue(); - assertNotNull(binding.getActivity()); - assertNotNull(binding.getLifecycle()); + // Verify plugin was given an ActivityPluginBinding. + ArgumentCaptor pluginBindingCaptor = + ArgumentCaptor.forClass(ActivityPluginBinding.class); + verify(activityAwarePlugin, times(1)) + .onAttachedToActivity(pluginBindingCaptor.capture()); + ActivityPluginBinding binding = pluginBindingCaptor.getValue(); + assertNotNull(binding.getActivity()); + assertNotNull(binding.getLifecycle()); - delegate.onRestoreInstanceState(null); + delegate.onRestoreInstanceState(null); - // Verify that after Activity creation, the plugin was allowed to restore state. - verify(mockSaveStateListener, times(1)).onRestoreInstanceState(isNull()); + // Verify that after Activity creation, the plugin was allowed to restore state. + verify(mockSaveStateListener, times(1)).onRestoreInstanceState(isNull()); - delegate.onCreateView(null, null, null, 0, true); - delegate.onStart(); - delegate.onResume(); - delegate.onPause(); - delegate.onStop(); - delegate.onSaveInstanceState(mock(Bundle.class)); + delegate.onCreateView(null, null, null, 0, true); + delegate.onStart(); + delegate.onResume(); + delegate.onPause(); + delegate.onStop(); + delegate.onSaveInstanceState(mock(Bundle.class)); - // Verify that the plugin was allowed to save state. - verify(mockSaveStateListener, times(1)).onSaveInstanceState(any(Bundle.class)); + // Verify that the plugin was allowed to save state. + verify(mockSaveStateListener, times(1)).onSaveInstanceState(any(Bundle.class)); - delegate.onDestroyView(); - delegate.onDetach(); + delegate.onDestroyView(); + delegate.onDetach(); - // Verify that the plugin was detached from the Activity. - verify(activityAwarePlugin, times(1)).onDetachedFromActivity(); + // Verify that the plugin was detached from the Activity. + verify(activityAwarePlugin, times(1)).onDetachedFromActivity(); + }); + } } @Test @@ -188,25 +202,30 @@ public class FlutterAndroidComponentTest { FlutterEngineCache.getInstance().put("my_flutter_engine", cachedEngine); // Create a fake Host, which is required by the delegate. - FakeHost fakeHost = new FakeHost(cachedEngine); + try (ActivityScenario scenario = ActivityScenario.launch(Activity.class)) { + scenario.onActivity( + activity -> { + FakeHost fakeHost = new FakeHost(cachedEngine, activity); - // Create the real object that we're testing. - FlutterActivityAndFragmentDelegate delegate = - spy(new FlutterActivityAndFragmentDelegate(fakeHost)); + // Create the real object that we're testing. + FlutterActivityAndFragmentDelegate delegate = + spy(new FlutterActivityAndFragmentDelegate(fakeHost)); - // --- Execute the behavior under test --- - // Push the delegate through all lifecycle methods all the way to destruction. - delegate.onAttach(ctx); - delegate.onRestoreInstanceState(null); - delegate.onCreateView(null, null, null, 0, true); - delegate.onStart(); - delegate.onResume(); - delegate.onPause(); - delegate.onStop(); - delegate.onDestroyView(); - delegate.onDetach(); + // --- Execute the behavior under test --- + // Push the delegate through all lifecycle methods all the way to destruction. + delegate.onAttach(ctx); + delegate.onRestoreInstanceState(null); + delegate.onCreateView(null, null, null, 0, true); + delegate.onStart(); + delegate.onResume(); + delegate.onPause(); + delegate.onStop(); + delegate.onDestroyView(); + delegate.onDetach(); - verify(delegate, never()).detachFromFlutterEngine(); + verify(delegate, never()).detachFromFlutterEngine(); + }); + } } @Test @@ -253,12 +272,13 @@ public class FlutterAndroidComponentTest { private static class FakeHost implements FlutterActivityAndFragmentDelegate.Host { final FlutterEngine cachedEngine; - Activity activity; + Activity cachedActivity; boolean shouldDestroyEngineWithHost = false; Lifecycle lifecycle = mock(Lifecycle.class); - private FakeHost(@NonNull FlutterEngine flutterEngine) { + private FakeHost(@NonNull FlutterEngine flutterEngine, @NonNull Activity activity) { cachedEngine = flutterEngine; + cachedActivity = activity; } @NonNull @@ -267,16 +287,10 @@ public class FlutterAndroidComponentTest { return ApplicationProvider.getApplicationContext(); } - @SuppressWarnings("deprecation") - // Robolectric.setupActivity - // TODO(reidbaker): https://github.com/flutter/flutter/issues/133151 - @Nullable + @NonNull @Override public Activity getActivity() { - if (activity == null) { - activity = Robolectric.setupActivity(Activity.class); - } - return activity; + return cachedActivity; } @NonNull diff --git a/engine/src/flutter/shell/platform/android/test/io/flutter/embedding/android/FlutterFragmentTest.java b/engine/src/flutter/shell/platform/android/test/io/flutter/embedding/android/FlutterFragmentTest.java index 992746b8b2c..b60408432ff 100644 --- a/engine/src/flutter/shell/platform/android/test/io/flutter/embedding/android/FlutterFragmentTest.java +++ b/engine/src/flutter/shell/platform/android/test/io/flutter/embedding/android/FlutterFragmentTest.java @@ -22,6 +22,7 @@ import static org.mockito.Mockito.when; import android.content.Context; import androidx.activity.OnBackPressedCallback; import androidx.fragment.app.FragmentActivity; +import androidx.test.core.app.ActivityScenario; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import io.flutter.embedding.engine.FlutterEngine; @@ -33,7 +34,6 @@ import java.util.Arrays; import java.util.concurrent.atomic.AtomicBoolean; import org.junit.Test; import org.junit.runner.RunWith; -import org.robolectric.Robolectric; @RunWith(AndroidJUnit4.class) public class FlutterFragmentTest { @@ -285,12 +285,6 @@ public class FlutterFragmentTest { assertEquals(fragment.getExclusiveAppComponent(), delegate); } - @SuppressWarnings("deprecation") - private FragmentActivity getMockFragmentActivity() { - // TODO(reidbaker): https://github.com/flutter/flutter/issues/133151 - return Robolectric.setupActivity(FragmentActivity.class); - } - @Test public void itDelegatesOnBackPressedWithSetFrameworkHandlesBack() { // We need to mock FlutterJNI to avoid triggering native code. @@ -307,36 +301,39 @@ public class FlutterFragmentTest { // sends backs to the framework if setFrameworkHandlesBack is true. .shouldAutomaticallyHandleOnBackPressed(true) .build(); - FragmentActivity activity = getMockFragmentActivity(); - activity - .getSupportFragmentManager() - .beginTransaction() - .add(android.R.id.content, fragment) - .commitNow(); - FlutterActivityAndFragmentDelegate mockDelegate = - mock(FlutterActivityAndFragmentDelegate.class); - isDelegateAttached = true; - when(mockDelegate.isAttached()).thenAnswer(invocation -> isDelegateAttached); - doAnswer(invocation -> isDelegateAttached = false).when(mockDelegate).onDetach(); - TestDelegateFactory delegateFactory = new TestDelegateFactory(mockDelegate); - fragment.setDelegateFactory(delegateFactory); + try (ActivityScenario scenario = + ActivityScenario.launch(FragmentActivity.class)) { + scenario.onActivity( + activity -> { + activity + .getSupportFragmentManager() + .beginTransaction() + .add(android.R.id.content, fragment) + .commitNow(); - // Calling onBackPressed now will still be handled by Android (the default), - // until setFrameworkHandlesBack is set to true. - activity.getOnBackPressedDispatcher().onBackPressed(); - verify(mockDelegate, times(0)).onBackPressed(); + FlutterActivityAndFragmentDelegate mockDelegate = + mock(FlutterActivityAndFragmentDelegate.class); + isDelegateAttached = true; + when(mockDelegate.isAttached()).thenAnswer(invocation -> isDelegateAttached); + doAnswer(invocation -> isDelegateAttached = false).when(mockDelegate).onDetach(); + TestDelegateFactory delegateFactory = new TestDelegateFactory(mockDelegate); + fragment.setDelegateFactory(delegateFactory); - // Setting setFrameworkHandlesBack to true means the delegate will receive - // the back and Android won't handle it. - fragment.setFrameworkHandlesBack(true); - activity.getOnBackPressedDispatcher().onBackPressed(); - verify(mockDelegate, times(1)).onBackPressed(); + // Calling onBackPressed now will still be handled by Android (the default), + // until setFrameworkHandlesBack is set to true. + activity.getOnBackPressedDispatcher().onBackPressed(); + verify(mockDelegate, times(0)).onBackPressed(); + + // Setting setFrameworkHandlesBack to true means the delegate will receive + // the back and Android won't handle it. + fragment.setFrameworkHandlesBack(true); + activity.getOnBackPressedDispatcher().onBackPressed(); + verify(mockDelegate, times(1)).onBackPressed(); + }); + } } - @SuppressWarnings("deprecation") - // Robolectric.setupActivity - // TODO(reidbaker): https://github.com/flutter/flutter/issues/133151 @Test public void itHandlesPopSystemNavigationAutomaticallyWhenEnabled() { // We need to mock FlutterJNI to avoid triggering native code. @@ -351,41 +348,47 @@ public class FlutterFragmentTest { FlutterFragment.withCachedEngine("my_cached_engine") .shouldAutomaticallyHandleOnBackPressed(true) .build(); - FragmentActivity activity = getMockFragmentActivity(); - activity - .getSupportFragmentManager() - .beginTransaction() - .add(android.R.id.content, fragment) - .commitNow(); - final AtomicBoolean onBackPressedCalled = new AtomicBoolean(false); - OnBackPressedCallback callback = - new OnBackPressedCallback(true) { - @Override - public void handleOnBackPressed() { - onBackPressedCalled.set(true); - } - }; - activity.getOnBackPressedDispatcher().addCallback(callback); - FlutterActivityAndFragmentDelegate mockDelegate = - mock(FlutterActivityAndFragmentDelegate.class); - TestDelegateFactory delegateFactory = new TestDelegateFactory(mockDelegate); - fragment.setDelegateFactory(delegateFactory); + try (ActivityScenario scenario = + ActivityScenario.launch(FragmentActivity.class)) { + scenario.onActivity( + activity -> { + activity + .getSupportFragmentManager() + .beginTransaction() + .add(android.R.id.content, fragment) + .commitNow(); + final AtomicBoolean onBackPressedCalled = new AtomicBoolean(false); + OnBackPressedCallback callback = + new OnBackPressedCallback(true) { + @Override + public void handleOnBackPressed() { + onBackPressedCalled.set(true); + } + }; + activity.getOnBackPressedDispatcher().addCallback(callback); - assertTrue(callback.isEnabled()); + FlutterActivityAndFragmentDelegate mockDelegate = + mock(FlutterActivityAndFragmentDelegate.class); + TestDelegateFactory delegateFactory = new TestDelegateFactory(mockDelegate); + fragment.setDelegateFactory(delegateFactory); - assertTrue(fragment.popSystemNavigator()); + assertTrue(callback.isEnabled()); - verify(mockDelegate, never()).onBackPressed(); - assertTrue(onBackPressedCalled.get()); - assertTrue(callback.isEnabled()); + assertTrue(fragment.popSystemNavigator()); - callback.setEnabled(false); - assertFalse(callback.isEnabled()); - assertTrue(fragment.popSystemNavigator()); + verify(mockDelegate, never()).onBackPressed(); + assertTrue(onBackPressedCalled.get()); + assertTrue(callback.isEnabled()); - verify(mockDelegate, never()).onBackPressed(); - assertFalse(callback.isEnabled()); + callback.setEnabled(false); + assertFalse(callback.isEnabled()); + assertTrue(fragment.popSystemNavigator()); + + verify(mockDelegate, never()).onBackPressed(); + assertFalse(callback.isEnabled()); + }); + } } @Test diff --git a/engine/src/flutter/shell/platform/android/test/io/flutter/embedding/android/FlutterViewTest.java b/engine/src/flutter/shell/platform/android/test/io/flutter/embedding/android/FlutterViewTest.java index 408d3bd39e7..6713103af88 100644 --- a/engine/src/flutter/shell/platform/android/test/io/flutter/embedding/android/FlutterViewTest.java +++ b/engine/src/flutter/shell/platform/android/test/io/flutter/embedding/android/FlutterViewTest.java @@ -42,6 +42,7 @@ import android.view.View; import android.view.WindowInsets; import android.widget.FrameLayout; import androidx.core.util.Consumer; +import androidx.test.core.app.ActivityScenario; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.window.layout.FoldingFeature; @@ -68,7 +69,6 @@ import org.mockito.MockitoAnnotations; import org.mockito.Spy; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; -import org.robolectric.Robolectric; import org.robolectric.Shadows; import org.robolectric.annotation.Config; import org.robolectric.annotation.Implementation; @@ -248,16 +248,22 @@ public class FlutterViewTest { // TODO(reidbaker): https://github.com/flutter/flutter/issues/133151 @Test public void onConfigurationChanged_notifiesEngineOfDisplaySize() { - FlutterView flutterView = new FlutterView(Robolectric.setupActivity(Activity.class)); - FlutterEngine flutterEngine = spy(new FlutterEngine(ctx, mockFlutterLoader, mockFlutterJni)); + try (ActivityScenario scenario = ActivityScenario.launch(Activity.class)) { + scenario.onActivity( + activity -> { + FlutterView flutterView = new FlutterView(activity); + FlutterEngine flutterEngine = + spy(new FlutterEngine(ctx, mockFlutterLoader, mockFlutterJni)); - Configuration configuration = ctx.getResources().getConfiguration(); + Configuration configuration = ctx.getResources().getConfiguration(); - flutterView.attachToFlutterEngine(flutterEngine); - flutterView.onConfigurationChanged(configuration); + flutterView.attachToFlutterEngine(flutterEngine); + flutterView.onConfigurationChanged(configuration); - verify(flutterEngine, times(1)) - .updateDisplayMetrics(any(Float.class), any(Float.class), any(Float.class)); + verify(flutterEngine, times(1)) + .updateDisplayMetrics(any(Float.class), any(Float.class), any(Float.class)); + }); + } } public void itSendsTextHidePasswordToFrameworkOnAttach() { @@ -743,179 +749,193 @@ public class FlutterViewTest { @Config(minSdk = 28) public void onApplyWindowInsetsSetsDisplayCutouts() { // Use an Activity context so that FlutterView.onAttachedToWindow completes. - Context context = Robolectric.setupActivity(Activity.class); - FlutterView flutterView = spy(new FlutterView(context)); - assertEquals(0, flutterView.getSystemUiVisibility()); - when(flutterView.getWindowSystemUiVisibility()).thenReturn(0); - when(flutterView.getContext()).thenReturn(context); + try (ActivityScenario scenario = ActivityScenario.launch(Activity.class)) { + scenario.onActivity( + activity -> { + FlutterView flutterView = spy(new FlutterView(activity)); + assertEquals(0, flutterView.getSystemUiVisibility()); + when(flutterView.getWindowSystemUiVisibility()).thenReturn(0); + when(flutterView.getContext()).thenReturn(activity); - FlutterEngine flutterEngine = spy(new FlutterEngine(ctx, mockFlutterLoader, mockFlutterJni)); - FlutterRenderer flutterRenderer = spy(new FlutterRenderer(mockFlutterJni)); - when(flutterEngine.getRenderer()).thenReturn(flutterRenderer); + FlutterEngine flutterEngine = + spy(new FlutterEngine(ctx, mockFlutterLoader, mockFlutterJni)); + FlutterRenderer flutterRenderer = spy(new FlutterRenderer(mockFlutterJni)); + when(flutterEngine.getRenderer()).thenReturn(flutterRenderer); - // When we attach a new FlutterView to the engine without any system insets, - // the viewport metrics default to 0. - flutterView.attachToFlutterEngine(flutterEngine); - ArgumentCaptor viewportMetricsCaptor = - ArgumentCaptor.forClass(FlutterRenderer.ViewportMetrics.class); - verify(flutterRenderer).setViewportMetrics(viewportMetricsCaptor.capture()); - assertEquals(0, viewportMetricsCaptor.getValue().viewPaddingTop); + // When we attach a new FlutterView to the engine without any system insets, + // the viewport metrics default to 0. + flutterView.attachToFlutterEngine(flutterEngine); + ArgumentCaptor viewportMetricsCaptor = + ArgumentCaptor.forClass(FlutterRenderer.ViewportMetrics.class); + verify(flutterRenderer).setViewportMetrics(viewportMetricsCaptor.capture()); + assertEquals(0, viewportMetricsCaptor.getValue().viewPaddingTop); - // Capture flutterView.setWindowInfoListenerDisplayFeatures. - WindowInfoRepositoryCallbackAdapterWrapper windowInfoRepo = - mock(WindowInfoRepositoryCallbackAdapterWrapper.class); - doReturn(windowInfoRepo).when(flutterView).createWindowInfoRepo(); - ArgumentCaptor> consumerCaptor = - ArgumentCaptor.forClass(Consumer.class); - flutterView.onAttachedToWindow(); - verify(windowInfoRepo).addWindowLayoutInfoListener(any(), any(), consumerCaptor.capture()); - Consumer consumer = consumerCaptor.getValue(); + // Capture flutterView.setWindowInfoListenerDisplayFeatures. + WindowInfoRepositoryCallbackAdapterWrapper windowInfoRepo = + mock(WindowInfoRepositoryCallbackAdapterWrapper.class); + doReturn(windowInfoRepo).when(flutterView).createWindowInfoRepo(); + ArgumentCaptor> consumerCaptor = + ArgumentCaptor.forClass(Consumer.class); + flutterView.onAttachedToWindow(); + verify(windowInfoRepo) + .addWindowLayoutInfoListener(any(), any(), consumerCaptor.capture()); + Consumer consumer = consumerCaptor.getValue(); - // Set display features in flutterView to ensure they are not overridden by display cutouts. - FoldingFeature displayFeature = mock(FoldingFeature.class); - Rect featureBounds = new Rect(10, 20, 30, 40); - when(displayFeature.getBounds()).thenReturn(featureBounds); - when(displayFeature.getOcclusionType()).thenReturn(FoldingFeature.OcclusionType.FULL); - when(displayFeature.getState()).thenReturn(FoldingFeature.State.FLAT); - WindowLayoutInfo windowLayout = new WindowLayoutInfo(Collections.singletonList(displayFeature)); - clearInvocations(flutterRenderer); - consumer.accept(windowLayout); + // Set display features in flutterView to ensure they are not overridden by display + // cutouts. + FoldingFeature displayFeature = mock(FoldingFeature.class); + Rect featureBounds = new Rect(10, 20, 30, 40); + when(displayFeature.getBounds()).thenReturn(featureBounds); + when(displayFeature.getOcclusionType()).thenReturn(FoldingFeature.OcclusionType.FULL); + when(displayFeature.getState()).thenReturn(FoldingFeature.State.FLAT); + WindowLayoutInfo windowLayout = + new WindowLayoutInfo(Collections.singletonList(displayFeature)); + clearInvocations(flutterRenderer); + consumer.accept(windowLayout); - // Assert the display feature is set. - verify(flutterRenderer).setViewportMetrics(viewportMetricsCaptor.capture()); - List features = - viewportMetricsCaptor.getValue().getDisplayFeatures(); - assertEquals(1, features.size()); - assertEquals(FlutterRenderer.DisplayFeatureType.HINGE, features.get(0).type); - assertEquals(FlutterRenderer.DisplayFeatureState.POSTURE_FLAT, features.get(0).state); - assertEquals(featureBounds, features.get(0).bounds); + // Assert the display feature is set. + verify(flutterRenderer).setViewportMetrics(viewportMetricsCaptor.capture()); + List features = + viewportMetricsCaptor.getValue().getDisplayFeatures(); + assertEquals(1, features.size()); + assertEquals(FlutterRenderer.DisplayFeatureType.HINGE, features.get(0).type); + assertEquals(FlutterRenderer.DisplayFeatureState.POSTURE_FLAT, features.get(0).state); + assertEquals(featureBounds, features.get(0).bounds); - // Then we simulate the system applying a window inset. - List cutoutBoundingRects = - Arrays.asList(new Rect(0, 200, 300, 400), new Rect(150, 0, 300, 150)); - WindowInsets windowInsets = setupMockDisplayCutout(cutoutBoundingRects); + // Then we simulate the system applying a window inset. + List cutoutBoundingRects = + Arrays.asList(new Rect(0, 200, 300, 400), new Rect(150, 0, 300, 150)); + WindowInsets windowInsets = setupMockDisplayCutout(cutoutBoundingRects); - clearInvocations(flutterRenderer); - flutterView.onApplyWindowInsets(windowInsets); - verify(flutterRenderer).setViewportMetrics(viewportMetricsCaptor.capture()); + clearInvocations(flutterRenderer); + flutterView.onApplyWindowInsets(windowInsets); + verify(flutterRenderer).setViewportMetrics(viewportMetricsCaptor.capture()); - features = viewportMetricsCaptor.getValue().getDisplayFeatures(); + features = viewportMetricsCaptor.getValue().getDisplayFeatures(); - // Assert the old display feature is still present. - assertEquals(1, features.size()); - assertEquals(FlutterRenderer.DisplayFeatureType.HINGE, features.get(0).type); - assertEquals(FlutterRenderer.DisplayFeatureState.POSTURE_FLAT, features.get(0).state); - assertEquals(featureBounds, features.get(0).bounds); + // Assert the old display feature is still present. + assertEquals(1, features.size()); + assertEquals(FlutterRenderer.DisplayFeatureType.HINGE, features.get(0).type); + assertEquals(FlutterRenderer.DisplayFeatureState.POSTURE_FLAT, features.get(0).state); + assertEquals(featureBounds, features.get(0).bounds); - List cutouts = - viewportMetricsCaptor.getValue().getDisplayCutouts(); - // Asserts for display cutouts. - assertEquals(2, cutouts.size()); - for (int i = 0; i < 2; i++) { - assertEquals(cutoutBoundingRects.get(i), cutouts.get(i).bounds); - assertEquals(FlutterRenderer.DisplayFeatureType.CUTOUT, cutouts.get(i).type); - assertEquals(FlutterRenderer.DisplayFeatureState.UNKNOWN, cutouts.get(i).state); + List cutouts = + viewportMetricsCaptor.getValue().getDisplayCutouts(); + // Asserts for display cutouts. + assertEquals(2, cutouts.size()); + for (int i = 0; i < 2; i++) { + assertEquals(cutoutBoundingRects.get(i), cutouts.get(i).bounds); + assertEquals(FlutterRenderer.DisplayFeatureType.CUTOUT, cutouts.get(i).type); + assertEquals(FlutterRenderer.DisplayFeatureState.UNKNOWN, cutouts.get(i).state); + } + }); } } - @SuppressWarnings("deprecation") - // Robolectric.setupActivity - // TODO(reidbaker): https://github.com/flutter/flutter/issues/133151 // getDefaultDisplay // TODO(jesswrd): https://github.com/flutter/flutter/issues/99421 @Test public void itRegistersAndUnregistersToWindowManager() { - Context context = Robolectric.setupActivity(Activity.class); - FlutterView flutterView = spy(new FlutterView(context)); - WindowInfoRepositoryCallbackAdapterWrapper windowInfoRepo = - mock(WindowInfoRepositoryCallbackAdapterWrapper.class); - // For reasoning behing using doReturn instead of when, read "Important gotcha" at - // https://www.javadoc.io/doc/org.mockito/mockito-core/1.10.19/org/mockito/Mockito.html#13 - doReturn(windowInfoRepo).when(flutterView).createWindowInfoRepo(); + try (ActivityScenario scenario = ActivityScenario.launch(Activity.class)) { + scenario.onActivity( + activity -> { + FlutterView flutterView = spy(new FlutterView(activity)); + WindowInfoRepositoryCallbackAdapterWrapper windowInfoRepo = + mock(WindowInfoRepositoryCallbackAdapterWrapper.class); + // For reasoning behing using doReturn instead of when, read "Important gotcha" at + // https://www.javadoc.io/doc/org.mockito/mockito-core/1.10.19/org/mockito/Mockito.html#13 + doReturn(windowInfoRepo).when(flutterView).createWindowInfoRepo(); - // When a new FlutterView is attached to the window - flutterView.onAttachedToWindow(); + // When a new FlutterView is attached to the window + flutterView.onAttachedToWindow(); - // Then the WindowManager callback is registered - verify(windowInfoRepo, times(1)).addWindowLayoutInfoListener(any(), any(), any()); + // Then the WindowManager callback is registered + verify(windowInfoRepo, times(1)).addWindowLayoutInfoListener(any(), any(), any()); - // When the FlutterView is detached from the window - flutterView.onDetachedFromWindow(); + // When the FlutterView is detached from the window + flutterView.onDetachedFromWindow(); - // Then the WindowManager callback is unregistered - verify(windowInfoRepo, times(1)).removeWindowLayoutInfoListener(any()); + // Then the WindowManager callback is unregistered + verify(windowInfoRepo, times(1)).removeWindowLayoutInfoListener(any()); + }); + } } - @SuppressWarnings("deprecation") - // Robolectric.setupActivity - // TODO(reidbaker): https://github.com/flutter/flutter/issues/133151 // getDefaultDisplay // TODO(jesswrd): https://github.com/flutter/flutter/issues/99421 @Test public void itSendsHingeDisplayFeatureToFlutter() { - Context context = Robolectric.setupActivity(Activity.class); - FlutterView flutterView = spy(new FlutterView(context)); - when(flutterView.getContext()).thenReturn(context); - WindowInfoRepositoryCallbackAdapterWrapper windowInfoRepo = - mock(WindowInfoRepositoryCallbackAdapterWrapper.class); - doReturn(windowInfoRepo).when(flutterView).createWindowInfoRepo(); - FlutterEngine flutterEngine = spy(new FlutterEngine(ctx, mockFlutterLoader, mockFlutterJni)); - FlutterRenderer flutterRenderer = spy(new FlutterRenderer(mockFlutterJni)); - when(flutterEngine.getRenderer()).thenReturn(flutterRenderer); + try (ActivityScenario scenario = ActivityScenario.launch(Activity.class)) { + scenario.onActivity( + activity -> { + FlutterView flutterView = spy(new FlutterView(activity)); + when(flutterView.getContext()).thenReturn(activity); + WindowInfoRepositoryCallbackAdapterWrapper windowInfoRepo = + mock(WindowInfoRepositoryCallbackAdapterWrapper.class); + doReturn(windowInfoRepo).when(flutterView).createWindowInfoRepo(); + FlutterEngine flutterEngine = + spy(new FlutterEngine(ctx, mockFlutterLoader, mockFlutterJni)); + FlutterRenderer flutterRenderer = spy(new FlutterRenderer(mockFlutterJni)); + when(flutterEngine.getRenderer()).thenReturn(flutterRenderer); - // Display features should be empty on attaching to engine. - flutterView.attachToFlutterEngine(flutterEngine); - ArgumentCaptor viewportMetricsCaptor = - ArgumentCaptor.forClass(FlutterRenderer.ViewportMetrics.class); - verify(flutterRenderer).setViewportMetrics(viewportMetricsCaptor.capture()); - assertEquals(Collections.emptyList(), viewportMetricsCaptor.getValue().getDisplayFeatures()); - clearInvocations(flutterRenderer); + // Display features should be empty on attaching to engine. + flutterView.attachToFlutterEngine(flutterEngine); + ArgumentCaptor viewportMetricsCaptor = + ArgumentCaptor.forClass(FlutterRenderer.ViewportMetrics.class); + verify(flutterRenderer).setViewportMetrics(viewportMetricsCaptor.capture()); + assertEquals( + Collections.emptyList(), viewportMetricsCaptor.getValue().getDisplayFeatures()); + clearInvocations(flutterRenderer); - // Test that display features do not override cutouts. - List cutoutBoundingRects = Collections.singletonList(new Rect(0, 200, 300, 400)); - WindowInsets windowInsets = setupMockDisplayCutout(cutoutBoundingRects); - flutterView.onApplyWindowInsets(windowInsets); - verify(flutterRenderer).setViewportMetrics(viewportMetricsCaptor.capture()); - assertEquals(1, viewportMetricsCaptor.getValue().getDisplayCutouts().size()); - assertEquals( - cutoutBoundingRects.get(0), - viewportMetricsCaptor.getValue().getDisplayCutouts().get(0).bounds); - clearInvocations(flutterRenderer); + // Test that display features do not override cutouts. + List cutoutBoundingRects = Collections.singletonList(new Rect(0, 200, 300, 400)); + WindowInsets windowInsets = setupMockDisplayCutout(cutoutBoundingRects); + flutterView.onApplyWindowInsets(windowInsets); + verify(flutterRenderer).setViewportMetrics(viewportMetricsCaptor.capture()); + assertEquals(1, viewportMetricsCaptor.getValue().getDisplayCutouts().size()); + assertEquals( + cutoutBoundingRects.get(0), + viewportMetricsCaptor.getValue().getDisplayCutouts().get(0).bounds); + clearInvocations(flutterRenderer); - FoldingFeature displayFeature = mock(FoldingFeature.class); - Rect featureRect = new Rect(0, 0, 100, 100); - when(displayFeature.getBounds()).thenReturn(featureRect); - when(displayFeature.getOcclusionType()).thenReturn(FoldingFeature.OcclusionType.FULL); - when(displayFeature.getState()).thenReturn(FoldingFeature.State.FLAT); + FoldingFeature displayFeature = mock(FoldingFeature.class); + Rect featureRect = new Rect(0, 0, 100, 100); + when(displayFeature.getBounds()).thenReturn(featureRect); + when(displayFeature.getOcclusionType()).thenReturn(FoldingFeature.OcclusionType.FULL); + when(displayFeature.getState()).thenReturn(FoldingFeature.State.FLAT); - WindowLayoutInfo testWindowLayout = - new WindowLayoutInfo(Collections.singletonList(displayFeature)); + WindowLayoutInfo testWindowLayout = + new WindowLayoutInfo(Collections.singletonList(displayFeature)); - // When FlutterView is attached to the engine and window, and a hinge display feature exists - flutterView.onAttachedToWindow(); - ArgumentCaptor> wmConsumerCaptor = - ArgumentCaptor.forClass(Consumer.class); - verify(windowInfoRepo).addWindowLayoutInfoListener(any(), any(), wmConsumerCaptor.capture()); - Consumer wmConsumer = wmConsumerCaptor.getValue(); - clearInvocations(flutterRenderer); - wmConsumer.accept(testWindowLayout); + // When FlutterView is attached to the engine and window, and a hinge display feature + // exists + flutterView.onAttachedToWindow(); + ArgumentCaptor> wmConsumerCaptor = + ArgumentCaptor.forClass(Consumer.class); + verify(windowInfoRepo) + .addWindowLayoutInfoListener(any(), any(), wmConsumerCaptor.capture()); + Consumer wmConsumer = wmConsumerCaptor.getValue(); + clearInvocations(flutterRenderer); + wmConsumer.accept(testWindowLayout); - // Then the Renderer receives the display feature - verify(flutterRenderer).setViewportMetrics(viewportMetricsCaptor.capture()); - assertEquals(1, viewportMetricsCaptor.getValue().getDisplayFeatures().size()); - FlutterRenderer.DisplayFeature feature = - viewportMetricsCaptor.getValue().getDisplayFeatures().get(0); - assertEquals(FlutterRenderer.DisplayFeatureType.HINGE, feature.type); - assertEquals(FlutterRenderer.DisplayFeatureState.POSTURE_FLAT, feature.state); - assertEquals(featureRect, feature.bounds); + // Then the Renderer receives the display feature + verify(flutterRenderer).setViewportMetrics(viewportMetricsCaptor.capture()); + assertEquals(1, viewportMetricsCaptor.getValue().getDisplayFeatures().size()); + FlutterRenderer.DisplayFeature feature = + viewportMetricsCaptor.getValue().getDisplayFeatures().get(0); + assertEquals(FlutterRenderer.DisplayFeatureType.HINGE, feature.type); + assertEquals(FlutterRenderer.DisplayFeatureState.POSTURE_FLAT, feature.state); + assertEquals(featureRect, feature.bounds); - // Assert the display cutout is unaffected. - assertEquals(1, viewportMetricsCaptor.getValue().getDisplayCutouts().size()); - FlutterRenderer.DisplayFeature cutout = - viewportMetricsCaptor.getValue().getDisplayCutouts().get(0); - assertEquals(cutoutBoundingRects.get(0), cutout.bounds); - assertEquals(FlutterRenderer.DisplayFeatureType.CUTOUT, cutout.type); - assertEquals(FlutterRenderer.DisplayFeatureState.UNKNOWN, cutout.state); + // Assert the display cutout is unaffected. + assertEquals(1, viewportMetricsCaptor.getValue().getDisplayCutouts().size()); + FlutterRenderer.DisplayFeature cutout = + viewportMetricsCaptor.getValue().getDisplayCutouts().get(0); + assertEquals(cutoutBoundingRects.get(0), cutout.bounds); + assertEquals(FlutterRenderer.DisplayFeatureType.CUTOUT, cutout.type); + assertEquals(FlutterRenderer.DisplayFeatureState.UNKNOWN, cutout.state); + }); + } } @Test diff --git a/engine/src/flutter/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java b/engine/src/flutter/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java index 79bef6736d1..a380fbe752b 100644 --- a/engine/src/flutter/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java +++ b/engine/src/flutter/shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java @@ -48,6 +48,7 @@ import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputMethodManager; import android.view.inputmethod.InputMethodSubtype; import androidx.core.view.inputmethod.EditorInfoCompat; +import androidx.test.core.app.ActivityScenario; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import io.flutter.embedding.android.FlutterView; @@ -79,7 +80,6 @@ import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; -import org.robolectric.Robolectric; import org.robolectric.annotation.Config; import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implements; @@ -1819,157 +1819,165 @@ public class TextInputPluginTest { if (Build.VERSION.SDK_INT < API_LEVELS.API_26) { return; } - FlutterView testView = getTestView(); - TextInputChannel textInputChannel = new TextInputChannel(mock(DartExecutor.class)); - ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class)); - TextInputPlugin textInputPlugin = - new TextInputPlugin( - testView, - textInputChannel, - scribeChannel, - mock(PlatformViewsController.class), - mock(PlatformViewsController2.class)); - final TextInputChannel.Configuration.Autofill autofill1 = - new TextInputChannel.Configuration.Autofill( - "1", - new String[] {"HINT1"}, - "placeholder1", - new TextInputChannel.TextEditState("", 0, 0, -1, -1)); - final TextInputChannel.Configuration.Autofill autofill2 = - new TextInputChannel.Configuration.Autofill( - "2", - new String[] {"HINT2", "EXTRA"}, - null, - new TextInputChannel.TextEditState("", 0, 0, -1, -1)); + try (ActivityScenario scenario = ActivityScenario.launch(Activity.class)) { + scenario.onActivity( + activity -> { + FlutterView testView = new FlutterView(activity); + TextInputChannel textInputChannel = new TextInputChannel(mock(DartExecutor.class)); + ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class)); + TextInputPlugin textInputPlugin = + new TextInputPlugin( + testView, + textInputChannel, + scribeChannel, + mock(PlatformViewsController.class), + mock(PlatformViewsController2.class)); + final TextInputChannel.Configuration.Autofill autofill1 = + new TextInputChannel.Configuration.Autofill( + "1", + new String[] {"HINT1"}, + "placeholder1", + new TextInputChannel.TextEditState("", 0, 0, -1, -1)); + final TextInputChannel.Configuration.Autofill autofill2 = + new TextInputChannel.Configuration.Autofill( + "2", + new String[] {"HINT2", "EXTRA"}, + null, + new TextInputChannel.TextEditState("", 0, 0, -1, -1)); - final TextInputChannel.Configuration config1 = - new TextInputChannel.Configuration( - false, - false, - true, - true, - false, - TextInputChannel.TextCapitalization.NONE, - null, - null, - null, - autofill1, - null, - null, - null); - final TextInputChannel.Configuration config2 = - new TextInputChannel.Configuration( - false, - false, - true, - true, - false, - TextInputChannel.TextCapitalization.NONE, - null, - null, - null, - autofill2, - null, - null, - null); + final TextInputChannel.Configuration config1 = + new TextInputChannel.Configuration( + false, + false, + true, + true, + false, + TextInputChannel.TextCapitalization.NONE, + null, + null, + null, + autofill1, + null, + null, + null); + final TextInputChannel.Configuration config2 = + new TextInputChannel.Configuration( + false, + false, + true, + true, + false, + TextInputChannel.TextCapitalization.NONE, + null, + null, + null, + autofill2, + null, + null, + null); - textInputPlugin.setTextInputClient( - 0, - new TextInputChannel.Configuration( - false, - false, - true, - true, - false, - TextInputChannel.TextCapitalization.NONE, - null, - null, - null, - autofill1, - null, - new TextInputChannel.Configuration[] {config1, config2}, - null)); + textInputPlugin.setTextInputClient( + 0, + new TextInputChannel.Configuration( + false, + false, + true, + true, + false, + TextInputChannel.TextCapitalization.NONE, + null, + null, + null, + autofill1, + null, + new TextInputChannel.Configuration[] {config1, config2}, + null)); - final ViewStructure viewStructure = mock(ViewStructure.class); - final ViewStructure[] children = {mock(ViewStructure.class), mock(ViewStructure.class)}; + final ViewStructure viewStructure = mock(ViewStructure.class); + final ViewStructure[] children = {mock(ViewStructure.class), mock(ViewStructure.class)}; - when(viewStructure.newChild(anyInt())) - .thenAnswer(invocation -> children[(int) invocation.getArgument(0)]); + when(viewStructure.newChild(anyInt())) + .thenAnswer(invocation -> children[(int) invocation.getArgument(0)]); - textInputPlugin.onProvideAutofillVirtualStructure(viewStructure, 0); + textInputPlugin.onProvideAutofillVirtualStructure(viewStructure, 0); - verify(viewStructure).newChild(0); - verify(viewStructure).newChild(1); + verify(viewStructure).newChild(0); + verify(viewStructure).newChild(1); - verify(children[0]).setAutofillId(any(), eq("1".hashCode())); - verify(children[0]).setAutofillHints(aryEq(new String[] {"HINT1"})); - verify(children[0]).setDimens(anyInt(), anyInt(), anyInt(), anyInt(), gt(0), gt(0)); - verify(children[0]).setHint("placeholder1"); + verify(children[0]).setAutofillId(any(), eq("1".hashCode())); + verify(children[0]).setAutofillHints(aryEq(new String[] {"HINT1"})); + verify(children[0]).setDimens(anyInt(), anyInt(), anyInt(), anyInt(), gt(0), gt(0)); + verify(children[0]).setHint("placeholder1"); - verify(children[1]).setAutofillId(any(), eq("2".hashCode())); - verify(children[1]).setAutofillHints(aryEq(new String[] {"HINT2", "EXTRA"})); - verify(children[1]).setDimens(anyInt(), anyInt(), anyInt(), anyInt(), gt(0), gt(0)); - verify(children[1], times(0)).setHint(any()); + verify(children[1]).setAutofillId(any(), eq("2".hashCode())); + verify(children[1]).setAutofillHints(aryEq(new String[] {"HINT2", "EXTRA"})); + verify(children[1]).setDimens(anyInt(), anyInt(), anyInt(), anyInt(), gt(0), gt(0)); + verify(children[1], times(0)).setHint(any()); + }); + } } - @SuppressWarnings("deprecation") @Config(minSdk = API_LEVELS.API_26) @Test public void autofill_onProvideVirtualViewStructure_singular_textfield() { if (Build.VERSION.SDK_INT < API_LEVELS.API_26) { return; } - // Migrate to ActivityScenario by following https://github.com/robolectric/robolectric/pull/4736 - FlutterView testView = getTestView(); - TextInputChannel textInputChannel = new TextInputChannel(mock(DartExecutor.class)); - ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class)); - TextInputPlugin textInputPlugin = - new TextInputPlugin( - testView, - textInputChannel, - scribeChannel, - mock(PlatformViewsController.class), - mock(PlatformViewsController2.class)); - final TextInputChannel.Configuration.Autofill autofill = - new TextInputChannel.Configuration.Autofill( - "1", - new String[] {"HINT1"}, - "placeholder", - new TextInputChannel.TextEditState("", 0, 0, -1, -1)); + try (ActivityScenario scenario = ActivityScenario.launch(Activity.class)) { + scenario.onActivity( + activity -> { + FlutterView testView = new FlutterView(activity); + TextInputChannel textInputChannel = new TextInputChannel(mock(DartExecutor.class)); + ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class)); + TextInputPlugin textInputPlugin = + new TextInputPlugin( + testView, + textInputChannel, + scribeChannel, + mock(PlatformViewsController.class), + mock(PlatformViewsController2.class)); + final TextInputChannel.Configuration.Autofill autofill = + new TextInputChannel.Configuration.Autofill( + "1", + new String[] {"HINT1"}, + "placeholder", + new TextInputChannel.TextEditState("", 0, 0, -1, -1)); - // Autofill should still work without AutofillGroup. - textInputPlugin.setTextInputClient( - 0, - new TextInputChannel.Configuration( - false, - false, - true, - true, - false, - TextInputChannel.TextCapitalization.NONE, - null, - null, - null, - autofill, - null, - null, - null)); + // Autofill should still work without AutofillGroup. + textInputPlugin.setTextInputClient( + 0, + new TextInputChannel.Configuration( + false, + false, + true, + true, + false, + TextInputChannel.TextCapitalization.NONE, + null, + null, + null, + autofill, + null, + null, + null)); - final ViewStructure viewStructure = mock(ViewStructure.class); - final ViewStructure[] children = {mock(ViewStructure.class)}; + final ViewStructure viewStructure = mock(ViewStructure.class); + final ViewStructure[] children = {mock(ViewStructure.class)}; - when(viewStructure.newChild(anyInt())) - .thenAnswer(invocation -> children[(int) invocation.getArgument(0)]); + when(viewStructure.newChild(anyInt())) + .thenAnswer(invocation -> children[(int) invocation.getArgument(0)]); - textInputPlugin.onProvideAutofillVirtualStructure(viewStructure, 0); + textInputPlugin.onProvideAutofillVirtualStructure(viewStructure, 0); - verify(viewStructure).newChild(0); + verify(viewStructure).newChild(0); - verify(children[0]).setAutofillId(any(), eq("1".hashCode())); - verify(children[0]).setAutofillHints(aryEq(new String[] {"HINT1"})); - verify(children[0]).setHint("placeholder"); - // Verifies that the child has a non-zero size. - verify(children[0]).setDimens(anyInt(), anyInt(), anyInt(), anyInt(), gt(0), gt(0)); + verify(children[0]).setAutofillId(any(), eq("1".hashCode())); + verify(children[0]).setAutofillHints(aryEq(new String[] {"HINT1"})); + verify(children[0]).setHint("placeholder"); + // Verifies that the child has a non-zero size. + verify(children[0]).setDimens(anyInt(), anyInt(), anyInt(), anyInt(), gt(0), gt(0)); + }); + } } @Config(minSdk = API_LEVELS.API_26) @@ -1980,142 +1988,148 @@ public class TextInputPluginTest { } TestAfm testAfm = Shadow.extract(ctx.getSystemService(AutofillManager.class)); - FlutterView testView = getTestView(); - TextInputChannel textInputChannel = new TextInputChannel(mock(DartExecutor.class)); - ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class)); - TextInputPlugin textInputPlugin = - new TextInputPlugin( - testView, - textInputChannel, - scribeChannel, - mock(PlatformViewsController.class), - mock(PlatformViewsController2.class)); + try (ActivityScenario scenario = ActivityScenario.launch(Activity.class)) { + scenario.onActivity( + activity -> { + FlutterView testView = new FlutterView(activity); + TextInputChannel textInputChannel = new TextInputChannel(mock(DartExecutor.class)); + ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class)); + TextInputPlugin textInputPlugin = + new TextInputPlugin( + testView, + textInputChannel, + scribeChannel, + mock(PlatformViewsController.class), + mock(PlatformViewsController2.class)); - // Set up an autofill scenario with 2 fields. - final TextInputChannel.Configuration.Autofill autofill1 = - new TextInputChannel.Configuration.Autofill( - "1", - new String[] {"HINT1"}, - "placeholder1", - new TextInputChannel.TextEditState("", 0, 0, -1, -1)); - final TextInputChannel.Configuration.Autofill autofill2 = - new TextInputChannel.Configuration.Autofill( - "2", - new String[] {"HINT2", "EXTRA"}, - null, - new TextInputChannel.TextEditState("", 0, 0, -1, -1)); + // Set up an autofill scenario with 2 fields. + final TextInputChannel.Configuration.Autofill autofill1 = + new TextInputChannel.Configuration.Autofill( + "1", + new String[] {"HINT1"}, + "placeholder1", + new TextInputChannel.TextEditState("", 0, 0, -1, -1)); + final TextInputChannel.Configuration.Autofill autofill2 = + new TextInputChannel.Configuration.Autofill( + "2", + new String[] {"HINT2", "EXTRA"}, + null, + new TextInputChannel.TextEditState("", 0, 0, -1, -1)); - final TextInputChannel.Configuration config1 = - new TextInputChannel.Configuration( - false, - false, - true, - true, - false, - TextInputChannel.TextCapitalization.NONE, - null, - null, - null, - autofill1, - null, - null, - null); - final TextInputChannel.Configuration config2 = - new TextInputChannel.Configuration( - false, - false, - true, - true, - false, - TextInputChannel.TextCapitalization.NONE, - null, - null, - null, - autofill2, - null, - null, - null); + final TextInputChannel.Configuration config1 = + new TextInputChannel.Configuration( + false, + false, + true, + true, + false, + TextInputChannel.TextCapitalization.NONE, + null, + null, + null, + autofill1, + null, + null, + null); + final TextInputChannel.Configuration config2 = + new TextInputChannel.Configuration( + false, + false, + true, + true, + false, + TextInputChannel.TextCapitalization.NONE, + null, + null, + null, + autofill2, + null, + null, + null); - // Set client. This should call notifyViewExited on the FlutterView if the previous client is - // also eligible for autofill. - final TextInputChannel.Configuration autofillConfiguration = - new TextInputChannel.Configuration( - false, - false, - true, - true, - false, - TextInputChannel.TextCapitalization.NONE, - null, - null, - null, - autofill1, - null, - new TextInputChannel.Configuration[] {config1, config2}, - null); + // Set client. This should call notifyViewExited on the FlutterView if the previous + // client is + // also eligible for autofill. + final TextInputChannel.Configuration autofillConfiguration = + new TextInputChannel.Configuration( + false, + false, + true, + true, + false, + TextInputChannel.TextCapitalization.NONE, + null, + null, + null, + autofill1, + null, + new TextInputChannel.Configuration[] {config1, config2}, + null); - textInputPlugin.setTextInputClient(0, autofillConfiguration); + textInputPlugin.setTextInputClient(0, autofillConfiguration); - // notifyViewExited should not be called as this is the first client we set. - assertEquals(testAfm.empty, testAfm.exitId); + // notifyViewExited should not be called as this is the first client we set. + assertEquals(testAfm.empty, testAfm.exitId); - // The framework updates the text, call notifyValueChanged. - textInputPlugin.setTextInputEditingState( - testView, new TextInputChannel.TextEditState("new text", -1, -1, -1, -1)); - assertEquals("new text", testAfm.changeString); - assertEquals("1".hashCode(), testAfm.changeVirtualId); + // The framework updates the text, call notifyValueChanged. + textInputPlugin.setTextInputEditingState( + testView, new TextInputChannel.TextEditState("new text", -1, -1, -1, -1)); + assertEquals("new text", testAfm.changeString); + assertEquals("1".hashCode(), testAfm.changeVirtualId); - // The input method updates the text, call notifyValueChanged. - testAfm.resetStates(); - final KeyboardManager mockKeyboardManager = mock(KeyboardManager.class); - InputConnectionAdaptor adaptor = - new InputConnectionAdaptor( - testView, - 0, - mock(TextInputChannel.class), - mock(ScribeChannel.class), - mockKeyboardManager, - (ListenableEditingState) textInputPlugin.getEditable(), - new EditorInfo()); - adaptor.commitText("input from IME ", 1); + // The input method updates the text, call notifyValueChanged. + testAfm.resetStates(); + final KeyboardManager mockKeyboardManager = mock(KeyboardManager.class); + InputConnectionAdaptor adaptor = + new InputConnectionAdaptor( + testView, + 0, + mock(TextInputChannel.class), + mock(ScribeChannel.class), + mockKeyboardManager, + (ListenableEditingState) textInputPlugin.getEditable(), + new EditorInfo()); + adaptor.commitText("input from IME ", 1); - assertEquals("input from IME new text", testAfm.changeString); - assertEquals("1".hashCode(), testAfm.changeVirtualId); + assertEquals("input from IME new text", testAfm.changeString); + assertEquals("1".hashCode(), testAfm.changeVirtualId); - // notifyViewExited should be called on the previous client. - testAfm.resetStates(); - textInputPlugin.setTextInputClient( - 1, - new TextInputChannel.Configuration( - false, - false, - true, - true, - false, - TextInputChannel.TextCapitalization.NONE, - null, - null, - null, - null, - null, - null, - null)); + // notifyViewExited should be called on the previous client. + testAfm.resetStates(); + textInputPlugin.setTextInputClient( + 1, + new TextInputChannel.Configuration( + false, + false, + true, + true, + false, + TextInputChannel.TextCapitalization.NONE, + null, + null, + null, + null, + null, + null, + null)); - assertEquals("1".hashCode(), testAfm.exitId); + assertEquals("1".hashCode(), testAfm.exitId); - // TextInputPlugin#clearTextInputClient calls notifyViewExited. - testAfm.resetStates(); - textInputPlugin.setTextInputClient(3, autofillConfiguration); - assertEquals(testAfm.empty, testAfm.exitId); - textInputPlugin.clearTextInputClient(); - assertEquals("1".hashCode(), testAfm.exitId); + // TextInputPlugin#clearTextInputClient calls notifyViewExited. + testAfm.resetStates(); + textInputPlugin.setTextInputClient(3, autofillConfiguration); + assertEquals(testAfm.empty, testAfm.exitId); + textInputPlugin.clearTextInputClient(); + assertEquals("1".hashCode(), testAfm.exitId); - // TextInputPlugin#destroy calls notifyViewExited. - testAfm.resetStates(); - textInputPlugin.setTextInputClient(4, autofillConfiguration); - assertEquals(testAfm.empty, testAfm.exitId); - textInputPlugin.destroy(); - assertEquals("1".hashCode(), testAfm.exitId); + // TextInputPlugin#destroy calls notifyViewExited. + testAfm.resetStates(); + textInputPlugin.setTextInputClient(4, autofillConfiguration); + assertEquals(testAfm.empty, testAfm.exitId); + textInputPlugin.destroy(); + assertEquals("1".hashCode(), testAfm.exitId); + }); + } } @Config(minSdk = API_LEVELS.API_26) @@ -2127,102 +2141,108 @@ public class TextInputPluginTest { } TestAfm testAfm = Shadow.extract(ctx.getSystemService(AutofillManager.class)); - FlutterView testView = getTestView(); - TextInputChannel textInputChannel = spy(new TextInputChannel(mock(DartExecutor.class))); - ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class)); - TextInputPlugin textInputPlugin = - new TextInputPlugin( - testView, - textInputChannel, - scribeChannel, - mock(PlatformViewsController.class), - mock(PlatformViewsController2.class)); + try (ActivityScenario scenario = ActivityScenario.launch(Activity.class)) { + scenario.onActivity( + activity -> { + FlutterView testView = new FlutterView(activity); + TextInputChannel textInputChannel = spy(new TextInputChannel(mock(DartExecutor.class))); + ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class)); + TextInputPlugin textInputPlugin = + new TextInputPlugin( + testView, + textInputChannel, + scribeChannel, + mock(PlatformViewsController.class), + mock(PlatformViewsController2.class)); - // Set up an autofill scenario with 2 fields. - final TextInputChannel.Configuration.Autofill autofill1 = - new TextInputChannel.Configuration.Autofill( - "1", - new String[] {"HINT1"}, - null, - new TextInputChannel.TextEditState("field 1", 0, 0, -1, -1)); - final TextInputChannel.Configuration.Autofill autofill2 = - new TextInputChannel.Configuration.Autofill( - "2", - new String[] {"HINT2", "EXTRA"}, - null, - new TextInputChannel.TextEditState("field 2", 0, 0, -1, -1)); + // Set up an autofill scenario with 2 fields. + final TextInputChannel.Configuration.Autofill autofill1 = + new TextInputChannel.Configuration.Autofill( + "1", + new String[] {"HINT1"}, + null, + new TextInputChannel.TextEditState("field 1", 0, 0, -1, -1)); + final TextInputChannel.Configuration.Autofill autofill2 = + new TextInputChannel.Configuration.Autofill( + "2", + new String[] {"HINT2", "EXTRA"}, + null, + new TextInputChannel.TextEditState("field 2", 0, 0, -1, -1)); - final TextInputChannel.Configuration config1 = - new TextInputChannel.Configuration( - false, - false, - true, - true, - false, - TextInputChannel.TextCapitalization.NONE, - null, - null, - null, - autofill1, - null, - null, - null); - final TextInputChannel.Configuration config2 = - new TextInputChannel.Configuration( - false, - false, - true, - true, - false, - TextInputChannel.TextCapitalization.NONE, - null, - null, - null, - autofill2, - null, - null, - null); + final TextInputChannel.Configuration config1 = + new TextInputChannel.Configuration( + false, + false, + true, + true, + false, + TextInputChannel.TextCapitalization.NONE, + null, + null, + null, + autofill1, + null, + null, + null); + final TextInputChannel.Configuration config2 = + new TextInputChannel.Configuration( + false, + false, + true, + true, + false, + TextInputChannel.TextCapitalization.NONE, + null, + null, + null, + autofill2, + null, + null, + null); - final TextInputChannel.Configuration autofillConfiguration = - new TextInputChannel.Configuration( - false, - false, - true, - true, - false, - TextInputChannel.TextCapitalization.NONE, - null, - null, - null, - autofill1, - null, - new TextInputChannel.Configuration[] {config1, config2}, - null); + final TextInputChannel.Configuration autofillConfiguration = + new TextInputChannel.Configuration( + false, + false, + true, + true, + false, + TextInputChannel.TextCapitalization.NONE, + null, + null, + null, + autofill1, + null, + new TextInputChannel.Configuration[] {config1, config2}, + null); - textInputPlugin.setTextInputClient(0, autofillConfiguration); - textInputPlugin.setTextInputEditingState( - testView, new TextInputChannel.TextEditState("", 0, 0, -1, -1)); + textInputPlugin.setTextInputClient(0, autofillConfiguration); + textInputPlugin.setTextInputEditingState( + testView, new TextInputChannel.TextEditState("", 0, 0, -1, -1)); - final SparseArray autofillValues = new SparseArray(); - autofillValues.append("1".hashCode(), AutofillValue.forText("focused field")); - autofillValues.append("2".hashCode(), AutofillValue.forText("unfocused field")); + final SparseArray autofillValues = new SparseArray(); + autofillValues.append("1".hashCode(), AutofillValue.forText("focused field")); + autofillValues.append("2".hashCode(), AutofillValue.forText("unfocused field")); - // Autofill both fields. - textInputPlugin.autofill(autofillValues); + // Autofill both fields. + textInputPlugin.autofill(autofillValues); - // Verify the Editable has been updated. - assertTrue(textInputPlugin.getEditable().toString().equals("focused field")); + // Verify the Editable has been updated. + assertTrue(textInputPlugin.getEditable().toString().equals("focused field")); - // The autofill value of the focused field is sent via updateEditingState. - verify(textInputChannel, times(1)) - .updateEditingState(anyInt(), eq("focused field"), eq(13), eq(13), eq(-1), eq(-1)); + // The autofill value of the focused field is sent via updateEditingState. + verify(textInputChannel, times(1)) + .updateEditingState(anyInt(), eq("focused field"), eq(13), eq(13), eq(-1), eq(-1)); - final ArgumentCaptor mapCaptor = ArgumentCaptor.forClass(HashMap.class); + final ArgumentCaptor mapCaptor = ArgumentCaptor.forClass(HashMap.class); - verify(textInputChannel, times(1)).updateEditingStateWithTag(anyInt(), mapCaptor.capture()); - final TextInputChannel.TextEditState editState = - (TextInputChannel.TextEditState) mapCaptor.getValue().get("2"); - assertEquals(editState.text, "unfocused field"); + verify(textInputChannel, times(1)) + .updateEditingStateWithTag(anyInt(), mapCaptor.capture()); + final TextInputChannel.TextEditState editState = + (TextInputChannel.TextEditState) mapCaptor.getValue().get("2"); + assertEquals(editState.text, "unfocused field"); + }); + } } @Config(minSdk = API_LEVELS.API_26) @@ -2289,93 +2309,92 @@ public class TextInputPluginTest { } TestAfm testAfm = Shadow.extract(ctx.getSystemService(AutofillManager.class)); - FlutterView testView = getTestView(); - TextInputChannel textInputChannel = new TextInputChannel(mock(DartExecutor.class)); - ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class)); - TextInputPlugin textInputPlugin = - new TextInputPlugin( - testView, - textInputChannel, - scribeChannel, - mock(PlatformViewsController.class), - mock(PlatformViewsController2.class)); + try (ActivityScenario scenario = ActivityScenario.launch(Activity.class)) { + scenario.onActivity( + activity -> { + FlutterView testView = new FlutterView(activity); + TextInputChannel textInputChannel = new TextInputChannel(mock(DartExecutor.class)); + ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class)); + TextInputPlugin textInputPlugin = + new TextInputPlugin( + testView, + textInputChannel, + scribeChannel, + mock(PlatformViewsController.class), + mock(PlatformViewsController2.class)); - // Set up an autofill scenario with 2 fields. - final TextInputChannel.Configuration.Autofill autofill1 = - new TextInputChannel.Configuration.Autofill( - "1", - new String[] {"HINT1"}, - "null", - new TextInputChannel.TextEditState("", 0, 0, -1, -1)); - final TextInputChannel.Configuration.Autofill autofill2 = - new TextInputChannel.Configuration.Autofill( - "2", - new String[] {"HINT2", "EXTRA"}, - "null", - new TextInputChannel.TextEditState( - "Unfocused fields need love like everything does", 0, 0, -1, -1)); + // Set up an autofill scenario with 2 fields. + final TextInputChannel.Configuration.Autofill autofill1 = + new TextInputChannel.Configuration.Autofill( + "1", + new String[] {"HINT1"}, + "null", + new TextInputChannel.TextEditState("", 0, 0, -1, -1)); + final TextInputChannel.Configuration.Autofill autofill2 = + new TextInputChannel.Configuration.Autofill( + "2", + new String[] {"HINT2", "EXTRA"}, + "null", + new TextInputChannel.TextEditState( + "Unfocused fields need love like everything does", 0, 0, -1, -1)); - final TextInputChannel.Configuration config1 = - new TextInputChannel.Configuration( - false, - false, - true, - true, - false, - TextInputChannel.TextCapitalization.NONE, - null, - null, - null, - autofill1, - null, - null, - null); - final TextInputChannel.Configuration config2 = - new TextInputChannel.Configuration( - false, - false, - true, - true, - false, - TextInputChannel.TextCapitalization.NONE, - null, - null, - null, - autofill2, - null, - null, - null); + final TextInputChannel.Configuration config1 = + new TextInputChannel.Configuration( + false, + false, + true, + true, + false, + TextInputChannel.TextCapitalization.NONE, + null, + null, + null, + autofill1, + null, + null, + null); + final TextInputChannel.Configuration config2 = + new TextInputChannel.Configuration( + false, + false, + true, + true, + false, + TextInputChannel.TextCapitalization.NONE, + null, + null, + null, + autofill2, + null, + null, + null); - final TextInputChannel.Configuration autofillConfiguration = - new TextInputChannel.Configuration( - false, - false, - true, - true, - false, - TextInputChannel.TextCapitalization.NONE, - null, - null, - null, - autofill1, - null, - new TextInputChannel.Configuration[] {config1, config2}, - null); + final TextInputChannel.Configuration autofillConfiguration = + new TextInputChannel.Configuration( + false, + false, + true, + true, + false, + TextInputChannel.TextCapitalization.NONE, + null, + null, + null, + autofill1, + null, + new TextInputChannel.Configuration[] {config1, config2}, + null); - textInputPlugin.setTextInputClient(0, autofillConfiguration); + textInputPlugin.setTextInputClient(0, autofillConfiguration); - // notifyValueChanged should be called for unfocused fields. - assertEquals("2".hashCode(), testAfm.changeVirtualId); - assertEquals("Unfocused fields need love like everything does", testAfm.changeString); + // notifyValueChanged should be called for unfocused fields. + assertEquals("2".hashCode(), testAfm.changeVirtualId); + assertEquals("Unfocused fields need love like everything does", testAfm.changeString); + }); + } } // -------- End: Autofill Tests ------- - @SuppressWarnings("deprecation") - private FlutterView getTestView() { - // TODO(reidbaker): https://github.com/flutter/flutter/issues/133151 - return new FlutterView(Robolectric.setupActivity(Activity.class)); - } - @SuppressWarnings("deprecation") // setMessageHandler is deprecated. @Test @@ -2488,82 +2507,92 @@ public class TextInputPluginTest { // getWindowSystemUiVisibility, SYSTEM_UI_FLAG_LAYOUT_STABLE. // flutter#133074 tracks migration work. public void ime_windowInsetsSync_notLaidOutBehindNavigation_excludesNavigationBars() { - FlutterView testView = spy(getTestView()); - when(testView.getWindowSystemUiVisibility()).thenReturn(View.SYSTEM_UI_FLAG_LAYOUT_STABLE); + try (ActivityScenario scenario = ActivityScenario.launch(Activity.class)) { + scenario.onActivity( + activity -> { + FlutterView testView = spy(new FlutterView(activity)); + when(testView.getWindowSystemUiVisibility()) + .thenReturn(View.SYSTEM_UI_FLAG_LAYOUT_STABLE); - TextInputChannel textInputChannel = new TextInputChannel(mock(DartExecutor.class)); - ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class)); - TextInputPlugin textInputPlugin = - new TextInputPlugin( - testView, - textInputChannel, - scribeChannel, - mock(PlatformViewsController.class), - mock(PlatformViewsController2.class)); - ImeSyncDeferringInsetsCallback imeSyncCallback = textInputPlugin.getImeSyncCallback(); - FlutterEngine flutterEngine = spy(new FlutterEngine(ctx, mockFlutterLoader, mockFlutterJni)); - FlutterRenderer flutterRenderer = spy(new FlutterRenderer(mockFlutterJni)); - when(flutterEngine.getRenderer()).thenReturn(flutterRenderer); - testView.attachToFlutterEngine(flutterEngine); + TextInputChannel textInputChannel = new TextInputChannel(mock(DartExecutor.class)); + ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class)); + TextInputPlugin textInputPlugin = + new TextInputPlugin( + testView, + textInputChannel, + scribeChannel, + mock(PlatformViewsController.class), + mock(PlatformViewsController2.class)); + ImeSyncDeferringInsetsCallback imeSyncCallback = textInputPlugin.getImeSyncCallback(); + FlutterEngine flutterEngine = + spy(new FlutterEngine(ctx, mockFlutterLoader, mockFlutterJni)); + FlutterRenderer flutterRenderer = spy(new FlutterRenderer(mockFlutterJni)); + when(flutterEngine.getRenderer()).thenReturn(flutterRenderer); + testView.attachToFlutterEngine(flutterEngine); - WindowInsetsAnimation animation = mock(WindowInsetsAnimation.class); - when(animation.getTypeMask()).thenReturn(WindowInsets.Type.ime()); + WindowInsetsAnimation animation = mock(WindowInsetsAnimation.class); + when(animation.getTypeMask()).thenReturn(WindowInsets.Type.ime()); - List animationList = new ArrayList(); - animationList.add(animation); + List animationList = new ArrayList(); + animationList.add(animation); - ArgumentCaptor viewportMetricsCaptor = - ArgumentCaptor.forClass(FlutterRenderer.ViewportMetrics.class); + ArgumentCaptor viewportMetricsCaptor = + ArgumentCaptor.forClass(FlutterRenderer.ViewportMetrics.class); - WindowInsets.Builder builder = new WindowInsets.Builder(); + WindowInsets.Builder builder = new WindowInsets.Builder(); - // Set the initial insets and verify that they were set and the bottom view inset is correct - imeSyncCallback.getInsetsListener().onApplyWindowInsets(testView, builder.build()); + // Set the initial insets and verify that they were set and the bottom view inset is + // correct + imeSyncCallback.getInsetsListener().onApplyWindowInsets(testView, builder.build()); - verify(flutterRenderer, atLeast(1)).setViewportMetrics(viewportMetricsCaptor.capture()); - assertEquals(0, viewportMetricsCaptor.getValue().viewInsetBottom); + verify(flutterRenderer, atLeast(1)).setViewportMetrics(viewportMetricsCaptor.capture()); + assertEquals(0, viewportMetricsCaptor.getValue().viewInsetBottom); - // Call onPrepare and set the lastWindowInsets - these should be stored for the end of the - // animation instead of being applied immediately - imeSyncCallback.getAnimationCallback().onPrepare(animation); - builder.setInsets(WindowInsets.Type.ime(), Insets.of(0, 0, 0, 100)); - builder.setInsets(WindowInsets.Type.navigationBars(), Insets.of(0, 0, 0, 0)); - imeSyncCallback.getInsetsListener().onApplyWindowInsets(testView, builder.build()); + // Call onPrepare and set the lastWindowInsets - these should be stored for the end of + // the + // animation instead of being applied immediately + imeSyncCallback.getAnimationCallback().onPrepare(animation); + builder.setInsets(WindowInsets.Type.ime(), Insets.of(0, 0, 0, 100)); + builder.setInsets(WindowInsets.Type.navigationBars(), Insets.of(0, 0, 0, 0)); + imeSyncCallback.getInsetsListener().onApplyWindowInsets(testView, builder.build()); - verify(flutterRenderer, atLeast(1)).setViewportMetrics(viewportMetricsCaptor.capture()); - assertEquals(0, viewportMetricsCaptor.getValue().viewInsetBottom); + verify(flutterRenderer, atLeast(1)).setViewportMetrics(viewportMetricsCaptor.capture()); + assertEquals(0, viewportMetricsCaptor.getValue().viewInsetBottom); - // Call onStart and apply new insets - these should be ignored completely - imeSyncCallback.getAnimationCallback().onStart(animation, null); - builder.setInsets(WindowInsets.Type.ime(), Insets.of(0, 0, 0, 50)); - builder.setInsets(WindowInsets.Type.navigationBars(), Insets.of(0, 0, 0, 40)); - imeSyncCallback.getInsetsListener().onApplyWindowInsets(testView, builder.build()); + // Call onStart and apply new insets - these should be ignored completely + imeSyncCallback.getAnimationCallback().onStart(animation, null); + builder.setInsets(WindowInsets.Type.ime(), Insets.of(0, 0, 0, 50)); + builder.setInsets(WindowInsets.Type.navigationBars(), Insets.of(0, 0, 0, 40)); + imeSyncCallback.getInsetsListener().onApplyWindowInsets(testView, builder.build()); - verify(flutterRenderer, atLeast(1)).setViewportMetrics(viewportMetricsCaptor.capture()); - assertEquals(0, viewportMetricsCaptor.getValue().viewInsetBottom); + verify(flutterRenderer, atLeast(1)).setViewportMetrics(viewportMetricsCaptor.capture()); + assertEquals(0, viewportMetricsCaptor.getValue().viewInsetBottom); - // Progress the animation and ensure that the navigation bar insets have been subtracted - // from the IME insets - builder.setInsets(WindowInsets.Type.ime(), Insets.of(0, 0, 0, 25)); - builder.setInsets(WindowInsets.Type.navigationBars(), Insets.of(0, 0, 0, 40)); - imeSyncCallback.getAnimationCallback().onProgress(builder.build(), animationList); + // Progress the animation and ensure that the navigation bar insets have been subtracted + // from the IME insets + builder.setInsets(WindowInsets.Type.ime(), Insets.of(0, 0, 0, 25)); + builder.setInsets(WindowInsets.Type.navigationBars(), Insets.of(0, 0, 0, 40)); + imeSyncCallback.getAnimationCallback().onProgress(builder.build(), animationList); - verify(flutterRenderer, atLeast(1)).setViewportMetrics(viewportMetricsCaptor.capture()); - assertEquals(0, viewportMetricsCaptor.getValue().viewInsetBottom); + verify(flutterRenderer, atLeast(1)).setViewportMetrics(viewportMetricsCaptor.capture()); + assertEquals(0, viewportMetricsCaptor.getValue().viewInsetBottom); - builder.setInsets(WindowInsets.Type.ime(), Insets.of(0, 0, 0, 50)); - builder.setInsets(WindowInsets.Type.navigationBars(), Insets.of(0, 0, 0, 40)); - imeSyncCallback.getAnimationCallback().onProgress(builder.build(), animationList); + builder.setInsets(WindowInsets.Type.ime(), Insets.of(0, 0, 0, 50)); + builder.setInsets(WindowInsets.Type.navigationBars(), Insets.of(0, 0, 0, 40)); + imeSyncCallback.getAnimationCallback().onProgress(builder.build(), animationList); - verify(flutterRenderer, atLeast(1)).setViewportMetrics(viewportMetricsCaptor.capture()); - assertEquals(10, viewportMetricsCaptor.getValue().viewInsetBottom); + verify(flutterRenderer, atLeast(1)).setViewportMetrics(viewportMetricsCaptor.capture()); + assertEquals(10, viewportMetricsCaptor.getValue().viewInsetBottom); - // End the animation and ensure that the bottom insets match the lastWindowInsets that we set - // during onPrepare - imeSyncCallback.getAnimationCallback().onEnd(animation); + // End the animation and ensure that the bottom insets match the lastWindowInsets that + // we set + // during onPrepare + imeSyncCallback.getAnimationCallback().onEnd(animation); - verify(flutterRenderer, atLeast(1)).setViewportMetrics(viewportMetricsCaptor.capture()); - assertEquals(100, viewportMetricsCaptor.getValue().viewInsetBottom); + verify(flutterRenderer, atLeast(1)).setViewportMetrics(viewportMetricsCaptor.capture()); + assertEquals(100, viewportMetricsCaptor.getValue().viewInsetBottom); + }); + } } @Test @@ -2573,84 +2602,94 @@ public class TextInputPluginTest { // getWindowSystemUiVisibility // flutter#133074 tracks migration work. public void ime_windowInsetsSync_laidOutBehindNavigation_includesNavigationBars() { - FlutterView testView = spy(getTestView()); - when(testView.getWindowSystemUiVisibility()) - .thenReturn( - View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION); + try (ActivityScenario scenario = ActivityScenario.launch(Activity.class)) { + scenario.onActivity( + activity -> { + FlutterView testView = spy(new FlutterView(activity)); + when(testView.getWindowSystemUiVisibility()) + .thenReturn( + View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION); - TextInputChannel textInputChannel = new TextInputChannel(mock(DartExecutor.class)); - ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class)); - TextInputPlugin textInputPlugin = - new TextInputPlugin( - testView, - textInputChannel, - scribeChannel, - mock(PlatformViewsController.class), - mock(PlatformViewsController2.class)); - ImeSyncDeferringInsetsCallback imeSyncCallback = textInputPlugin.getImeSyncCallback(); - FlutterEngine flutterEngine = spy(new FlutterEngine(ctx, mockFlutterLoader, mockFlutterJni)); - FlutterRenderer flutterRenderer = spy(new FlutterRenderer(mockFlutterJni)); - when(flutterEngine.getRenderer()).thenReturn(flutterRenderer); - testView.attachToFlutterEngine(flutterEngine); + TextInputChannel textInputChannel = new TextInputChannel(mock(DartExecutor.class)); + ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class)); + TextInputPlugin textInputPlugin = + new TextInputPlugin( + testView, + textInputChannel, + scribeChannel, + mock(PlatformViewsController.class), + mock(PlatformViewsController2.class)); + ImeSyncDeferringInsetsCallback imeSyncCallback = textInputPlugin.getImeSyncCallback(); + FlutterEngine flutterEngine = + spy(new FlutterEngine(ctx, mockFlutterLoader, mockFlutterJni)); + FlutterRenderer flutterRenderer = spy(new FlutterRenderer(mockFlutterJni)); + when(flutterEngine.getRenderer()).thenReturn(flutterRenderer); + testView.attachToFlutterEngine(flutterEngine); - WindowInsetsAnimation animation = mock(WindowInsetsAnimation.class); - when(animation.getTypeMask()).thenReturn(WindowInsets.Type.ime()); + WindowInsetsAnimation animation = mock(WindowInsetsAnimation.class); + when(animation.getTypeMask()).thenReturn(WindowInsets.Type.ime()); - List animationList = new ArrayList(); - animationList.add(animation); + List animationList = new ArrayList(); + animationList.add(animation); - ArgumentCaptor viewportMetricsCaptor = - ArgumentCaptor.forClass(FlutterRenderer.ViewportMetrics.class); + ArgumentCaptor viewportMetricsCaptor = + ArgumentCaptor.forClass(FlutterRenderer.ViewportMetrics.class); - WindowInsets.Builder builder = new WindowInsets.Builder(); + WindowInsets.Builder builder = new WindowInsets.Builder(); - // Set the initial insets and verify that they were set and the bottom view inset is correct - imeSyncCallback.getInsetsListener().onApplyWindowInsets(testView, builder.build()); + // Set the initial insets and verify that they were set and the bottom view inset is + // correct + imeSyncCallback.getInsetsListener().onApplyWindowInsets(testView, builder.build()); - verify(flutterRenderer, atLeast(1)).setViewportMetrics(viewportMetricsCaptor.capture()); - assertEquals(0, viewportMetricsCaptor.getValue().viewInsetBottom); + verify(flutterRenderer, atLeast(1)).setViewportMetrics(viewportMetricsCaptor.capture()); + assertEquals(0, viewportMetricsCaptor.getValue().viewInsetBottom); - // Call onPrepare and set the lastWindowInsets - these should be stored for the end of the - // animation instead of being applied immediately - imeSyncCallback.getAnimationCallback().onPrepare(animation); - builder.setInsets(WindowInsets.Type.ime(), Insets.of(0, 0, 0, 100)); - builder.setInsets(WindowInsets.Type.navigationBars(), Insets.of(0, 0, 0, 0)); - imeSyncCallback.getInsetsListener().onApplyWindowInsets(testView, builder.build()); + // Call onPrepare and set the lastWindowInsets - these should be stored for the end of + // the + // animation instead of being applied immediately + imeSyncCallback.getAnimationCallback().onPrepare(animation); + builder.setInsets(WindowInsets.Type.ime(), Insets.of(0, 0, 0, 100)); + builder.setInsets(WindowInsets.Type.navigationBars(), Insets.of(0, 0, 0, 0)); + imeSyncCallback.getInsetsListener().onApplyWindowInsets(testView, builder.build()); - verify(flutterRenderer, atLeast(1)).setViewportMetrics(viewportMetricsCaptor.capture()); - assertEquals(0, viewportMetricsCaptor.getValue().viewInsetBottom); + verify(flutterRenderer, atLeast(1)).setViewportMetrics(viewportMetricsCaptor.capture()); + assertEquals(0, viewportMetricsCaptor.getValue().viewInsetBottom); - // Call onStart and apply new insets - these should be ignored completely - imeSyncCallback.getAnimationCallback().onStart(animation, null); - builder.setInsets(WindowInsets.Type.ime(), Insets.of(0, 0, 0, 50)); - builder.setInsets(WindowInsets.Type.navigationBars(), Insets.of(0, 0, 0, 40)); - imeSyncCallback.getInsetsListener().onApplyWindowInsets(testView, builder.build()); + // Call onStart and apply new insets - these should be ignored completely + imeSyncCallback.getAnimationCallback().onStart(animation, null); + builder.setInsets(WindowInsets.Type.ime(), Insets.of(0, 0, 0, 50)); + builder.setInsets(WindowInsets.Type.navigationBars(), Insets.of(0, 0, 0, 40)); + imeSyncCallback.getInsetsListener().onApplyWindowInsets(testView, builder.build()); - verify(flutterRenderer, atLeast(1)).setViewportMetrics(viewportMetricsCaptor.capture()); - assertEquals(0, viewportMetricsCaptor.getValue().viewInsetBottom); + verify(flutterRenderer, atLeast(1)).setViewportMetrics(viewportMetricsCaptor.capture()); + assertEquals(0, viewportMetricsCaptor.getValue().viewInsetBottom); - // Progress the animation and ensure that the navigation bar insets have not been - // subtracted from the IME insets - builder.setInsets(WindowInsets.Type.ime(), Insets.of(0, 0, 0, 25)); - builder.setInsets(WindowInsets.Type.navigationBars(), Insets.of(0, 0, 0, 40)); - imeSyncCallback.getAnimationCallback().onProgress(builder.build(), animationList); + // Progress the animation and ensure that the navigation bar insets have not been + // subtracted from the IME insets + builder.setInsets(WindowInsets.Type.ime(), Insets.of(0, 0, 0, 25)); + builder.setInsets(WindowInsets.Type.navigationBars(), Insets.of(0, 0, 0, 40)); + imeSyncCallback.getAnimationCallback().onProgress(builder.build(), animationList); - verify(flutterRenderer, atLeast(1)).setViewportMetrics(viewportMetricsCaptor.capture()); - assertEquals(25, viewportMetricsCaptor.getValue().viewInsetBottom); + verify(flutterRenderer, atLeast(1)).setViewportMetrics(viewportMetricsCaptor.capture()); + assertEquals(25, viewportMetricsCaptor.getValue().viewInsetBottom); - builder.setInsets(WindowInsets.Type.ime(), Insets.of(0, 0, 0, 50)); - builder.setInsets(WindowInsets.Type.navigationBars(), Insets.of(0, 0, 0, 40)); - imeSyncCallback.getAnimationCallback().onProgress(builder.build(), animationList); + builder.setInsets(WindowInsets.Type.ime(), Insets.of(0, 0, 0, 50)); + builder.setInsets(WindowInsets.Type.navigationBars(), Insets.of(0, 0, 0, 40)); + imeSyncCallback.getAnimationCallback().onProgress(builder.build(), animationList); - verify(flutterRenderer, atLeast(1)).setViewportMetrics(viewportMetricsCaptor.capture()); - assertEquals(50, viewportMetricsCaptor.getValue().viewInsetBottom); + verify(flutterRenderer, atLeast(1)).setViewportMetrics(viewportMetricsCaptor.capture()); + assertEquals(50, viewportMetricsCaptor.getValue().viewInsetBottom); - // End the animation and ensure that the bottom insets match the lastWindowInsets that we set - // during onPrepare - imeSyncCallback.getAnimationCallback().onEnd(animation); + // End the animation and ensure that the bottom insets match the lastWindowInsets that + // we set + // during onPrepare + imeSyncCallback.getAnimationCallback().onEnd(animation); - verify(flutterRenderer, atLeast(1)).setViewportMetrics(viewportMetricsCaptor.capture()); - assertEquals(100, viewportMetricsCaptor.getValue().viewInsetBottom); + verify(flutterRenderer, atLeast(1)).setViewportMetrics(viewportMetricsCaptor.capture()); + assertEquals(100, viewportMetricsCaptor.getValue().viewInsetBottom); + }); + } } @Test @@ -2660,87 +2699,98 @@ public class TextInputPluginTest { // getWindowSystemUiVisibility, SYSTEM_UI_FLAG_LAYOUT_STABLE // flutter#133074 tracks migration work. public void lastWindowInsets_updatedOnSecondOnProgressCall() { - FlutterView testView = spy(getTestView()); - when(testView.getWindowSystemUiVisibility()).thenReturn(View.SYSTEM_UI_FLAG_LAYOUT_STABLE); + try (ActivityScenario scenario = ActivityScenario.launch(Activity.class)) { + scenario.onActivity( + activity -> { + FlutterView testView = spy(new FlutterView(activity)); + when(testView.getWindowSystemUiVisibility()) + .thenReturn(View.SYSTEM_UI_FLAG_LAYOUT_STABLE); - TextInputChannel textInputChannel = new TextInputChannel(mock(DartExecutor.class)); - ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class)); - TextInputPlugin textInputPlugin = - new TextInputPlugin( - testView, - textInputChannel, - scribeChannel, - mock(PlatformViewsController.class), - mock(PlatformViewsController2.class)); - ImeSyncDeferringInsetsCallback imeSyncCallback = textInputPlugin.getImeSyncCallback(); - FlutterEngine flutterEngine = spy(new FlutterEngine(ctx, mockFlutterLoader, mockFlutterJni)); - FlutterRenderer flutterRenderer = spy(new FlutterRenderer(mockFlutterJni)); - when(flutterEngine.getRenderer()).thenReturn(flutterRenderer); - testView.attachToFlutterEngine(flutterEngine); + TextInputChannel textInputChannel = new TextInputChannel(mock(DartExecutor.class)); + ScribeChannel scribeChannel = new ScribeChannel(mock(DartExecutor.class)); + TextInputPlugin textInputPlugin = + new TextInputPlugin( + testView, + textInputChannel, + scribeChannel, + mock(PlatformViewsController.class), + mock(PlatformViewsController2.class)); + ImeSyncDeferringInsetsCallback imeSyncCallback = textInputPlugin.getImeSyncCallback(); + FlutterEngine flutterEngine = + spy(new FlutterEngine(ctx, mockFlutterLoader, mockFlutterJni)); + FlutterRenderer flutterRenderer = spy(new FlutterRenderer(mockFlutterJni)); + when(flutterEngine.getRenderer()).thenReturn(flutterRenderer); + testView.attachToFlutterEngine(flutterEngine); - WindowInsetsAnimation imeAnimation = mock(WindowInsetsAnimation.class); - when(imeAnimation.getTypeMask()).thenReturn(WindowInsets.Type.ime()); - WindowInsetsAnimation navigationBarAnimation = mock(WindowInsetsAnimation.class); - when(navigationBarAnimation.getTypeMask()).thenReturn(WindowInsets.Type.navigationBars()); + WindowInsetsAnimation imeAnimation = mock(WindowInsetsAnimation.class); + when(imeAnimation.getTypeMask()).thenReturn(WindowInsets.Type.ime()); + WindowInsetsAnimation navigationBarAnimation = mock(WindowInsetsAnimation.class); + when(navigationBarAnimation.getTypeMask()) + .thenReturn(WindowInsets.Type.navigationBars()); - List animationList = new ArrayList(); - animationList.add(imeAnimation); - animationList.add(navigationBarAnimation); + List animationList = new ArrayList(); + animationList.add(imeAnimation); + animationList.add(navigationBarAnimation); - ArgumentCaptor viewportMetricsCaptor = - ArgumentCaptor.forClass(FlutterRenderer.ViewportMetrics.class); + ArgumentCaptor viewportMetricsCaptor = + ArgumentCaptor.forClass(FlutterRenderer.ViewportMetrics.class); - WindowInsets.Builder builder = new WindowInsets.Builder(); + WindowInsets.Builder builder = new WindowInsets.Builder(); - // Set the initial insets and verify that they were set and the bottom view padding is correct - builder.setInsets(WindowInsets.Type.ime(), Insets.of(0, 0, 0, 1000)); - builder.setInsets(WindowInsets.Type.navigationBars(), Insets.of(0, 0, 0, 100)); - imeSyncCallback.getInsetsListener().onApplyWindowInsets(testView, builder.build()); + // Set the initial insets and verify that they were set and the bottom view padding is + // correct + builder.setInsets(WindowInsets.Type.ime(), Insets.of(0, 0, 0, 1000)); + builder.setInsets(WindowInsets.Type.navigationBars(), Insets.of(0, 0, 0, 100)); + imeSyncCallback.getInsetsListener().onApplyWindowInsets(testView, builder.build()); - verify(flutterRenderer, atLeast(1)).setViewportMetrics(viewportMetricsCaptor.capture()); - assertEquals(100, viewportMetricsCaptor.getValue().viewPaddingBottom); + verify(flutterRenderer, atLeast(1)).setViewportMetrics(viewportMetricsCaptor.capture()); + assertEquals(100, viewportMetricsCaptor.getValue().viewPaddingBottom); - // Call onPrepare and set the lastWindowInsets - these should be stored for the end of the - // animation instead of being applied immediately - imeSyncCallback.getAnimationCallback().onPrepare(imeAnimation); - builder.setInsets(WindowInsets.Type.ime(), Insets.of(0, 0, 0, 0)); - builder.setInsets(WindowInsets.Type.navigationBars(), Insets.of(0, 0, 0, 100)); - imeSyncCallback.getInsetsListener().onApplyWindowInsets(testView, builder.build()); + // Call onPrepare and set the lastWindowInsets - these should be stored for the end of + // the + // animation instead of being applied immediately + imeSyncCallback.getAnimationCallback().onPrepare(imeAnimation); + builder.setInsets(WindowInsets.Type.ime(), Insets.of(0, 0, 0, 0)); + builder.setInsets(WindowInsets.Type.navigationBars(), Insets.of(0, 0, 0, 100)); + imeSyncCallback.getInsetsListener().onApplyWindowInsets(testView, builder.build()); - verify(flutterRenderer, atLeast(1)).setViewportMetrics(viewportMetricsCaptor.capture()); - assertEquals(100, viewportMetricsCaptor.getValue().viewPaddingBottom); + verify(flutterRenderer, atLeast(1)).setViewportMetrics(viewportMetricsCaptor.capture()); + assertEquals(100, viewportMetricsCaptor.getValue().viewPaddingBottom); - // Call onPrepare again and apply new insets - these should overrite lastWindowInsets - imeSyncCallback.getAnimationCallback().onPrepare(navigationBarAnimation); - builder.setInsets(WindowInsets.Type.ime(), Insets.of(0, 0, 0, 0)); - builder.setInsets(WindowInsets.Type.navigationBars(), Insets.of(0, 0, 0, 0)); - imeSyncCallback.getInsetsListener().onApplyWindowInsets(testView, builder.build()); + // Call onPrepare again and apply new insets - these should overrite lastWindowInsets + imeSyncCallback.getAnimationCallback().onPrepare(navigationBarAnimation); + builder.setInsets(WindowInsets.Type.ime(), Insets.of(0, 0, 0, 0)); + builder.setInsets(WindowInsets.Type.navigationBars(), Insets.of(0, 0, 0, 0)); + imeSyncCallback.getInsetsListener().onApplyWindowInsets(testView, builder.build()); - verify(flutterRenderer, atLeast(1)).setViewportMetrics(viewportMetricsCaptor.capture()); - assertEquals(100, viewportMetricsCaptor.getValue().viewPaddingBottom); + verify(flutterRenderer, atLeast(1)).setViewportMetrics(viewportMetricsCaptor.capture()); + assertEquals(100, viewportMetricsCaptor.getValue().viewPaddingBottom); - // Progress the animation and ensure that the navigation bar insets have not been - // subtracted from the IME insets - builder.setInsets(WindowInsets.Type.ime(), Insets.of(0, 0, 0, 500)); - builder.setInsets(WindowInsets.Type.navigationBars(), Insets.of(0, 0, 0, 0)); - imeSyncCallback.getAnimationCallback().onProgress(builder.build(), animationList); + // Progress the animation and ensure that the navigation bar insets have not been + // subtracted from the IME insets + builder.setInsets(WindowInsets.Type.ime(), Insets.of(0, 0, 0, 500)); + builder.setInsets(WindowInsets.Type.navigationBars(), Insets.of(0, 0, 0, 0)); + imeSyncCallback.getAnimationCallback().onProgress(builder.build(), animationList); - verify(flutterRenderer, atLeast(1)).setViewportMetrics(viewportMetricsCaptor.capture()); - assertEquals(0, viewportMetricsCaptor.getValue().viewPaddingBottom); + verify(flutterRenderer, atLeast(1)).setViewportMetrics(viewportMetricsCaptor.capture()); + assertEquals(0, viewportMetricsCaptor.getValue().viewPaddingBottom); - builder.setInsets(WindowInsets.Type.ime(), Insets.of(0, 0, 0, 250)); - builder.setInsets(WindowInsets.Type.navigationBars(), Insets.of(0, 0, 0, 0)); - imeSyncCallback.getAnimationCallback().onProgress(builder.build(), animationList); + builder.setInsets(WindowInsets.Type.ime(), Insets.of(0, 0, 0, 250)); + builder.setInsets(WindowInsets.Type.navigationBars(), Insets.of(0, 0, 0, 0)); + imeSyncCallback.getAnimationCallback().onProgress(builder.build(), animationList); - verify(flutterRenderer, atLeast(1)).setViewportMetrics(viewportMetricsCaptor.capture()); - assertEquals(0, viewportMetricsCaptor.getValue().viewPaddingBottom); + verify(flutterRenderer, atLeast(1)).setViewportMetrics(viewportMetricsCaptor.capture()); + assertEquals(0, viewportMetricsCaptor.getValue().viewPaddingBottom); - // End the animation and ensure that the bottom insets match the lastWindowInsets that we set - // during onPrepare - imeSyncCallback.getAnimationCallback().onEnd(imeAnimation); + // End the animation and ensure that the bottom insets match the lastWindowInsets that + // we set + // during onPrepare + imeSyncCallback.getAnimationCallback().onEnd(imeAnimation); - verify(flutterRenderer, atLeast(1)).setViewportMetrics(viewportMetricsCaptor.capture()); - assertEquals(0, viewportMetricsCaptor.getValue().viewPaddingBottom); + verify(flutterRenderer, atLeast(1)).setViewportMetrics(viewportMetricsCaptor.capture()); + assertEquals(0, viewportMetricsCaptor.getValue().viewPaddingBottom); + }); + } } @Test diff --git a/engine/src/flutter/shell/platform/android/test/io/flutter/plugin/mouse/MouseCursorPluginTest.java b/engine/src/flutter/shell/platform/android/test/io/flutter/plugin/mouse/MouseCursorPluginTest.java index 46594d138f7..9624c04cde4 100644 --- a/engine/src/flutter/shell/platform/android/test/io/flutter/plugin/mouse/MouseCursorPluginTest.java +++ b/engine/src/flutter/shell/platform/android/test/io/flutter/plugin/mouse/MouseCursorPluginTest.java @@ -11,6 +11,7 @@ import static org.mockito.Mockito.verify; import android.annotation.TargetApi; import android.app.Activity; import android.view.PointerIcon; +import androidx.test.core.app.ActivityScenario; import androidx.test.ext.junit.runners.AndroidJUnit4; import io.flutter.embedding.android.FlutterView; import io.flutter.embedding.engine.dart.DartExecutor; @@ -21,7 +22,6 @@ import java.util.HashMap; import org.json.JSONException; import org.junit.Test; import org.junit.runner.RunWith; -import org.robolectric.Robolectric; import org.robolectric.annotation.Config; @Config( @@ -31,34 +31,36 @@ import org.robolectric.annotation.Config; @TargetApi(API_LEVELS.API_24) public class MouseCursorPluginTest { - @SuppressWarnings("deprecation") - // Robolectric.setupActivity. - // TODO(reidbaker): https://github.com/flutter/flutter/issues/133151 @Test public void mouseCursorPlugin_SetsSystemCursorOnRequest() throws JSONException { - // Migrate to ActivityScenario by following https://github.com/robolectric/robolectric/pull/4736 - // Initialize a general MouseCursorPlugin. - FlutterView testView = spy(new FlutterView(Robolectric.setupActivity(Activity.class))); - MouseCursorChannel mouseCursorChannel = new MouseCursorChannel(mock(DartExecutor.class)); + try (ActivityScenario scenario = ActivityScenario.launch(Activity.class)) { + scenario.onActivity( + activity -> { + FlutterView testView = spy(new FlutterView(activity)); + MouseCursorChannel mouseCursorChannel = + new MouseCursorChannel(mock(DartExecutor.class)); - MouseCursorPlugin mouseCursorPlugin = new MouseCursorPlugin(testView, mouseCursorChannel); + MouseCursorPlugin mouseCursorPlugin = + new MouseCursorPlugin(testView, mouseCursorChannel); - final StoredResult methodResult = new StoredResult(); - mouseCursorChannel.synthesizeMethodCall( - new MethodCall( - "activateSystemCursor", - new HashMap() { - private static final long serialVersionUID = 1L; + final StoredResult methodResult = new StoredResult(); + mouseCursorChannel.synthesizeMethodCall( + new MethodCall( + "activateSystemCursor", + new HashMap() { + private static final long serialVersionUID = 1L; - { - put("device", 1); - put("kind", "text"); - } - }), - methodResult); - verify(testView, times(1)).getSystemPointerIcon(PointerIcon.TYPE_TEXT); - verify(testView, times(1)).setPointerIcon(any(PointerIcon.class)); - assertEquals(methodResult.result, Boolean.TRUE); + { + put("device", 1); + put("kind", "text"); + } + }), + methodResult); + verify(testView, times(1)).getSystemPointerIcon(PointerIcon.TYPE_TEXT); + verify(testView, times(1)).setPointerIcon(any(PointerIcon.class)); + assertEquals(methodResult.result, Boolean.TRUE); + }); + } } } diff --git a/engine/src/flutter/shell/platform/android/test/io/flutter/plugin/platform/PlatformPluginTest.java b/engine/src/flutter/shell/platform/android/test/io/flutter/plugin/platform/PlatformPluginTest.java index 823f74254dd..9d2abb0adef 100644 --- a/engine/src/flutter/shell/platform/android/test/io/flutter/plugin/platform/PlatformPluginTest.java +++ b/engine/src/flutter/shell/platform/android/test/io/flutter/plugin/platform/PlatformPluginTest.java @@ -39,6 +39,7 @@ import android.view.WindowInsetsController; import android.view.WindowManager; import androidx.activity.OnBackPressedCallback; import androidx.fragment.app.FragmentActivity; +import androidx.test.core.app.ActivityScenario; import androidx.test.core.app.ApplicationProvider; import io.flutter.embedding.engine.systemchannels.PlatformChannel; import io.flutter.embedding.engine.systemchannels.PlatformChannel.Brightness; @@ -645,51 +646,56 @@ public class PlatformPluginTest { verify(mockActivity, never()).finish(); } - @SuppressWarnings("deprecation") - // Robolectric.setupActivity. - // TODO(reidbaker): https://github.com/flutter/flutter/issues/133151 @Test public void popSystemNavigatorFlutterFragment() { - // Migrate to ActivityScenario by following https://github.com/robolectric/robolectric/pull/4736 - FragmentActivity activity = spy(Robolectric.setupActivity(FragmentActivity.class)); - final AtomicBoolean onBackPressedCalled = new AtomicBoolean(false); - OnBackPressedCallback backCallback = - new OnBackPressedCallback(true) { - @Override - public void handleOnBackPressed() { - onBackPressedCalled.set(true); - } - }; - activity.getOnBackPressedDispatcher().addCallback(backCallback); + try (ActivityScenario scenario = + ActivityScenario.launch(FragmentActivity.class)) { + scenario.onActivity( + fragmentActivity -> { + FragmentActivity activity = spy(fragmentActivity); + final AtomicBoolean onBackPressedCalled = new AtomicBoolean(false); + OnBackPressedCallback backCallback = + new OnBackPressedCallback(true) { + @Override + public void handleOnBackPressed() { + onBackPressedCalled.set(true); + } + }; + activity.getOnBackPressedDispatcher().addCallback(backCallback); - PlatformPluginDelegate mockPlatformPluginDelegate = mock(PlatformPluginDelegate.class); - when(mockPlatformPluginDelegate.popSystemNavigator()).thenReturn(false); - PlatformPlugin platformPlugin = - new PlatformPlugin(activity, mockPlatformChannel, mockPlatformPluginDelegate); + PlatformPluginDelegate mockPlatformPluginDelegate = mock(PlatformPluginDelegate.class); + when(mockPlatformPluginDelegate.popSystemNavigator()).thenReturn(false); + PlatformPlugin platformPlugin = + new PlatformPlugin(activity, mockPlatformChannel, mockPlatformPluginDelegate); - platformPlugin.mPlatformMessageHandler.popSystemNavigator(); + platformPlugin.mPlatformMessageHandler.popSystemNavigator(); - verify(activity, never()).finish(); - verify(mockPlatformPluginDelegate, times(1)).popSystemNavigator(); - assertTrue(onBackPressedCalled.get()); + verify(activity, never()).finish(); + verify(mockPlatformPluginDelegate, times(1)).popSystemNavigator(); + assertTrue(onBackPressedCalled.get()); + }); + } } - @SuppressWarnings("deprecation") - // Robolectric.setupActivity. - // TODO(reidbaker): https://github.com/flutter/flutter/issues/133151 @Test public void doesNotDoAnythingByDefaultIfFragmentPopSystemNavigatorOverridden() { - FragmentActivity activity = spy(Robolectric.setupActivity(FragmentActivity.class)); - PlatformPluginDelegate mockPlatformPluginDelegate = mock(PlatformPluginDelegate.class); - when(mockPlatformPluginDelegate.popSystemNavigator()).thenReturn(true); - PlatformPlugin platformPlugin = - new PlatformPlugin(activity, mockPlatformChannel, mockPlatformPluginDelegate); + try (ActivityScenario scenario = + ActivityScenario.launch(FragmentActivity.class)) { + scenario.onActivity( + fragmentActivity -> { + FragmentActivity activity = spy(fragmentActivity); + PlatformPluginDelegate mockPlatformPluginDelegate = mock(PlatformPluginDelegate.class); + when(mockPlatformPluginDelegate.popSystemNavigator()).thenReturn(true); + PlatformPlugin platformPlugin = + new PlatformPlugin(activity, mockPlatformChannel, mockPlatformPluginDelegate); - platformPlugin.mPlatformMessageHandler.popSystemNavigator(); + platformPlugin.mPlatformMessageHandler.popSystemNavigator(); - verify(mockPlatformPluginDelegate, times(1)).popSystemNavigator(); - // No longer perform the default action when overridden. - verify(activity, never()).finish(); + verify(mockPlatformPluginDelegate, times(1)).popSystemNavigator(); + // No longer perform the default action when overridden. + verify(activity, never()).finish(); + }); + } } @Test diff --git a/engine/src/flutter/shell/platform/android/test_runner/src/main/AndroidManifest.xml b/engine/src/flutter/shell/platform/android/test_runner/src/main/AndroidManifest.xml index 65ca9afc771..ccebc01df9b 100644 --- a/engine/src/flutter/shell/platform/android/test_runner/src/main/AndroidManifest.xml +++ b/engine/src/flutter/shell/platform/android/test_runner/src/main/AndroidManifest.xml @@ -7,6 +7,10 @@ + + + +