diff --git a/engine/src/build/config/ios/BUILD.gn b/engine/src/build/config/ios/BUILD.gn index deff58788e..6f02af6f25 100644 --- a/engine/src/build/config/ios/BUILD.gn +++ b/engine/src/build/config/ios/BUILD.gn @@ -5,3 +5,9 @@ config("sdk") { cflags_cc = [ "-stdlib=libc++" ] } + +config("ios_application_extension") { + cflags_objc = [ "-fapplication-extension" ] + cflags_objcc = [ "-fapplication-extension" ] + ldflags = [ "-fapplication-extension" ] +} diff --git a/engine/src/flutter/ci/licenses_golden/licenses_flutter b/engine/src/flutter/ci/licenses_golden/licenses_flutter index 8fe7bf3855..78919cf0fb 100644 --- a/engine/src/flutter/ci/licenses_golden/licenses_flutter +++ b/engine/src/flutter/ci/licenses_golden/licenses_flutter @@ -52788,6 +52788,9 @@ ORIGIN: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterResto ORIGIN: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterRestorationPluginTest.mm + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterSemanticsScrollView.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterSemanticsScrollView.mm + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterSharedApplication.h + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterSharedApplication.mm + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterSharedApplicationTest.mm + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterSpellCheckPlugin.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterSpellCheckPlugin.mm + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterSpellCheckPluginTest.mm + ../../../flutter/LICENSE @@ -55789,6 +55792,9 @@ FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterRestora FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterRestorationPluginTest.mm FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterSemanticsScrollView.h FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterSemanticsScrollView.mm +FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterSharedApplication.h +FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterSharedApplication.mm +FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterSharedApplicationTest.mm FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterSpellCheckPlugin.h FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterSpellCheckPlugin.mm FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterSpellCheckPluginTest.mm diff --git a/engine/src/flutter/common/config.gni b/engine/src/flutter/common/config.gni index b2ae1118e2..318d305bd3 100644 --- a/engine/src/flutter/common/config.gni +++ b/engine/src/flutter/common/config.gni @@ -23,12 +23,6 @@ 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 - # Whether binary size optimizations with the assumption that Impeller is the # only supported rendering engine are enabled. # diff --git a/engine/src/flutter/shell/platform/darwin/ios/BUILD.gn b/engine/src/flutter/shell/platform/darwin/ios/BUILD.gn index edcdbb71d0..900fc461b9 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/BUILD.gn +++ b/engine/src/flutter/shell/platform/darwin/ios/BUILD.gn @@ -45,14 +45,13 @@ source_set("flutter_framework_source") { cflags_objcc = flutter_cflags_objcc defines = [ "FLUTTER_FRAMEWORK=1" ] - if (darwin_extension_safe) { - defines += [ "APPLICATION_EXTENSION_API_ONLY=1" ] - } public_configs = [ ":ios_gpu_configuration_config", "//flutter:config", ] + configs += [ "//build/config/ios:ios_application_extension" ] + sources = [ "framework/Source/FlutterAppDelegate.mm", "framework/Source/FlutterCallbackCache.mm", @@ -88,6 +87,8 @@ source_set("flutter_framework_source") { "framework/Source/FlutterRestorationPlugin.mm", "framework/Source/FlutterSemanticsScrollView.h", "framework/Source/FlutterSemanticsScrollView.mm", + "framework/Source/FlutterSharedApplication.h", + "framework/Source/FlutterSharedApplication.mm", "framework/Source/FlutterSpellCheckPlugin.h", "framework/Source/FlutterSpellCheckPlugin.mm", "framework/Source/FlutterTextInputDelegate.h", @@ -225,6 +226,7 @@ shared_library("ios_test_flutter") { "framework/Source/FlutterPlatformViewsTest.mm", "framework/Source/FlutterPluginAppLifeCycleDelegateTest.mm", "framework/Source/FlutterRestorationPluginTest.mm", + "framework/Source/FlutterSharedApplicationTest.mm", "framework/Source/FlutterSpellCheckPluginTest.mm", "framework/Source/FlutterTextInputPluginTest.mm", "framework/Source/FlutterTextureRegistryRelayTest.mm", @@ -262,10 +264,6 @@ 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") { @@ -275,15 +273,12 @@ 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 = [ ":flutter_framework_source" ] public_configs = [ "//flutter:config" ] + configs += [ "//build/config/ios:ios_application_extension" ] } copy("copy_dylib") { @@ -375,9 +370,11 @@ copy("copy_license") { shared_library("copy_and_verify_framework_module") { framework_search_path = rebase_path("$root_out_dir") visibility = [ ":*" ] - cflags_objc = [ + cflags_objc = [ "-F$framework_search_path" ] + ldflags = [ "-F$framework_search_path", - "-fapplication-extension", + "-Xlinker", + "-fatal_warnings", ] sources = [ "framework/Source/FlutterUmbrellaImport.m" ] @@ -388,16 +385,7 @@ shared_library("copy_and_verify_framework_module") { ":copy_framework_privacy_manifest", ] - if (darwin_extension_safe) { - ldflags = [ - "-F$framework_search_path", - "-fapplication-extension", - "-Xlinker", - "-fatal_warnings", - ] - deps += [ ":copy_dylib" ] - frameworks = [ "Flutter.framework" ] - } + configs += [ "//build/config/ios:ios_application_extension" ] } group("universal_flutter_framework") { diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterAppDelegate.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterAppDelegate.mm index c4ef0790cb..74e6902c18 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterAppDelegate.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterAppDelegate.mm @@ -10,6 +10,7 @@ #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterAppDelegate_Test.h" #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine_Internal.h" #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterPluginAppLifeCycleDelegate_internal.h" +#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterSharedApplication.h" #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterViewController_Internal.h" FLUTTER_ASSERT_ARC @@ -154,6 +155,10 @@ static NSString* const kRestorationStateAppModificationKey = @"mod-date"; - (BOOL)handleOpenURL:(NSURL*)url options:(NSDictionary*)options relayToSystemIfUnhandled:(BOOL)throwBack { + UIApplication* flutterApplication = FlutterSharedApplication.application; + if (flutterApplication == nil) { + return NO; + } if (![self isFlutterDeepLinkingEnabled]) { return NO; } @@ -164,9 +169,9 @@ static NSString* const kRestorationStateAppModificationKey = @"mod-date"; completionHandler:^(BOOL success) { if (!success && throwBack) { // throw it back to iOS - [UIApplication.sharedApplication openURL:url - options:@{} - completionHandler:nil]; + [flutterApplication openURL:url + options:@{} + completionHandler:nil]; } }]; } else { 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 505a453487..c791cf2861 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 @@ -26,6 +26,7 @@ #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterDartVMServicePublisher.h" #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterIndirectScribbleDelegate.h" #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformPlugin.h" +#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterSharedApplication.h" #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterSpellCheckPlugin.h" #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputDelegate.h" #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterTextureRegistryRelay.h" @@ -227,15 +228,7 @@ static constexpr int kNumProfilerSamplesPerSec = 5; 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 + [self setUpLifecycleNotifications:center]; [center addObserver:self selector:@selector(onLocaleUpdated:) @@ -250,18 +243,21 @@ static constexpr int kNumProfilerSamplesPerSec = 5; return (__bridge FlutterEngine*)reinterpret_cast(identifier); } -- (void)setUpSceneLifecycleNotifications:(NSNotificationCenter*)center API_AVAILABLE(ios(13.0)) { - [center addObserver:self - selector:@selector(sceneWillEnterForeground:) - name:UISceneWillEnterForegroundNotification - object:nil]; - [center addObserver:self - selector:@selector(sceneDidEnterBackground:) - name:UISceneDidEnterBackgroundNotification - object:nil]; -} - -- (void)setUpApplicationLifecycleNotifications:(NSNotificationCenter*)center { +- (void)setUpLifecycleNotifications:(NSNotificationCenter*)center { + // If the application is not available, use the scene for lifecycle notifications if available. + if (!FlutterSharedApplication.isAvailable) { + if (@available(iOS 13.0, *)) { + [center addObserver:self + selector:@selector(sceneWillEnterForeground:) + name:UISceneWillEnterForegroundNotification + object:nil]; + [center addObserver:self + selector:@selector(sceneDidEnterBackground:) + name:UISceneDidEnterBackgroundNotification + object:nil]; + return; + } + } [center addObserver:self selector:@selector(applicationWillEnterForeground:) name:UIApplicationWillEnterForegroundNotification @@ -855,19 +851,8 @@ static void SetEntryPoint(flutter::Settings* settings, NSString* entrypoint, NSS _threadHost->io_thread->GetTaskRunner() // io ); -#if APPLICATION_EXTENSION_API_ONLY - if (@available(iOS 13.0, *)) { - _isGpuDisabled = self.viewController.flutterWindowSceneIfViewLoaded.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 + // Disable GPU if the app or scene is running in the background. + self.isGpuDisabled = self.viewController.stateIsBackground; // Create the shell. This is a blocking operation. std::unique_ptr shell = flutter::Shell::Create( @@ -1342,7 +1327,6 @@ static void SetEntryPoint(flutter::Settings* settings, NSString* entrypoint, NSS #pragma mark - Notifications -#if APPLICATION_EXTENSION_API_ONLY - (void)sceneWillEnterForeground:(NSNotification*)notification API_AVAILABLE(ios(13.0)) { [self flutterWillEnterForeground:notification]; } @@ -1350,7 +1334,7 @@ static void SetEntryPoint(flutter::Settings* settings, NSString* entrypoint, NSS - (void)sceneDidEnterBackground:(NSNotification*)notification API_AVAILABLE(ios(13.0)) { [self flutterDidEnterBackground:notification]; } -#else + - (void)applicationWillEnterForeground:(NSNotification*)notification { [self flutterWillEnterForeground:notification]; } @@ -1358,7 +1342,6 @@ static void SetEntryPoint(flutter::Settings* settings, NSString* entrypoint, NSS - (void)applicationDidEnterBackground:(NSNotification*)notification { [self flutterDidEnterBackground:notification]; } -#endif - (void)flutterWillEnterForeground:(NSNotification*)notification { [self setIsGpuDisabled:NO]; diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEngineTest.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEngineTest.mm index 785b084ec7..3770bf8c6a 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEngineTest.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEngineTest.mm @@ -15,9 +15,9 @@ #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterDartProject_Internal.h" #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine_Internal.h" #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine_Test.h" +#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterSharedApplication.h" #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.h" #import "flutter/shell/platform/darwin/ios/platform_view_ios.h" - FLUTTER_ASSERT_ARC @interface FlutterEngineSpy : FlutterEngine @@ -381,7 +381,7 @@ FLUTTER_ASSERT_ARC OCMVerify(times(2), [mockEngine updateDisplays]); } -- (void)testLifeCycleNotificationDidEnterBackground { +- (void)testLifeCycleNotificationDidEnterBackgroundForApplication { FlutterDartProject* project = [[FlutterDartProject alloc] init]; FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar" project:project]; [engine run]; @@ -394,23 +394,49 @@ FLUTTER_ASSERT_ARC 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 + [NSNotificationCenter.defaultCenter postNotification:sceneNotification]; + [NSNotificationCenter.defaultCenter postNotification:applicationNotification]; OCMVerify(times(1), [mockEngine applicationDidEnterBackground:[OCMArg any]]); -#endif XCTAssertTrue(engine.isGpuDisabled); - bool switch_value = false; + BOOL gpuDisabled = NO; [engine shell].GetIsGpuDisabledSyncSwitch()->Execute( - fml::SyncSwitch::Handlers().SetIfTrue([&] { switch_value = true; }).SetIfFalse([&] { - switch_value = false; + fml::SyncSwitch::Handlers().SetIfTrue([&] { gpuDisabled = YES; }).SetIfFalse([&] { + gpuDisabled = NO; })); - XCTAssertTrue(switch_value); + XCTAssertTrue(gpuDisabled); } -- (void)testLifeCycleNotificationWillEnterForeground { +- (void)testLifeCycleNotificationDidEnterBackgroundForScene { + id mockBundle = OCMPartialMock([NSBundle mainBundle]); + OCMStub([mockBundle objectForInfoDictionaryKey:@"NSExtension"]).andReturn(@{ + @"NSExtensionPointIdentifier" : @"com.apple.share-services" + }); + 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]; + OCMVerify(times(1), [mockEngine sceneDidEnterBackground:[OCMArg any]]); + XCTAssertTrue(engine.isGpuDisabled); + BOOL gpuDisabled = NO; + [engine shell].GetIsGpuDisabledSyncSwitch()->Execute( + fml::SyncSwitch::Handlers().SetIfTrue([&] { gpuDisabled = YES; }).SetIfFalse([&] { + gpuDisabled = NO; + })); + XCTAssertTrue(gpuDisabled); + [mockBundle stopMocking]; +} + +- (void)testLifeCycleNotificationWillEnterForegroundForApplication { FlutterDartProject* project = [[FlutterDartProject alloc] init]; FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar" project:project]; [engine run]; @@ -423,20 +449,46 @@ FLUTTER_ASSERT_ARC 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 + [NSNotificationCenter.defaultCenter postNotification:sceneNotification]; + [NSNotificationCenter.defaultCenter postNotification:applicationNotification]; OCMVerify(times(1), [mockEngine applicationWillEnterForeground:[OCMArg any]]); -#endif XCTAssertFalse(engine.isGpuDisabled); - bool switch_value = true; + BOOL gpuDisabled = YES; [engine shell].GetIsGpuDisabledSyncSwitch()->Execute( - fml::SyncSwitch::Handlers().SetIfTrue([&] { switch_value = true; }).SetIfFalse([&] { - switch_value = false; + fml::SyncSwitch::Handlers().SetIfTrue([&] { gpuDisabled = YES; }).SetIfFalse([&] { + gpuDisabled = NO; })); - XCTAssertFalse(switch_value); + XCTAssertFalse(gpuDisabled); +} + +- (void)testLifeCycleNotificationWillEnterForegroundForScene { + id mockBundle = OCMPartialMock([NSBundle mainBundle]); + OCMStub([mockBundle objectForInfoDictionaryKey:@"NSExtension"]).andReturn(@{ + @"NSExtensionPointIdentifier" : @"com.apple.share-services" + }); + 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]; + OCMVerify(times(1), [mockEngine sceneWillEnterForeground:[OCMArg any]]); + XCTAssertFalse(engine.isGpuDisabled); + BOOL gpuDisabled = YES; + [engine shell].GetIsGpuDisabledSyncSwitch()->Execute( + fml::SyncSwitch::Handlers().SetIfTrue([&] { gpuDisabled = YES; }).SetIfFalse([&] { + gpuDisabled = NO; + })); + XCTAssertFalse(gpuDisabled); + [mockBundle stopMocking]; } - (void)testSpawnsShareGpuContext { 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 6a662381fd..f716407e3a 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 @@ -11,6 +11,7 @@ #include "flutter/fml/logging.h" #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine_Internal.h" +#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterSharedApplication.h" #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.h" #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterViewController_Internal.h" #import "flutter/shell/platform/darwin/ios/framework/Source/UIViewController+FlutterScreenAndSceneIfLoaded.h" @@ -22,9 +23,7 @@ namespace { constexpr char kTextPlainFormat[] = "text/plain"; const UInt32 kKeyPressClickSoundId = 1306; -#if not APPLICATION_EXTENSION_API_ONLY NSString* const kSearchURLPrefix = @"x-web-search://?"; -#endif } // namespace @@ -45,21 +44,23 @@ const char* const kOverlayStyleUpdateNotificationKey = using namespace flutter; static void SetStatusBarHiddenForSharedApplication(BOOL hidden) { -#if not APPLICATION_EXTENSION_API_ONLY - [UIApplication sharedApplication].statusBarHidden = hidden; -#else - FML_LOG(WARNING) << "Application based status bar styling is not available in app extension."; -#endif + UIApplication* flutterApplication = FlutterSharedApplication.application; + if (flutterApplication) { + flutterApplication.statusBarHidden = hidden; + } else { + FML_LOG(WARNING) << "Application based status bar styling is not available in app extension."; + } } static void SetStatusBarStyleForSharedApplication(UIStatusBarStyle style) { -#if not 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 + UIApplication* flutterApplication = FlutterSharedApplication.application; + if (flutterApplication) { + // Note: -[UIApplication setStatusBarStyle] is deprecated in iOS9 + // in favor of delegating to the view controller. + [flutterApplication setStatusBarStyle:style]; + } else { + FML_LOG(WARNING) << "Application based status bar styling is not available in app extension."; + } } @interface FlutterPlatformPlugin () @@ -220,18 +221,18 @@ static void SetStatusBarStyleForSharedApplication(UIStatusBarStyle style) { } - (void)searchWeb:(NSString*)searchTerm { -#if APPLICATION_EXTENSION_API_ONLY - FML_LOG(WARNING) << "SearchWeb.invoke is not availabe in app extension."; -#else + UIApplication* flutterApplication = FlutterSharedApplication.application; + if (flutterApplication == nil) { + FML_LOG(WARNING) << "SearchWeb.invoke is not availabe in app extension."; + return; + } + NSString* escapedText = [searchTerm stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLHostAllowedCharacterSet]]; NSString* searchURL = [NSString stringWithFormat:@"%@%@", kSearchURLPrefix, escapedText]; - [[UIApplication sharedApplication] openURL:[NSURL URLWithString:searchURL] - options:@{} - completionHandler:nil]; -#endif + [flutterApplication openURL:[NSURL URLWithString:searchURL] options:@{} completionHandler:nil]; } - (void)playSystemSound:(NSString*)soundType { @@ -381,17 +382,19 @@ static void SetStatusBarStyleForSharedApplication(UIStatusBarStyle style) { [navigationController popViewControllerAnimated:isAnimated]; } else { UIViewController* rootViewController = nil; -#if APPLICATION_EXTENSION_API_ONLY - if (@available(iOS 15.0, *)) { - rootViewController = - [engineViewController flutterWindowSceneIfViewLoaded].keyWindow.rootViewController; + UIApplication* flutterApplication = FlutterSharedApplication.application; + if (flutterApplication) { + rootViewController = flutterApplication.keyWindow.rootViewController; } else { - FML_LOG(WARNING) - << "rootViewController is not available in application extension prior to iOS 15.0."; + if (@available(iOS 15.0, *)) { + rootViewController = + [engineViewController flutterWindowSceneIfViewLoaded].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/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformPluginTest.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformPluginTest.mm index 779a2158c5..d3eec569a9 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 @@ -31,6 +31,7 @@ FLUTTER_ASSERT_ARC @end @implementation FlutterPlatformPluginTest + - (void)testSearchWebInvokedWithEscapedTerm { id mockApplication = OCMClassMock([UIApplication class]); OCMStub([mockApplication sharedApplication]).andReturn(mockApplication); @@ -49,11 +50,9 @@ FLUTTER_ASSERT_ARC 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]; }; @@ -62,10 +61,14 @@ FLUTTER_ASSERT_ARC [mockApplication stopMocking]; } -- (void)testSearchWebInvokedWithNonEscapedTerm { +- (void)testSearchWebSkippedIfAppExtension { + id mockBundle = OCMPartialMock([NSBundle mainBundle]); + OCMStub([mockBundle objectForInfoDictionaryKey:@"NSExtension"]).andReturn(@{ + @"NSExtensionPointIdentifier" : @"com.apple.share-services" + }); id mockApplication = OCMClassMock([UIApplication class]); OCMStub([mockApplication sharedApplication]).andReturn(mockApplication); - + OCMReject([mockApplication openURL:OCMOCK_ANY options:OCMOCK_ANY completionHandler:OCMOCK_ANY]); FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"test" project:nil]; [engine runWithEntrypoint:nil]; @@ -80,16 +83,13 @@ FLUTTER_ASSERT_ARC 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]; }; [mockPlugin handleMethodCall:methodCall result:result]; [self waitForExpectationsWithTimeout:1 handler:nil]; + [mockBundle stopMocking]; [mockApplication stopMocking]; } @@ -389,37 +389,83 @@ FLUTTER_ASSERT_ARC FlutterViewController* flutterViewController = [[FlutterViewController alloc] initWithEngine:engine nibName:nil bundle:nil]; - // Update to hidden. + // Update the visibility of the status bar to hidden. FlutterPlatformPlugin* plugin = [engine platformPlugin]; - XCTestExpectation* enableSystemUIOverlaysCalled = + XCTestExpectation* systemOverlaysBottomExpectation = [self expectationWithDescription:@"setEnabledSystemUIOverlays"]; - FlutterResult resultSet = ^(id result) { - [enableSystemUIOverlaysCalled fulfill]; + FlutterResult systemOverlaysBottomResult = ^(id result) { + [systemOverlaysBottomExpectation fulfill]; }; - FlutterMethodCall* methodCallSet = + FlutterMethodCall* setSystemOverlaysBottomCall = [FlutterMethodCall methodCallWithMethodName:@"SystemChrome.setEnabledSystemUIOverlays" arguments:@[ @"SystemUiOverlay.bottom" ]]; - [plugin handleMethodCall:methodCallSet result:resultSet]; + [plugin handleMethodCall:setSystemOverlaysBottomCall result:systemOverlaysBottomResult]; [self waitForExpectationsWithTimeout:1 handler:nil]; -#if not APPLICATION_EXTENSION_API_ONLY OCMVerify([mockApplication setStatusBarHidden:YES]); -#endif - // Update to shown. - XCTestExpectation* enableSystemUIOverlaysCalled2 = + // Update the visibility of the status bar to shown. + XCTestExpectation* systemOverlaysTopExpectation = [self expectationWithDescription:@"setEnabledSystemUIOverlays"]; - FlutterResult resultSet2 = ^(id result) { - [enableSystemUIOverlaysCalled2 fulfill]; + FlutterResult systemOverlaysTopResult = ^(id result) { + [systemOverlaysTopExpectation fulfill]; }; - FlutterMethodCall* methodCallSet2 = + FlutterMethodCall* setSystemOverlaysTopCall = [FlutterMethodCall methodCallWithMethodName:@"SystemChrome.setEnabledSystemUIOverlays" arguments:@[ @"SystemUiOverlay.top" ]]; - [plugin handleMethodCall:methodCallSet2 result:resultSet2]; + [plugin handleMethodCall:setSystemOverlaysTopCall result:systemOverlaysTopResult]; [self waitForExpectationsWithTimeout:1 handler:nil]; -#if not APPLICATION_EXTENSION_API_ONLY OCMVerify([mockApplication setStatusBarHidden:NO]); -#endif + + [flutterViewController deregisterNotifications]; + [mockApplication stopMocking]; + [bundleMock stopMocking]; +} + +- (void)testStatusBarHiddenNotUpdatedInAppExtension { + id bundleMock = OCMPartialMock([NSBundle mainBundle]); + OCMStub([bundleMock objectForInfoDictionaryKey:@"UIViewControllerBasedStatusBarAppearance"]) + .andReturn(@NO); + OCMStub([bundleMock objectForInfoDictionaryKey:@"NSExtension"]).andReturn(@{ + @"NSExtensionPointIdentifier" : @"com.apple.share-services" + }); + id mockApplication = OCMClassMock([UIApplication class]); + OCMStub([mockApplication sharedApplication]).andReturn(mockApplication); + OCMReject([mockApplication setStatusBarHidden:OCMOCK_ANY]); + + // Enabling system UI overlays to update status bar. + FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"test" project:nil]; + [engine runWithEntrypoint:nil]; + FlutterViewController* flutterViewController = + [[FlutterViewController alloc] initWithEngine:engine nibName:nil bundle:nil]; + + // Update the visibility of the status bar to hidden does not occur with extensions. + FlutterPlatformPlugin* plugin = [engine platformPlugin]; + + XCTestExpectation* systemOverlaysBottomExpectation = + [self expectationWithDescription:@"setEnabledSystemUIOverlays"]; + FlutterResult systemOverlaysBottomResult = ^(id result) { + [systemOverlaysBottomExpectation fulfill]; + }; + FlutterMethodCall* setSystemOverlaysBottomCall = + [FlutterMethodCall methodCallWithMethodName:@"SystemChrome.setEnabledSystemUIOverlays" + arguments:@[ @"SystemUiOverlay.bottom" ]]; + [plugin handleMethodCall:setSystemOverlaysBottomCall result:systemOverlaysBottomResult]; + [self waitForExpectationsWithTimeout:1 handler:nil]; + OCMReject([mockApplication setStatusBarHidden:YES]); + + // Update the visibility of the status bar to shown does not occur with extensions. + XCTestExpectation* systemOverlaysTopExpectation = + [self expectationWithDescription:@"setEnabledSystemUIOverlays"]; + FlutterResult systemOverlaysTopResult = ^(id result) { + [systemOverlaysTopExpectation fulfill]; + }; + FlutterMethodCall* setSystemOverlaysTopCall = + [FlutterMethodCall methodCallWithMethodName:@"SystemChrome.setEnabledSystemUIOverlays" + arguments:@[ @"SystemUiOverlay.top" ]]; + [plugin handleMethodCall:setSystemOverlaysTopCall result:systemOverlaysTopResult]; + [self waitForExpectationsWithTimeout:1 handler:nil]; + OCMReject([mockApplication setStatusBarHidden:NO]); [flutterViewController deregisterNotifications]; [mockApplication stopMocking]; @@ -452,9 +498,42 @@ FLUTTER_ASSERT_ARC [plugin handleMethodCall:methodCallSet result:resultSet]; [self waitForExpectationsWithTimeout:1 handler:nil]; -#if not APPLICATION_EXTENSION_API_ONLY OCMVerify([mockApplication setStatusBarStyle:UIStatusBarStyleLightContent]); -#endif + + [flutterViewController deregisterNotifications]; + [mockApplication stopMocking]; + [bundleMock stopMocking]; +} + +- (void)testStatusBarStyleNotUpdatedInAppExtension { + id bundleMock = OCMPartialMock([NSBundle mainBundle]); + OCMStub([bundleMock objectForInfoDictionaryKey:@"UIViewControllerBasedStatusBarAppearance"]) + .andReturn(@NO); + OCMStub([bundleMock objectForInfoDictionaryKey:@"NSExtension"]).andReturn(@{ + @"NSExtensionPointIdentifier" : @"com.apple.share-services" + }); + id mockApplication = OCMClassMock([UIApplication class]); + OCMStub([mockApplication sharedApplication]).andReturn(mockApplication); + OCMReject([mockApplication setStatusBarHidden:OCMOCK_ANY]); + + FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"test" project:nil]; + [engine runWithEntrypoint:nil]; + FlutterViewController* flutterViewController = + [[FlutterViewController alloc] initWithEngine:engine nibName:nil bundle:nil]; + XCTAssertFalse(flutterViewController.prefersStatusBarHidden); + + FlutterPlatformPlugin* plugin = [engine platformPlugin]; + + XCTestExpectation* enableSystemUIModeCalled = + [self expectationWithDescription:@"setSystemUIOverlayStyle"]; + FlutterResult resultSet = ^(id result) { + [enableSystemUIModeCalled fulfill]; + }; + FlutterMethodCall* methodCallSet = + [FlutterMethodCall methodCallWithMethodName:@"SystemChrome.setSystemUIOverlayStyle" + arguments:@{@"statusBarBrightness" : @"Brightness.dark"}]; + [plugin handleMethodCall:methodCallSet result:resultSet]; + [self waitForExpectationsWithTimeout:1 handler:nil]; [flutterViewController deregisterNotifications]; [mockApplication stopMocking]; 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 11451d38e4..7b9dcdf074 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 @@ -8,6 +8,7 @@ #include "flutter/fml/paths.h" #include "flutter/lib/ui/plugins/callback_cache.h" #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterCallbackCache_Internal.h" +#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterSharedApplication.h" FLUTTER_ASSERT_ARC @@ -45,18 +46,18 @@ static const SEL kSelectorsHandledByPlugins[] = { if (self = [super 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 - selector:@selector(handleWillEnterForeground:)]; - [self addObserverFor:UIApplicationWillResignActiveNotification - selector:@selector(handleWillResignActive:)]; - [self addObserverFor:UIApplicationDidBecomeActiveNotification - selector:@selector(handleDidBecomeActive:)]; - [self addObserverFor:UIApplicationWillTerminateNotification - selector:@selector(handleWillTerminate:)]; -#endif + if (FlutterSharedApplication.isAvailable) { + [self addObserverFor:UIApplicationDidEnterBackgroundNotification + selector:@selector(handleDidEnterBackground:)]; + [self addObserverFor:UIApplicationWillEnterForegroundNotification + selector:@selector(handleWillEnterForeground:)]; + [self addObserverFor:UIApplicationWillResignActiveNotification + selector:@selector(handleWillResignActive:)]; + [self addObserverFor:UIApplicationDidBecomeActiveNotification + selector:@selector(handleDidBecomeActive:)]; + [self addObserverFor:UIApplicationWillTerminateNotification + selector:@selector(handleWillTerminate:)]; + } _delegates = [NSPointerArray weakObjectsPointerArray]; _debugBackgroundTask = UIBackgroundTaskInvalid; } 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 d96ae4cb90..e82861b24d 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 @@ -26,7 +26,6 @@ FLUTTER_ASSERT_ARC XCTAssertNotNil(delegate); } -#if not APPLICATION_EXTENSION_API_ONLY - (void)testDidEnterBackground { XCTNSNotificationExpectation* expectation = [[XCTNSNotificationExpectation alloc] initWithName:UIApplicationDidEnterBackgroundNotification]; @@ -110,6 +109,4 @@ FLUTTER_ASSERT_ARC XCTAssertNil(weakDelegate); } -#endif - @end diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterSharedApplication.h b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterSharedApplication.h new file mode 100644 index 0000000000..f7882505da --- /dev/null +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterSharedApplication.h @@ -0,0 +1,29 @@ +// 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. + +#ifndef FLUTTER_SHELL_PLATFORM_DARWIN_IOS_FRAMEWORK_SOURCE_FLUTTERSHAREDAPPLICATION_H_ +#define FLUTTER_SHELL_PLATFORM_DARWIN_IOS_FRAMEWORK_SOURCE_FLUTTERSHAREDAPPLICATION_H_ + +#import + +@interface FlutterSharedApplication : NSObject + +/** + * Returns YES if the main bundle is an iOS App Extension. + */ +@property(class, nonatomic, readonly) BOOL isAppExtension; + +/** + * Returns YES if the UIApplication is available. UIApplication is not available for App Extensions. + */ +@property(class, nonatomic, readonly) BOOL isAvailable; + +/** + * Returns the `UIApplication.sharedApplication` is available. Otherwise returns nil. + */ +@property(class, nonatomic, readonly) UIApplication* application; + +@end + +#endif // FLUTTER_SHELL_PLATFORM_DARWIN_IOS_FRAMEWORK_SOURCE_FLUTTERSHAREDAPPLICATION_H_ diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterSharedApplication.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterSharedApplication.mm new file mode 100644 index 0000000000..4c941b3607 --- /dev/null +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterSharedApplication.mm @@ -0,0 +1,37 @@ +// 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. + +#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterSharedApplication.h" + +#include "flutter/fml/logging.h" +#import "flutter/shell/platform/darwin/common/framework/Headers/FlutterMacros.h" + +FLUTTER_ASSERT_ARC + +@implementation FlutterSharedApplication + ++ (BOOL)isAppExtension { + NSDictionary* nsExtension = [NSBundle.mainBundle objectForInfoDictionaryKey:@"NSExtension"]; + return [nsExtension isKindOfClass:[NSDictionary class]]; +} + ++ (BOOL)isAvailable { + // If the bundle is an App Extension, the application is not available. + // Therefore access to `UIApplication.sharedApplication` is not allowed. + return !FlutterSharedApplication.isAppExtension; +} + ++ (UIApplication*)application { + if (FlutterSharedApplication.isAvailable) { + return FlutterSharedApplication.sharedApplication; + } + return nil; +} + ++ (UIApplication*) + sharedApplication NS_EXTENSION_UNAVAILABLE_IOS("Accesses unavailable sharedApplication.") { + return UIApplication.sharedApplication; +} + +@end diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterSharedApplicationTest.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterSharedApplicationTest.mm new file mode 100644 index 0000000000..2f4553b5c9 --- /dev/null +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterSharedApplicationTest.mm @@ -0,0 +1,66 @@ +// 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. + +#import +#import + +#include "flutter/common/constants.h" +#include "flutter/shell/platform/darwin/common/framework/Headers/FlutterMacros.h" +#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterSharedApplication.h" + +FLUTTER_ASSERT_ARC + +@interface FlutterSharedApplicationTest : XCTestCase +@end + +@implementation FlutterSharedApplicationTest + +- (void)testWhenNSExtensionInBundle { + id mockBundle = OCMPartialMock([NSBundle mainBundle]); + OCMStub([mockBundle objectForInfoDictionaryKey:@"NSExtension"]).andReturn(@{ + @"NSExtensionPointIdentifier" : @"com.apple.share-services" + }); + XCTAssertTrue(FlutterSharedApplication.isAppExtension); + XCTAssertFalse(FlutterSharedApplication.isAvailable); + [mockBundle stopMocking]; +} + +- (void)testWhenNSExtensionEmptyInBundle { + id mockBundle = OCMPartialMock([NSBundle mainBundle]); + OCMStub([mockBundle objectForInfoDictionaryKey:@"NSExtension"]) + .andReturn([[NSDictionary alloc] init]); + XCTAssertTrue(FlutterSharedApplication.isAppExtension); + XCTAssertFalse(FlutterSharedApplication.isAvailable); + [mockBundle stopMocking]; +} + +- (void)testWhenNSExtensionNotInBundle { + id mockBundle = OCMPartialMock([NSBundle mainBundle]); + XCTAssertFalse(FlutterSharedApplication.isAppExtension); + XCTAssertTrue(FlutterSharedApplication.isAvailable); + [mockBundle stopMocking]; +} + +- (void)testSharedApplicationNotCalledIfIsAvailableFalse { + id mockBundle = OCMPartialMock([NSBundle mainBundle]); + OCMStub([mockBundle objectForInfoDictionaryKey:@"NSExtension"]).andReturn(@{ + @"NSExtensionPointIdentifier" : @"com.apple.share-services" + }); + id mockApplication = OCMClassMock([UIApplication class]); + XCTAssertFalse(FlutterSharedApplication.isAvailable); + OCMReject([mockApplication sharedApplication]); + XCTAssertNil(FlutterSharedApplication.application); + [mockBundle stopMocking]; +} + +- (void)testSharedApplicationCalledIfIsAvailableTrue { + id mockBundle = OCMPartialMock([NSBundle mainBundle]); + id mockApplication = OCMClassMock([UIApplication class]); + XCTAssertTrue(FlutterSharedApplication.isAvailable); + XCTAssertNotNil(FlutterSharedApplication.application); + OCMVerify([mockApplication sharedApplication]); + [mockBundle stopMocking]; +} + +@end diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm index 2b53e96b59..827cd377c1 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm @@ -12,6 +12,7 @@ #include "flutter/fml/logging.h" #include "flutter/fml/platform/darwin/string_range_sanitization.h" +#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterSharedApplication.h" FLUTTER_ASSERT_ARC @@ -2657,7 +2658,12 @@ static BOOL IsSelectionRectBoundaryCloserToPoint(CGPoint point, - (void)hideKeyboardWithoutAnimationAndAvoidCursorDismissUpdate { [UIView setAnimationsEnabled:NO]; - _cachedFirstResponder = UIApplication.sharedApplication.keyWindow.flutterFirstResponder; + UIApplication* flutterApplication = FlutterSharedApplication.application; + _cachedFirstResponder = + flutterApplication + ? flutterApplication.keyWindow.flutterFirstResponder + : self.viewController.flutterWindowSceneIfViewLoaded.keyWindow.flutterFirstResponder; + _activeView.preventCursorDismissWhenResignFirstResponder = YES; [_cachedFirstResponder resignFirstResponder]; _activeView.preventCursorDismissWhenResignFirstResponder = NO; @@ -2674,8 +2680,11 @@ static BOOL IsSelectionRectBoundaryCloserToPoint(CGPoint point, _keyboardView = keyboardSnap; [_keyboardViewContainer addSubview:_keyboardView]; if (_keyboardViewContainer.superview == nil) { - [UIApplication.sharedApplication.delegate.window.rootViewController.view - addSubview:_keyboardViewContainer]; + UIApplication* flutterApplication = FlutterSharedApplication.application; + UIView* rootView = flutterApplication + ? flutterApplication.delegate.window.rootViewController.view + : self.viewController.viewIfLoaded.window.rootViewController.view; + [rootView addSubview:_keyboardViewContainer]; } _keyboardViewContainer.layer.zPosition = NSIntegerMax; _keyboardViewContainer.frame = _keyboardRect; diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterView.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterView.mm index dd37e8a553..d1f813b13a 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterView.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterView.mm @@ -5,6 +5,7 @@ #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterView.h" #include "flutter/fml/platform/darwin/cf_utils.h" +#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterSharedApplication.h" #import "flutter/shell/platform/darwin/ios/framework/Source/SemanticsObject.h" FLUTTER_ASSERT_ARC @@ -53,6 +54,12 @@ FLUTTER_ASSERT_ARC - (BOOL)isWideGamutSupported { FML_DCHECK(self.screen); + // Wide Gamut is not supported for iOS Extensions due to memory limitations + // (see https://github.com/flutter/flutter/issues/165086). + if (FlutterSharedApplication.isAppExtension) { + return NO; + } + // This predicates the decision on the capabilities of the iOS device's // display. This means external displays will not support wide gamut if the // device's display doesn't support it. It practice that should be never. 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 a38b268a65..2556a5b063 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 @@ -23,6 +23,7 @@ #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterKeyboardManager.h" #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformPlugin.h" #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h" +#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterSharedApplication.h" #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputDelegate.h" #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.h" #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterView.h" @@ -319,15 +320,15 @@ typedef struct MouseState { name:@(flutter::kOverlayStyleUpdateNotificationName) object:nil]; -#if APPLICATION_EXTENSION_API_ONLY - if (@available(iOS 13.0, *)) { - [self setUpSceneLifecycleNotifications:center]; - } else { + if (FlutterSharedApplication.isAvailable) { [self setUpApplicationLifecycleNotifications:center]; + } else { + if (@available(iOS 13.0, *)) { + [self setUpSceneLifecycleNotifications:center]; + } else { + [self setUpApplicationLifecycleNotifications:center]; + } } -#else - [self setUpApplicationLifecycleNotifications:center]; -#endif [center addObserver:self selector:@selector(keyboardWillChangeFrame:) @@ -722,6 +723,58 @@ static void SendFakeTouchEvent(UIScreen* screen, _flutterViewRenderedCallback = callback; } +- (UISceneActivationState)activationState { + return self.flutterWindowSceneIfViewLoaded.activationState; +} + +- (BOOL)stateIsActive { + BOOL isActive = YES; + + UIApplication* flutterApplication = FlutterSharedApplication.application; + if (flutterApplication) { + isActive = [self isApplicationStateMatching:UIApplicationStateActive + withApplication:flutterApplication]; + } else if (@available(iOS 13.0, *)) { + isActive = [self isSceneStateMatching:UISceneActivationStateForegroundActive]; + } + return isActive; +} + +- (BOOL)stateIsBackground { + // [UIApplication sharedApplication API is not available for app extension. + // Assume the app is not in the background if we're unable to get the state. + BOOL isBackground = NO; + + UIApplication* flutterApplication = FlutterSharedApplication.application; + if (flutterApplication) { + isBackground = [self isApplicationStateMatching:UIApplicationStateBackground + withApplication:flutterApplication]; + } else if (@available(iOS 13.0, *)) { + isBackground = [self isSceneStateMatching:UISceneActivationStateBackground]; + } + return isBackground; +} + +- (BOOL)isApplicationStateMatching:(UIApplicationState)match + withApplication:(UIApplication*)application { + switch (application.applicationState) { + case UIApplicationStateActive: + case UIApplicationStateInactive: + case UIApplicationStateBackground: + return application.applicationState == match; + } +} + +- (BOOL)isSceneStateMatching:(UISceneActivationState)match API_AVAILABLE(ios(13.0)) { + switch (self.activationState) { + case UISceneActivationStateForegroundActive: + case UISceneActivationStateUnattached: + case UISceneActivationStateForegroundInactive: + case UISceneActivationStateBackground: + return self.activationState == match; + } +} + #pragma mark - Surface creation and teardown updates - (void)surfaceUpdated:(BOOL)appeared { @@ -848,16 +901,8 @@ static void SendFakeTouchEvent(UIScreen* screen, if (self.engine.viewController == self) { [self onUserSettingsChanged:nil]; [self onAccessibilityStatusChanged:nil]; - BOOL stateIsActive = YES; -#if APPLICATION_EXTENSION_API_ONLY - if (@available(iOS 13.0, *)) { - stateIsActive = self.flutterWindowSceneIfViewLoaded.activationState == - UISceneActivationStateForegroundActive; - } -#else - stateIsActive = UIApplication.sharedApplication.applicationState == UIApplicationStateActive; -#endif - if (stateIsActive) { + + if (self.stateIsActive) { [self.engine.lifecycleChannel sendMessage:@"AppLifecycleState.resumed"]; } } @@ -1385,20 +1430,9 @@ static flutter::PointerData::DeviceKind DeviceKindFromTouchType(UITouch* touch) // 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.flutterWindowSceneIfViewLoaded.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 && applicationOrSceneIsActive && self.engine) { + if (firstViewBoundsUpdate && self.stateIsActive && self.engine) { [self surfaceUpdated:YES]; #if FLUTTER_RUNTIME_MODE == FLUTTER_RUNTIME_MODE_DEBUG NSTimeInterval timeout = 0.2; @@ -2003,18 +2037,17 @@ static flutter::PointerData::DeviceKind DeviceKindFromTouchType(UITouch* touch) self.orientationPreferences = new_preferences; if (@available(iOS 16.0, *)) { - NSSet* scenes = -#if APPLICATION_EXTENSION_API_ONLY - self.flutterWindowSceneIfViewLoaded - ? [NSSet setWithObject:self.flutterWindowSceneIfViewLoaded] - : [NSSet set]; -#else - [UIApplication.sharedApplication.connectedScenes - filteredSetUsingPredicate:[NSPredicate predicateWithBlock:^BOOL( - id scene, NSDictionary* bindings) { - return [scene isKindOfClass:[UIWindowScene class]]; - }]]; -#endif + UIApplication* flutterApplication = FlutterSharedApplication.application; + NSSet* scenes = [NSSet set]; + if (flutterApplication) { + scenes = [flutterApplication.connectedScenes + filteredSetUsingPredicate:[NSPredicate predicateWithBlock:^BOOL( + id scene, NSDictionary* bindings) { + return [scene isKindOfClass:[UIWindowScene class]]; + }]]; + } else if (self.flutterWindowSceneIfViewLoaded) { + scenes = [NSSet setWithObject:self.flutterWindowSceneIfViewLoaded]; + } [self requestGeometryUpdateForWindowScenes:scenes]; } else { UIInterfaceOrientationMask currentInterfaceOrientation = 0; @@ -2027,13 +2060,14 @@ 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 + UIApplication* flutterApplication = FlutterSharedApplication.application; + if (flutterApplication) { + currentInterfaceOrientation = 1 << [flutterApplication statusBarOrientation]; + } else { + FML_LOG(ERROR) << "Application based status bar orentiation update is not supported in " + "app extension. Orientation: " + << currentInterfaceOrientation; + } } if (!(self.orientationPreferences & currentInterfaceOrientation)) { [UIViewController attemptRotationToDeviceOrientation]; @@ -2167,11 +2201,13 @@ 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; + UIApplication* flutterApplication = FlutterSharedApplication.application; + if (flutterApplication == nil) { + FML_LOG(WARNING) << "Dynamic content size update is not supported in app extension."; + return 1.0; + } + + UIContentSizeCategory category = flutterApplication.preferredContentSizeCategory; // The delta is computed by approximating Apple's typography guidelines: // https://developer.apple.com/ios/human-interface-guidelines/visual-design/typography/ // @@ -2221,7 +2257,6 @@ static flutter::PointerData::DeviceKind DeviceKindFromTouchType(UITouch* touch) } else { return 1.0; } -#endif } - (BOOL)supportsShowingSystemContextMenu { diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.mm index 2803040861..654716535f 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.mm @@ -1650,12 +1650,12 @@ extern NSNotificationName const FlutterViewControllerWillDealloc; FlutterViewController* realVC = [[FlutterViewController alloc] initWithEngine:engine nibName:nil bundle:nil]; - [[NSNotificationCenter defaultCenter] addObserverForName:FlutterViewControllerWillDealloc - object:nil - queue:[NSOperationQueue mainQueue] - usingBlock:^(NSNotification* _Nonnull note) { - [expectation fulfill]; - }]; + [NSNotificationCenter.defaultCenter addObserverForName:FlutterViewControllerWillDealloc + object:nil + queue:[NSOperationQueue mainQueue] + usingBlock:^(NSNotification* _Nonnull note) { + [expectation fulfill]; + }]; XCTAssertNotNil(realVC); realVC = nil; } @@ -1696,8 +1696,8 @@ extern NSNotificationName const FlutterViewControllerWillDealloc; nibName:nil bundle:nil]; XCTAssertFalse(realVC.prefersHomeIndicatorAutoHidden, @""); - [[NSNotificationCenter defaultCenter] postNotificationName:FlutterViewControllerHideHomeIndicator - object:nil]; + [NSNotificationCenter.defaultCenter postNotificationName:FlutterViewControllerHideHomeIndicator + object:nil]; XCTAssertTrue(realVC.prefersHomeIndicatorAutoHidden, @""); engine.viewController = nil; } @@ -1906,7 +1906,7 @@ extern NSNotificationName const FlutterViewControllerWillDealloc; [flutterViewController setSplashScreenView:nil]; } -- (void)testLifeCycleNotificationBecameActive { +- (void)testLifeCycleNotificationApplicationBecameActive { FlutterEngine* engine = [[FlutterEngine alloc] init]; [engine runWithEntrypoint:nil]; FlutterViewController* flutterViewController = @@ -1922,15 +1922,10 @@ extern NSNotificationName const FlutterViewControllerWillDealloc; 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 + [NSNotificationCenter.defaultCenter postNotification:sceneNotification]; + [NSNotificationCenter.defaultCenter postNotification:applicationNotification]; OCMReject([mockVC sceneBecameActive:[OCMArg any]]); OCMVerify([mockVC applicationBecameActive:[OCMArg any]]); -#endif XCTAssertFalse(flutterViewController.isKeyboardInOrTransitioningFromBackground); OCMVerify([mockVC surfaceUpdated:YES]); XCTestExpectation* timeoutApplicationLifeCycle = @@ -1944,7 +1939,45 @@ extern NSNotificationName const FlutterViewControllerWillDealloc; [self waitForExpectationsWithTimeout:5.0 handler:nil]; } -- (void)testLifeCycleNotificationWillResignActive { +- (void)testLifeCycleNotificationSceneBecameActive { + id mockBundle = OCMPartialMock([NSBundle mainBundle]); + OCMStub([mockBundle objectForInfoDictionaryKey:@"NSExtension"]).andReturn(@{ + @"NSExtensionPointIdentifier" : @"com.apple.share-services" + }); + 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]; + OCMVerify([mockVC sceneBecameActive:[OCMArg any]]); + OCMReject([mockVC applicationBecameActive:[OCMArg any]]); + XCTAssertFalse(flutterViewController.isKeyboardInOrTransitioningFromBackground); + OCMVerify([mockVC surfaceUpdated:YES]); + XCTestExpectation* timeoutApplicationLifeCycle = + [self expectationWithDescription:@"timeoutApplicationLifeCycle"]; + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), + dispatch_get_main_queue(), ^{ + [timeoutApplicationLifeCycle fulfill]; + OCMVerify([mockVC goToApplicationLifecycle:@"AppLifecycleState.resumed"]); + [flutterViewController deregisterNotifications]; + }); + [self waitForExpectationsWithTimeout:5.0 handler:nil]; + [mockBundle stopMocking]; +} + +- (void)testLifeCycleNotificationApplicationWillResignActive { FlutterEngine* engine = [[FlutterEngine alloc] init]; [engine runWithEntrypoint:nil]; FlutterViewController* flutterViewController = @@ -1958,20 +1991,42 @@ extern NSNotificationName const FlutterViewControllerWillDealloc; 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 + [NSNotificationCenter.defaultCenter postNotification:sceneNotification]; + [NSNotificationCenter.defaultCenter postNotification:applicationNotification]; OCMReject([mockVC sceneWillResignActive:[OCMArg any]]); OCMVerify([mockVC applicationWillResignActive:[OCMArg any]]); -#endif OCMVerify([mockVC goToApplicationLifecycle:@"AppLifecycleState.inactive"]); [flutterViewController deregisterNotifications]; } -- (void)testLifeCycleNotificationWillTerminate { +- (void)testLifeCycleNotificationSceneWillResignActive { + id mockBundle = OCMPartialMock([NSBundle mainBundle]); + OCMStub([mockBundle objectForInfoDictionaryKey:@"NSExtension"]).andReturn(@{ + @"NSExtensionPointIdentifier" : @"com.apple.share-services" + }); + 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]; + OCMVerify([mockVC sceneWillResignActive:[OCMArg any]]); + OCMReject([mockVC applicationWillResignActive:[OCMArg any]]); + OCMVerify([mockVC goToApplicationLifecycle:@"AppLifecycleState.inactive"]); + [flutterViewController deregisterNotifications]; + [mockBundle stopMocking]; +} + +- (void)testLifeCycleNotificationApplicationWillTerminate { FlutterEngine* engine = [[FlutterEngine alloc] init]; [engine runWithEntrypoint:nil]; FlutterViewController* flutterViewController = @@ -1987,21 +2042,46 @@ extern NSNotificationName const FlutterViewControllerWillDealloc; 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 + [NSNotificationCenter.defaultCenter postNotification:sceneNotification]; + [NSNotificationCenter.defaultCenter postNotification:applicationNotification]; OCMReject([mockVC sceneWillDisconnect:[OCMArg any]]); OCMVerify([mockVC applicationWillTerminate:[OCMArg any]]); -#endif OCMVerify([mockVC goToApplicationLifecycle:@"AppLifecycleState.detached"]); OCMVerify([mockEngine destroyContext]); [flutterViewController deregisterNotifications]; } -- (void)testLifeCycleNotificationDidEnterBackground { +- (void)testLifeCycleNotificationSceneWillTerminate { + id mockBundle = OCMPartialMock([NSBundle mainBundle]); + OCMStub([mockBundle objectForInfoDictionaryKey:@"NSExtension"]).andReturn(@{ + @"NSExtensionPointIdentifier" : @"com.apple.share-services" + }); + 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]; + OCMVerify([mockVC sceneWillDisconnect:[OCMArg any]]); + OCMReject([mockVC applicationWillTerminate:[OCMArg any]]); + OCMVerify([mockVC goToApplicationLifecycle:@"AppLifecycleState.detached"]); + OCMVerify([mockEngine destroyContext]); + [flutterViewController deregisterNotifications]; + [mockBundle stopMocking]; +} + +- (void)testLifeCycleNotificationApplicationDidEnterBackground { FlutterEngine* engine = [[FlutterEngine alloc] init]; [engine runWithEntrypoint:nil]; FlutterViewController* flutterViewController = @@ -2015,22 +2095,46 @@ extern NSNotificationName const FlutterViewControllerWillDealloc; 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 + [NSNotificationCenter.defaultCenter postNotification:sceneNotification]; + [NSNotificationCenter.defaultCenter postNotification:applicationNotification]; 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 { +- (void)testLifeCycleNotificationSceneDidEnterBackground { + id mockBundle = OCMPartialMock([NSBundle mainBundle]); + OCMStub([mockBundle objectForInfoDictionaryKey:@"NSExtension"]).andReturn(@{ + @"NSExtensionPointIdentifier" : @"com.apple.share-services" + }); + 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]; + OCMVerify([mockVC sceneDidEnterBackground:[OCMArg any]]); + OCMReject([mockVC applicationDidEnterBackground:[OCMArg any]]); + XCTAssertTrue(flutterViewController.isKeyboardInOrTransitioningFromBackground); + OCMVerify([mockVC surfaceUpdated:NO]); + OCMVerify([mockVC goToApplicationLifecycle:@"AppLifecycleState.paused"]); + [flutterViewController deregisterNotifications]; + [mockBundle stopMocking]; +} + +- (void)testLifeCycleNotificationApplicationWillEnterForeground { FlutterEngine* engine = [[FlutterEngine alloc] init]; [engine runWithEntrypoint:nil]; FlutterViewController* flutterViewController = @@ -2044,19 +2148,41 @@ extern NSNotificationName const FlutterViewControllerWillDealloc; 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 + [NSNotificationCenter.defaultCenter postNotification:sceneNotification]; + [NSNotificationCenter.defaultCenter postNotification:applicationNotification]; OCMReject([mockVC sceneWillEnterForeground:[OCMArg any]]); OCMVerify([mockVC applicationWillEnterForeground:[OCMArg any]]); -#endif OCMVerify([mockVC goToApplicationLifecycle:@"AppLifecycleState.inactive"]); [flutterViewController deregisterNotifications]; } +- (void)testLifeCycleNotificationSceneWillEnterForeground { + id mockBundle = OCMPartialMock([NSBundle mainBundle]); + OCMStub([mockBundle objectForInfoDictionaryKey:@"NSExtension"]).andReturn(@{ + @"NSExtensionPointIdentifier" : @"com.apple.share-services" + }); + 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]; + OCMVerify([mockVC sceneWillEnterForeground:[OCMArg any]]); + OCMReject([mockVC applicationWillEnterForeground:[OCMArg any]]); + OCMVerify([mockVC goToApplicationLifecycle:@"AppLifecycleState.inactive"]); + [flutterViewController deregisterNotifications]; + [mockBundle stopMocking]; +} + - (void)testLifeCycleNotificationCancelledInvalidResumed { FlutterEngine* engine = [[FlutterEngine alloc] init]; [engine runWithEntrypoint:nil]; @@ -2071,12 +2197,9 @@ extern NSNotificationName const FlutterViewControllerWillDealloc; object:nil userInfo:nil]; id mockVC = OCMPartialMock(flutterViewController); - [[NSNotificationCenter defaultCenter] postNotification:applicationDidBecomeActiveNotification]; - [[NSNotificationCenter defaultCenter] postNotification:applicationWillResignActiveNotification]; -#if APPLICATION_EXTENSION_API_ONLY -#else + [NSNotificationCenter.defaultCenter postNotification:applicationDidBecomeActiveNotification]; + [NSNotificationCenter.defaultCenter postNotification:applicationWillResignActiveNotification]; OCMVerify([mockVC goToApplicationLifecycle:@"AppLifecycleState.inactive"]); -#endif XCTestExpectation* timeoutApplicationLifeCycle = [self expectationWithDescription:@"timeoutApplicationLifeCycle"]; @@ -2259,4 +2382,100 @@ extern NSNotificationName const FlutterViewControllerWillDealloc; } } +- (void)testStateIsActiveAndBackgroundWhenApplicationStateIsActive { + FlutterEngine* engine = [[FlutterEngine alloc] init]; + [engine runWithEntrypoint:nil]; + FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine + nibName:nil + bundle:nil]; + id mockApplication = OCMClassMock([UIApplication class]); + OCMStub([mockApplication applicationState]).andReturn(UIApplicationStateActive); + OCMStub([mockApplication sharedApplication]).andReturn(mockApplication); + XCTAssertTrue(viewController.stateIsActive); + XCTAssertFalse(viewController.stateIsBackground); +} + +- (void)testStateIsActiveAndBackgroundWhenApplicationStateIsBackground { + FlutterEngine* engine = [[FlutterEngine alloc] init]; + [engine runWithEntrypoint:nil]; + FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine + nibName:nil + bundle:nil]; + id mockApplication = OCMClassMock([UIApplication class]); + OCMStub([mockApplication applicationState]).andReturn(UIApplicationStateBackground); + OCMStub([mockApplication sharedApplication]).andReturn(mockApplication); + XCTAssertFalse(viewController.stateIsActive); + XCTAssertTrue(viewController.stateIsBackground); +} + +- (void)testStateIsActiveAndBackgroundWhenApplicationStateIsInactive { + FlutterEngine* engine = [[FlutterEngine alloc] init]; + [engine runWithEntrypoint:nil]; + FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine + nibName:nil + bundle:nil]; + id mockApplication = OCMClassMock([UIApplication class]); + OCMStub([mockApplication applicationState]).andReturn(UIApplicationStateInactive); + OCMStub([mockApplication sharedApplication]).andReturn(mockApplication); + XCTAssertFalse(viewController.stateIsActive); + XCTAssertFalse(viewController.stateIsBackground); +} + +- (void)testStateIsActiveAndBackgroundWhenSceneStateIsActive { + id mockBundle = OCMPartialMock([NSBundle mainBundle]); + OCMStub([mockBundle objectForInfoDictionaryKey:@"NSExtension"]).andReturn(@{ + @"NSExtensionPointIdentifier" : @"com.apple.share-services" + }); + FlutterEngine* engine = [[FlutterEngine alloc] init]; + [engine runWithEntrypoint:nil]; + FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine + nibName:nil + bundle:nil]; + id mockVC = OCMPartialMock(viewController); + OCMStub([mockVC activationState]).andReturn(UISceneActivationStateForegroundActive); + XCTAssertTrue(viewController.stateIsActive); + XCTAssertFalse(viewController.stateIsBackground); + + [mockBundle stopMocking]; + [mockVC stopMocking]; +} + +- (void)testStateIsActiveAndBackgroundWhenSceneStateIsBackground { + id mockBundle = OCMPartialMock([NSBundle mainBundle]); + OCMStub([mockBundle objectForInfoDictionaryKey:@"NSExtension"]).andReturn(@{ + @"NSExtensionPointIdentifier" : @"com.apple.share-services" + }); + FlutterEngine* engine = [[FlutterEngine alloc] init]; + [engine runWithEntrypoint:nil]; + FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine + nibName:nil + bundle:nil]; + id mockVC = OCMPartialMock(viewController); + OCMStub([mockVC activationState]).andReturn(UISceneActivationStateBackground); + XCTAssertFalse(viewController.stateIsActive); + XCTAssertTrue(viewController.stateIsBackground); + + [mockBundle stopMocking]; + [mockVC stopMocking]; +} + +- (void)testStateIsActiveAndBackgroundWhenSceneStateIsInactive { + id mockBundle = OCMPartialMock([NSBundle mainBundle]); + OCMStub([mockBundle objectForInfoDictionaryKey:@"NSExtension"]).andReturn(@{ + @"NSExtensionPointIdentifier" : @"com.apple.share-services" + }); + FlutterEngine* engine = [[FlutterEngine alloc] init]; + [engine runWithEntrypoint:nil]; + FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine + nibName:nil + bundle:nil]; + id mockVC = OCMPartialMock(viewController); + OCMStub([mockVC activationState]).andReturn(UISceneActivationStateForegroundInactive); + XCTAssertFalse(viewController.stateIsActive); + XCTAssertFalse(viewController.stateIsBackground); + + [mockBundle stopMocking]; + [mockVC stopMocking]; +} + @end diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterViewController_Internal.h b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterViewController_Internal.h index 3645d2c376..f62eec2bb2 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterViewController_Internal.h +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterViewController_Internal.h @@ -71,6 +71,8 @@ typedef void (^FlutterKeyboardAnimationCallback)(fml::TimePoint); - (int32_t)accessibilityFlags; - (BOOL)supportsShowingSystemContextMenu; +- (BOOL)stateIsActive; +- (BOOL)stateIsBackground; @end #endif // FLUTTER_SHELL_PLATFORM_DARWIN_IOS_FRAMEWORK_SOURCE_FLUTTERVIEWCONTROLLER_INTERNAL_H_ diff --git a/engine/src/flutter/testing/ios/IosUnitTests/IosUnitTests.xcodeproj/project.pbxproj b/engine/src/flutter/testing/ios/IosUnitTests/IosUnitTests.xcodeproj/project.pbxproj index f0211a07c6..c75a1e3309 100644 --- a/engine/src/flutter/testing/ios/IosUnitTests/IosUnitTests.xcodeproj/project.pbxproj +++ b/engine/src/flutter/testing/ios/IosUnitTests/IosUnitTests.xcodeproj/project.pbxproj @@ -71,6 +71,7 @@ 3DD7D38C27D2B81000DA365C /* FlutterUndoManagerPluginTest.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = FlutterUndoManagerPluginTest.mm; sourceTree = ""; }; 689EC1E2281B30D3008FEB58 /* FlutterSpellCheckPluginTest.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = FlutterSpellCheckPluginTest.mm; sourceTree = ""; }; 68B6091227F62F990036AC78 /* VsyncWaiterIosTest.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = VsyncWaiterIosTest.mm; sourceTree = ""; }; + 78E4ED342D88A77C00FD954E /* FlutterSharedApplicationTest.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = FlutterSharedApplicationTest.mm; sourceTree = ""; }; D2D361A52B234EAC0018964E /* FlutterMetalLayerTest.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = FlutterMetalLayerTest.mm; sourceTree = ""; }; F7521D7226BB671E005F15C5 /* libios_test_flutter.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libios_test_flutter.dylib; path = "../../../../out/$(FLUTTER_ENGINE)/libios_test_flutter.dylib"; sourceTree = ""; }; F7521D7526BB673E005F15C5 /* libocmock_shared.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libocmock_shared.dylib; path = "../../../../out/$(FLUTTER_ENGINE)/libocmock_shared.dylib"; sourceTree = ""; }; @@ -100,6 +101,7 @@ 0AC232E924BA71D300A85907 /* Source */ = { isa = PBXGroup; children = ( + 78E4ED342D88A77C00FD954E /* FlutterSharedApplicationTest.mm */, F76A3A892BE48F2F00A654F1 /* FlutterPlatformViewsTest.mm */, 689EC1E2281B30D3008FEB58 /* FlutterSpellCheckPluginTest.mm */, 68B6091227F62F990036AC78 /* VsyncWaiterIosTest.mm */,