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:
Victoria Ashworth
2025-03-27 13:51:17 -05:00
committed by GitHub
parent 3efb8cc359
commit 975a677529
20 changed files with 794 additions and 274 deletions

View File

@@ -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" ]
}

View File

@@ -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

View File

@@ -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.
#

View File

@@ -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") {

View File

@@ -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 {

View File

@@ -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];

View File

@@ -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 {

View File

@@ -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];
}

View File

@@ -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];

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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_

View File

@@ -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

View File

@@ -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

View File

@@ -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;

View File

@@ -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.

View File

@@ -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 {

View File

@@ -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

View File

@@ -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_

View File

@@ -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 */,