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..8723a418d50ea 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,148 @@ - (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); + id mockEngine = OCMPartialMock(engine); + OCMStub([mockVC engine]).andReturn(mockEngine); + [[NSNotificationCenter defaultCenter] postNotification:sceneNotification]; + [[NSNotificationCenter defaultCenter] postNotification:applicationNotification]; +#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 + XCTAssertTrue(flutterViewController.isKeyboardInOrTransitioningFromBackground); + OCMVerify([mockVC surfaceUpdated:NO]); + 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')