Skip to content

Commit

Permalink
[macOS] Handle termination in FlutterAppDelegate
Browse files Browse the repository at this point in the history
Moves application termination handling to FlutterAppDelegate.
Previously, we required macOS applications using Flutter to ensure their
main application class was FlutterApplication. Instead, we now do all
handling in FlutterAppDelegate and FlutterEngine.

There are two termination workflows to consider:
* Termination requested from framework side: In this case, then engine
  receives a `System.exitApplication` method call, and starts the app
  termination workflow via
  `[FlutterEngine requestApplicationTermination:exitType:result]`.
* Termination requested from macOS (e.g. Cmd-Q): In this case,
  `FlutterAppDelegate`'s `applicationShouldTerminate:` handler is
  invoked by AppKit, and the delegate starts the app termination
  workflow via
  `[FlutterEngine requestApplicationTermination:exitType:result]`.

In either case, at this point, if the request is not cancellable, the
app immediately exits. If it is cancellable, the embedder sends a
`System.requestAppExit` method channel invocation to the framework,
which responds with either `exit` or `cancel`. In the case of `exit` we
immediately exit, otherwise we do nothing and the app continues running.

This is a minor refactoring of the original approach we took in:
#39836

This does not remove the FlutterApplication class, since the framework
migration from NSApplication to FlutterApplication still depends on it.
A followup patch with replace the migration with a reverse migration
will land, then FlutterApplication will be removed.

Issue: flutter/flutter#30735
  • Loading branch information
cbracken committed Apr 4, 2023
1 parent 65515ad commit aa9bed4
Show file tree
Hide file tree
Showing 6 changed files with 29 additions and 129 deletions.
25 changes: 18 additions & 7 deletions shell/platform/darwin/macos/framework/Source/FlutterAppDelegate.mm
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,6 @@ - (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 {
Expand All @@ -60,4 +53,22 @@ - (NSString*)applicationName {
return applicationName;
}

- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication* _Nonnull)sender {
// If the framework has already told us to terminate, terminate immediately.
if ([[self terminationHandler] shouldTerminate]) {
return NSTerminateNow;
}

// Send a termination request to the framework.
FlutterEngineTerminationHandler* terminationHandler = [self terminationHandler];
[terminationHandler requestApplicationTermination:sender
exitType:kFlutterAppExitTypeCancelable
result:nil];

// Cancel termination to allow the framework to handle the request asynchronously. When the
// termination request returns from the app, if termination is desired, this method will be
// reinvoked with self.terminationHandler.shouldTerminate set to YES.
return NSTerminateCancel;
}

@end
82 changes: 0 additions & 82 deletions shell/platform/darwin/macos/framework/Source/FlutterApplication.mm
Original file line number Diff line number Diff line change
Expand Up @@ -3,88 +3,6 @@
// 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"

#import "flutter/shell/platform/darwin/macos/framework/Headers/FlutterAppDelegate.h"
#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterAppDelegate_Internal.h"
#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterEngine_Internal.h"
#include "flutter/shell/platform/embedder/embedder.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<FlutterAppDelegate*>([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

This file was deleted.

17 changes: 7 additions & 10 deletions shell/platform/darwin/macos/framework/Source/FlutterEngine.mm
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,7 @@
#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"
Expand Down Expand Up @@ -176,15 +174,11 @@ - (instancetype)initWithEngine:(FlutterEngine*)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];
}
NSApplication* flutterApp = [NSApplication sharedApplication];
[flutterApp terminate:sender];
};
FlutterAppDelegate* appDelegate =
(FlutterAppDelegate*)[[FlutterApplication sharedApplication] delegate];
(FlutterAppDelegate*)[[NSApplication sharedApplication] delegate];
appDelegate.terminationHandler = self;
return self;
}
Expand All @@ -202,7 +196,7 @@ - (void)handleRequestAppExitMethodCall:(NSDictionary<NSString*, id>*)arguments
FlutterAppExitType exitType =
[type isEqualTo:@"cancelable"] ? kFlutterAppExitTypeCancelable : kFlutterAppExitTypeRequired;

[self requestApplicationTermination:[FlutterApplication sharedApplication]
[self requestApplicationTermination:[NSApplication sharedApplication]
exitType:exitType
result:result];
}
Expand All @@ -212,6 +206,7 @@ - (void)handleRequestAppExitMethodCall:(NSDictionary<NSString*, id>*)arguments
- (void)requestApplicationTermination:(id)sender
exitType:(FlutterAppExitType)type
result:(nullable FlutterResult)result {
_shouldTerminate = YES;
switch (type) {
case kFlutterAppExitTypeCancelable: {
FlutterJSONMethodCodec* codec = [FlutterJSONMethodCodec sharedInstance];
Expand All @@ -238,6 +233,8 @@ - (void)requestApplicationTermination:(id)sender
NSDictionary* replyArgs = (NSDictionary*)decoded_reply;
if ([replyArgs[@"response"] isEqual:@"exit"]) {
_terminator(sender);
} else if ([replyArgs[@"response"] isEqual:@"cancel"]) {
_shouldTerminate = NO;
}
if (result != nil) {
result(replyArgs);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
#include "flutter/shell/platform/common/accessibility_bridge.h"
#import "flutter/shell/platform/darwin/common/framework/Headers/FlutterChannels.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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@

#include <memory>

#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"
Expand Down Expand Up @@ -52,11 +51,14 @@ typedef NS_ENUM(NSInteger, FlutterAppExitResponse) {
* messages through the platform channel managed by the engine.
*/
@interface FlutterEngineTerminationHandler : NSObject

@property(nonatomic, readonly) BOOL shouldTerminate;

- (instancetype)initWithEngine:(FlutterEngine*)engine
terminator:(nullable FlutterTerminationCallback)terminator;
- (void)handleRequestAppExitMethodCall:(NSDictionary<NSString*, id>*)data
result:(FlutterResult)result;
- (void)requestApplicationTermination:(FlutterApplication*)sender
- (void)requestApplicationTermination:(NSApplication*)sender
exitType:(FlutterAppExitType)type
result:(nullable FlutterResult)result;
@end
Expand Down

0 comments on commit aa9bed4

Please sign in to comment.