Skip to content

Commit

Permalink
[iOS] Add darwin_extension_safe flag and use UIScene api when buildin…
Browse files Browse the repository at this point in the history
…g for extensions (#43449)

iOS extensions forbids the usage of UIApplication.sharedApplication. This PR refactors the engine to use UISceneAPI when darwin_extension_safe flag is on. Using UISceneAPI can help avoid the usage of `UIApplication.sharedApplication` as much as possible.
This PR also added a new `_extension_safe` variant for the engine build so all the logic with the `darwin_extension_safe` flag is on can be tested separately.

This PR doesn't enable the engine to build for the extension even when darwin_extension_safe is true.

part of flutter/flutter#124289

There are several issues related to UIApplication life cycle and I manually tested they still work with the scene API:

[C++, Objective-C, Java style guides]: https://github.com/flutter/engine/blob/main/CONTRIBUTING.md#style
  • Loading branch information
Chris Yang authored Aug 1, 2023
1 parent 10a1f9c commit d5f6b00
Show file tree
Hide file tree
Showing 11 changed files with 574 additions and 70 deletions.
71 changes: 71 additions & 0 deletions ci/builders/mac_unopt.json
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,77 @@
"script": "flutter/testing/scenario_app/run_ios_tests.sh"
}

]
},
{
"archives": [
{
"base_path": "out/ios_debug_sim_arm64_extension_safe/zip_archives/",
"type": "gcs",
"include_paths": [
],
"name": "ios_debug_sim_arm64_extension_safe"
}
],
"properties": {
"$flutter/osx_sdk": {
"runtime_versions": [
"ios-16-4_14e300c",
"ios-16-2_14c18"
],
"sdk_version": "14e300c"
}
},
"drone_dimensions": [
"device_type=none",
"os=Mac-12",
"cpu=arm64"
],
"gclient_variables": {
"download_android_deps": false
},
"gn": [
"--ios",
"--runtime-mode",
"debug",
"--simulator",
"--no-lto",
"--force-mac-arm64",
"--simulator-cpu",
"arm64",
"--darwin-extension-safe"
],
"name": "ios_debug_sim_arm64_extension_safe",
"ninja": {
"config": "ios_debug_sim_arm64_extension_safe",
"targets": [
"flutter/testing/scenario_app",
"flutter/shell/platform/darwin/ios:ios_test_flutter"
]
},
"tests": [
{
"language": "python3",
"name": "Tests for ios_debug_sim_arm64_extension_safe",
"parameters": [
"--variant",
"ios_debug_sim_arm64_extension_safe",
"--type",
"objc",
"--engine-capture-core-dump",
"--ios-variant",
"ios_debug_sim_arm64_extension_safe"
],
"script": "flutter/testing/run_tests.py"
},
{
"name": "Scenario App Integration Tests",
"parameters": [
"ios_debug_sim_arm64_extension_safe"
],
"script": "flutter/testing/scenario_app/run_ios_tests.sh"
}

]
}
]
Expand Down
6 changes: 6 additions & 0 deletions common/config.gni
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ 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
}

