Check FlutterAppDelegate selector support before calling (flutter/engine#43425)
## Description This adds checks for the app delegate to make sure that it supports the Flutter-specific selectors before calling them, so that a non `FlutterAppDelegate` can be used for the `NSApplicationDelegate` on `NSApp`. ## Related Issues - https://github.com/flutter/flutter/issues/124829 - https://github.com/flutter/flutter/issues/127476 ## Tests - Added a test to make sure things don't crash if the app delegate isn't a `FlutterAppDelegate`.
This commit is contained in:
@@ -178,9 +178,11 @@ constexpr char kTextPlainFormat[] = "text/plain";
|
||||
// allow tests to override it so that an actual exit doesn't occur.
|
||||
[[NSApplication sharedApplication] terminate:sender];
|
||||
};
|
||||
FlutterAppDelegate* appDelegate =
|
||||
(FlutterAppDelegate*)[[NSApplication sharedApplication] delegate];
|
||||
appDelegate.terminationHandler = self;
|
||||
id<NSApplicationDelegate> appDelegate = [[NSApplication sharedApplication] delegate];
|
||||
if ([appDelegate respondsToSelector:@selector(setTerminationHandler:)]) {
|
||||
FlutterAppDelegate* flutterAppDelegate = reinterpret_cast<FlutterAppDelegate*>(appDelegate);
|
||||
flutterAppDelegate.terminationHandler = self;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
@@ -420,10 +422,7 @@ static void OnPlatformMessage(const FlutterPlatformMessage* message, FlutterEngi
|
||||
_semanticsEnabled = NO;
|
||||
_isResponseValid = [[NSMutableArray alloc] initWithCapacity:1];
|
||||
[_isResponseValid addObject:@YES];
|
||||
_terminationHandler = [[FlutterEngineTerminationHandler alloc] initWithEngine:self
|
||||
terminator:nil];
|
||||
// kFlutterImplicitViewId is reserved for the implicit view.
|
||||
// All IDs above it are for regular views.
|
||||
_nextViewId = kFlutterImplicitViewId + 1;
|
||||
|
||||
_embedderAPI.struct_size = sizeof(FlutterEngineProcTable);
|
||||
@@ -443,9 +442,16 @@ static void OnPlatformMessage(const FlutterPlatformMessage* message, FlutterEngi
|
||||
[self setUpPlatformViewChannel];
|
||||
[self setUpAccessibilityChannel];
|
||||
[self setUpNotificationCenterListeners];
|
||||
FlutterAppDelegate* appDelegate =
|
||||
reinterpret_cast<FlutterAppDelegate*>([[NSApplication sharedApplication] delegate]);
|
||||
[appDelegate addApplicationLifecycleDelegate:self];
|
||||
id<NSApplicationDelegate> appDelegate = [[NSApplication sharedApplication] delegate];
|
||||
const SEL selector = @selector(addApplicationLifecycleDelegate:);
|
||||
if ([appDelegate respondsToSelector:selector]) {
|
||||
_terminationHandler = [[FlutterEngineTerminationHandler alloc] initWithEngine:self
|
||||
terminator:nil];
|
||||
FlutterAppDelegate* flutterAppDelegate = reinterpret_cast<FlutterAppDelegate*>(appDelegate);
|
||||
[flutterAppDelegate addApplicationLifecycleDelegate:self];
|
||||
} else {
|
||||
_terminationHandler = nil;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
@@ -1097,9 +1103,20 @@ static void OnPlatformMessage(const FlutterPlatformMessage* message, FlutterEngi
|
||||
} else if ([call.method isEqualToString:@"Clipboard.hasStrings"]) {
|
||||
result(@{@"value" : @([self clipboardHasStrings])});
|
||||
} else if ([call.method isEqualToString:@"System.exitApplication"]) {
|
||||
[[self terminationHandler] handleRequestAppExitMethodCall:call.arguments result:result];
|
||||
if ([self terminationHandler] == nil) {
|
||||
// If the termination handler isn't set, then either we haven't
|
||||
// initialized it yet, or (more likely) the NSApp delegate isn't a
|
||||
// FlutterAppDelegate, so it can't cancel requests to exit. So, in that
|
||||
// case, just terminate when requested.
|
||||
[NSApp terminate:self];
|
||||
result(nil);
|
||||
} else {
|
||||
[[self terminationHandler] handleRequestAppExitMethodCall:call.arguments result:result];
|
||||
}
|
||||
} else if ([call.method isEqualToString:@"System.initializationComplete"]) {
|
||||
[self terminationHandler].acceptingRequests = YES;
|
||||
if ([self terminationHandler] != nil) {
|
||||
[self terminationHandler].acceptingRequests = YES;
|
||||
}
|
||||
result(nil);
|
||||
} else {
|
||||
result(FlutterMethodNotImplemented);
|
||||
|
||||
@@ -47,6 +47,16 @@ constexpr int64_t kImplicitViewId = 0ll;
|
||||
|
||||
@end
|
||||
|
||||
@interface PlainAppDelegate : NSObject <NSApplicationDelegate>
|
||||
@end
|
||||
|
||||
@implementation PlainAppDelegate
|
||||
- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication* _Nonnull)sender {
|
||||
// Always cancel, so that the test doesn't exit.
|
||||
return NSTerminateCancel;
|
||||
}
|
||||
@end
|
||||
|
||||
namespace flutter::testing {
|
||||
|
||||
TEST_F(FlutterEngineTest, CanLaunch) {
|
||||
@@ -782,6 +792,22 @@ TEST_F(FlutterEngineTest, HandlesTerminationRequest) {
|
||||
EXPECT_TRUE(triedToTerminate);
|
||||
}
|
||||
|
||||
TEST_F(FlutterEngineTest, IgnoresTerminationRequestIfNotFlutterAppDelegate) {
|
||||
id<NSApplicationDelegate> previousDelegate = [[NSApplication sharedApplication] delegate];
|
||||
id<NSApplicationDelegate> plainDelegate = [[PlainAppDelegate alloc] init];
|
||||
[NSApplication sharedApplication].delegate = plainDelegate;
|
||||
|
||||
// Creating the engine shouldn't fail here, even though the delegate isn't a
|
||||
// FlutterAppDelegate.
|
||||
CreateMockFlutterEngine(nil);
|
||||
|
||||
// Asking to terminate the app should cancel.
|
||||
EXPECT_EQ([[[NSApplication sharedApplication] delegate] applicationShouldTerminate:NSApp],
|
||||
NSTerminateCancel);
|
||||
|
||||
[NSApplication sharedApplication].delegate = previousDelegate;
|
||||
}
|
||||
|
||||
TEST_F(FlutterEngineTest, HandleAccessibilityEvent) {
|
||||
__block BOOL announced = NO;
|
||||
id engineMock = CreateMockFlutterEngine(nil);
|
||||
|
||||
Reference in New Issue
Block a user