forked from firka/flutter
Make iOS Flutter framework extension-safe (#165346)
Our current [adoption/documentation](https://docs.flutter.dev/platform-integration/ios/app-extensions) for iOS Extensions does not currently work because it's disallowed to nest bundles (see https://github.com/flutter/flutter/issues/142531). As of [Xcode 13](https://developer.apple.com/documentation/xcode-release-notes/xcode-13-release-notes/#:~:text=Linking%20Swift%20packages%20from%20application%20extension%20targets%20or%20watchOS%20applications%20no%20longer%20emits%20unresolvable%20warnings%20about%20linking%20to%20libraries%20not%20safe%20for%20use%20in%20application%20extensions%2E), linking to frameworks that contain non-extension-safe code no longer gives warnings (or blocks from App Store it seems). Therefore, it has become a runtime issue to ensure non-extension-safe code is not used. Previously, we were building and shipping 2 separate Flutter.xcframeworks. One that was extension-safe and one that was not. This PR removes the "extension_safe" framework and instead makes the entire framework extension-safe by annotating non-extension-safe code with `NS_EXTENSION_UNAVAILABLE_IOS` and ensuring that code is not used at runtime if the bundle is for an app extension. This PR also disables wide gamut for app extensions to decrease the chances of crashes (see https://github.com/flutter/flutter/issues/165086). Fixes https://github.com/flutter/flutter/issues/142531. --- For reference: App extensions were first evaluated in https://flutter.dev/go/app-extensions. Here is the reasoning why neither method described there is opportune. Option 1 - I did look into splitting the framework into 2 frameworks (one with all extension-safe code and one with the non-extension-safe code). However, the original idea was to use objc Categories/Extensions - this doesn’t quite fit our needs. Categories/Extensions only allow you to add new methods/properties, not override existing ones. We could hypothetically add new methods, but that would require the user to change their code to use the new methods. I also looked into subclasses which does allow overrides, but it would also require the user to change their code to use the new class. We could do method swizzling, but opinion of that on the internet is that it's not very safe. I’m of the opinion that anything that requires the user to change code isn’t super feasible due to plugins. Option 2 - We could still do the 2 frameworks but rename one to `FlutterExtentionSafe`. This works without users needing to change any code (including imports like `@import Flutter` / `#import <Flutter/Flutter.h>`). I believe the reason this works is because at compile time, it finds the `Flutter` framework on the framework search path and it imports in the headers. Then at link time, `FlutterExtentionSafe` is explicitly linked so it uses that framework first when checking for symbols and since it finds all the symbols in `FlutterExtentionSafe`, it doesn’t need/try to auto-link the `Flutter` framework (despite `Flutter` being the framework imported). This seems precarious to me since we’re relying on Xcode to not auto-link the `Flutter` framework. If for some reason `Flutter` framework did get auto-linked (such as the user using a symbol that’s not in the `FlutterExtensionSafe` framework but is in the `Flutter` framework - this is unlikely though), we’d get name collision issues ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
This commit is contained in:
committed by
GitHub
parent
3efb8cc359
commit
975a677529
@@ -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" ]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
#
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -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<UIApplicationOpenURLOptionsKey, id>*)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 {
|
||||
|
||||
@@ -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<void*>(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<flutter::Shell> 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];
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 <UIKit/UIKit.h>
|
||||
|
||||
@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_
|
||||
@@ -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
|
||||
@@ -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 <OCMock/OCMock.h>
|
||||
#import <XCTest/XCTest.h>
|
||||
|
||||
#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
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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<UIScene*>* 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<UIScene*>* 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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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_
|
||||
|
||||
@@ -71,6 +71,7 @@
|
||||
3DD7D38C27D2B81000DA365C /* FlutterUndoManagerPluginTest.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = FlutterUndoManagerPluginTest.mm; sourceTree = "<group>"; };
|
||||
689EC1E2281B30D3008FEB58 /* FlutterSpellCheckPluginTest.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = FlutterSpellCheckPluginTest.mm; sourceTree = "<group>"; };
|
||||
68B6091227F62F990036AC78 /* VsyncWaiterIosTest.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = VsyncWaiterIosTest.mm; sourceTree = "<group>"; };
|
||||
78E4ED342D88A77C00FD954E /* FlutterSharedApplicationTest.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = FlutterSharedApplicationTest.mm; sourceTree = "<group>"; };
|
||||
D2D361A52B234EAC0018964E /* FlutterMetalLayerTest.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = FlutterMetalLayerTest.mm; sourceTree = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
F7521D7526BB673E005F15C5 /* libocmock_shared.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libocmock_shared.dylib; path = "../../../../out/$(FLUTTER_ENGINE)/libocmock_shared.dylib"; sourceTree = "<group>"; };
|
||||
@@ -100,6 +101,7 @@
|
||||
0AC232E924BA71D300A85907 /* Source */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
78E4ED342D88A77C00FD954E /* FlutterSharedApplicationTest.mm */,
|
||||
F76A3A892BE48F2F00A654F1 /* FlutterPlatformViewsTest.mm */,
|
||||
689EC1E2281B30D3008FEB58 /* FlutterSpellCheckPluginTest.mm */,
|
||||
68B6091227F62F990036AC78 /* VsyncWaiterIosTest.mm */,
|
||||
|
||||
Reference in New Issue
Block a user