# feature_defines_list ---------------------------------------------------------
Expand Down
14 changes: 14 additions & 0 deletions shell/platform/darwin/ios/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ source_set("flutter_framework_source_arc") {
cflags_objcc = flutter_cflags_objcc_arc

defines = [ "FLUTTER_FRAMEWORK=1" ]
if (darwin_extension_safe) {
defines += [ "APPLICATION_EXTENSION_API_ONLY=1" ]
}
allow_circular_includes_from = [ ":flutter_framework_source" ]
deps = [
":flutter_framework_source",
Expand Down Expand Up @@ -153,6 +156,9 @@ source_set("flutter_framework_source") {
sources += _flutter_framework_headers

defines = [ "FLUTTER_FRAMEWORK=1" ]
if (darwin_extension_safe) {
defines += [ "APPLICATION_EXTENSION_API_ONLY=1" ]
}

if (shell_enable_metal) {
sources += [
Expand Down Expand Up @@ -249,6 +255,10 @@ source_set("ios_test_flutter_mrc") {
if (shell_enable_vulkan) {
deps += [ "//flutter/vulkan" ]
}

if (darwin_extension_safe) {
defines = [ "APPLICATION_EXTENSION_API_ONLY=1" ]
}
}

shared_library("ios_test_flutter") {
Expand Down Expand Up @@ -312,6 +322,10 @@ 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") {
Expand Down
70 changes: 61 additions & 9 deletions shell/platform/darwin/ios/framework/Source/FlutterEngine.mm
Original file line number Diff line number Diff line change
Expand Up @@ -225,22 +225,44 @@ - (instancetype)initWithName:(NSString*)labelPrefix
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

[center addObserver:self
selector:@selector(applicationWillEnterForeground:)
name:UIApplicationWillEnterForegroundNotification
selector:@selector(onLocaleUpdated:)
name:NSCurrentLocaleDidChangeNotification
object:nil];

return self;
}

- (void)setUpSceneLifecycleNotifications:(NSNotificationCenter*)center API_AVAILABLE(ios(13.0)) {
[center addObserver:self
selector:@selector(applicationDidEnterBackground:)
name:UIApplicationDidEnterBackgroundNotification
selector:@selector(sceneWillEnterForeground:)
name:UISceneWillEnterForegroundNotification
object:nil];

[center addObserver:self
selector:@selector(onLocaleUpdated:)
name:NSCurrentLocaleDidChangeNotification
selector:@selector(sceneDidEnterBackground:)
name:UISceneDidEnterBackgroundNotification
object:nil];
}

return self;
- (void)setUpApplicationLifecycleNotifications:(NSNotificationCenter*)center {
[center addObserver:self
selector:@selector(applicationWillEnterForeground:)
name:UIApplicationWillEnterForegroundNotification
object:nil];
[center addObserver:self
selector:@selector(applicationDidEnterBackground:)
name:UIApplicationDidEnterBackgroundNotification
object:nil];
}

- (void)recreatePlatformViewController {
Expand Down Expand Up @@ -856,8 +878,20 @@ - (BOOL)createShell:(NSString*)entrypoint
_threadHost->io_thread->GetTaskRunner() // io
);

#if APPLICATION_EXTENSION_API_ONLY
if (@available(iOS 13.0, *)) {
_isGpuDisabled = self.viewController.windowSceneIfViewLoaded.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

// Create the shell. This is a blocking operation.
std::unique_ptr<flutter::Shell> shell = flutter::Shell::Create(
/*platform_data=*/platformData,
Expand Down Expand Up @@ -1302,11 +1336,29 @@ - (NSObject*)valuePublishedByPlugin:(NSString*)pluginKey {

#pragma mark - Notifications

#if APPLICATION_EXTENSION_API_ONLY
- (void)sceneWillEnterForeground:(NSNotification*)notification API_AVAILABLE(ios(13.0)) {
[self flutterWillEnterForeground:notification];
}

- (void)sceneDidEnterBackground:(NSNotification*)notification API_AVAILABLE(ios(13.0)) {
[self flutterDidEnterBackground:notification];
}
#else
- (void)applicationWillEnterForeground:(NSNotification*)notification {
[self setIsGpuDisabled:NO];
[self flutterWillEnterForeground:notification];
}

- (void)applicationDidEnterBackground:(NSNotification*)notification {
[self flutterDidEnterBackground:notification];
}
#endif

- (void)flutterWillEnterForeground:(NSNotification*)notification {
[self setIsGpuDisabled:NO];
}

- (void)flutterDidEnterBackground:(NSNotification*)notification {
[self setIsGpuDisabled:YES];
[self notifyLowMemory];
}
Expand Down
59 changes: 59 additions & 0 deletions shell/platform/darwin/ios/framework/Source/FlutterEngineTest.mm
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#import <objc/runtime.h>

#import "flutter/common/settings.h"
#include "flutter/fml/synchronization/sync_switch.h"
#import "flutter/shell/platform/darwin/common/framework/Headers/FlutterMacros.h"
#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterBinaryMessengerRelay.h"
#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterDartProject_Internal.h"
Expand Down Expand Up @@ -341,4 +342,62 @@ - (void)testFlutterEngineUpdatesDisplays {
OCMVerify(times(2), [mockEngine updateDisplays]);
}

- (void)testLifeCycleNotificationDidEnterBackground {
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];
#if APPLICATION_EXTENSION_API_ONLY
OCMVerify(times(1), [mockEngine sceneDidEnterBackground:[OCMArg any]]);
#else
OCMVerify(times(1), [mockEngine applicationDidEnterBackground:[OCMArg any]]);
#endif
XCTAssertTrue(engine.isGpuDisabled);
bool switch_value = false;
[engine shell].GetIsGpuDisabledSyncSwitch()->Execute(
fml::SyncSwitch::Handlers().SetIfTrue([&] { switch_value = true; }).SetIfFalse([&] {
switch_value = false;
}));
XCTAssertTrue(switch_value);
}

- (void)testLifeCycleNotificationWillEnterForeground {
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];
#if APPLICATION_EXTENSION_API_ONLY
OCMVerify(times(1), [mockEngine sceneWillEnterForeground:[OCMArg any]]);
#else
OCMVerify(times(1), [mockEngine applicationWillEnterForeground:[OCMArg any]]);
#endif
XCTAssertFalse(engine.isGpuDisabled);
bool switch_value = true;
[engine shell].GetIsGpuDisabledSyncSwitch()->Execute(
fml::SyncSwitch::Handlers().SetIfTrue([&] { switch_value = true; }).SetIfFalse([&] {
switch_value = false;
}));
XCTAssertFalse(switch_value);
}

@end
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,9 @@ class ThreadHost;
- (void)flutterTextInputView:(FlutterTextInputView*)textInputView
performAction:(FlutterTextInputAction)action
withClient:(int)client;
- (void)sceneWillEnterForeground:(NSNotification*)notification API_AVAILABLE(ios(13.0));
- (void)sceneDidEnterBackground:(NSNotification*)notification API_AVAILABLE(ios(13.0));
- (void)applicationWillEnterForeground:(NSNotification*)notification;
- (void)applicationDidEnterBackground:(NSNotification*)notification;

@end
Original file line number Diff line number Diff line change
Expand Up @@ -269,13 +269,23 @@ - (void)popSystemNavigator:(BOOL)isAnimated {
// It's also possible in an Add2App scenario that the FlutterViewController was presented
// outside the context of a UINavigationController, and still wants to be popped.

UIViewController* engineViewController = [_engine.get() viewController];
FlutterViewController* engineViewController = [_engine.get() viewController];
UINavigationController* navigationController = [engineViewController navigationController];
if (navigationController) {
[navigationController popViewControllerAnimated:isAnimated];
} else {
UIViewController* rootViewController =
[UIApplication sharedApplication].keyWindow.rootViewController;
UIViewController* rootViewController = nil;
#if APPLICATION_EXTENSION_API_ONLY
if (@available(iOS 15.0, *)) {
rootViewController =
[engineViewController windowSceneIfViewLoaded].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];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,8 @@ - (void)testLookUpCallInitiated {
XCTestExpectation* presentExpectation =
[self expectationWithDescription:@"Look Up view controller presented"];

FlutterViewController* engineViewController = [[FlutterViewController alloc] initWithEngine:engine
nibName:nil
bundle:nil];
FlutterViewController* engineViewController =
[[[FlutterViewController alloc] initWithEngine:engine nibName:nil bundle:nil] autorelease];
FlutterViewController* mockEngineViewController = OCMPartialMock(engineViewController);

FlutterPlatformPlugin* plugin =
Expand Down
Loading

0 comments on commit d5f6b00

Please sign in to comment.