diff --git a/common/config.gni b/common/config.gni index 12a4141cb7c82..809377b5fdfd6 100644 --- a/common/config.gni +++ b/common/config.gni @@ -22,6 +22,9 @@ declare_args() { # Whether to include backtrace support. enable_backtrace = true + + # Whether to include --fapplication-extension when build iOS framework. + darwin_extension_safe = false } # feature_defines_list --------------------------------------------------------- @@ -61,10 +64,9 @@ if (is_ios || is_mac) { "-Werror=overriding-method-mismatch", "-Werror=undeclared-selector", ] - if (is_mac) { + if (darwin_extension_safe || is_mac) { flutter_cflags_objc += [ "-fapplication-extension" ] } - flutter_cflags_objcc = flutter_cflags_objc flutter_cflags_objc_arc = flutter_cflags_objc + [ "-fobjc-arc" ] flutter_cflags_objcc_arc = flutter_cflags_objc_arc diff --git a/shell/platform/darwin/ios/BUILD.gn b/shell/platform/darwin/ios/BUILD.gn index 4acf7e2bcf062..bfdb890003da1 100644 --- a/shell/platform/darwin/ios/BUILD.gn +++ b/shell/platform/darwin/ios/BUILD.gn @@ -13,6 +13,7 @@ import("//flutter/shell/platform/darwin/common/framework_shared.gni") import("//flutter/testing/testing.gni") _flutter_framework_dir = "$root_out_dir/Flutter.framework" +_flutter_xcframework_dir = "$root_out_dir/Flutter.xcframework" shell_gpu_configuration("ios_gpu_configuration") { enable_software = true @@ -44,6 +45,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 +157,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 += [ @@ -321,6 +328,10 @@ shared_library("create_flutter_framework_dylib") { ldflags = [ "-Wl,-install_name,@rpath/Flutter.framework/Flutter" ] + if (darwin_extension_safe) { + ldflags += [ "-fapplication-extension" ] + } + public = _flutter_framework_headers deps = [ @@ -411,10 +422,24 @@ copy("copy_license") { shared_library("copy_and_verify_framework_module") { framework_search_path = rebase_path("$root_out_dir") visibility = [ ":*" ] - cflags_objc = [ "-F$framework_search_path" ] + cflags_objc = [ + "-F$framework_search_path", + "-fapplication-extension", + ] + + if (darwin_extension_safe) { + ldflags = [ + "-F$framework_search_path", + "-fapplication-extension", + "-Xlinker", + "-fatal_warnings", + ] + frameworks = [ "Flutter.framework" ] + deps = [ ":copy_dylib" ] + } sources = [ "framework/Source/FlutterUmbrellaImport.m" ] - deps = [ + deps += [ ":copy_framework_headers", ":copy_framework_info_plist", ":copy_framework_module_map", @@ -439,7 +464,7 @@ group("universal_flutter_framework") { action("flutter_framework") { script = "//flutter/sky/tools/create_xcframework.py" - outputs = [ "$root_out_dir/Flutter.xcframework" ] + outputs = [ "$_flutter_xcframework_dir" ] args = [ "--frameworks", rebase_path("$_flutter_framework_dir"), 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 b270414900692..524e68293c95a 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 ba317f4214291..10cdf3fe8618e 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 07334dbdd9fbb..54e5c2b0d3cff 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterDartProject.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterDartProject.mm @@ -32,6 +32,39 @@ static const char* kApplicationKernelSnapshotFileName = "kernel_blob.bin"; +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, NSProcessInfo* processInfoOrNil) { auto command_line = flutter::CommandLineFromNSProcessInfo(processInfoOrNil); @@ -46,10 +79,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); @@ -122,29 +152,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); } } } @@ -331,11 +356,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 a124d2b0296a4..cc6e8bbf0d8a5 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm @@ -850,7 +850,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, @@ -1452,7 +1456,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 1831b4d2f546b..69c1e23883629 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm @@ -813,9 +813,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]; } @@ -1287,8 +1292,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,6 +1818,10 @@ - (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; @@ -1825,7 +1839,9 @@ - (void)performOrientationUpdate:(UIInterfaceOrientationMask)new_preferences { }]; [self setNeedsUpdateOfSupportedInterfaceOrientations]; } +#endif } else { +#if !APPLICATION_EXTENSION_API_ONLY UIInterfaceOrientationMask currentInterfaceOrientation = 1 << [[UIApplication sharedApplication] statusBarOrientation]; if (!(_orientationPreferences & currentInterfaceOrientation)) { @@ -1848,10 +1864,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 +1985,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 da8152cd86497..bc51f8410b56d 100644 --- a/shell/platform/darwin/macos/BUILD.gn +++ b/shell/platform/darwin/macos/BUILD.gn @@ -52,6 +52,9 @@ _flutter_framework_headers_copy_dir = source_set("flutter_framework_source") { visibility = [ ":*" ] + cflags_objcc = flutter_cflags_objcc_arc + cflags_objcc += [ "-fapplication-extension" ] + sources = [ "framework/Source/AccessibilityBridgeMac.h", "framework/Source/AccessibilityBridgeMac.mm", @@ -131,8 +134,9 @@ source_set("flutter_framework_source") { "FLUTTER_FRAMEWORK", "FLUTTER_ENGINE_NO_PROTOTYPES", ] - - cflags_objcc = flutter_cflags_objcc_arc + if (darwin_extension_safe) { + defines += [ "APPLICATION_EXTENSION_API_ONLY=1" ] + } frameworks = [ "Carbon.framework", @@ -153,6 +157,10 @@ shared_library("flutter_framework_dylib") { "-fapplication-extension", ] + if (darwin_extension_safe) { + ldflags += [ "-fapplication-extension" ] + } + deps = [ ":flutter_framework_source" ] } @@ -351,6 +359,5 @@ shared_library("_generate_symlinks_and_verify_framework_module") { frameworks = [ "FlutterMacOS.framework" ] sources = [ "framework/Source/FlutterUmbrellaImportTests.m" ] - deps = [ ":_generate_symlinks" ] } diff --git a/shell/platform/darwin/macos/framework/Source/FlutterUmbrellaImport.m b/shell/platform/darwin/macos/framework/Source/FlutterUmbrellaImport.m new file mode 100644 index 0000000000000..b81280d464c19 --- /dev/null +++ b/shell/platform/darwin/macos/framework/Source/FlutterUmbrellaImport.m @@ -0,0 +1,11 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// FLUTTER_NOLINT: https://github.com/flutter/flutter/issues/93360 + +// The only point of this file is to ensure that the Flutter framework umbrella header can be +// cleanly imported from an Objective-C translation unit. The target that uses this file copies the +// headers to a path that simulates how users would actually import the framework outside of the +// engine source root. +#import diff --git a/tools/gn b/tools/gn index 1b2b52fc9e5c4..efba472afd78f 100755 --- a/tools/gn +++ b/tools/gn @@ -629,6 +629,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 @@ -1113,6 +1116,14 @@ 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 and FlutterMacOS.framework ' + 'is app extension safe. Only for iOS and macOS.' + ) + # Verbose output. parser.add_argument('--verbose', default=False, action='store_true')