Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -148,11 +148,38 @@ - (BOOL)application:(UIApplication*)application
if (_delegates.count > 0) {
self.didForwardApplicationDidLaunch = YES;
}
for (NSObject<FlutterApplicationLifeCycleDelegate>* delegate in [_delegates allObjects]) {
if (!delegate) {
return [self application:application
didFinishLaunchingWithOptions:launchOptions
isFallbackForScene:NO];
}

- (BOOL)sceneWillConnectFallback:(UISceneConnectionOptions*)connectionOptions {
UIApplication* application = FlutterSharedApplication.application;
if (!application) {
return NO;
}
NSDictionary<UIApplicationLaunchOptionsKey, id>* convertedLaunchOptions =
ConvertConnectionOptions(connectionOptions);
if (convertedLaunchOptions.count == 0) {
// Only use fallback if there are meaningful launch options.
return NO;
}
if (![self application:application
didFinishLaunchingWithOptions:convertedLaunchOptions
isFallbackForScene:YES]) {
return YES;
}
return NO;
}

- (BOOL)application:(UIApplication*)application
didFinishLaunchingWithOptions:(NSDictionary*)launchOptions
isFallbackForScene:(BOOL)isFallback {
for (NSObject<FlutterApplicationLifeCycleDelegate>* delegate in _delegates) {
if (!delegate || (isFallback && [self pluginSupportsSceneLifecycle:delegate])) {
continue;
}
if ([delegate respondsToSelector:_cmd]) {
if ([delegate respondsToSelector:@selector(application:didFinishLaunchingWithOptions:)]) {
if (![delegate application:application didFinishLaunchingWithOptions:launchOptions]) {
return NO;
}
Expand All @@ -161,6 +188,35 @@ - (BOOL)application:(UIApplication*)application
return YES;
}

/* Makes a best attempt to convert UISceneConnectionOptions from the scene event
* (`scene:willConnectToSession:options:`) to a NSDictionary of options used to the application
* lifecycle event.
*
* For more information on UISceneConnectionOptions, see
* https://developer.apple.com/documentation/uikit/uiscene/connectionoptions.
*
* For information about the possible keys in the NSDictionary and how to handle them, see
* https://developer.apple.com/documentation/uikit/uiapplication/launchoptionskey
*/
static NSDictionary<UIApplicationLaunchOptionsKey, id>* ConvertConnectionOptions(
UISceneConnectionOptions* connectionOptions) {
NSMutableDictionary<UIApplicationLaunchOptionsKey, id>* convertedOptions =
[NSMutableDictionary dictionary];

if (connectionOptions.shortcutItem) {
convertedOptions[UIApplicationLaunchOptionsShortcutItemKey] = connectionOptions.shortcutItem;
}
if (connectionOptions.sourceApplication) {
convertedOptions[UIApplicationLaunchOptionsSourceApplicationKey] =
connectionOptions.sourceApplication;
}
if (connectionOptions.URLContexts.anyObject.URL) {
convertedOptions[UIApplicationLaunchOptionsURLKey] =
connectionOptions.URLContexts.anyObject.URL;
}
return convertedOptions;
}

- (BOOL)application:(UIApplication*)application
willFinishLaunchingWithOptions:(NSDictionary*)launchOptions {
if (_delegates.count > 0) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ @interface FakeTestFlutterPluginWithSceneEvents : NSObject <TestFlutterPluginWit
@end

@implementation FakeTestFlutterPluginWithSceneEvents
- (BOOL)application:(UIApplication*)application
didFinishLaunchingWithOptions:(NSDictionary*)launchOptions {
return NO;
}

- (BOOL)application:(UIApplication*)application
openURL:(NSURL*)url
options:(NSDictionary<UIApplicationOpenURLOptionsKey, id>*)options {
Expand Down Expand Up @@ -86,6 +91,65 @@ - (void)testCreate {
XCTAssertNotNil(delegate);
}

- (void)testSceneWillConnectFallback {
FlutterPluginAppLifeCycleDelegate* delegate = [[FlutterPluginAppLifeCycleDelegate alloc] init];
id plugin = [[FakePlugin alloc] init];
id mockPlugin = OCMPartialMock(plugin);
[delegate addDelegate:mockPlugin];

id mockOptions = OCMClassMock([UISceneConnectionOptions class]);
id mockShortcutItem = OCMClassMock([UIApplicationShortcutItem class]);
OCMStub([mockOptions shortcutItem]).andReturn(mockShortcutItem);
OCMStub([mockOptions sourceApplication]).andReturn(@"bundle_id");
id urlContext = OCMClassMock([UIOpenURLContext class]);
NSURL* url = [NSURL URLWithString:@"http://example.com"];
OCMStub([urlContext URL]).andReturn(url);
NSSet<UIOpenURLContext*>* urlContexts = [NSSet setWithObjects:urlContext, nil];
OCMStub([mockOptions URLContexts]).andReturn(urlContexts);

NSDictionary<UIApplicationOpenURLOptionsKey, id>* expectedApplicationOptions = @{
UIApplicationLaunchOptionsShortcutItemKey : mockShortcutItem,
UIApplicationLaunchOptionsSourceApplicationKey : @"bundle_id",
UIApplicationLaunchOptionsURLKey : url,
};

[delegate sceneWillConnectFallback:mockOptions];
OCMVerify([mockPlugin application:[UIApplication sharedApplication]
didFinishLaunchingWithOptions:expectedApplicationOptions]);
}

- (void)testSceneWillConnectFallbackSkippedSupportsScenes {
FlutterPluginAppLifeCycleDelegate* delegate = [[FlutterPluginAppLifeCycleDelegate alloc] init];
id plugin = [[FakeTestFlutterPluginWithSceneEvents alloc] init];
id mockPlugin = OCMPartialMock(plugin);
[delegate addDelegate:mockPlugin];

id mockOptions = OCMClassMock([UISceneConnectionOptions class]);
id mockShortcutItem = OCMClassMock([UIApplicationShortcutItem class]);
OCMStub([mockOptions shortcutItem]).andReturn(mockShortcutItem);
OCMStub([mockOptions sourceApplication]).andReturn(@"bundle_id");
id urlContext = OCMClassMock([UIOpenURLContext class]);
NSURL* url = [NSURL URLWithString:@"http://example.com"];
OCMStub([urlContext URL]).andReturn(url);
NSSet<UIOpenURLContext*>* urlContexts = [NSSet setWithObjects:urlContext, nil];
OCMStub([mockOptions URLContexts]).andReturn(urlContexts);

[delegate sceneWillConnectFallback:mockOptions];
OCMReject([mockPlugin application:[OCMArg any] didFinishLaunchingWithOptions:[OCMArg any]]);
}

- (void)testSceneWillConnectFallbackSkippedNoOptions {
FlutterPluginAppLifeCycleDelegate* delegate = [[FlutterPluginAppLifeCycleDelegate alloc] init];
id plugin = [[FakePlugin alloc] init];
id mockPlugin = OCMPartialMock(plugin);
[delegate addDelegate:mockPlugin];

id mockOptions = OCMClassMock([UISceneConnectionOptions class]);

[delegate sceneWillConnectFallback:mockOptions];
OCMReject([mockPlugin application:[OCMArg any] didFinishLaunchingWithOptions:[OCMArg any]]);
}

- (void)testDidEnterBackground {
XCTNSNotificationExpectation* expectation = [[XCTNSNotificationExpectation alloc]
initWithName:UIApplicationDidEnterBackgroundNotification];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,13 @@
*/
- (void)sceneFallbackWillFinishLaunchingApplication:(UIApplication*)application;

/**
* Forwards the application equivalent lifecycle event of
* `scene:willConnectToSession:options:` -> `application:didFinishLaunchingWithOptions:` to plugins
* that have not adopted the FlutterSceneLifeCycleDelegate protocol.
*/
- (BOOL)sceneWillConnectFallback:(UISceneConnectionOptions*)connectionOptions;

/**
* Forwards the application equivalent lifecycle event of
* `sceneWillEnterForeground:` -> `applicationWillEnterForeground:` to plugins that have not adopted
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,18 @@ @interface FlutterPluginSceneLifeCycleDelegate ()
@property(nonatomic, strong) NSPointerArray* developerManagedEngines;

@property(nonatomic, strong) UISceneConnectionOptions* connectionOptions;
@property(nonatomic, assign) BOOL sceneWillConnectEventHandledByPlugin;
@property(nonatomic, assign) BOOL sceneWillConnectFallbackCalled;

@end

@implementation FlutterPluginSceneLifeCycleDelegate
- (instancetype)init {
if (self = [super init]) {
_flutterManagedEngines = [NSPointerArray weakObjectsPointerArray];
_developerManagedEngines = [NSPointerArray weakObjectsPointerArray];
_sceneWillConnectFallbackCalled = NO;
_sceneWillConnectEventHandledByPlugin = NO;
}
return self;
}
Expand Down Expand Up @@ -208,10 +213,28 @@ - (void)scene:(UIScene*)scene
willConnectToSession:(UISceneSession*)session
flutterEngine:(FlutterEngine*)engine
options:(UISceneConnectionOptions*)connectionOptions {
// Don't send connection options if a plugin has already used them.
UISceneConnectionOptions* availableOptions = connectionOptions;
if (self.sceneWillConnectEventHandledByPlugin) {
availableOptions = nil;
}
BOOL handledByPlugin = [engine.sceneLifeCycleDelegate scene:scene
willConnectToSession:session
options:connectionOptions];
if (!handledByPlugin) {
options:availableOptions];

// If no plugins handled this, give the application fallback a chance to handle it.
// Only call the fallback once since it's per application.
if (!handledByPlugin && !self.sceneWillConnectFallbackCalled) {
self.sceneWillConnectFallbackCalled = YES;
if ([[self applicationLifeCycleDelegate] sceneWillConnectFallback:connectionOptions]) {
handledByPlugin = YES;
}
}
if (handledByPlugin) {
self.sceneWillConnectEventHandledByPlugin = YES;
}

if (!self.sceneWillConnectEventHandledByPlugin) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Once set this flag seems to never resets back to NO. Just want to make sure that we have the guarantee that each FlutterPluginSceneLifeCycleDelegate can only be associated with one scene and thus will never receive a second willConnect event?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is my understanding, yeah. If the scene is destroyed, for example, a new FlutterSceneDelegate and therefore FlutterPluginSceneLifeCycleDelegate is created and will receive a new willConnect event

// Only process deeplinks if a plugin has not already done something to handle this event.
[self handleDeeplinkingForEngine:engine options:connectionOptions];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,7 @@ - (void)testEngineReceivedConnectNotificationForSceneBeforeActualEvent {
options:options]);
OCMVerify(times(1), [mockLifecycleDelegate2 scene:mockScene
willConnectToSession:session
options:options]);
options:nil]);
XCTAssertEqual(delegate.flutterManagedEngines.count, 2.0);
}

Expand Down Expand Up @@ -360,16 +360,13 @@ - (void)testEngineReceivedConnectNotificationForSceneAfterActualEvent {
OCMVerify(times(1), [mockDelegate addFlutterManagedEngine:mockEngine]);
OCMVerify(times(1), [mockDelegate addFlutterManagedEngine:mockEngine2]);
XCTAssertEqual(delegate.flutterManagedEngines.count, 2.0);
OCMVerify(times(1), [mockDelegate scene:mockScene
willConnectToSession:session
options:options]); // This is called twice because once is
// within the test itself.
OCMVerify(times(1), [mockDelegate scene:mockScene willConnectToSession:session options:options]);
OCMVerify(times(1), [mockLifecycleDelegate scene:mockScene
willConnectToSession:session
options:options]);
OCMVerify(times(1), [mockLifecycleDelegate2 scene:mockScene
willConnectToSession:session
options:options]);
options:nil]);
}

- (void)testSceneWillConnectToSessionOptionsHandledByScenePlugin {
Expand All @@ -385,6 +382,39 @@ - (void)testSceneWillConnectToSessionOptionsHandledByScenePlugin {
willConnectToSession:[OCMArg any]
options:[OCMArg any]])
.andReturn(YES);
id mockAppLifecycleDelegate = mocks[@"mockAppLifecycleDelegate"];
OCMStub([mockAppLifecycleDelegate sceneWillConnectFallback:[OCMArg any]]).andReturn(YES);

id session = OCMClassMock([UISceneSession class]);
id options = OCMClassMock([UISceneConnectionOptions class]);

[delegate addFlutterManagedEngine:mockEngine];
XCTAssertEqual(delegate.flutterManagedEngines.count, 1.0);

[delegate scene:mockScene willConnectToSession:session options:options];
OCMVerify(times(1), [mockLifecycleDelegate scene:mockScene
willConnectToSession:session
options:options]);
OCMVerify(times(0), [mockAppLifecycleDelegate sceneWillConnectFallback:options]);
OCMVerify(times(0), [mockEngine sendDeepLinkToFramework:[OCMArg any]
completionHandler:[OCMArg any]]);
}

- (void)testSceneWillConnectToSessionOptionsHandledByApplicationPlugin {
FlutterPluginSceneLifeCycleDelegate* delegate =
[[FlutterPluginSceneLifeCycleDelegate alloc] init];

id mocks = [self mocksForEvents];
id mockEngine = mocks[@"mockEngine"];
id mockScene = mocks[@"mockScene"];
FlutterEnginePluginSceneLifeCycleDelegate* mockLifecycleDelegate =
(FlutterEnginePluginSceneLifeCycleDelegate*)mocks[@"mockLifecycleDelegate"];
OCMStub([mockLifecycleDelegate scene:[OCMArg any]
willConnectToSession:[OCMArg any]
options:[OCMArg any]])
.andReturn(NO);
id mockAppLifecycleDelegate = mocks[@"mockAppLifecycleDelegate"];
OCMStub([mockAppLifecycleDelegate sceneWillConnectFallback:[OCMArg any]]).andReturn(YES);

id session = OCMClassMock([UISceneSession class]);
id options = OCMClassMock([UISceneConnectionOptions class]);
Expand All @@ -396,6 +426,53 @@ - (void)testSceneWillConnectToSessionOptionsHandledByScenePlugin {
OCMVerify(times(1), [mockLifecycleDelegate scene:mockScene
willConnectToSession:session
options:options]);
OCMVerify(times(1), [mockAppLifecycleDelegate sceneWillConnectFallback:options]);
OCMVerify(times(0), [mockEngine sendDeepLinkToFramework:[OCMArg any]
completionHandler:[OCMArg any]]);
}

- (void)testSceneWillConnectToSessionOptionsHandledByApplicationPluginMultipleEngines {
FlutterPluginSceneLifeCycleDelegate* delegate =
[[FlutterPluginSceneLifeCycleDelegate alloc] init];

id mocks = [self mocksForEvents];
id mockEngine = mocks[@"mockEngine"];
id mockScene = mocks[@"mockScene"];
FlutterEnginePluginSceneLifeCycleDelegate* mockLifecycleDelegate =
(FlutterEnginePluginSceneLifeCycleDelegate*)mocks[@"mockLifecycleDelegate"];
OCMStub([mockLifecycleDelegate scene:[OCMArg any]
willConnectToSession:[OCMArg any]
options:[OCMArg any]])
.andReturn(NO);

id mocks2 = [self mocksForEvents];
id mockEngine2 = mocks2[@"mockEngine"];
FlutterEnginePluginSceneLifeCycleDelegate* mockLifecycleDelegate2 =
(FlutterEnginePluginSceneLifeCycleDelegate*)mocks2[@"mockLifecycleDelegate"];
OCMStub([mockLifecycleDelegate2 scene:[OCMArg any]
willConnectToSession:[OCMArg any]
options:[OCMArg any]])
.andReturn(NO);

id mockAppLifecycleDelegate = mocks2[@"mockAppLifecycleDelegate"];
OCMStub([mockAppLifecycleDelegate sceneWillConnectFallback:[OCMArg any]]).andReturn(YES);
id session = OCMClassMock([UISceneSession class]);
id options = OCMClassMock([UISceneConnectionOptions class]);

[delegate addFlutterManagedEngine:mockEngine];
[delegate addFlutterManagedEngine:mockEngine2];
XCTAssertEqual(delegate.flutterManagedEngines.count, 2.0);

[delegate scene:mockScene willConnectToSession:session options:options];
OCMVerify(times(1), [mockLifecycleDelegate scene:mockScene
willConnectToSession:session
options:options]);
OCMVerify(times(1), [mockLifecycleDelegate2 scene:mockScene
willConnectToSession:session
options:nil]);
OCMVerify(times(1), [mockAppLifecycleDelegate sceneWillConnectFallback:options]);
OCMVerify(times(0), [mockEngine sendDeepLinkToFramework:[OCMArg any]
completionHandler:[OCMArg any]]);
}

- (void)testSceneWillConnectToSessionOptionsHandledByUniversalLinks {
Expand Down