diff --git a/common/config.gni b/common/config.gni index 142701b8542ff..e35ff9830bebc 100644 --- a/common/config.gni +++ b/common/config.gni @@ -57,6 +57,7 @@ if (is_ios || is_mac) { flutter_cflags_objc = [ "-Werror=overriding-method-mismatch", "-Werror=undeclared-selector", + "-fapplication-extension", ] flutter_cflags_objcc = flutter_cflags_objc flutter_cflags_objc_arc = flutter_cflags_objc + [ "-fobjc-arc" ] diff --git a/shell/platform/darwin/ios/BUILD.gn b/shell/platform/darwin/ios/BUILD.gn index 7a30a1fb16d03..0455663d318fb 100644 --- a/shell/platform/darwin/ios/BUILD.gn +++ b/shell/platform/darwin/ios/BUILD.gn @@ -44,7 +44,10 @@ source_set("flutter_framework_source_arc") { cflags_objc = flutter_cflags_objc_arc cflags_objcc = flutter_cflags_objcc_arc - defines = [ "FLUTTER_FRAMEWORK=1" ] + defines = [ + "FLUTTER_FRAMEWORK=1", + "APPLICATION_EXTENSION_API_ONLY=1", + ] allow_circular_includes_from = [ ":flutter_framework_source" ] deps = [ ":flutter_framework_source", @@ -153,7 +156,10 @@ source_set("flutter_framework_source") { sources += _flutter_framework_headers - defines = [ "FLUTTER_FRAMEWORK=1" ] + defines = [ + "FLUTTER_FRAMEWORK=1", + "APPLICATION_EXTENSION_API_ONLY=1", + ] if (shell_enable_metal) { sources += [ @@ -320,7 +326,10 @@ shared_library("create_flutter_framework_dylib") { output_name = "Flutter" - ldflags = [ "-Wl,-install_name,@rpath/Flutter.framework/Flutter" ] + ldflags = [ + "-Wl,-install_name,@rpath/Flutter.framework/Flutter", + "-fapplication-extension", + ] public = _flutter_framework_headers diff --git a/shell/platform/darwin/ios/framework/Headers/FlutterAppDelegate.h b/shell/platform/darwin/ios/framework/Headers/FlutterAppDelegate.h index ed7589a53c4ca..d4990a09532ae 100644 --- a/shell/platform/darwin/ios/framework/Headers/FlutterAppDelegate.h +++ b/shell/platform/darwin/ios/framework/Headers/FlutterAppDelegate.h @@ -24,6 +24,7 @@ * code as necessary from FlutterAppDelegate.mm. */ FLUTTER_DARWIN_EXPORT +NS_EXTENSION_UNAVAILABLE_IOS("Disallowed in app extensions") @interface FlutterAppDelegate : UIResponder diff --git a/shell/platform/darwin/ios/framework/Headers/FlutterPlugin.h b/shell/platform/darwin/ios/framework/Headers/FlutterPlugin.h index e86ce58344b3d..11fcf46d7e273 100644 --- a/shell/platform/darwin/ios/framework/Headers/FlutterPlugin.h +++ b/shell/platform/darwin/ios/framework/Headers/FlutterPlugin.h @@ -353,7 +353,8 @@ typedef enum { * * @param delegate The receiving object, such as the plugin's main class. */ -- (void)addApplicationDelegate:(NSObject*)delegate; +- (void)addApplicationDelegate:(NSObject*)delegate + NS_EXTENSION_UNAVAILABLE_IOS("Disallowed in plugins used in app extensions"); /** * Returns the file name for the given asset. diff --git a/shell/platform/darwin/ios/framework/Headers/FlutterPluginAppLifeCycleDelegate.h b/shell/platform/darwin/ios/framework/Headers/FlutterPluginAppLifeCycleDelegate.h index 883dccb8673f3..5d1098b73ff95 100644 --- a/shell/platform/darwin/ios/framework/Headers/FlutterPluginAppLifeCycleDelegate.h +++ b/shell/platform/darwin/ios/framework/Headers/FlutterPluginAppLifeCycleDelegate.h @@ -13,6 +13,7 @@ NS_ASSUME_NONNULL_BEGIN * Propagates `UIAppDelegate` callbacks to registered plugins. */ FLUTTER_DARWIN_EXPORT +NS_EXTENSION_UNAVAILABLE_IOS("Disallowed in plugins used in app extensions") @interface FlutterPluginAppLifeCycleDelegate : NSObject /** diff --git a/shell/platform/darwin/ios/framework/Source/FlutterDartProject.mm b/shell/platform/darwin/ios/framework/Source/FlutterDartProject.mm index 3c6f8550f4887..941709f11d36f 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterDartProject.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterDartProject.mm @@ -76,6 +76,39 @@ return [NSBundle bundleWithIdentifier:bundleID]; } +NS_INLINE NSBundle* FLTFrameworkBundle() { + NSBundle* mainBundle = [NSBundle mainBundle]; + NSBundle* bundle = FLTFrameworkBundleWithIdentifier([FlutterDartProject defaultBundleIdentifier]); + + // App extension bundle is in Runner.app/PlugIns/Extension.appex. + if ([mainBundle.bundleURL.pathExtension isEqualToString:@"appex"]) { + // Up two levels. + NSBundle* appBundle = + [NSBundle bundleWithURL:mainBundle.bundleURL.URLByDeletingLastPathComponent + .URLByDeletingLastPathComponent]; + bundle = FLTFrameworkBundleInternal([FlutterDartProject defaultBundleIdentifier], + appBundle.privateFrameworksURL); + } + + if (bundle == nil) { + bundle = mainBundle; + } + + return bundle; +} + +NS_INLINE NSURL* FLTAssetsFromBundle(NSBundle* bundle) { + NSString* assetsPathFromInfoPlist = [bundle objectForInfoDictionaryKey:@"FLTAssetsPath"]; + NSString* flutterAssetsName = + assetsPathFromInfoPlist != nil ? assetsPathFromInfoPlist : @"flutter_assets"; + NSURL* assets = [bundle URLForResource:flutterAssetsName withExtension:nil]; + + if ([assets checkResourceIsReachableAndReturnError:NULL]) { + return assets; + } + return nil; +} + flutter::Settings FLTDefaultSettingsForBundle(NSBundle* bundle) { auto command_line = flutter::CommandLineFromNSProcessInfo(); @@ -90,10 +123,7 @@ bool hasExplicitBundle = bundle != nil; if (bundle == nil) { - bundle = FLTFrameworkBundleWithIdentifier([FlutterDartProject defaultBundleIdentifier]); - } - if (bundle == nil) { - bundle = mainBundle; + bundle = FLTFrameworkBundle(); } auto settings = flutter::SettingsFromCommandLine(command_line); @@ -166,29 +196,24 @@ // Checks to see if the flutter assets directory is already present. if (settings.assets_path.empty()) { - NSString* assetsName = [FlutterDartProject flutterAssetsName:bundle]; - NSString* assetsPath = [bundle pathForResource:assetsName ofType:@""]; + NSURL* assetsURL = FLTAssetsFromBundle(bundle); - if (assetsPath.length == 0) { - assetsPath = [mainBundle pathForResource:assetsName ofType:@""]; - } - - if (assetsPath.length == 0) { - NSLog(@"Failed to find assets path for \"%@\"", assetsName); + if (assetsURL == nil) { + NSLog(@"Failed to find assets path for \"%@\"", bundle); } else { - settings.assets_path = assetsPath.UTF8String; + settings.assets_path = assetsURL.path.UTF8String; // Check if there is an application kernel snapshot in the assets directory we could // potentially use. Looking for the snapshot makes sense only if we have a VM that can use // it. if (!flutter::DartVM::IsRunningPrecompiledCode()) { NSURL* applicationKernelSnapshotURL = - [NSURL URLWithString:@(kApplicationKernelSnapshotFileName) - relativeToURL:[NSURL fileURLWithPath:assetsPath]]; - if ([[NSFileManager defaultManager] fileExistsAtPath:applicationKernelSnapshotURL.path]) { + [assetsURL URLByAppendingPathComponent:@(kApplicationKernelSnapshotFileName)]; + NSError* error; + if ([applicationKernelSnapshotURL checkResourceIsReachableAndReturnError:&error]) { settings.application_kernel_asset = applicationKernelSnapshotURL.path.UTF8String; } else { - NSLog(@"Failed to find snapshot: %@", applicationKernelSnapshotURL.path); + NSLog(@"Failed to find snapshot at %@: %@", applicationKernelSnapshotURL.path, error); } } } @@ -361,11 +386,9 @@ - (instancetype)initWithSettings:(const flutter::Settings&)settings { + (NSString*)flutterAssetsName:(NSBundle*)bundle { if (bundle == nil) { - bundle = FLTFrameworkBundleWithIdentifier([FlutterDartProject defaultBundleIdentifier]); - } - if (bundle == nil) { - bundle = [NSBundle mainBundle]; + bundle = FLTFrameworkBundle(); } + NSString* flutterAssetsName = [bundle objectForInfoDictionaryKey:@"FLTAssetsPath"]; if (flutterAssetsName == nil) { flutterAssetsName = @"Frameworks/App.framework/flutter_assets"; diff --git a/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm b/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm index ff94acaa2797b..10b3667b317ef 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm @@ -849,7 +849,11 @@ - (BOOL)createShell:(NSString*)entrypoint ); _isGpuDisabled = +#if APPLICATION_EXTENSION_API_ONLY + NO; +#else [UIApplication sharedApplication].applicationState == UIApplicationStateBackground; +#endif // Create the shell. This is a blocking operation. std::unique_ptr shell = flutter::Shell::Create( /*platform_data=*/platformData, @@ -1433,7 +1437,7 @@ - (void)addMethodCallDelegate:(NSObject*)delegate }]; } -- (void)addApplicationDelegate:(NSObject*)delegate { +- (void)addApplicationDelegate:(NSObject*)delegate NS_EXTENSION_UNAVAILABLE_IOS("") { id appDelegate = [[UIApplication sharedApplication] delegate]; if ([appDelegate conformsToProtocol:@protocol(FlutterAppLifeCycleProvider)]) { id lifeCycleProvider = diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformPlugin.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformPlugin.mm index 3e81a39f2c779..4697ab344730a 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformPlugin.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformPlugin.mm @@ -158,6 +158,7 @@ - (void)setSystemChromeApplicationSwitcherDescription:(NSDictionary*)object { } - (void)setSystemChromeEnabledSystemUIOverlays:(NSArray*)overlays { +#if !APPLICATION_EXTENSION_API_ONLY // Checks if the top status bar should be visible. This platform ignores all // other overlays @@ -175,9 +176,11 @@ - (void)setSystemChromeEnabledSystemUIOverlays:(NSArray*)overlays { postNotificationName:FlutterViewControllerHideHomeIndicator object:nil]; } +#endif } - (void)setSystemChromeEnabledSystemUIMode:(NSString*)mode { +#if !APPLICATION_EXTENSION_API_ONLY // Checks if the top status bar should be visible, reflected by edge to edge setting. This // platform ignores all other system ui modes. @@ -195,6 +198,7 @@ - (void)setSystemChromeEnabledSystemUIMode:(NSString*)mode { postNotificationName:FlutterViewControllerHideHomeIndicator object:nil]; } +#endif } - (void)restoreSystemChromeSystemUIOverlays { @@ -231,9 +235,11 @@ - (void)setSystemChromeSystemUIOverlayStyle:(NSDictionary*)message { object:nil userInfo:@{@(kOverlayStyleUpdateNotificationKey) : @(statusBarStyle)}]; } else { +#if !APPLICATION_EXTENSION_API_ONLY // Note: -[UIApplication setStatusBarStyle] is deprecated in iOS9 // in favor of delegating to the view controller [[UIApplication sharedApplication] setStatusBarStyle:statusBarStyle]; +#endif } } @@ -249,11 +255,15 @@ - (void)popSystemNavigator:(BOOL)isAnimated { if (navigationController) { [navigationController popViewControllerAnimated:isAnimated]; } else { +#if !APPLICATION_EXTENSION_API_ONLY UIViewController* rootViewController = [UIApplication sharedApplication].keyWindow.rootViewController; if (engineViewController != rootViewController) { +#endif [engineViewController dismissViewControllerAnimated:isAnimated completion:nil]; +#if !APPLICATION_EXTENSION_API_ONLY } +#endif } } diff --git a/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm b/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm index 77742b0114c9e..9e883fa2b6ad4 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm @@ -814,9 +814,14 @@ - (void)viewDidAppear:(BOOL)animated { if ([_engine.get() viewController] == self) { [self onUserSettingsChanged:nil]; [self onAccessibilityStatusChanged:nil]; + +#if !APPLICATION_EXTENSION_API_ONLY if (UIApplication.sharedApplication.applicationState == UIApplicationStateActive) { +#endif [[_engine.get() lifecycleChannel] sendMessage:@"AppLifecycleState.resumed"]; +#if !APPLICATION_EXTENSION_API_ONLY } +#endif } [super viewDidAppear:animated]; } @@ -1304,8 +1309,13 @@ - (void)viewDidLayoutSubviews { // 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 = + BOOL applicationIsActive = + +#if APPLICATION_EXTENSION_API_ONLY + YES; +#else [UIApplication sharedApplication].applicationState == UIApplicationStateActive; +#endif // This must run after updateViewportMetrics so that the surface creation tasks are queued after // the viewport metrics update tasks. @@ -1808,24 +1818,19 @@ - (void)performOrientationUpdate:(UIInterfaceOrientationMask)new_preferences { _orientationPreferences = new_preferences; if (@available(iOS 16.0, *)) { +#if APPLICATION_EXTENSION_API_ONLY + UIWindowScene* windowScene = self.viewIfLoaded.window.windowScene; + [self performOrientationUpdateOnWindowScene:windowScene]; +#else for (UIScene* scene in UIApplication.sharedApplication.connectedScenes) { if (![scene isKindOfClass:[UIWindowScene class]]) { continue; } - UIWindowScene* windowScene = (UIWindowScene*)scene; - UIWindowSceneGeometryPreferencesIOS* preference = - [[UIWindowSceneGeometryPreferencesIOS alloc] - initWithInterfaceOrientations:_orientationPreferences]; - [windowScene - requestGeometryUpdateWithPreferences:preference - errorHandler:^(NSError* error) { - os_log_error(OS_LOG_DEFAULT, - "Failed to change device orientation: %@", - error); - }]; - [self setNeedsUpdateOfSupportedInterfaceOrientations]; + [self performOrientationUpdateOnWindowScene:(UIWindowScene*)scene]; } +#endif } else { +#if !APPLICATION_EXTENSION_API_ONLY UIInterfaceOrientationMask currentInterfaceOrientation = 1 << [[UIApplication sharedApplication] statusBarOrientation]; if (!(_orientationPreferences & currentInterfaceOrientation)) { @@ -1848,10 +1853,28 @@ - (void)performOrientationUpdate:(UIInterfaceOrientationMask)new_preferences { forKey:@"orientation"]; } } +#endif } } } +- (void)performOrientationUpdateOnWindowScene:(UIWindowScene*)windowScene API_AVAILABLE(ios(16.0)) { + if (windowScene == nil) { + return; + } + + UIWindowSceneGeometryPreferencesIOS* preference = [[UIWindowSceneGeometryPreferencesIOS alloc] + initWithInterfaceOrientations:_orientationPreferences]; + [windowScene + requestGeometryUpdateWithPreferences:preference + errorHandler:^(NSError* error) { + os_log_error(OS_LOG_DEFAULT, + "Failed to change device orientation: %@", error); + }]; + [self setNeedsUpdateOfSupportedInterfaceOrientations]; + [preference release]; +} + - (void)onHideHomeIndicatorNotification:(NSNotification*)notification { self.isHomeIndicatorHidden = YES; } @@ -1951,7 +1974,12 @@ - (void)onUserSettingsChanged:(NSNotification*)notification { } - (CGFloat)textScaleFactor { +#if APPLICATION_EXTENSION_API_ONLY + UIContentSizeCategory category = + self.mainScreenIfViewLoaded.traitCollection.preferredContentSizeCategory; +#else UIContentSizeCategory category = [UIApplication sharedApplication].preferredContentSizeCategory; +#endif // The delta is computed by approximating Apple's typography guidelines: // https://developer.apple.com/ios/human-interface-guidelines/visual-design/typography/ // diff --git a/shell/platform/darwin/macos/BUILD.gn b/shell/platform/darwin/macos/BUILD.gn index 4cbf37b8c0513..436807b17f655 100644 --- a/shell/platform/darwin/macos/BUILD.gn +++ b/shell/platform/darwin/macos/BUILD.gn @@ -133,6 +133,7 @@ source_set("flutter_framework_source") { defines = [ "FLUTTER_FRAMEWORK", "FLUTTER_ENGINE_NO_PROTOTYPES", + "APPLICATION_EXTENSION_API_ONLY=1", ] cflags_objcc = flutter_cflags_objcc_arc @@ -151,7 +152,10 @@ shared_library("flutter_framework_dylib") { visibility = [ ":*" ] output_name = "$_flutter_framework_name" - ldflags = [ "-Wl,-install_name,@rpath/$_flutter_framework_filename/$_framework_binary_subpath" ] + ldflags = [ + "-Wl,-install_name,@rpath/$_flutter_framework_filename/$_framework_binary_subpath", + "-fapplication-extension", + ] deps = [ ":flutter_framework_source" ] }