From 1bfb928e071674a21779cee94908fbcae1c2e657 Mon Sep 17 00:00:00 2001 From: chunhtai <47866232+chunhtai@users.noreply.github.com> Date: Tue, 5 Nov 2019 14:52:16 -0800 Subject: [PATCH] Issues/39832 reland (#13642) * Reland "Added new lifecycle enum (#11913)" --- lib/ui/window.dart | 16 ++--- lib/web_ui/lib/src/ui/window.dart | 13 ++-- runtime/runtime_controller.h | 2 +- shell/common/engine.cc | 2 +- .../FlutterActivityAndFragmentDelegate.java | 2 + .../systemchannels/LifecycleChannel.java | 4 ++ ...lutterActivityAndFragmentDelegateTest.java | 11 ++++ .../ios/framework/Source/FlutterEngine.mm | 24 ++++--- .../ScenariosTests/AppLifecycleTests.m | 66 +++++++++++++++++++ 9 files changed, 111 insertions(+), 29 deletions(-) diff --git a/lib/ui/window.dart b/lib/ui/window.dart index 3bd28a248bbb..954640ab7916 100644 --- a/lib/ui/window.dart +++ b/lib/ui/window.dart @@ -171,18 +171,16 @@ enum AppLifecycleState { /// /// When the application is in this state, the engine will not call the /// [Window.onBeginFrame] and [Window.onDrawFrame] callbacks. - /// - /// Android apps in this state should assume that they may enter the - /// [suspending] state at any time. paused, - /// The application will be suspended momentarily. - /// - /// When the application is in this state, the engine will not call the - /// [Window.onBeginFrame] and [Window.onDrawFrame] callbacks. + /// The application is still hosted on a flutter engine but is detached from + /// any host views. /// - /// On iOS, this state is currently unused. - suspending, + /// When the application is in this state, the engine is running without + /// a view. It can either be in the progress of attaching a view when engine + /// was first initializes, or after the view being destroyed due to a Navigator + /// pop. + detached, } /// A representation of distances for each of the four edges of a rectangle, diff --git a/lib/web_ui/lib/src/ui/window.dart b/lib/web_ui/lib/src/ui/window.dart index 58c82410de7c..731874e6dff9 100644 --- a/lib/web_ui/lib/src/ui/window.dart +++ b/lib/web_ui/lib/src/ui/window.dart @@ -73,18 +73,13 @@ enum AppLifecycleState { /// /// When the application is in this state, the engine will not call the /// [Window.onBeginFrame] and [Window.onDrawFrame] callbacks. - /// - /// Android apps in this state should assume that they may enter the - /// [suspending] state at any time. paused, - /// The application will be suspended momentarily. - /// - /// When the application is in this state, the engine will not call the - /// [Window.onBeginFrame] and [Window.onDrawFrame] callbacks. + /// The application is detached from view. /// - /// On iOS, this state is currently unused. - suspending, + /// When the application is in this state, the engine is running without + /// a platform UI. + detached, } /// A representation of distances for each of the four edges of a rectangle, diff --git a/runtime/runtime_controller.h b/runtime/runtime_controller.h index c0ee45d762e8..5ade4672cbec 100644 --- a/runtime/runtime_controller.h +++ b/runtime/runtime_controller.h @@ -120,7 +120,7 @@ class RuntimeController final : public WindowClient { std::string variant_code; std::vector locale_data; std::string user_settings_data = "{}"; - std::string lifecycle_state; + std::string lifecycle_state = "AppLifecycleState.detached"; bool semantics_enabled = false; bool assistive_technology_enabled = false; int32_t accessibility_feature_flags_ = 0; diff --git a/shell/common/engine.cc b/shell/common/engine.cc index 38b034d52d0d..77b657727151 100644 --- a/shell/common/engine.cc +++ b/shell/common/engine.cc @@ -311,7 +311,7 @@ bool Engine::HandleLifecyclePlatformMessage(PlatformMessage* message) { const auto& data = message->data(); std::string state(reinterpret_cast(data.data()), data.size()); if (state == "AppLifecycleState.paused" || - state == "AppLifecycleState.suspending") { + state == "AppLifecycleState.detached") { activity_running_ = false; StopAnimator(); } else if (state == "AppLifecycleState.resumed" || diff --git a/shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java b/shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java index eddacf2b2553..6cebfcbef623 100644 --- a/shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java +++ b/shell/platform/android/io/flutter/embedding/android/FlutterActivityAndFragmentDelegate.java @@ -461,6 +461,8 @@ void onDetach() { platformPlugin = null; } + flutterEngine.getLifecycleChannel().appIsDetached(); + // Destroy our FlutterEngine if we're not set to retain it. if (host.shouldDestroyEngineWithHost()) { flutterEngine.destroy(); diff --git a/shell/platform/android/io/flutter/embedding/engine/systemchannels/LifecycleChannel.java b/shell/platform/android/io/flutter/embedding/engine/systemchannels/LifecycleChannel.java index abc6323907d0..cd244ff8251e 100644 --- a/shell/platform/android/io/flutter/embedding/engine/systemchannels/LifecycleChannel.java +++ b/shell/platform/android/io/flutter/embedding/engine/systemchannels/LifecycleChannel.java @@ -39,4 +39,8 @@ public void appIsPaused() { channel.send("AppLifecycleState.paused"); } + public void appIsDetached() { + Log.v(TAG, "Sending AppLifecycleState.detached message."); + channel.send("AppLifecycleState.detached"); + } } diff --git a/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java b/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java index 154c59f17319..d335fa5064db 100644 --- a/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java +++ b/shell/platform/android/test/io/flutter/embedding/android/FlutterActivityAndFragmentDelegateTest.java @@ -85,18 +85,21 @@ public void itSendsLifecycleEventsToFlutter() { verify(mockFlutterEngine.getLifecycleChannel(), never()).appIsResumed(); verify(mockFlutterEngine.getLifecycleChannel(), never()).appIsPaused(); verify(mockFlutterEngine.getLifecycleChannel(), never()).appIsInactive(); + 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(), 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)).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 @@ -105,6 +108,14 @@ public void itSendsLifecycleEventsToFlutter() { 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)).appIsResumed(); + verify(mockFlutterEngine.getLifecycleChannel(), times(1)).appIsInactive(); + verify(mockFlutterEngine.getLifecycleChannel(), times(1)).appIsPaused(); + verify(mockFlutterEngine.getLifecycleChannel(), times(1)).appIsDetached(); } @Test diff --git a/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm b/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm index 83551d2aed04..a9f20708f15b 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm @@ -175,18 +175,23 @@ - (void)ensureSemanticsEnabled { - (void)setViewController:(FlutterViewController*)viewController { FML_DCHECK(self.iosPlatformView); - _viewController = [viewController getWeakPtr]; + _viewController = + viewController ? [viewController getWeakPtr] : fml::WeakPtr(); self.iosPlatformView->SetOwnerViewController(_viewController); [self maybeSetupPlatformViewChannels]; - __block FlutterEngine* blockSelf = self; - self.flutterViewControllerWillDeallocObserver = - [[NSNotificationCenter defaultCenter] addObserverForName:FlutterViewControllerWillDealloc - object:viewController - queue:[NSOperationQueue mainQueue] - usingBlock:^(NSNotification* note) { - [blockSelf notifyViewControllerDeallocated]; - }]; + if (viewController) { + __block FlutterEngine* blockSelf = self; + self.flutterViewControllerWillDeallocObserver = + [[NSNotificationCenter defaultCenter] addObserverForName:FlutterViewControllerWillDealloc + object:viewController + queue:[NSOperationQueue mainQueue] + usingBlock:^(NSNotification* note) { + [blockSelf notifyViewControllerDeallocated]; + }]; + } else { + self.flutterViewControllerWillDeallocObserver = nil; + } } - (void)setFlutterViewControllerWillDeallocObserver:(id)observer { @@ -201,6 +206,7 @@ - (void)setFlutterViewControllerWillDeallocObserver:(id)observer { } - (void)notifyViewControllerDeallocated { + [[self lifecycleChannel] sendMessage:@"AppLifecycleState.detached"]; if (!_allowHeadlessExecution) { [self destroyContext]; } else { diff --git a/testing/scenario_app/ios/Scenarios/ScenariosTests/AppLifecycleTests.m b/testing/scenario_app/ios/Scenarios/ScenariosTests/AppLifecycleTests.m index 3646e3fd5ee4..7f8f6902bde2 100644 --- a/testing/scenario_app/ios/Scenarios/ScenariosTests/AppLifecycleTests.m +++ b/testing/scenario_app/ios/Scenarios/ScenariosTests/AppLifecycleTests.m @@ -225,4 +225,70 @@ - (void)testVisibleFlutterViewControllerRespondsToApplicationLifecycle { [engine setViewController:nil]; } +- (void)testFlutterViewControllerDetachingSendsApplicationLifecycle { + XCTestExpectation* engineStartedExpectation = [self expectationWithDescription:@"Engine started"]; + + // Let the engine finish booting (at the end of which the channels are properly set-up) before + // moving onto the next step of showing the next view controller. + ScreenBeforeFlutter* rootVC = [[ScreenBeforeFlutter alloc] initWithEngineRunCompletion:^void() { + [engineStartedExpectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:5 handler:nil]; + + UIApplication* application = UIApplication.sharedApplication; + application.delegate.window.rootViewController = rootVC; + FlutterEngine* engine = rootVC.engine; + + NSMutableArray* lifecycleExpectations = [NSMutableArray arrayWithCapacity:10]; + + // Expected sequence from showing the FlutterViewController is inactive and resumed. + [lifecycleExpectations addObjectsFromArray:@[ + [[XCAppLifecycleTestExpectation alloc] initForLifecycle:@"AppLifecycleState.inactive" + forStep:@"showing a FlutterViewController"], + [[XCAppLifecycleTestExpectation alloc] initForLifecycle:@"AppLifecycleState.resumed" + forStep:@"showing a FlutterViewController"] + ]]; + // At the end of Flutter VC, we want to make sure it deallocs and sends detached signal. + // Using autoreleasepool will guarantee that. + FlutterViewController* flutterVC; + @autoreleasepool { + flutterVC = [rootVC showFlutter]; + [engine.lifecycleChannel setMessageHandler:^(id message, FlutterReply callback) { + if (lifecycleExpectations.count == 0) { + XCTFail(@"Unexpected lifecycle transition: %@", message); + return; + } + XCAppLifecycleTestExpectation* nextExpectation = [lifecycleExpectations objectAtIndex:0]; + if (![[nextExpectation expectedLifecycle] isEqualToString:message]) { + XCTFail(@"Expected lifecycle %@ but instead received %@", + [nextExpectation expectedLifecycle], message); + return; + } + + [nextExpectation fulfill]; + [lifecycleExpectations removeObjectAtIndex:0]; + }]; + + [self waitForExpectations:lifecycleExpectations timeout:5]; + + // Starts dealloc flutter VC. + [lifecycleExpectations addObjectsFromArray:@[ + [[XCAppLifecycleTestExpectation alloc] initForLifecycle:@"AppLifecycleState.inactive" + forStep:@"detaching a FlutterViewController"], + [[XCAppLifecycleTestExpectation alloc] initForLifecycle:@"AppLifecycleState.paused" + forStep:@"detaching a FlutterViewController"], + [[XCAppLifecycleTestExpectation alloc] + initForLifecycle:@"AppLifecycleState.detached" + forStep:@"detaching a FlutterViewController"] + ]]; + [flutterVC dismissViewControllerAnimated:NO completion:nil]; + flutterVC = nil; + } + [self waitForExpectations:lifecycleExpectations timeout:5]; + + [engine.lifecycleChannel setMessageHandler:nil]; + [engine setViewController:nil]; +} + @end