diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index fbf31268b6c09..c7359b0f560d2 100644 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -2550,6 +2550,7 @@ ORIGIN: ../../../flutter/shell/platform/darwin/ios/platform_view_ios.mm + ../../ ORIGIN: ../../../flutter/shell/platform/darwin/ios/rendering_api_selection.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/darwin/ios/rendering_api_selection.mm + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/darwin/macos/framework/Headers/FlutterAppDelegate.h + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/shell/platform/darwin/macos/framework/Headers/FlutterApplication.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/darwin/macos/framework/Headers/FlutterDartProject.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/darwin/macos/framework/Headers/FlutterEngine.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/darwin/macos/framework/Headers/FlutterMacOS.h + ../../../flutter/LICENSE @@ -2561,6 +2562,9 @@ ORIGIN: ../../../flutter/shell/platform/darwin/macos/framework/Source/Accessibil ORIGIN: ../../../flutter/shell/platform/darwin/macos/framework/Source/AccessibilityBridgeMac.mm + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/darwin/macos/framework/Source/AccessibilityBridgeMacTest.mm + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterAppDelegate.mm + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterAppDelegate_Internal.h + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterApplication.mm + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterApplication_Internal.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterBackingStore.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterBackingStore.mm + ../../../flutter/LICENSE ORIGIN: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterChannelKeyResponder.h + ../../../flutter/LICENSE @@ -5083,6 +5087,7 @@ FILE: ../../../flutter/shell/platform/darwin/ios/platform_view_ios.mm FILE: ../../../flutter/shell/platform/darwin/ios/rendering_api_selection.h FILE: ../../../flutter/shell/platform/darwin/ios/rendering_api_selection.mm FILE: ../../../flutter/shell/platform/darwin/macos/framework/Headers/FlutterAppDelegate.h +FILE: ../../../flutter/shell/platform/darwin/macos/framework/Headers/FlutterApplication.h FILE: ../../../flutter/shell/platform/darwin/macos/framework/Headers/FlutterDartProject.h FILE: ../../../flutter/shell/platform/darwin/macos/framework/Headers/FlutterEngine.h FILE: ../../../flutter/shell/platform/darwin/macos/framework/Headers/FlutterMacOS.h @@ -5095,6 +5100,9 @@ FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/Accessibilit FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/AccessibilityBridgeMac.mm FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/AccessibilityBridgeMacTest.mm FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterAppDelegate.mm +FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterAppDelegate_Internal.h +FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterApplication.mm +FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterApplication_Internal.h FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterBackingStore.h FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterBackingStore.mm FILE: ../../../flutter/shell/platform/darwin/macos/framework/Source/FlutterChannelKeyResponder.h diff --git a/lib/ui/platform_dispatcher.dart b/lib/ui/platform_dispatcher.dart index 90112aad40f78..cac65e9858d7e 100644 --- a/lib/ui/platform_dispatcher.dart +++ b/lib/ui/platform_dispatcher.dart @@ -1733,6 +1733,43 @@ enum AppLifecycleState { detached, } +/// The possible responses to a request to exit the application. +/// +/// The request is typically responded to by a [WidgetsBindingObserver]. +// TODO(gspencergoog): Insert doc references here to AppLifecycleListener and to +// the actual function called on WidgetsBindingObserver once those have landed +// in the framework. https://github.com/flutter/flutter/issues/121721 +enum AppExitResponse { + /// Exiting the application can proceed. + exit, + /// Cancel the exit: do not exit the application. + cancel, +} + +/// The type of application exit to perform when calling +/// `ServicesBinding.exitApplication`. +// TODO(gspencergoog): Insert doc references here to +// ServicesBinding.exitApplication that has landed in the framework. +// https://github.com/flutter/flutter/issues/121721 +enum AppExitType { + /// Requests that the application start an orderly exit, sending a request + /// back to the framework through the [WidgetsBinding]. If that responds + /// with [AppExitResponse.exit], then proceed with the same steps as a + /// [required] exit. If that responds with [AppExitResponse.cancel], then the + /// exit request is canceled and the application continues executing normally. + cancelable, + + /// A non-cancelable orderly exit request. The engine will shut down the + /// engine and call the native UI toolkit's exit API. + /// + /// If you need an even faster and more dangerous exit, then call `dart:io`'s + /// `exit()` directly, and even the native toolkit's exit API won't be called. + /// This is quite dangerous, though, since it's possible that the engine will + /// crash because it hasn't been properly shut down, causing the app to crash + /// on exit. + required, +} + /// A representation of distances for each of the four edges of a rectangle, /// used to encode the view insets and padding that applications should place /// around their user interface, as exposed by [FlutterView.viewInsets] and diff --git a/lib/web_ui/lib/platform_dispatcher.dart b/lib/web_ui/lib/platform_dispatcher.dart index 416361236456a..9c4a11c64cbe3 100644 --- a/lib/web_ui/lib/platform_dispatcher.dart +++ b/lib/web_ui/lib/platform_dispatcher.dart @@ -374,6 +374,16 @@ enum AppLifecycleState { detached, } +enum AppExitResponse { + exit, + cancel, +} + +enum AppExitType { + cancelable, + required, +} + abstract class ViewPadding { const factory ViewPadding._( {required double left, diff --git a/shell/platform/darwin/macos/BUILD.gn b/shell/platform/darwin/macos/BUILD.gn index d672f9551736c..4cbf37b8c0513 100644 --- a/shell/platform/darwin/macos/BUILD.gn +++ b/shell/platform/darwin/macos/BUILD.gn @@ -38,6 +38,7 @@ _framework_binary_subpath = "Versions/A/$_flutter_framework_name" # the Flutter engine source root. _flutter_framework_headers = [ "framework/Headers/FlutterAppDelegate.h", + "framework/Headers/FlutterApplication.h", "framework/Headers/FlutterDartProject.h", "framework/Headers/FlutterEngine.h", "framework/Headers/FlutterMacOS.h", @@ -57,6 +58,7 @@ source_set("flutter_framework_source") { "framework/Source/AccessibilityBridgeMac.h", "framework/Source/AccessibilityBridgeMac.mm", "framework/Source/FlutterAppDelegate.mm", + "framework/Source/FlutterApplication.mm", "framework/Source/FlutterBackingStore.h", "framework/Source/FlutterBackingStore.mm", "framework/Source/FlutterChannelKeyResponder.h", diff --git a/shell/platform/darwin/macos/framework/Headers/FlutterApplication.h b/shell/platform/darwin/macos/framework/Headers/FlutterApplication.h new file mode 100644 index 0000000000000..38b5439ede7e8 --- /dev/null +++ b/shell/platform/darwin/macos/framework/Headers/FlutterApplication.h @@ -0,0 +1,47 @@ +// 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_FLUTTERAPPLICATION_H_ +#define FLUTTER_FLUTTERAPPLICATION_H_ + +#import + +/** + * A Flutter-specific subclass of NSApplication that overrides |terminate| and + * provides an additional |terminateApplication| method so that Flutter can + * handle requests for termination in an asynchronous fashion. + * + * When a call to |terminate| comes in, either from the OS through a Quit menu + * item, through the Quit item in the dock context menu, or from the application + * itself, a request is sent to the Flutter framework. If that request is + * granted, this subclass will (in |terminateApplication|) call + * |NSApplication|'s version of |terminate| to proceed with terminating the + * application normally by calling |applicationShouldTerminate|, etc. + * + * If the termination request is denied by the framework, then the application + * will continue to execute normally, as if no |terminate| call were made. + * + * The |FlutterAppDelegate| always returns |NSTerminateNow| from + * |applicationShouldTerminate|, since it has already decided by that point that + * it should terminate. + * + * In order for this class to be used in place of |NSApplication|, the + * "NSPrincipalClass" entry in the Info.plist for the application must be set to + * "FlutterApplication". If it is not, then the application will not be given + * the chance to deny a termination request, and calls to requestAppExit on the + * engine (from the framework, typically) will simply exit the application + * without ceremony. + * + * If the |NSApp| global isn't of type |FlutterApplication|, a log message will + * be printed once in debug mode when the application is first accessed through + * the singleton's |sharedApplication|, describing how to fix this. + * + * Flutter applications are *not* required to inherit from this class. + * Developers of custom |NSApplication| subclasses should copy and paste code as + * necessary from FlutterApplication.mm. + */ +@interface FlutterApplication : NSApplication +@end + +#endif // FLUTTER_FLUTTERAPPLICATION_H_ diff --git a/shell/platform/darwin/macos/framework/Headers/FlutterMacOS.h b/shell/platform/darwin/macos/framework/Headers/FlutterMacOS.h index 5fc794b92995d..13c7f78775ec4 100644 --- a/shell/platform/darwin/macos/framework/Headers/FlutterMacOS.h +++ b/shell/platform/darwin/macos/framework/Headers/FlutterMacOS.h @@ -3,6 +3,7 @@ // found in the LICENSE file. #import "FlutterAppDelegate.h" +#import "FlutterApplication.h" #import "FlutterBinaryMessenger.h" #import "FlutterChannels.h" #import "FlutterCodecs.h" diff --git a/shell/platform/darwin/macos/framework/Source/FlutterAppDelegate.mm b/shell/platform/darwin/macos/framework/Source/FlutterAppDelegate.mm index 0442fdd70232d..269d58f71a86b 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterAppDelegate.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterAppDelegate.mm @@ -3,6 +3,12 @@ // found in the LICENSE file. #import "flutter/shell/platform/darwin/macos/framework/Headers/FlutterAppDelegate.h" +#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterAppDelegate_Internal.h" + +#import + +#include "flutter/fml/logging.h" +#include "flutter/shell/platform/embedder/embedder.h" @interface FlutterAppDelegate () @@ -15,8 +21,16 @@ - (NSString*)applicationName; @implementation FlutterAppDelegate -// TODO(stuartmorgan): Implement application lifecycle forwarding to plugins here, as is done +// TODO(gspencergoog): Implement application lifecycle forwarding to plugins here, as is done // on iOS. Currently macOS plugins don't have access to lifecycle messages. +// https://github.com/flutter/flutter/issues/30735 + +- (instancetype)init { + if (self = [super init]) { + _terminationHandler = nil; + } + return self; +} - (void)applicationWillFinishLaunching:(NSNotification*)notification { // Update UI elements to match the application name. @@ -28,6 +42,13 @@ - (void)applicationWillFinishLaunching:(NSNotification*)notification { } } +// This always returns NSTerminateNow, since by the time we get here, the +// application has already been asked if it should terminate or not, and if not, +// then termination never gets this far. +- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication*)sender { + return NSTerminateNow; +} + #pragma mark Private Methods - (NSString*)applicationName { diff --git a/shell/platform/darwin/macos/framework/Source/FlutterAppDelegate_Internal.h b/shell/platform/darwin/macos/framework/Source/FlutterAppDelegate_Internal.h new file mode 100644 index 0000000000000..b811b9995a2c2 --- /dev/null +++ b/shell/platform/darwin/macos/framework/Source/FlutterAppDelegate_Internal.h @@ -0,0 +1,21 @@ +// 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_FLUTTERAPPDELEGATE_INTERNAL_H_ +#define FLUTTER_FLUTTERAPPDELEGATE_INTERNAL_H_ + +#import "flutter/shell/platform/darwin/macos/framework/Headers/FlutterAppDelegate.h" +#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterEngine_Internal.h" + +@interface FlutterAppDelegate () + +/** + * Holds a weak reference to the termination handler owned by the engine. + * Called by the |FlutterApplication| when termination is requested by the OS. + */ +@property(readwrite, nullable, weak) FlutterEngineTerminationHandler* terminationHandler; + +@end + +#endif // FLUTTER_FLUTTERAPPDELEGATE_INTERNAL_H_ diff --git a/shell/platform/darwin/macos/framework/Source/FlutterApplication.mm b/shell/platform/darwin/macos/framework/Source/FlutterApplication.mm new file mode 100644 index 0000000000000..5667dfa1f5a37 --- /dev/null +++ b/shell/platform/darwin/macos/framework/Source/FlutterApplication.mm @@ -0,0 +1,90 @@ +// 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/macos/framework/Headers/FlutterApplication.h" +#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterApplication_Internal.h" + +#include "flutter/shell/platform/embedder/embedder.h" +#import "shell/platform/darwin/macos/framework/Headers/FlutterAppDelegate.h" +#import "shell/platform/darwin/macos/framework/Source/FlutterAppDelegate_Internal.h" +#import "shell/platform/darwin/macos/framework/Source/FlutterEngine_Internal.h" + +// An NSApplication subclass that implements overrides necessary for some +// Flutter features, like application lifecycle handling. +@implementation FlutterApplication + +// Initialize NSApplication using the custom subclass. Check whether NSApp was +// already initialized using another class, because that would break some +// things. Warn about the mismatch only once, and only in debug builds. ++ (NSApplication*)sharedApplication { + NSApplication* app = [super sharedApplication]; + + // +sharedApplication initializes the global NSApp, so if we're delivering + // something other than a FlutterApplication, warn the developer once. +#ifndef FLUTTER_RELEASE + static dispatch_once_t onceToken = 0; + dispatch_once(&onceToken, ^{ + if (![app respondsToSelector:@selector(terminateApplication:)]) { + NSLog(@"NSApp should be of type %s, not %s.\n" + "System requests for the application to terminate will not be sent to " + "the Flutter framework, so the framework will be unable to cancel " + "those requests.\n" + "Modify the application's NSPrincipleClass to be %s in the " + "Info.plist to fix this.", + [[self className] UTF8String], [[NSApp className] UTF8String], + [[self className] UTF8String]); + } + }); +#endif // !FLUTTER_RELEASE + return app; +} + +// |terminate| is the entry point for orderly "quit" operations in Cocoa. This +// includes the application menu's Quit menu item and keyboard equivalent, the +// application's dock icon menu's Quit menu item, "quit" (not "force quit") in +// the Activity Monitor, and quits triggered by user logout and system restart +// and shutdown. +// +// We override the normal |terminate| implementation. Our implementation, which +// is specific to the asynchronous nature of Flutter, works by asking the +// application delegate to terminate using its |requestApplicationTermination| +// method instead of going through |applicationShouldTerminate|. +// +// The standard |applicationShouldTerminate| is not used because returning +// NSTerminateLater from that function moves the run loop into a modal dialog +// mode (NSModalPanelRunLoopMode), which stops the main run loop from processing +// messages like, for instance, the response to the method channel call, and +// code paths leading to it must be redirected to |requestApplicationTermination|. +// +// |requestApplicationTermination| differs from the standard +// |applicationShouldTerminate| in that no special event loop is run in the case +// that immediate termination is not possible (e.g., if dialog boxes allowing +// the user to cancel have to be shown, or data needs to be saved). Instead, +// requestApplicationTermination sends a method channel call to the framework asking +// it if it is OK to terminate. When that method channel call returns with a +// result, the application either terminates or continues running. +- (void)terminate:(id)sender { + FlutterAppDelegate* delegate = [self delegate]; + if (!delegate || ![delegate respondsToSelector:@selector(terminationHandler)] || + [delegate terminationHandler] == nil) { + // If there's no termination handler, then just terminate. + [super terminate:sender]; + } + FlutterEngineTerminationHandler* terminationHandler = + [static_cast([self delegate]) terminationHandler]; + [terminationHandler requestApplicationTermination:sender + exitType:kFlutterAppExitTypeCancelable + result:nil]; + // Return, don't exit. The application delegate is responsible for exiting on + // its own by calling |terminateApplication|. +} + +// Starts the regular Cocoa application termination flow, so that plugins will +// get the appropriate notifications after the application has already decided +// to quit. This is called after the application has decided that +// it's OK to terminate. +- (void)terminateApplication:(id)sender { + [super terminate:sender]; +} +@end diff --git a/shell/platform/darwin/macos/framework/Source/FlutterApplication_Internal.h b/shell/platform/darwin/macos/framework/Source/FlutterApplication_Internal.h new file mode 100644 index 0000000000000..0a79b225ba144 --- /dev/null +++ b/shell/platform/darwin/macos/framework/Source/FlutterApplication_Internal.h @@ -0,0 +1,27 @@ +// 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_FLUTTERAPPLICATION_INTERNAL_H_ +#define FLUTTER_FLUTTERAPPLICATION_INTERNAL_H_ + +#import "flutter/shell/platform/darwin/macos/framework/Headers/FlutterApplication.h" + +/** + * Define |terminateApplication| for internal use. + */ +@interface FlutterApplication () + +/** + * FlutterApplication's implementation of |terminate| doesn't terminate the + * application: that is left up to the engine, which will call this function if + * it decides that termination request is granted, which will start the regular + * Cocoa flow for terminating the application, calling + * |applicationShouldTerminate|, etc. + * + * @param(sender) The id of the object requesting the termination, or nil. + */ +- (void)terminateApplication:(id)sender; +@end + +#endif // FLUTTER_FLUTTERAPPLICATION_INTERNAL_H_ diff --git a/shell/platform/darwin/macos/framework/Source/FlutterEngine.mm b/shell/platform/darwin/macos/framework/Source/FlutterEngine.mm index 46a12265b4114..6c33c3dd372eb 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterEngine.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterEngine.mm @@ -10,6 +10,12 @@ #include #include "flutter/shell/platform/common/engine_switches.h" +#include "flutter/shell/platform/embedder/embedder.h" + +#import "flutter/shell/platform/darwin/macos/framework/Headers/FlutterAppDelegate.h" +#import "flutter/shell/platform/darwin/macos/framework/Headers/FlutterApplication.h" +#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterAppDelegate_Internal.h" +#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterApplication_Internal.h" #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterCompositor.h" #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterDartProject_Internal.h" #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterMenuPlugin.h" @@ -18,10 +24,11 @@ #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterRenderer.h" #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterViewController_Internal.h" #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterViewEngineProvider.h" -#include "flutter/shell/platform/embedder/embedder.h" const uint64_t kFlutterDefaultViewId = 0; +NSString* const kFlutterPlatformChannel = @"flutter/platform"; + /** * Constructs and returns a FlutterLocale struct corresponding to |locale|, which must outlive * the returned struct. @@ -152,6 +159,84 @@ - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result; #pragma mark - +@implementation FlutterEngineTerminationHandler { + FlutterEngine* _engine; + FlutterTerminationCallback _terminator; +} + +- (instancetype)initWithEngine:(FlutterEngine*)engine + terminator:(FlutterTerminationCallback)terminator { + self = [super init]; + _engine = engine; + _terminator = terminator ? terminator : ^(id sender) { + // Default to actually terminating the application. The terminator exists to + // allow tests to override it so that an actual exit doesn't occur. + FlutterApplication* flutterApp = [FlutterApplication sharedApplication]; + if (flutterApp && [flutterApp respondsToSelector:@selector(terminateApplication:)]) { + [[FlutterApplication sharedApplication] terminateApplication:sender]; + } else if (flutterApp) { + [flutterApp terminate:sender]; + } + }; + FlutterAppDelegate* appDelegate = + (FlutterAppDelegate*)[[FlutterApplication sharedApplication] delegate]; + appDelegate.terminationHandler = self; + return self; +} + +// This is called by the method call handler in the engine when the application +// requests termination itself. +- (void)handleRequestAppExitMethodCall:(NSDictionary*)arguments + result:(FlutterResult)result { + NSString* type = arguments[@"type"]; + // Ignore the "exitCode" value in the arguments because AppKit doesn't have + // any good way to set the process exit code other than calling exit(), and + // that bypasses all of the native applicationShouldExit shutdown events, + // etc., which we don't want to skip. + + FlutterAppExitType exitType = + [type isEqualTo:@"cancelable"] ? kFlutterAppExitTypeCancelable : kFlutterAppExitTypeRequired; + + [self requestApplicationTermination:[FlutterApplication sharedApplication] + exitType:exitType + result:result]; +} + +// This is called by the FlutterAppDelegate whenever any termination request is +// received. +- (void)requestApplicationTermination:(id)sender + exitType:(FlutterAppExitType)type + result:(nullable FlutterResult)result { + switch (type) { + case kFlutterAppExitTypeCancelable: { + FlutterJSONMethodCodec* codec = [FlutterJSONMethodCodec sharedInstance]; + FlutterMethodCall* methodCall = + [FlutterMethodCall methodCallWithMethodName:@"System.requestAppExit" arguments:nil]; + [_engine sendOnChannel:kFlutterPlatformChannel + message:[codec encodeMethodCall:methodCall] + binaryReply:^(NSData* _Nullable reply) { + NSDictionary* replyArgs = [codec decodeEnvelope:reply]; + if ([replyArgs[@"response"] isEqual:@"exit"]) { + NSAssert(_terminator, @"terminator shouldn't be nil"); + _terminator(sender); + } + if (result != nil) { + result(replyArgs); + } + }]; + break; + } + case kFlutterAppExitTypeRequired: + NSAssert(_terminator, @"terminator shouldn't be nil"); + _terminator(sender); + break; + } +} + +@end + +#pragma mark - + /** * `FlutterPluginRegistrar` implementation handling a single plugin. */ @@ -290,6 +375,8 @@ - (instancetype)initWithName:(NSString*)labelPrefix _semanticsEnabled = NO; _isResponseValid = [[NSMutableArray alloc] initWithCapacity:1]; [_isResponseValid addObject:@YES]; + _terminationHandler = [[FlutterEngineTerminationHandler alloc] initWithEngine:self + terminator:nil]; // kFlutterDefaultViewId is reserved for the default view. // All IDs above it are for regular views. _nextViewId = kFlutterDefaultViewId + 1; @@ -881,6 +968,8 @@ - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { result(nil); } 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]; } else { result(FlutterMethodNotImplemented); } diff --git a/shell/platform/darwin/macos/framework/Source/FlutterEngineTest.mm b/shell/platform/darwin/macos/framework/Source/FlutterEngineTest.mm index fd5f27d1f5c34..fc105dd710bdd 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterEngineTest.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterEngineTest.mm @@ -4,6 +4,7 @@ #import "flutter/shell/platform/darwin/macos/framework/Headers/FlutterEngine.h" #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterEngine_Internal.h" +#include "gtest/gtest.h" #include #include @@ -12,6 +13,7 @@ #include "flutter/lib/ui/window/platform_message.h" #include "flutter/shell/platform/common/accessibility_bridge.h" #import "flutter/shell/platform/darwin/macos/framework/Headers/FlutterAppDelegate.h" +#import "flutter/shell/platform/darwin/macos/framework/Headers/FlutterApplication.h" #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterEngineTestUtils.h" #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterViewControllerTestUtils.h" #include "flutter/shell/platform/embedder/embedder.h" @@ -407,7 +409,7 @@ - (nonnull NSView*)createWithViewIdentifier:(int64_t)viewId arguments:(nullable ASSERT_TRUE(latch_called); } -TEST(FlutterEngine, Compositor) { +TEST_F(FlutterEngineTest, Compositor) { NSString* fixtures = @(flutter::testing::GetFixturesPath()); FlutterDartProject* project = [[FlutterDartProject alloc] initWithAssetsPath:fixtures @@ -446,7 +448,7 @@ - (nonnull NSView*)createWithViewIdentifier:(int64_t)viewId arguments:(nullable [engine shutDownEngine]; } // namespace flutter::testing -TEST(FlutterEngine, DartEntrypointArguments) { +TEST_F(FlutterEngineTest, DartEntrypointArguments) { NSString* fixtures = @(flutter::testing::GetFixturesPath()); FlutterDartProject* project = [[FlutterDartProject alloc] initWithAssetsPath:fixtures @@ -541,7 +543,7 @@ - (nonnull NSView*)createWithViewIdentifier:(int64_t)viewId arguments:(nullable EXPECT_EQ(record, 21); } -TEST(FlutterEngine, HasStringsWhenPasteboardEmpty) { +TEST_F(FlutterEngineTest, HasStringsWhenPasteboardEmpty) { id engineMock = CreateMockFlutterEngine(nil); // Call hasStrings and expect it to be false. @@ -559,7 +561,7 @@ - (nonnull NSView*)createWithViewIdentifier:(int64_t)viewId arguments:(nullable EXPECT_FALSE(valueAfterClear); } -TEST(FlutterEngine, HasStringsWhenPasteboardFull) { +TEST_F(FlutterEngineTest, HasStringsWhenPasteboardFull) { id engineMock = CreateMockFlutterEngine(@"some string"); // Call hasStrings and expect it to be true. @@ -619,7 +621,7 @@ - (nonnull NSView*)createWithViewIdentifier:(int64_t)viewId arguments:(nullable } } -TEST(EngineTest, ThreadSynchronizerNotBlockingRasterThreadAfterShutdown) { +TEST_F(FlutterEngineTest, ThreadSynchronizerNotBlockingRasterThreadAfterShutdown) { FlutterThreadSynchronizer* threadSynchronizer = [[FlutterThreadSynchronizer alloc] init]; [threadSynchronizer shutdown]; @@ -695,6 +697,55 @@ - (nonnull NSView*)createWithViewIdentifier:(int64_t)viewId arguments:(nullable EXPECT_EQ(viewController1.id, 0ull); } +TEST_F(FlutterEngineTest, HandlesTerminationRequest) { + id engineMock = CreateMockFlutterEngine(nil); + __block NSString* nextResponse = @"exit"; + __block BOOL triedToTerminate = FALSE; + FlutterEngineTerminationHandler* terminationHandler = + [[FlutterEngineTerminationHandler alloc] initWithEngine:engineMock + terminator:^(id sender) { + triedToTerminate = TRUE; + // Don't actually terminate, of course. + }]; + OCMStub([engineMock terminationHandler]).andReturn(terminationHandler); + id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger)); + OCMStub( // NOLINT(google-objc-avoid-throwing-exception) + [engineMock binaryMessenger]) + .andReturn(binaryMessengerMock); + OCMStub([engineMock sendOnChannel:@"flutter/platform" + message:[OCMArg any] + binaryReply:[OCMArg any]]) + .andDo((^(NSInvocation* invocation) { + [invocation retainArguments]; + FlutterBinaryReply callback; + [invocation getArgument:&callback atIndex:4]; + NSDictionary* responseDict = @{@"response" : nextResponse}; + NSData* returnedMessage = + [[FlutterJSONMethodCodec sharedInstance] encodeSuccessEnvelope:responseDict]; + callback(returnedMessage); + })); + __block NSString* calledAfterTerminate = @""; + FlutterResult appExitResult = ^(id result) { + NSDictionary* resultDict = result; + calledAfterTerminate = resultDict[@"response"]; + }; + FlutterMethodCall* methodExitApplication = + [FlutterMethodCall methodCallWithMethodName:@"System.exitApplication" + arguments:@{@"type" : @"cancelable"}]; + + triedToTerminate = FALSE; + nextResponse = @"exit"; + [engineMock handleMethodCall:methodExitApplication result:appExitResult]; + EXPECT_STREQ([calledAfterTerminate UTF8String], "exit"); + EXPECT_TRUE(triedToTerminate); + + triedToTerminate = FALSE; + nextResponse = @"cancel"; + [engineMock handleMethodCall:methodExitApplication result:appExitResult]; + EXPECT_STREQ([calledAfterTerminate UTF8String], "cancel"); + EXPECT_FALSE(triedToTerminate); +} + } // namespace flutter::testing // NOLINTEND(clang-analyzer-core.StackAddressEscape) diff --git a/shell/platform/darwin/macos/framework/Source/FlutterEngine_Internal.h b/shell/platform/darwin/macos/framework/Source/FlutterEngine_Internal.h index c617363c66eb8..bbc4527543c9a 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterEngine_Internal.h +++ b/shell/platform/darwin/macos/framework/Source/FlutterEngine_Internal.h @@ -8,11 +8,59 @@ #include +#import "flutter/shell/platform/darwin/macos/framework/Headers/FlutterApplication.h" #import "flutter/shell/platform/darwin/macos/framework/Source/AccessibilityBridgeMac.h" #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterCompositor.h" #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterPlatformViewController.h" #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterRenderer.h" +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - Typedefs + +typedef void (^FlutterTerminationCallback)(id _Nullable sender); + +#pragma mark - Enumerations + +/** + * An enum for defining the different request types allowed when requesting an + * application exit. + * + * Must match the entries in the `AppExitType` enum in the Dart code. + */ +typedef NS_ENUM(NSInteger, FlutterAppExitType) { + kFlutterAppExitTypeCancelable = 0, + kFlutterAppExitTypeRequired = 1, +}; + +/** + * An enum for defining the different responses the framework can give to an + * application exit request from the engine. + * + * Must match the entries in the `AppExitResponse` enum in the Dart code. + */ +typedef NS_ENUM(NSInteger, FlutterAppExitResponse) { + kFlutterAppExitResponseCancel = 0, + kFlutterAppExitResponseExit = 1, +}; + +#pragma mark - FlutterEngineTerminationHandler + +/** + * A handler interface for handling application termination that the + * FlutterAppDelegate can use to coordinate an application exit by sending + * messages through the platform channel managed by the engine. + */ +@interface FlutterEngineTerminationHandler : NSObject +- (instancetype)initWithEngine:(FlutterEngine*)engine + terminator:(nullable FlutterTerminationCallback)terminator; +- (void)handleRequestAppExitMethodCall:(NSDictionary*)data + result:(FlutterResult)result; +- (void)requestApplicationTermination:(FlutterApplication*)sender + exitType:(FlutterAppExitType)type + result:(nullable FlutterResult)result; +@end + @interface FlutterEngine () /** @@ -52,6 +100,11 @@ */ @property(nonatomic, readonly) std::vector switches; +/** + * Provides the |FlutterEngineTerminationHandler| to be used for this engine. + */ +@property(nonatomic, readonly) FlutterEngineTerminationHandler* terminationHandler; + /** * Attach a view controller to the engine as its default controller. * @@ -64,7 +117,7 @@ * If the given view controller is already attached to an engine, this call * throws an assertion. */ -- (void)addViewController:(nonnull FlutterViewController*)viewController; +- (void)addViewController:(FlutterViewController*)viewController; /** * Dissociate the given view controller from this engine. @@ -75,17 +128,17 @@ * If the view controller is not associated with this engine, this call throws an * assertion. */ -- (void)removeViewController:(nonnull FlutterViewController*)viewController; +- (void)removeViewController:(FlutterViewController*)viewController; /** - * The `FlutterViewController` associated with the given view ID, if any. + * The |FlutterViewController| associated with the given view ID, if any. */ - (nullable FlutterViewController*)viewControllerForId:(uint64_t)viewId; /** * Informs the engine that the specified view controller's window metrics have changed. */ -- (void)updateWindowMetricsForViewController:(nonnull FlutterViewController*)viewController; +- (void)updateWindowMetricsForViewController:(FlutterViewController*)viewController; /** * Dispatches the given pointer event data to engine. @@ -127,3 +180,5 @@ withData:(fml::MallocMapping)data; @end + +NS_ASSUME_NONNULL_END diff --git a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm index 7129b5e7cb193..ebf5057d6e7ae 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.mm @@ -14,7 +14,6 @@ #include "flutter/shell/platform/common/text_editing_delta.h" #include "flutter/shell/platform/common/text_input_model.h" #import "flutter/shell/platform/darwin/common/framework/Headers/FlutterCodecs.h" -#import "flutter/shell/platform/darwin/macos/framework/Headers/FlutterAppDelegate.h" #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterTextInputSemanticsObject.h" #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterViewController_Internal.h" diff --git a/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm b/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm index 994cc8f468d69..a921738d6d73a 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm @@ -9,7 +9,6 @@ #import "flutter/shell/platform/darwin/common/framework/Headers/FlutterChannels.h" #import "flutter/shell/platform/darwin/common/framework/Headers/FlutterCodecs.h" -#import "flutter/shell/platform/darwin/macos/framework/Headers/FlutterAppDelegate.h" #import "flutter/shell/platform/darwin/macos/framework/Headers/FlutterEngine.h" #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterEngine_Internal.h" #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterKeyPrimaryResponder.h"