From c0e0627533391363c7a516ec5abaf4f5523aa9aa Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Tue, 20 Jun 2023 10:39:12 -0700 Subject: [PATCH 1/2] draft draft format draf format draf fix non extension build add extension safe builders generate extension safe output fix build config fix clang tidy complains add to tests fix tests remove test logs fox tests fix test fix memory leak in platform plugin test format review review bug fix format --- ci/builders/mac_unopt.json | 71 ++++++ common/config.gni | 6 + shell/platform/darwin/ios/BUILD.gn | 14 ++ .../ios/framework/Source/FlutterEngine.mm | 70 +++++- .../ios/framework/Source/FlutterEngineTest.mm | 59 +++++ .../ios/framework/Source/FlutterEngine_Test.h | 5 + .../framework/Source/FlutterPlatformPlugin.mm | 16 +- .../Source/FlutterPlatformPluginTest.mm | 5 +- .../framework/Source/FlutterViewController.mm | 223 +++++++++++++----- .../Source/FlutterViewControllerTest.mm | 161 ++++++++++++- tools/gn | 13 + 11 files changed, 573 insertions(+), 70 deletions(-) diff --git a/ci/builders/mac_unopt.json b/ci/builders/mac_unopt.json index 72778ab4df159..1be5de70ffcab 100644 --- a/ci/builders/mac_unopt.json +++ b/ci/builders/mac_unopt.json @@ -223,6 +223,77 @@ "script": "flutter/testing/scenario_app/run_ios_tests.sh" } + ] + }, + { + "archives": [ + { + "base_path": "out/ios_debug_sim_arm64_extension_safe/zip_archives/", + "type": "gcs", + "include_paths": [ + ], + "name": "ios_debug_sim_arm64_extension_safe" + } + ], + "properties": { + "$flutter/osx_sdk": { + "runtime_versions": [ + "ios-16-4_14e300c", + "ios-16-2_14c18" + ], + "sdk_version": "14e300c" + } + }, + "drone_dimensions": [ + "device_type=none", + "os=Mac-12", + "cpu=arm64" + ], + "gclient_variables": { + "download_android_deps": false + }, + "gn": [ + "--ios", + "--runtime-mode", + "debug", + "--simulator", + "--no-lto", + "--force-mac-arm64", + "--simulator-cpu", + "arm64", + "--darwin-extension-safe" + ], + "name": "ios_debug_sim_arm64_extension_safe", + "ninja": { + "config": "ios_debug_sim_arm64_extension_safe", + "targets": [ + "flutter/testing/scenario_app", + "flutter/shell/platform/darwin/ios:ios_test_flutter" + ] + }, + "tests": [ + { + "language": "python3", + "name": "Tests for ios_debug_sim_arm64_extension_safe", + "parameters": [ + "--variant", + "ios_debug_sim_arm64_extension_safe", + "--type", + "objc", + "--engine-capture-core-dump", + "--ios-variant", + "ios_debug_sim_arm64_extension_safe" + ], + "script": "flutter/testing/run_tests.py" + }, + { + "name": "Scenario App Integration Tests", + "parameters": [ + "ios_debug_sim_arm64_extension_safe" + ], + "script": "flutter/testing/scenario_app/run_ios_tests.sh" + } + ] } ] diff --git a/common/config.gni b/common/config.gni index 12a4141cb7c82..56035f10a3961 100644 --- a/common/config.gni +++ b/common/config.gni @@ -22,6 +22,12 @@ declare_args() { # Whether to include backtrace support. enable_backtrace = true + + # Whether to include --fapplication-extension when build iOS framework. + # This is currently a test flag and does not work properly. + #TODO(cyanglaz): Remove above comment about test flag when the entire iOS embedder supports app extension + #https://github.com/flutter/flutter/issues/124289 + darwin_extension_safe = false } # feature_defines_list --------------------------------------------------------- diff --git a/shell/platform/darwin/ios/BUILD.gn b/shell/platform/darwin/ios/BUILD.gn index 4acf7e2bcf062..762d3ca94a228 100644 --- a/shell/platform/darwin/ios/BUILD.gn +++ b/shell/platform/darwin/ios/BUILD.gn @@ -44,6 +44,9 @@ source_set("flutter_framework_source_arc") { cflags_objcc = flutter_cflags_objcc_arc defines = [ "FLUTTER_FRAMEWORK=1" ] + if (darwin_extension_safe) { + defines += [ "APPLICATION_EXTENSION_API_ONLY=1" ] + } allow_circular_includes_from = [ ":flutter_framework_source" ] deps = [ ":flutter_framework_source", @@ -153,6 +156,9 @@ source_set("flutter_framework_source") { sources += _flutter_framework_headers defines = [ "FLUTTER_FRAMEWORK=1" ] + if (darwin_extension_safe) { + defines += [ "APPLICATION_EXTENSION_API_ONLY=1" ] + } if (shell_enable_metal) { sources += [ @@ -249,6 +255,10 @@ source_set("ios_test_flutter_mrc") { if (shell_enable_vulkan) { deps += [ "//flutter/vulkan" ] } + + if (darwin_extension_safe) { + defines = [ "APPLICATION_EXTENSION_API_ONLY=1" ] + } } shared_library("ios_test_flutter") { @@ -312,6 +322,10 @@ shared_library("ios_test_flutter") { ":ios_gpu_configuration_config", "//flutter:config", ] + + if (darwin_extension_safe) { + defines = [ "APPLICATION_EXTENSION_API_ONLY=1" ] + } } shared_library("create_flutter_framework_dylib") { diff --git a/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm b/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm index 3a51f4ab3c697..37451fd2da1bf 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm @@ -225,22 +225,44 @@ - (instancetype)initWithName:(NSString*)labelPrefix name:UIApplicationDidReceiveMemoryWarningNotification object:nil]; +#if APPLICATION_EXTENSION_API_ONLY + if (@available(iOS 13.0, *)) { + [self setUpSceneLifecycleNotifications:center]; + } else { + [self setUpApplicationLifecycleNotifications:center]; + } +#else + [self setUpApplicationLifecycleNotifications:center]; +#endif + [center addObserver:self - selector:@selector(applicationWillEnterForeground:) - name:UIApplicationWillEnterForegroundNotification + selector:@selector(onLocaleUpdated:) + name:NSCurrentLocaleDidChangeNotification object:nil]; + return self; +} + +- (void)setUpSceneLifecycleNotifications:(NSNotificationCenter*)center API_AVAILABLE(ios(13.0)) { [center addObserver:self - selector:@selector(applicationDidEnterBackground:) - name:UIApplicationDidEnterBackgroundNotification + selector:@selector(sceneWillEnterForeground:) + name:UISceneWillEnterForegroundNotification object:nil]; - [center addObserver:self - selector:@selector(onLocaleUpdated:) - name:NSCurrentLocaleDidChangeNotification + selector:@selector(sceneDidEnterBackground:) + name:UISceneDidEnterBackgroundNotification object:nil]; +} - return self; +- (void)setUpApplicationLifecycleNotifications:(NSNotificationCenter*)center { + [center addObserver:self + selector:@selector(applicationWillEnterForeground:) + name:UIApplicationWillEnterForegroundNotification + object:nil]; + [center addObserver:self + selector:@selector(applicationDidEnterBackground:) + name:UIApplicationDidEnterBackgroundNotification + object:nil]; } - (void)recreatePlatformViewController { @@ -856,8 +878,20 @@ - (BOOL)createShell:(NSString*)entrypoint _threadHost->io_thread->GetTaskRunner() // io ); +#if APPLICATION_EXTENSION_API_ONLY + if (@available(iOS 13.0, *)) { + _isGpuDisabled = self.viewController.windowSceneIfViewLoaded.activationState == + UISceneActivationStateBackground; + } else { + // [UIApplication sharedApplication API is not available for app extension. + // We intialize the shell assuming the GPU is required. + _isGpuDisabled = NO; + } +#else _isGpuDisabled = [UIApplication sharedApplication].applicationState == UIApplicationStateBackground; +#endif + // Create the shell. This is a blocking operation. std::unique_ptr shell = flutter::Shell::Create( /*platform_data=*/platformData, @@ -1302,11 +1336,29 @@ - (NSObject*)valuePublishedByPlugin:(NSString*)pluginKey { #pragma mark - Notifications +#if APPLICATION_EXTENSION_API_ONLY +- (void)sceneWillEnterForeground:(NSNotification*)notification API_AVAILABLE(ios(13.0)) { + [self flutterWillEnterForeground:notification]; +} + +- (void)sceneDidEnterBackground:(NSNotification*)notification API_AVAILABLE(ios(13.0)) { + [self flutterDidEnterBackground:notification]; +} +#else - (void)applicationWillEnterForeground:(NSNotification*)notification { - [self setIsGpuDisabled:NO]; + [self flutterWillEnterForeground:notification]; } - (void)applicationDidEnterBackground:(NSNotification*)notification { + [self flutterDidEnterBackground:notification]; +} +#endif + +- (void)flutterWillEnterForeground:(NSNotification*)notification { + [self setIsGpuDisabled:NO]; +} + +- (void)flutterDidEnterBackground:(NSNotification*)notification { [self setIsGpuDisabled:YES]; [self notifyLowMemory]; } diff --git a/shell/platform/darwin/ios/framework/Source/FlutterEngineTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterEngineTest.mm index 2d55b14826692..34743904f7cf6 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterEngineTest.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterEngineTest.mm @@ -9,6 +9,7 @@ #import #import "flutter/common/settings.h" +#include "flutter/fml/synchronization/sync_switch.h" #import "flutter/shell/platform/darwin/common/framework/Headers/FlutterMacros.h" #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterBinaryMessengerRelay.h" #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterDartProject_Internal.h" @@ -341,4 +342,62 @@ - (void)testFlutterEngineUpdatesDisplays { OCMVerify(times(2), [mockEngine updateDisplays]); } +- (void)testLifeCycleNotificationDidEnterBackground { + FlutterDartProject* project = [[FlutterDartProject alloc] init]; + FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar" project:project]; + [engine run]; + NSNotification* sceneNotification = + [NSNotification notificationWithName:UISceneDidEnterBackgroundNotification + object:nil + userInfo:nil]; + NSNotification* applicationNotification = + [NSNotification notificationWithName:UIApplicationDidEnterBackgroundNotification + object:nil + userInfo:nil]; + id mockEngine = OCMPartialMock(engine); + [[NSNotificationCenter defaultCenter] postNotification:sceneNotification]; + [[NSNotificationCenter defaultCenter] postNotification:applicationNotification]; +#if APPLICATION_EXTENSION_API_ONLY + OCMVerify(times(1), [mockEngine sceneDidEnterBackground:[OCMArg any]]); +#else + OCMVerify(times(1), [mockEngine applicationDidEnterBackground:[OCMArg any]]); +#endif + XCTAssertTrue(engine.isGpuDisabled); + bool switch_value = false; + [engine shell].GetIsGpuDisabledSyncSwitch()->Execute( + fml::SyncSwitch::Handlers().SetIfTrue([&] { switch_value = true; }).SetIfFalse([&] { + switch_value = false; + })); + XCTAssertTrue(switch_value); +} + +- (void)testLifeCycleNotificationWillEnterForeground { + FlutterDartProject* project = [[FlutterDartProject alloc] init]; + FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar" project:project]; + [engine run]; + NSNotification* sceneNotification = + [NSNotification notificationWithName:UISceneWillEnterForegroundNotification + object:nil + userInfo:nil]; + NSNotification* applicationNotification = + [NSNotification notificationWithName:UIApplicationWillEnterForegroundNotification + object:nil + userInfo:nil]; + id mockEngine = OCMPartialMock(engine); + [[NSNotificationCenter defaultCenter] postNotification:sceneNotification]; + [[NSNotificationCenter defaultCenter] postNotification:applicationNotification]; +#if APPLICATION_EXTENSION_API_ONLY + OCMVerify(times(1), [mockEngine sceneWillEnterForeground:[OCMArg any]]); +#else + OCMVerify(times(1), [mockEngine applicationWillEnterForeground:[OCMArg any]]); +#endif + XCTAssertFalse(engine.isGpuDisabled); + bool switch_value = true; + [engine shell].GetIsGpuDisabledSyncSwitch()->Execute( + fml::SyncSwitch::Handlers().SetIfTrue([&] { switch_value = true; }).SetIfFalse([&] { + switch_value = false; + })); + XCTAssertFalse(switch_value); +} + @end diff --git a/shell/platform/darwin/ios/framework/Source/FlutterEngine_Test.h b/shell/platform/darwin/ios/framework/Source/FlutterEngine_Test.h index 96ffe45f67994..2605d75e83853 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterEngine_Test.h +++ b/shell/platform/darwin/ios/framework/Source/FlutterEngine_Test.h @@ -35,4 +35,9 @@ class ThreadHost; - (void)flutterTextInputView:(FlutterTextInputView*)textInputView performAction:(FlutterTextInputAction)action withClient:(int)client; +- (void)sceneWillEnterForeground:(NSNotification*)notification API_AVAILABLE(ios(13.0)); +- (void)sceneDidEnterBackground:(NSNotification*)notification API_AVAILABLE(ios(13.0)); +- (void)applicationWillEnterForeground:(NSNotification*)notification; +- (void)applicationDidEnterBackground:(NSNotification*)notification; + @end diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformPlugin.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformPlugin.mm index 1785923100c89..e360381053644 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformPlugin.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformPlugin.mm @@ -269,13 +269,23 @@ - (void)popSystemNavigator:(BOOL)isAnimated { // It's also possible in an Add2App scenario that the FlutterViewController was presented // outside the context of a UINavigationController, and still wants to be popped. - UIViewController* engineViewController = [_engine.get() viewController]; + FlutterViewController* engineViewController = [_engine.get() viewController]; UINavigationController* navigationController = [engineViewController navigationController]; if (navigationController) { [navigationController popViewControllerAnimated:isAnimated]; } else { - UIViewController* rootViewController = - [UIApplication sharedApplication].keyWindow.rootViewController; + UIViewController* rootViewController = nil; +#if APPLICATION_EXTENSION_API_ONLY + if (@available(iOS 15.0, *)) { + rootViewController = + [engineViewController windowSceneIfViewLoaded].keyWindow.rootViewController; + } else { + FML_LOG(WARNING) + << "rootViewController is not available in application extension prior to iOS 15.0."; + } +#else + rootViewController = [UIApplication sharedApplication].keyWindow.rootViewController; +#endif if (engineViewController != rootViewController) { [engineViewController dismissViewControllerAnimated:isAnimated completion:nil]; } diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformPluginTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformPluginTest.mm index 2c0279902745e..a50fefe335792 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformPluginTest.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformPluginTest.mm @@ -37,9 +37,8 @@ - (void)testLookUpCallInitiated { XCTestExpectation* presentExpectation = [self expectationWithDescription:@"Look Up view controller presented"]; - FlutterViewController* engineViewController = [[FlutterViewController alloc] initWithEngine:engine - nibName:nil - bundle:nil]; + FlutterViewController* engineViewController = + [[[FlutterViewController alloc] initWithEngine:engine nibName:nil bundle:nil] autorelease]; FlutterViewController* mockEngineViewController = OCMPartialMock(engineViewController); FlutterPlatformPlugin* plugin = diff --git a/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm b/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm index 0cd16905d3804..7d9c226d1d567 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm @@ -301,30 +301,15 @@ - (void)setUpNotificationCenterObservers { name:@(flutter::kOverlayStyleUpdateNotificationName) object:nil]; - [center addObserver:self - selector:@selector(applicationBecameActive:) - name:UIApplicationDidBecomeActiveNotification - object:nil]; - - [center addObserver:self - selector:@selector(applicationWillResignActive:) - name:UIApplicationWillResignActiveNotification - object:nil]; - - [center addObserver:self - selector:@selector(applicationWillTerminate:) - name:UIApplicationWillTerminateNotification - object:nil]; - - [center addObserver:self - selector:@selector(applicationDidEnterBackground:) - name:UIApplicationDidEnterBackgroundNotification - object:nil]; - - [center addObserver:self - selector:@selector(applicationWillEnterForeground:) - name:UIApplicationWillEnterForegroundNotification - object:nil]; +#if APPLICATION_EXTENSION_API_ONLY + if (@available(iOS 13.0, *)) { + [self setUpSceneLifecycleNotifications:center]; + } else { + [self setUpApplicationLifecycleNotifications:center]; + } +#else + [self setUpApplicationLifecycleNotifications:center]; +#endif [center addObserver:self selector:@selector(keyboardWillChangeFrame:) @@ -399,6 +384,60 @@ - (void)setUpNotificationCenterObservers { object:nil]; } +- (void)setUpSceneLifecycleNotifications:(NSNotificationCenter*)center API_AVAILABLE(ios(13.0)) { + [center addObserver:self + selector:@selector(sceneBecameActive:) + name:UISceneDidActivateNotification + object:nil]; + + [center addObserver:self + selector:@selector(sceneWillResignActive:) + name:UISceneWillDeactivateNotification + object:nil]; + + [center addObserver:self + selector:@selector(sceneWillDisconnect:) + name:UISceneDidDisconnectNotification + object:nil]; + + [center addObserver:self + selector:@selector(sceneDidEnterBackground:) + name:UISceneDidEnterBackgroundNotification + object:nil]; + + [center addObserver:self + selector:@selector(sceneWillEnterForeground:) + name:UISceneWillEnterForegroundNotification + object:nil]; +} + +- (void)setUpApplicationLifecycleNotifications:(NSNotificationCenter*)center { + [center addObserver:self + selector:@selector(applicationBecameActive:) + name:UIApplicationDidBecomeActiveNotification + object:nil]; + + [center addObserver:self + selector:@selector(applicationWillResignActive:) + name:UIApplicationWillResignActiveNotification + object:nil]; + + [center addObserver:self + selector:@selector(applicationWillTerminate:) + name:UIApplicationWillTerminateNotification + object:nil]; + + [center addObserver:self + selector:@selector(applicationDidEnterBackground:) + name:UIApplicationDidEnterBackgroundNotification + object:nil]; + + [center addObserver:self + selector:@selector(applicationWillEnterForeground:) + name:UIApplicationWillEnterForegroundNotification + object:nil]; +} + - (void)setInitialRoute:(NSString*)route { [[_engine.get() navigationChannel] invokeMethod:@"setInitialRoute" arguments:route]; } @@ -827,7 +866,16 @@ - (void)viewDidAppear:(BOOL)animated { if ([_engine.get() viewController] == self) { [self onUserSettingsChanged:nil]; [self onAccessibilityStatusChanged:nil]; - if (UIApplication.sharedApplication.applicationState == UIApplicationStateActive) { + BOOL stateIsActive = YES; +#if APPLICATION_EXTENSION_API_ONLY + if (@available(iOS 13.0, *)) { + stateIsActive = + self.windowSceneIfViewLoaded.activationState == UISceneActivationStateForegroundActive; + } +#else + stateIsActive = UIApplication.sharedApplication.applicationState == UIApplicationStateActive; +#endif + if (stateIsActive) { [[_engine.get() lifecycleChannel] sendMessage:@"AppLifecycleState.resumed"]; } } @@ -950,6 +998,57 @@ - (void)dealloc { - (void)applicationBecameActive:(NSNotification*)notification { TRACE_EVENT0("flutter", "applicationBecameActive"); + [self appOrSceneBecameActive]; +} + +- (void)applicationWillResignActive:(NSNotification*)notification { + TRACE_EVENT0("flutter", "applicationWillResignActive"); + [self appOrSceneWillResignActive]; +} + +- (void)applicationWillTerminate:(NSNotification*)notification { + [self appOrSceneWillTerminate]; +} + +- (void)applicationDidEnterBackground:(NSNotification*)notification { + TRACE_EVENT0("flutter", "applicationDidEnterBackground"); + [self appOrSceneDidEnterBackground]; +} + +- (void)applicationWillEnterForeground:(NSNotification*)notification { + TRACE_EVENT0("flutter", "applicationWillEnterForeground"); + [self appOrSceneWillEnterForeground]; +} + +#pragma mark - Scene lifecycle notifications + +- (void)sceneBecameActive:(NSNotification*)notification API_AVAILABLE(ios(13.0)) { + TRACE_EVENT0("flutter", "sceneBecameActive"); + [self appOrSceneBecameActive]; +} + +- (void)sceneWillResignActive:(NSNotification*)notification API_AVAILABLE(ios(13.0)) { + TRACE_EVENT0("flutter", "sceneWillResignActive"); + [self appOrSceneWillResignActive]; +} + +- (void)sceneWillDisconnect:(NSNotification*)notification API_AVAILABLE(ios(13.0)) { + [self appOrSceneWillTerminate]; +} + +- (void)sceneDidEnterBackground:(NSNotification*)notification API_AVAILABLE(ios(13.0)) { + TRACE_EVENT0("flutter", "sceneDidEnterBackground"); + [self appOrSceneDidEnterBackground]; +} + +- (void)sceneWillEnterForeground:(NSNotification*)notification API_AVAILABLE(ios(13.0)) { + TRACE_EVENT0("flutter", "sceneWillEnterForeground"); + [self appOrSceneWillEnterForeground]; +} + +#pragma mark - Lifecycle shared + +- (void)appOrSceneBecameActive { self.isKeyboardInOrTransitioningFromBackground = NO; if (_viewportMetrics.physical_width) { [self surfaceUpdated:YES]; @@ -957,25 +1056,22 @@ - (void)applicationBecameActive:(NSNotification*)notification { [self goToApplicationLifecycle:@"AppLifecycleState.resumed"]; } -- (void)applicationWillResignActive:(NSNotification*)notification { - TRACE_EVENT0("flutter", "applicationWillResignActive"); +- (void)appOrSceneWillResignActive { [self goToApplicationLifecycle:@"AppLifecycleState.inactive"]; } -- (void)applicationWillTerminate:(NSNotification*)notification { +- (void)appOrSceneWillTerminate { [self goToApplicationLifecycle:@"AppLifecycleState.detached"]; [self.engine destroyContext]; } -- (void)applicationDidEnterBackground:(NSNotification*)notification { - TRACE_EVENT0("flutter", "applicationDidEnterBackground"); +- (void)appOrSceneDidEnterBackground { self.isKeyboardInOrTransitioningFromBackground = YES; [self surfaceUpdated:NO]; [self goToApplicationLifecycle:@"AppLifecycleState.paused"]; } -- (void)applicationWillEnterForeground:(NSNotification*)notification { - TRACE_EVENT0("flutter", "applicationWillEnterForeground"); +- (void)appOrSceneWillEnterForeground { [self goToApplicationLifecycle:@"AppLifecycleState.inactive"]; } @@ -1298,15 +1394,23 @@ - (void)viewDidLayoutSubviews { [self setViewportMetricsPaddings]; [self updateViewportMetricsIfNeeded]; - // There is no guarantee that UIKit will layout subviews when the application is active. Creating - // the surface when inactive will cause GPU accesses from the background. Only wait for the first - // frame to render when the application is actually active. - bool applicationIsActive = + // There is no guarantee that UIKit will layout subviews when the application/scene is active. + // Creating the surface when inactive will cause GPU accesses from the background. Only wait for + // the first frame to render when the application/scene is actually active. + bool applicationOrSceneIsActive = YES; +#if APPLICATION_EXTENSION_API_ONLY + if (@available(iOS 13.0, *)) { + applicationOrSceneIsActive = + self.windowSceneIfViewLoaded.activationState == UISceneActivationStateForegroundActive; + } +#else + applicationOrSceneIsActive = [UIApplication sharedApplication].applicationState == UIApplicationStateActive; +#endif // This must run after updateViewportMetrics so that the surface creation tasks are queued after // the viewport metrics update tasks. - if (firstViewBoundsUpdate && applicationIsActive && _engine) { + if (firstViewBoundsUpdate && applicationOrSceneIsActive && _engine) { [self surfaceUpdated:YES]; flutter::Shell& shell = [_engine.get() shell]; @@ -1847,28 +1951,39 @@ - (void)onOrientationPreferencesUpdated:(NSNotification*)notification { }); } +- (void)requestGeometryUpdateForWindowScenes:(NSSet*)windowScenes + API_AVAILABLE(ios(16.0)) { + for (UIScene* windowScene in windowScenes) { + FML_DCHECK([windowScene isKindOfClass:[UIWindowScene class]]); + UIWindowSceneGeometryPreferencesIOS* preference = [[[UIWindowSceneGeometryPreferencesIOS alloc] + initWithInterfaceOrientations:_orientationPreferences] autorelease]; + [(UIWindowScene*)windowScene + requestGeometryUpdateWithPreferences:preference + errorHandler:^(NSError* error) { + os_log_error(OS_LOG_DEFAULT, + "Failed to change device orientation: %@", error); + }]; + [self setNeedsUpdateOfSupportedInterfaceOrientations]; + } +} + - (void)performOrientationUpdate:(UIInterfaceOrientationMask)new_preferences { if (new_preferences != _orientationPreferences) { _orientationPreferences = new_preferences; if (@available(iOS 16.0, *)) { - for (UIScene* scene in UIApplication.sharedApplication.connectedScenes) { - if (![scene isKindOfClass:[UIWindowScene class]]) { - continue; - } - UIWindowScene* windowScene = (UIWindowScene*)scene; - UIWindowSceneGeometryPreferencesIOS* preference = - [[[UIWindowSceneGeometryPreferencesIOS alloc] - initWithInterfaceOrientations:_orientationPreferences] autorelease]; - [windowScene - requestGeometryUpdateWithPreferences:preference - errorHandler:^(NSError* error) { - os_log_error(OS_LOG_DEFAULT, - "Failed to change device orientation: %@", - error); - }]; - [self setNeedsUpdateOfSupportedInterfaceOrientations]; - } + NSSet* scenes = +#if APPLICATION_EXTENSION_API_ONLY + self.windowSceneIfViewLoaded ? [NSSet setWithObject:self.windowSceneIfViewLoaded] + : [NSSet set]; +#else + [UIApplication.sharedApplication.connectedScenes + filteredSetUsingPredicate:[NSPredicate predicateWithBlock:^BOOL( + id scene, NSDictionary* bindings) { + return [scene isKindOfClass:[UIWindowScene class]]; + }]]; +#endif + [self requestGeometryUpdateForWindowScenes:scenes]; } else { UIInterfaceOrientationMask currentInterfaceOrientation; if (@available(iOS 13.0, *)) { diff --git a/shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.mm index 63e36587002c8..c32348f5f6396 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.mm @@ -147,6 +147,16 @@ - (void)addInternalPlugins; - (flutter::PointerData)generatePointerDataForFake; - (void)sharedSetupWithProject:(nullable FlutterDartProject*)project initialRoute:(nullable NSString*)initialRoute; +- (void)applicationBecameActive:(NSNotification*)notification; +- (void)applicationWillResignActive:(NSNotification*)notification; +- (void)applicationWillTerminate:(NSNotification*)notification; +- (void)applicationDidEnterBackground:(NSNotification*)notification; +- (void)applicationWillEnterForeground:(NSNotification*)notification; +- (void)sceneBecameActive:(NSNotification*)notification API_AVAILABLE(ios(13.0)); +- (void)sceneWillResignActive:(NSNotification*)notification API_AVAILABLE(ios(13.0)); +- (void)sceneWillDisconnect:(NSNotification*)notification API_AVAILABLE(ios(13.0)); +- (void)sceneDidEnterBackground:(NSNotification*)notification API_AVAILABLE(ios(13.0)); +- (void)sceneWillEnterForeground:(NSNotification*)notification API_AVAILABLE(ios(13.0)); @end @interface FlutterViewControllerTest : XCTestCase @@ -1492,13 +1502,17 @@ - (void)orientationTestWithOrientationUpdate:(UIInterfaceOrientationMask)mask id mockApplication = OCMClassMock([UIApplication class]); id mockWindowScene; id deviceMock; + id mockVC; __block __weak id weakPreferences; @autoreleasepool { FlutterViewController* realVC = [[FlutterViewController alloc] initWithEngine:self.mockEngine nibName:nil bundle:nil]; + if (@available(iOS 16.0, *)) { mockWindowScene = OCMClassMock([UIWindowScene class]); + mockVC = OCMPartialMock(realVC); + OCMStub([mockVC windowSceneIfViewLoaded]).andReturn(mockWindowScene); if (realVC.supportedInterfaceOrientations == mask) { OCMReject([mockWindowScene requestGeometryUpdateWithPreferences:[OCMArg any] errorHandler:[OCMArg any]]); @@ -1524,7 +1538,9 @@ - (void)orientationTestWithOrientationUpdate:(UIInterfaceOrientationMask)mask OCMExpect([deviceMock setValue:@(resultingOrientation) forKey:@"orientation"]); } if (@available(iOS 13.0, *)) { - mockWindowScene = OCMPartialMock(realVC.view.window.windowScene); + mockWindowScene = OCMClassMock([UIWindowScene class]); + mockVC = OCMPartialMock(realVC); + OCMStub([mockVC windowSceneIfViewLoaded]).andReturn(mockWindowScene); OCMStub(((UIWindowScene*)mockWindowScene).interfaceOrientation) .andReturn(currentOrientation); } else { @@ -1815,4 +1831,147 @@ - (void)testSplashScreenViewCanSetNil { [flutterViewController setSplashScreenView:nil]; } +- (void)testLifeCycleNotificationBecameActive { + FlutterEngine* engine = [[FlutterEngine alloc] init]; + [engine runWithEntrypoint:nil]; + FlutterViewController* flutterViewController = + [[FlutterViewController alloc] initWithEngine:engine nibName:nil bundle:nil]; + UIWindow* window = [[UIWindow alloc] init]; + [window addSubview:flutterViewController.view]; + flutterViewController.view.bounds = CGRectMake(0, 0, 100, 100); + [flutterViewController viewDidLayoutSubviews]; + NSNotification* sceneNotification = + [NSNotification notificationWithName:UISceneDidActivateNotification object:nil userInfo:nil]; + NSNotification* applicationNotification = + [NSNotification notificationWithName:UIApplicationDidBecomeActiveNotification + object:nil + userInfo:nil]; + id mockVC = OCMPartialMock(flutterViewController); + [[NSNotificationCenter defaultCenter] postNotification:sceneNotification]; + [[NSNotificationCenter defaultCenter] postNotification:applicationNotification]; +#if APPLICATION_EXTENSION_API_ONLY + OCMVerify([mockVC sceneBecameActive:[OCMArg any]]); + OCMReject([mockVC applicationBecameActive:[OCMArg any]]); +#else + OCMReject([mockVC sceneBecameActive:[OCMArg any]]); + OCMVerify([mockVC applicationBecameActive:[OCMArg any]]); +#endif + XCTAssertFalse(flutterViewController.isKeyboardInOrTransitioningFromBackground); + OCMVerify([mockVC surfaceUpdated:YES]); + OCMVerify([mockVC goToApplicationLifecycle:@"AppLifecycleState.resumed"]); + [flutterViewController deregisterNotifications]; +} + +- (void)testLifeCycleNotificationWillResignActive { + FlutterEngine* engine = [[FlutterEngine alloc] init]; + [engine runWithEntrypoint:nil]; + FlutterViewController* flutterViewController = + [[FlutterViewController alloc] initWithEngine:engine nibName:nil bundle:nil]; + NSNotification* sceneNotification = + [NSNotification notificationWithName:UISceneWillDeactivateNotification + object:nil + userInfo:nil]; + NSNotification* applicationNotification = + [NSNotification notificationWithName:UIApplicationWillResignActiveNotification + object:nil + userInfo:nil]; + id mockVC = OCMPartialMock(flutterViewController); + [[NSNotificationCenter defaultCenter] postNotification:sceneNotification]; + [[NSNotificationCenter defaultCenter] postNotification:applicationNotification]; +#if APPLICATION_EXTENSION_API_ONLY + OCMVerify([mockVC sceneWillResignActive:[OCMArg any]]); + OCMReject([mockVC applicationWillResignActive:[OCMArg any]]); +#else + OCMReject([mockVC sceneWillResignActive:[OCMArg any]]); + OCMVerify([mockVC applicationWillResignActive:[OCMArg any]]); +#endif + OCMVerify([mockVC goToApplicationLifecycle:@"AppLifecycleState.inactive"]); + [flutterViewController deregisterNotifications]; +} + +- (void)testLifeCycleNotificationWillTerminate { + FlutterEngine* engine = [[FlutterEngine alloc] init]; + [engine runWithEntrypoint:nil]; + FlutterViewController* flutterViewController = + [[FlutterViewController alloc] initWithEngine:engine nibName:nil bundle:nil]; + NSNotification* sceneNotification = + [NSNotification notificationWithName:UISceneDidDisconnectNotification + object:nil + userInfo:nil]; + NSNotification* applicationNotification = + [NSNotification notificationWithName:UIApplicationWillTerminateNotification + object:nil + userInfo:nil]; + id mockVC = OCMPartialMock(flutterViewController); + [[NSNotificationCenter defaultCenter] postNotification:sceneNotification]; + [[NSNotificationCenter defaultCenter] postNotification:applicationNotification]; + id mockEngine = OCMPartialMock(engine); +#if APPLICATION_EXTENSION_API_ONLY + OCMVerify([mockVC sceneWillDisconnect:[OCMArg any]]); + OCMReject([mockVC applicationWillTerminate:[OCMArg any]]); +#else + OCMReject([mockVC sceneWillDisconnect:[OCMArg any]]); + OCMVerify([mockVC applicationWillTerminate:[OCMArg any]]); +#endif + OCMVerify([mockVC goToApplicationLifecycle:@"AppLifecycleState.detached"]); + OCMVerify([mockEngine destroyContext]); + [flutterViewController deregisterNotifications]; +} + +- (void)testLifeCycleNotificationDidEnterBackground { + FlutterEngine* engine = [[FlutterEngine alloc] init]; + [engine runWithEntrypoint:nil]; + FlutterViewController* flutterViewController = + [[FlutterViewController alloc] initWithEngine:engine nibName:nil bundle:nil]; + NSNotification* sceneNotification = + [NSNotification notificationWithName:UISceneDidEnterBackgroundNotification + object:nil + userInfo:nil]; + NSNotification* applicationNotification = + [NSNotification notificationWithName:UIApplicationDidEnterBackgroundNotification + object:nil + userInfo:nil]; + id mockVC = OCMPartialMock(flutterViewController); + [[NSNotificationCenter defaultCenter] postNotification:sceneNotification]; + [[NSNotificationCenter defaultCenter] postNotification:applicationNotification]; +#if APPLICATION_EXTENSION_API_ONLY + OCMVerify([mockVC sceneDidEnterBackground:[OCMArg any]]); + OCMReject([mockVC applicationDidEnterBackground:[OCMArg any]]); +#else + OCMReject([mockVC sceneDidEnterBackground:[OCMArg any]]); + OCMVerify([mockVC applicationDidEnterBackground:[OCMArg any]]); +#endif + XCTAssertFalse(flutterViewController.isKeyboardInOrTransitioningFromBackground); + OCMVerify([mockVC surfaceUpdated:YES]); + OCMVerify([mockVC goToApplicationLifecycle:@"AppLifecycleState.paused"]); + [flutterViewController deregisterNotifications]; +} + +- (void)testLifeCycleNotificationWillEnterForeground { + FlutterEngine* engine = [[FlutterEngine alloc] init]; + [engine runWithEntrypoint:nil]; + FlutterViewController* flutterViewController = + [[FlutterViewController alloc] initWithEngine:engine nibName:nil bundle:nil]; + NSNotification* sceneNotification = + [NSNotification notificationWithName:UISceneWillEnterForegroundNotification + object:nil + userInfo:nil]; + NSNotification* applicationNotification = + [NSNotification notificationWithName:UIApplicationWillEnterForegroundNotification + object:nil + userInfo:nil]; + id mockVC = OCMPartialMock(flutterViewController); + [[NSNotificationCenter defaultCenter] postNotification:sceneNotification]; + [[NSNotificationCenter defaultCenter] postNotification:applicationNotification]; +#if APPLICATION_EXTENSION_API_ONLY + OCMVerify([mockVC sceneWillEnterForeground:[OCMArg any]]); + OCMReject([mockVC applicationWillEnterForeground:[OCMArg any]]); +#else + OCMReject([mockVC sceneWillEnterForeground:[OCMArg any]]); + OCMVerify([mockVC applicationWillEnterForeground:[OCMArg any]]); +#endif + OCMVerify([mockVC goToApplicationLifecycle:@"AppLifecycleState.inactive"]); + [flutterViewController deregisterNotifications]; +} + @end diff --git a/tools/gn b/tools/gn index b7227f3461fce..4ddaf65919e24 100755 --- a/tools/gn +++ b/tools/gn @@ -67,6 +67,9 @@ def get_out_dir(args): if args.target_dir != '': target_dir = [args.target_dir] + if args.darwin_extension_safe: + target_dir.append('extension_safe') + return os.path.join(args.out_dir, 'out', '_'.join(target_dir)) @@ -652,6 +655,9 @@ def to_gn_args(args): gn_args['angle_vulkan_tools_dir' ] = '//third_party/vulkan-deps/vulkan-tools/src' + if args.darwin_extension_safe: + gn_args['darwin_extension_safe'] = True + return gn_args @@ -1142,6 +1148,13 @@ def parse_args(args): 'format in the build directory.' ) + parser.add_argument( + '--darwin-extension-safe', + default=False, + action='store_true', + help='Whether the produced Flutter.framework is app extension safe. Only for iOS.' + ) + # Verbose output. parser.add_argument('--verbose', default=False, action='store_true') From 03d6b06a05307d74c258e6fee3199b233ba6a060 Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Tue, 1 Aug 2023 10:29:50 -0700 Subject: [PATCH 2/2] fix test --- .../ios/framework/Source/FlutterViewControllerTest.mm | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.mm index c32348f5f6396..8723a418d50ea 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.mm @@ -1903,9 +1903,10 @@ - (void)testLifeCycleNotificationWillTerminate { object:nil userInfo:nil]; id mockVC = OCMPartialMock(flutterViewController); + id mockEngine = OCMPartialMock(engine); + OCMStub([mockVC engine]).andReturn(mockEngine); [[NSNotificationCenter defaultCenter] postNotification:sceneNotification]; [[NSNotificationCenter defaultCenter] postNotification:applicationNotification]; - id mockEngine = OCMPartialMock(engine); #if APPLICATION_EXTENSION_API_ONLY OCMVerify([mockVC sceneWillDisconnect:[OCMArg any]]); OCMReject([mockVC applicationWillTerminate:[OCMArg any]]); @@ -1941,8 +1942,8 @@ - (void)testLifeCycleNotificationDidEnterBackground { OCMReject([mockVC sceneDidEnterBackground:[OCMArg any]]); OCMVerify([mockVC applicationDidEnterBackground:[OCMArg any]]); #endif - XCTAssertFalse(flutterViewController.isKeyboardInOrTransitioningFromBackground); - OCMVerify([mockVC surfaceUpdated:YES]); + XCTAssertTrue(flutterViewController.isKeyboardInOrTransitioningFromBackground); + OCMVerify([mockVC surfaceUpdated:NO]); OCMVerify([mockVC goToApplicationLifecycle:@"AppLifecycleState.paused"]); [flutterViewController deregisterNotifications]; }