From b3fc48b91b692deb202bd1445ba3fb249ad4147d Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Fri, 1 Sep 2023 09:56:49 -0700 Subject: [PATCH] Reland "ios: remove shared_application and support app extension build #44732" (flutter/engine#45351) Relands https://github.com/flutter/engine/pull/44732 with fix. The original PR returns nil when the assets is not reachable, in some cases, the assets are not loaded yet but will be loaded later, so we should return the asset URL regardless. Also added a fallback to main bundle to match the previous implementation. The original PR was failed in internal tests in b/297654739 Now with the fix, all tests passed: cl/561449914 fixes https://github.com/flutter/flutter/issues/124289 [C++, Objective-C, Java style guides]: https://github.com/flutter/engine/blob/main/CONTRIBUTING.md#style --- .../framework/Source/FlutterNSBundleUtils.h | 28 ++++++++-- .../framework/Source/FlutterNSBundleUtils.mm | 52 +++++++++++++++---- .../shell/platform/darwin/ios/BUILD.gn | 20 ++++++- .../ios/framework/Headers/FlutterPlugin.h | 3 +- .../framework/Source/FlutterDartProject.mm | 35 ++++--------- .../Source/FlutterDartProjectTest.mm | 44 ++++++++++++++++ .../ios/framework/Source/FlutterEngine.mm | 3 +- .../framework/Source/FlutterPlatformPlugin.mm | 33 ++++++++++-- .../Source/FlutterPlatformPluginTest.mm | 4 ++ .../FlutterPluginAppLifeCycleDelegate.mm | 32 ++++++++---- .../FlutterPluginAppLifeCycleDelegateTest.mm | 2 + .../framework/Source/FlutterViewController.mm | 13 ++++- 12 files changed, 211 insertions(+), 58 deletions(-) diff --git a/engine/src/flutter/shell/platform/darwin/common/framework/Source/FlutterNSBundleUtils.h b/engine/src/flutter/shell/platform/darwin/common/framework/Source/FlutterNSBundleUtils.h index 569b551cfa9..e9c9f323ccb 100644 --- a/engine/src/flutter/shell/platform/darwin/common/framework/Source/FlutterNSBundleUtils.h +++ b/engine/src/flutter/shell/platform/darwin/common/framework/Source/FlutterNSBundleUtils.h @@ -9,12 +9,14 @@ NS_ASSUME_NONNULL_BEGIN -// Finds a bundle with the named `bundleID` within `searchURL`. +extern const NSString* kDefaultAssetPath; + +// Finds a bundle with the named `flutterFrameworkBundleID` within `searchURL`. // // Returns `nil` if the bundle cannot be found or if errors are encountered. -NSBundle* FLTFrameworkBundleInternal(NSString* bundleID, NSURL* searchURL); +NSBundle* FLTFrameworkBundleInternal(NSString* flutterFrameworkBundleID, NSURL* searchURL); -// Finds a bundle with the named `bundleID`. +// Finds a bundle with the named `flutterFrameworkBundleID`. // // `+[NSBundle bundleWithIdentifier:]` is slow, and can take in the order of // tens of milliseconds in a minimal flutter app, and closer to 100 milliseconds @@ -28,7 +30,25 @@ NSBundle* FLTFrameworkBundleInternal(NSString* bundleID, NSURL* searchURL); // frameworks used by this file are placed. If the desired bundle cannot be // found here, the implementation falls back to // `+[NSBundle bundleWithIdentifier:]`. -NSBundle* FLTFrameworkBundleWithIdentifier(NSString* bundleID); +NSBundle* FLTFrameworkBundleWithIdentifier(NSString* flutterFrameworkBundleID); + +// Finds the bundle of the application. +// +// Returns [NSBundle mainBundle] if the current running process is the application. +NSBundle* FLTGetApplicationBundle(); + +// Gets the flutter assets path directory from `bundle`. +// +// Returns `kDefaultAssetPath` if unable to find asset path from info.plist in `bundle`. +NSString* FLTAssetPath(NSBundle* bundle); + +// Finds the Flutter asset directory from `bundle`. +// +// The raw path can be set by the application via info.plist's `FLTAssetsPath` key. +// If the key is not set, `flutter_assets` is used as the raw path value. +// +// If no valid asset is found under the raw path, returns nil. +NSURL* FLTAssetsURLFromBundle(NSBundle* bundle); NS_ASSUME_NONNULL_END diff --git a/engine/src/flutter/shell/platform/darwin/common/framework/Source/FlutterNSBundleUtils.mm b/engine/src/flutter/shell/platform/darwin/common/framework/Source/FlutterNSBundleUtils.mm index ab403f9b8d5..bf0002d9ade 100644 --- a/engine/src/flutter/shell/platform/darwin/common/framework/Source/FlutterNSBundleUtils.mm +++ b/engine/src/flutter/shell/platform/darwin/common/framework/Source/FlutterNSBundleUtils.mm @@ -8,7 +8,9 @@ FLUTTER_ASSERT_ARC -NSBundle* FLTFrameworkBundleInternal(NSString* bundleID, NSURL* searchURL) { +const NSString* kDefaultAssetPath = @"Frameworks/App.framework/flutter_assets"; + +NSBundle* FLTFrameworkBundleInternal(NSString* flutterFrameworkBundleID, NSURL* searchURL) { NSDirectoryEnumerator* frameworkEnumerator = [NSFileManager.defaultManager enumeratorAtURL:searchURL includingPropertiesForKeys:nil @@ -18,19 +20,49 @@ NSBundle* FLTFrameworkBundleInternal(NSString* bundleID, NSURL* searchURL) { errorHandler:nil]; for (NSURL* candidate in frameworkEnumerator) { - NSBundle* bundle = [NSBundle bundleWithURL:candidate]; - if ([bundle.bundleIdentifier isEqualToString:bundleID]) { - return bundle; + NSBundle* flutterFrameworkBundle = [NSBundle bundleWithURL:candidate]; + if ([flutterFrameworkBundle.bundleIdentifier isEqualToString:flutterFrameworkBundleID]) { + return flutterFrameworkBundle; } } return nil; } -NSBundle* FLTFrameworkBundleWithIdentifier(NSString* bundleID) { - NSBundle* bundle = FLTFrameworkBundleInternal(bundleID, NSBundle.mainBundle.privateFrameworksURL); - if (bundle != nil) { - return bundle; +NSBundle* FLTGetApplicationBundle() { + NSBundle* mainBundle = [NSBundle mainBundle]; + // App extension bundle is in .app/PlugIns/Extension.appex. + if ([mainBundle.bundleURL.pathExtension isEqualToString:@"appex"]) { + // Up two levels. + return [NSBundle bundleWithURL:mainBundle.bundleURL.URLByDeletingLastPathComponent + .URLByDeletingLastPathComponent]; } - // Fallback to slow implementation. - return [NSBundle bundleWithIdentifier:bundleID]; + return mainBundle; +} + +NSBundle* FLTFrameworkBundleWithIdentifier(NSString* flutterFrameworkBundleID) { + NSBundle* appBundle = FLTGetApplicationBundle(); + NSBundle* flutterFrameworkBundle = + FLTFrameworkBundleInternal(flutterFrameworkBundleID, appBundle.privateFrameworksURL); + if (flutterFrameworkBundle == nil) { + // Fallback to slow implementation. + flutterFrameworkBundle = [NSBundle bundleWithIdentifier:flutterFrameworkBundleID]; + } + if (flutterFrameworkBundle == nil) { + flutterFrameworkBundle = [NSBundle mainBundle]; + } + return flutterFrameworkBundle; +} + +NSString* FLTAssetPath(NSBundle* bundle) { + return [bundle objectForInfoDictionaryKey:@"FLTAssetsPath"] ?: kDefaultAssetPath; +} + +NSURL* FLTAssetsURLFromBundle(NSBundle* bundle) { + NSString* flutterAssetsPath = FLTAssetPath(bundle); + NSURL* assets = [bundle URLForResource:flutterAssetsPath withExtension:nil]; + + if (!assets) { + assets = [[NSBundle mainBundle] URLForResource:flutterAssetsPath withExtension:nil]; + } + return assets; } diff --git a/engine/src/flutter/shell/platform/darwin/ios/BUILD.gn b/engine/src/flutter/shell/platform/darwin/ios/BUILD.gn index 8aa967ae6ca..f726f41f23a 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/BUILD.gn +++ b/engine/src/flutter/shell/platform/darwin/ios/BUILD.gn @@ -348,6 +348,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 = [ @@ -438,7 +442,10 @@ 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", + ] sources = [ "framework/Source/FlutterUmbrellaImport.m" ] deps = [ @@ -446,6 +453,17 @@ shared_library("copy_and_verify_framework_module") { ":copy_framework_info_plist", ":copy_framework_module_map", ] + + if (darwin_extension_safe) { + ldflags = [ + "-F$framework_search_path", + "-fapplication-extension", + "-Xlinker", + "-fatal_warnings", + ] + deps += [ ":copy_dylib" ] + frameworks = [ "Flutter.framework" ] + } } group("universal_flutter_framework") { diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Headers/FlutterPlugin.h b/engine/src/flutter/shell/platform/darwin/ios/framework/Headers/FlutterPlugin.h index b2704149006..524e68293c9 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Headers/FlutterPlugin.h +++ b/engine/src/flutter/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/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterDartProject.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterDartProject.mm index 4873102e722..3d434eb2179 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterDartProject.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterDartProject.mm @@ -41,16 +41,13 @@ flutter::Settings FLTDefaultSettingsForBundle(NSBundle* bundle, NSProcessInfo* p // 3. Settings from the NSBundle with the default bundle ID. // 4. Settings from the main NSBundle and default values. - NSBundle* mainBundle = [NSBundle mainBundle]; + NSBundle* mainBundle = FLTGetApplicationBundle(); NSBundle* engineBundle = [NSBundle bundleForClass:[FlutterViewController class]]; bool hasExplicitBundle = bundle != nil; if (bundle == nil) { bundle = FLTFrameworkBundleWithIdentifier([FlutterDartProject defaultBundleIdentifier]); } - if (bundle == nil) { - bundle = mainBundle; - } auto settings = flutter::SettingsFromCommandLine(command_line); @@ -122,29 +119,24 @@ flutter::Settings FLTDefaultSettingsForBundle(NSBundle* bundle, NSProcessInfo* p // 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 = FLTAssetsURLFromBundle(bundle); - if (assetsPath.length == 0) { - assetsPath = [mainBundle pathForResource:assetsName ofType:@""]; - } - - if (assetsPath.length == 0) { - NSLog(@"Failed to find assets path for \"%@\"", assetsName); + if (!assetsURL) { + 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); } } } @@ -339,14 +331,7 @@ flutter::Settings FLTDefaultSettingsForBundle(NSBundle* bundle, NSProcessInfo* p if (bundle == nil) { bundle = FLTFrameworkBundleWithIdentifier([FlutterDartProject defaultBundleIdentifier]); } - if (bundle == nil) { - bundle = [NSBundle mainBundle]; - } - NSString* flutterAssetsName = [bundle objectForInfoDictionaryKey:@"FLTAssetsPath"]; - if (flutterAssetsName == nil) { - flutterAssetsName = @"Frameworks/App.framework/flutter_assets"; - } - return flutterAssetsName; + return FLTAssetPath(bundle); } + (NSString*)domainNetworkPolicy:(NSDictionary*)appTransportSecurity { diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterDartProjectTest.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterDartProjectTest.mm index 7e180603a0a..a28aaf8beea 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterDartProjectTest.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterDartProjectTest.mm @@ -73,6 +73,50 @@ FLUTTER_ASSERT_ARC XCTAssertNotNil(found); } +- (void)testFLTGetApplicationBundleWhenCurrentTargetIsNotExtension { + NSBundle* bundle = FLTGetApplicationBundle(); + XCTAssertEqual(bundle, [NSBundle mainBundle]); +} + +- (void)testFLTGetApplicationBundleWhenCurrentTargetIsExtension { + id mockMainBundle = OCMPartialMock([NSBundle mainBundle]); + NSURL* url = [[NSBundle mainBundle].bundleURL URLByAppendingPathComponent:@"foo/ext.appex"]; + OCMStub([mockMainBundle bundleURL]).andReturn(url); + NSBundle* bundle = FLTGetApplicationBundle(); + [mockMainBundle stopMocking]; + XCTAssertEqualObjects(bundle.bundleURL, [NSBundle mainBundle].bundleURL); +} + +- (void)testFLTAssetsURLFromBundle { + { + // Found asset path in info.plist (even not reachable) + id mockBundle = OCMClassMock([NSBundle class]); + OCMStub([mockBundle objectForInfoDictionaryKey:@"FLTAssetsPath"]).andReturn(@"foo/assets"); + NSURL* mockAssetsURL = OCMClassMock([NSURL class]); + OCMStub([mockBundle URLForResource:@"foo/assets" withExtension:nil]).andReturn(mockAssetsURL); + OCMStub([mockAssetsURL checkResourceIsReachableAndReturnError:NULL]).andReturn(NO); + OCMStub([mockAssetsURL path]).andReturn(@"foo/assets"); + NSURL* url = FLTAssetsURLFromBundle(mockBundle); + XCTAssertEqualObjects(url.path, @"foo/assets"); + } + { + // No asset path in info.plist, defaults to main bundle + id mockBundle = OCMClassMock([NSBundle class]); + id mockMainBundle = OCMPartialMock([NSBundle mainBundle]); + NSURL* mockAssetsURL = OCMClassMock([NSURL class]); + OCMStub([mockBundle URLForResource:@"Frameworks/App.framework/flutter_assets" + withExtension:nil]) + .andReturn(nil); + OCMStub([mockAssetsURL checkResourceIsReachableAndReturnError:NULL]).andReturn(NO); + OCMStub([mockAssetsURL path]).andReturn(@"path/to/foo/assets"); + OCMStub([mockMainBundle URLForResource:@"Frameworks/App.framework/flutter_assets" + withExtension:nil]) + .andReturn(mockAssetsURL); + NSURL* url = FLTAssetsURLFromBundle(mockBundle); + XCTAssertEqualObjects(url.path, @"path/to/foo/assets"); + } +} + - (void)testDisableImpellerSettingIsCorrectlyParsed { id mockMainBundle = OCMPartialMock([NSBundle mainBundle]); OCMStub([mockMainBundle objectForInfoDictionaryKey:@"FLTEnableImpeller"]).andReturn(@"NO"); diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm index b300714f166..df4bd038036 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm @@ -1515,7 +1515,8 @@ static void SetEntryPoint(flutter::Settings* settings, NSString* entrypoint, NSS }]; } -- (void)addApplicationDelegate:(NSObject*)delegate { +- (void)addApplicationDelegate:(NSObject*)delegate + NS_EXTENSION_UNAVAILABLE_IOS("Disallowed in plugins used in app extensions") { id appDelegate = [[UIApplication sharedApplication] delegate]; if ([appDelegate conformsToProtocol:@protocol(FlutterAppLifeCycleProvider)]) { id lifeCycleProvider = diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformPlugin.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformPlugin.mm index ef454d0d440..722571bda0a 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformPlugin.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformPlugin.mm @@ -17,7 +17,10 @@ namespace { constexpr char kTextPlainFormat[] = "text/plain"; const UInt32 kKeyPressClickSoundId = 1306; + +#if not APPLICATION_EXTENSION_API_ONLY const NSString* searchURLPrefix = @"x-web-search://?"; +#endif } // namespace @@ -37,6 +40,24 @@ const char* const kOverlayStyleUpdateNotificationKey = using namespace flutter; +static void SetStatusBarHiddenForSharedApplication(BOOL hidden) { +#if APPLICATION_EXTENSION_API_ONLY + [UIApplication sharedApplication].statusBarHidden = hidden; +#else + FML_LOG(WARNING) << "Application based status bar styling is not available in app extension."; +#endif +} + +static void SetStatusBarStyleForSharedApplication(UIStatusBarStyle style) { +#if APPLICATION_EXTENSION_API_ONLY + // Note: -[UIApplication setStatusBarStyle] is deprecated in iOS9 + // in favor of delegating to the view controller. + [[UIApplication sharedApplication] setStatusBarStyle:style]; +#else + FML_LOG(WARNING) << "Application based status bar styling is not available in app extension."; +#endif +} + @interface FlutterPlatformPlugin () /** @@ -141,6 +162,9 @@ using namespace flutter; } - (void)searchWeb:(NSString*)searchTerm { +#if APPLICATION_EXTENSION_API_ONLY + FML_LOG(WARNING) << "SearchWeb.invoke is not availabe in app extension."; +#else NSString* escapedText = [searchTerm stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLHostAllowedCharacterSet]]; @@ -149,6 +173,7 @@ using namespace flutter; [[UIApplication sharedApplication] openURL:[NSURL URLWithString:searchURL] options:@{} completionHandler:nil]; +#endif } - (void)playSystemSound:(NSString*)soundType { @@ -231,7 +256,7 @@ using namespace flutter; // We opt out of view controller based status bar visibility since we want // to be able to modify this on the fly. The key used is // UIViewControllerBasedStatusBarAppearance. - [UIApplication sharedApplication].statusBarHidden = statusBarShouldBeHidden; + SetStatusBarHiddenForSharedApplication(statusBarShouldBeHidden); } } @@ -246,7 +271,7 @@ using namespace flutter; // We opt out of view controller based status bar visibility since we want // to be able to modify this on the fly. The key used is // UIViewControllerBasedStatusBarAppearance. - [UIApplication sharedApplication].statusBarHidden = !edgeToEdge; + SetStatusBarHiddenForSharedApplication(!edgeToEdge); } [[NSNotificationCenter defaultCenter] postNotificationName:edgeToEdge ? FlutterViewControllerShowHomeIndicator @@ -284,9 +309,7 @@ using namespace flutter; object:nil userInfo:@{@(kOverlayStyleUpdateNotificationKey) : @(statusBarStyle)}]; } else { - // Note: -[UIApplication setStatusBarStyle] is deprecated in iOS9 - // in favor of delegating to the view controller. - [[UIApplication sharedApplication] setStatusBarStyle:statusBarStyle]; + SetStatusBarStyleForSharedApplication(statusBarStyle); } } diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformPluginTest.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformPluginTest.mm index 3e19665973e..05764102a8e 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformPluginTest.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformPluginTest.mm @@ -50,9 +50,11 @@ FlutterResult result = ^(id result) { OCMVerify([mockPlugin searchWeb:@"Testing Word!"]); +#if not APPLICATION_EXTENSION_API_ONLY OCMVerify([mockApplication openURL:[NSURL URLWithString:@"x-web-search://?Testing%20Word!"] options:@{} completionHandler:nil]); +#endif [invokeExpectation fulfill]; }; @@ -82,9 +84,11 @@ FlutterResult result = ^(id result) { OCMVerify([mockPlugin searchWeb:@"Test"]); +#if not APPLICATION_EXTENSION_API_ONLY OCMVerify([mockApplication openURL:[NSURL URLWithString:@"x-web-search://?Test"] options:@{} completionHandler:nil]); +#endif [invokeExpectation fulfill]; }; diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPluginAppLifeCycleDelegate.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPluginAppLifeCycleDelegate.mm index d493f30d11f..def41bbca43 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPluginAppLifeCycleDelegate.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPluginAppLifeCycleDelegate.mm @@ -17,11 +17,16 @@ static const SEL kSelectorsHandledByPlugins[] = { @selector(application:performFetchWithCompletionHandler:)}; @interface FlutterPluginAppLifeCycleDelegate () -- (void)handleDidEnterBackground:(NSNotification*)notification; -- (void)handleWillEnterForeground:(NSNotification*)notification; -- (void)handleWillResignActive:(NSNotification*)notification; -- (void)handleDidBecomeActive:(NSNotification*)notification; -- (void)handleWillTerminate:(NSNotification*)notification; +- (void)handleDidEnterBackground:(NSNotification*)notification + NS_EXTENSION_UNAVAILABLE_IOS("Disallowed in app extensions"); +- (void)handleWillEnterForeground:(NSNotification*)notification + NS_EXTENSION_UNAVAILABLE_IOS("Disallowed in app extensions"); +- (void)handleWillResignActive:(NSNotification*)notification + NS_EXTENSION_UNAVAILABLE_IOS("Disallowed in app extensions"); +- (void)handleDidBecomeActive:(NSNotification*)notification + NS_EXTENSION_UNAVAILABLE_IOS("Disallowed in app extensions"); +- (void)handleWillTerminate:(NSNotification*)notification + NS_EXTENSION_UNAVAILABLE_IOS("Disallowed in app extensions"); @end @implementation FlutterPluginAppLifeCycleDelegate { @@ -46,6 +51,7 @@ static const SEL kSelectorsHandledByPlugins[] = { _notificationUnsubscribers = [[NSMutableArray alloc] init]; std::string cachePath = fml::paths::JoinPaths({getenv("HOME"), kCallbackCacheSubDir}); [FlutterCallbackCache setCachePath:[NSString stringWithUTF8String:cachePath.c_str()]]; +#if not APPLICATION_EXTENSION_API_ONLY [self addObserverFor:UIApplicationDidEnterBackgroundNotification selector:@selector(handleDidEnterBackground:)]; [self addObserverFor:UIApplicationWillEnterForegroundNotification @@ -56,6 +62,7 @@ static const SEL kSelectorsHandledByPlugins[] = { selector:@selector(handleDidBecomeActive:)]; [self addObserverFor:UIApplicationWillTerminateNotification selector:@selector(handleWillTerminate:)]; +#endif _delegates = [[NSPointerArray weakObjectsPointerArray] retain]; _debugBackgroundTask = UIBackgroundTaskInvalid; } @@ -134,7 +141,8 @@ static BOOL IsPowerOfTwo(NSUInteger x) { return YES; } -- (void)handleDidEnterBackground:(NSNotification*)notification { +- (void)handleDidEnterBackground:(NSNotification*)notification + NS_EXTENSION_UNAVAILABLE_IOS("Disallowed in app extensions") { UIApplication* application = [UIApplication sharedApplication]; #if FLUTTER_RUNTIME_MODE == FLUTTER_RUNTIME_MODE_DEBUG // The following keeps the Flutter session alive when the device screen locks @@ -166,7 +174,8 @@ static BOOL IsPowerOfTwo(NSUInteger x) { } } -- (void)handleWillEnterForeground:(NSNotification*)notification { +- (void)handleWillEnterForeground:(NSNotification*)notification + NS_EXTENSION_UNAVAILABLE_IOS("Disallowed in app extensions") { UIApplication* application = [UIApplication sharedApplication]; #if FLUTTER_RUNTIME_MODE == FLUTTER_RUNTIME_MODE_DEBUG if (_debugBackgroundTask != UIBackgroundTaskInvalid) { @@ -184,7 +193,8 @@ static BOOL IsPowerOfTwo(NSUInteger x) { } } -- (void)handleWillResignActive:(NSNotification*)notification { +- (void)handleWillResignActive:(NSNotification*)notification + NS_EXTENSION_UNAVAILABLE_IOS("Disallowed in app extensions") { UIApplication* application = [UIApplication sharedApplication]; for (NSObject* delegate in _delegates) { if (!delegate) { @@ -196,7 +206,8 @@ static BOOL IsPowerOfTwo(NSUInteger x) { } } -- (void)handleDidBecomeActive:(NSNotification*)notification { +- (void)handleDidBecomeActive:(NSNotification*)notification + NS_EXTENSION_UNAVAILABLE_IOS("Disallowed in app extensions") { UIApplication* application = [UIApplication sharedApplication]; for (NSObject* delegate in _delegates) { if (!delegate) { @@ -208,7 +219,8 @@ static BOOL IsPowerOfTwo(NSUInteger x) { } } -- (void)handleWillTerminate:(NSNotification*)notification { +- (void)handleWillTerminate:(NSNotification*)notification + NS_EXTENSION_UNAVAILABLE_IOS("Disallowed in app extensions") { UIApplication* application = [UIApplication sharedApplication]; for (NSObject* delegate in _delegates) { if (!delegate) { diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPluginAppLifeCycleDelegateTest.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPluginAppLifeCycleDelegateTest.mm index 9891f52cf8c..9fcc645575f 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPluginAppLifeCycleDelegateTest.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPluginAppLifeCycleDelegateTest.mm @@ -20,6 +20,7 @@ FLUTTER_ASSERT_ARC XCTAssertNotNil(delegate); } +#if not APPLICATION_EXTENSION_API_ONLY - (void)testDidEnterBackground { XCTNSNotificationExpectation* expectation = [[XCTNSNotificationExpectation alloc] initWithName:UIApplicationDidEnterBackgroundNotification]; @@ -88,5 +89,6 @@ FLUTTER_ASSERT_ARC [self waitForExpectations:@[ expectation ] timeout:5.0]; OCMVerify([plugin applicationWillTerminate:[UIApplication sharedApplication]]); } +#endif @end diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm index f5a64719564..f78ca85f79e 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm @@ -1973,7 +1973,7 @@ static flutter::PointerData::DeviceKind DeviceKindFromTouchType(UITouch* touch) #endif [self requestGeometryUpdateForWindowScenes:scenes]; } else { - UIInterfaceOrientationMask currentInterfaceOrientation; + UIInterfaceOrientationMask currentInterfaceOrientation = 0; if (@available(iOS 13.0, *)) { UIWindowScene* windowScene = [self flutterWindowSceneIfViewLoaded]; if (!windowScene) { @@ -1983,7 +1983,13 @@ static flutter::PointerData::DeviceKind DeviceKindFromTouchType(UITouch* touch) } currentInterfaceOrientation = 1 << windowScene.interfaceOrientation; } else { +#if APPLICATION_EXTENSION_API_ONLY + FML_LOG(ERROR) << "Application based status bar orentiation update is not supported in " + "app extension. Orientation: " + << currentInterfaceOrientation; +#else currentInterfaceOrientation = 1 << [[UIApplication sharedApplication] statusBarOrientation]; +#endif } if (!(_orientationPreferences & currentInterfaceOrientation)) { [UIViewController attemptRotationToDeviceOrientation]; @@ -2108,6 +2114,10 @@ static flutter::PointerData::DeviceKind DeviceKindFromTouchType(UITouch* touch) } - (CGFloat)textScaleFactor { +#if APPLICATION_EXTENSION_API_ONLY + FML_LOG(WARNING) << "Dynamic content size update is not supported in app extension."; + return 1.0; +#else UIContentSizeCategory category = [UIApplication sharedApplication].preferredContentSizeCategory; // The delta is computed by approximating Apple's typography guidelines: // https://developer.apple.com/ios/human-interface-guidelines/visual-design/typography/ @@ -2158,6 +2168,7 @@ static flutter::PointerData::DeviceKind DeviceKindFromTouchType(UITouch* touch) } else { return 1.0; } +#endif } - (BOOL)isAlwaysUse24HourFormat {