Skip to content

Commit 7ec7388

Browse files
stuartmorgan-ggaaclarke
authored andcommitted
Allow macOS plugins to register as app delegates (flutter#44587)
Adds `addApplicationDelegate:` to the macOS plugin registrar, following the corresponding iOS method, and wires it up to the existing app delegation forwarding that was recently added for use at the application level. (The actual delegate is non-trivially different between iOS and macOS, but that's not resolveable without a complex migration on the iOS side, so the APIs currently diverge after the level of the `addApplicationDelegate:` method itself.) This doesn't add any new methods to the delegation; those will be added in a follow-up PR. Also fixes a retain cycle in the termination handler that prevented the new test from working. Most of flutter/flutter#41471 [C++, Objective-C, Java style guides]: https://github.com/flutter/engine/blob/main/CONTRIBUTING.md#style
1 parent accdd98 commit 7ec7388

File tree

7 files changed

+169
-27
lines changed

7 files changed

+169
-27
lines changed

shell/platform/darwin/common/framework/Source/FlutterBinaryMessengerRelay.mm

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ @implementation FlutterBinaryMessengerRelay
1414
- (instancetype)initWithParent:(NSObject<FlutterBinaryMessenger>*)parent {
1515
self = [super init];
1616
if (self != nil) {
17-
self.parent = parent;
17+
_parent = parent;
1818
}
1919
return self;
2020
}

shell/platform/darwin/macos/framework/Headers/FlutterAppDelegate.h

+26-13
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,30 @@
1010
#import "FlutterAppLifecycleDelegate.h"
1111
#import "FlutterMacros.h"
1212

13+
/**
14+
* A protocol to be implemented by the `NSApplicationDelegate` of an application to enable the
15+
* Flutter framework and any Flutter plugins to register to receive application life cycle events.
16+
*
17+
* Implementers should forward all of the `NSApplicationDelegate` methods corresponding to the
18+
* handlers in FlutterAppLifecycleDelegate to any registered delegates.
19+
*/
20+
FLUTTER_DARWIN_EXPORT
21+
@protocol FlutterAppLifecycleProvider <NSObject>
22+
23+
/**
24+
* Adds an object implementing |FlutterAppLifecycleDelegate| to the list of
25+
* delegates to be informed of application lifecycle events.
26+
*/
27+
- (void)addApplicationLifecycleDelegate:(nonnull NSObject<FlutterAppLifecycleDelegate>*)delegate;
28+
29+
/**
30+
* Removes an object implementing |FlutterAppLifecycleDelegate| to the list of
31+
* delegates to be informed of application lifecycle events.
32+
*/
33+
- (void)removeApplicationLifecycleDelegate:(nonnull NSObject<FlutterAppLifecycleDelegate>*)delegate;
34+
35+
@end
36+
1337
/**
1438
* |NSApplicationDelegate| subclass for simple apps that want default behavior.
1539
*
@@ -20,13 +44,14 @@
2044
* * Updates the main Flutter window's title to match the name in the app's Info.plist.
2145
* |mainFlutterWindow| must be set before the application finishes launching for this to take
2246
* effect.
47+
* * Forwards `NSApplicationDelegate` callbacks to plugins that register for them.
2348
*
2449
* App delegates for Flutter applications are *not* required to inherit from
2550
* this class. Developers of custom app delegate classes should copy and paste
2651
* code as necessary from FlutterAppDelegate.mm.
2752
*/
2853
FLUTTER_DARWIN_EXPORT
29-
@interface FlutterAppDelegate : NSObject <NSApplicationDelegate>
54+
@interface FlutterAppDelegate : NSObject <NSApplicationDelegate, FlutterAppLifecycleProvider>
3055

3156
/**
3257
* The application menu in the menu bar.
@@ -39,18 +64,6 @@ FLUTTER_DARWIN_EXPORT
3964
*/
4065
@property(weak, nonatomic) IBOutlet NSWindow* mainFlutterWindow;
4166

42-
/**
43-
* Adds an object implementing |FlutterAppLifecycleDelegate| to the list of
44-
* delegates to be informed of application lifecycle events.
45-
*/
46-
- (void)addApplicationLifecycleDelegate:(NSObject<FlutterAppLifecycleDelegate>*)delegate;
47-
48-
/**
49-
* Removes an object implementing |FlutterAppLifecycleDelegate| to the list of
50-
* delegates to be informed of application lifecycle events.
51-
*/
52-
- (void)removeApplicationLifecycleDelegate:(NSObject<FlutterAppLifecycleDelegate>*)delegate;
53-
5467
@end
5568

5669
#endif // FLUTTER_FLUTTERAPPDELEGATE_H_

shell/platform/darwin/macos/framework/Headers/FlutterAppLifecycleDelegate.h

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
#include <Foundation/Foundation.h>
1010

1111
#import "FlutterMacros.h"
12-
#import "FlutterPluginMacOS.h"
1312

1413
NS_ASSUME_NONNULL_BEGIN
1514

@@ -18,6 +17,7 @@ NS_ASSUME_NONNULL_BEGIN
1817
* Protocol for listener of lifecycle events from the NSApplication, typically a
1918
* FlutterPlugin.
2019
*/
20+
FLUTTER_DARWIN_EXPORT
2121
@protocol FlutterAppLifecycleDelegate <NSObject>
2222

2323
@optional

shell/platform/darwin/macos/framework/Headers/FlutterPluginMacOS.h

+2-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
#import <Foundation/Foundation.h>
66

7+
#import "FlutterAppLifecycleDelegate.h"
78
#import "FlutterChannels.h"
89
#import "FlutterCodecs.h"
910
#import "FlutterMacros.h"
@@ -22,7 +23,7 @@ NS_ASSUME_NONNULL_BEGIN
2223
* expand over time to more closely match the functionality of the iOS FlutterPlugin.
2324
*/
2425
FLUTTER_DARWIN_EXPORT
25-
@protocol FlutterPlugin <NSObject>
26+
@protocol FlutterPlugin <NSObject, FlutterAppLifecycleDelegate>
2627

2728
/**
2829
* Creates an instance of the plugin to register with |registrar| using the desired

shell/platform/darwin/macos/framework/Headers/FlutterPluginRegistrarMacOS.h

+7
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,13 @@ FLUTTER_DARWIN_EXPORT
5252
- (void)addMethodCallDelegate:(nonnull id<FlutterPlugin>)delegate
5353
channel:(nonnull FlutterMethodChannel*)channel;
5454

55+
/**
56+
* Registers the plugin as a receiver of `NSApplicationDelegate` calls.
57+
*
58+
* @param delegate The receiving object, such as the plugin's main class.
59+
*/
60+
- (void)addApplicationDelegate:(nonnull NSObject<FlutterAppLifecycleDelegate>*)delegate;
61+
5562
/**
5663
* Registers a `FlutterPlatformViewFactory` for creation of platform views.
5764
*

shell/platform/darwin/macos/framework/Source/FlutterEngine.mm

+36-9
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,11 @@ @interface FlutterEngine () <FlutterBinaryMessenger>
9090
*/
9191
@property(nonatomic, strong) NSMutableArray<NSNumber*>* isResponseValid;
9292

93+
/**
94+
* All delegates added via plugin calls to addApplicationDelegate.
95+
*/
96+
@property(nonatomic, strong) NSPointerArray* pluginAppDelegates;
97+
9398
- (nullable FlutterViewController*)viewControllerForId:(FlutterViewId)viewId;
9499

95100
/**
@@ -165,7 +170,7 @@ - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result;
165170
#pragma mark -
166171

167172
@implementation FlutterEngineTerminationHandler {
168-
FlutterEngine* _engine;
173+
__weak FlutterEngine* _engine;
169174
FlutterTerminationCallback _terminator;
170175
}
171176

@@ -320,6 +325,16 @@ - (void)addMethodCallDelegate:(nonnull id<FlutterPlugin>)delegate
320325
}];
321326
}
322327

328+
- (void)addApplicationDelegate:(NSObject<FlutterAppLifecycleDelegate>*)delegate {
329+
id<NSApplicationDelegate> appDelegate = [[NSApplication sharedApplication] delegate];
330+
if ([appDelegate conformsToProtocol:@protocol(FlutterAppLifecycleProvider)]) {
331+
id<FlutterAppLifecycleProvider> lifeCycleProvider =
332+
static_cast<id<FlutterAppLifecycleProvider>>(appDelegate);
333+
[lifeCycleProvider addApplicationLifecycleDelegate:delegate];
334+
[_flutterEngine.pluginAppDelegates addPointer:(__bridge void*)delegate];
335+
}
336+
}
337+
323338
- (void)registerViewFactory:(nonnull NSObject<FlutterPlatformViewFactory>*)factory
324339
withId:(nonnull NSString*)factoryId {
325340
[[_flutterEngine platformViewController] registerViewFactory:factory withId:factoryId];
@@ -421,6 +436,8 @@ - (instancetype)initWithName:(NSString*)labelPrefix
421436
_visible = NO;
422437
_project = project ?: [[FlutterDartProject alloc] init];
423438
_messengerHandlers = [[NSMutableDictionary alloc] init];
439+
_binaryMessenger = [[FlutterBinaryMessengerRelay alloc] initWithParent:self];
440+
_pluginAppDelegates = [NSPointerArray weakObjectsPointerArray];
424441
_currentMessengerConnection = 1;
425442
_allowHeadlessExecution = allowHeadlessExecution;
426443
_semanticsEnabled = NO;
@@ -448,12 +465,12 @@ - (instancetype)initWithName:(NSString*)labelPrefix
448465
[self setUpAccessibilityChannel];
449466
[self setUpNotificationCenterListeners];
450467
id<NSApplicationDelegate> appDelegate = [[NSApplication sharedApplication] delegate];
451-
const SEL selector = @selector(addApplicationLifecycleDelegate:);
452-
if ([appDelegate respondsToSelector:selector]) {
468+
if ([appDelegate conformsToProtocol:@protocol(FlutterAppLifecycleProvider)]) {
453469
_terminationHandler = [[FlutterEngineTerminationHandler alloc] initWithEngine:self
454470
terminator:nil];
455-
FlutterAppDelegate* flutterAppDelegate = reinterpret_cast<FlutterAppDelegate*>(appDelegate);
456-
[flutterAppDelegate addApplicationLifecycleDelegate:self];
471+
id<FlutterAppLifecycleProvider> lifecycleProvider =
472+
static_cast<id<FlutterAppLifecycleProvider>>(appDelegate);
473+
[lifecycleProvider addApplicationLifecycleDelegate:self];
457474
} else {
458475
_terminationHandler = nil;
459476
}
@@ -462,10 +479,20 @@ - (instancetype)initWithName:(NSString*)labelPrefix
462479
}
463480

464481
- (void)dealloc {
465-
FlutterAppDelegate* appDelegate =
466-
reinterpret_cast<FlutterAppDelegate*>([[NSApplication sharedApplication] delegate]);
467-
if (appDelegate != nil) {
468-
[appDelegate removeApplicationLifecycleDelegate:self];
482+
id<NSApplicationDelegate> appDelegate = [[NSApplication sharedApplication] delegate];
483+
if ([appDelegate conformsToProtocol:@protocol(FlutterAppLifecycleProvider)]) {
484+
id<FlutterAppLifecycleProvider> lifecycleProvider =
485+
static_cast<id<FlutterAppLifecycleProvider>>(appDelegate);
486+
[lifecycleProvider removeApplicationLifecycleDelegate:self];
487+
488+
// Unregister any plugins that registered as app delegates, since they are not guaranteed to
489+
// live after the engine is destroyed, and their delegation registration is intended to be bound
490+
// to the engine and its lifetime.
491+
for (id<FlutterAppLifecycleDelegate> delegate in _pluginAppDelegates) {
492+
if (delegate) {
493+
[lifecycleProvider removeApplicationLifecycleDelegate:delegate];
494+
}
495+
}
469496
}
470497
@synchronized(_isResponseValid) {
471498
[_isResponseValid removeAllObjects];

shell/platform/darwin/macos/framework/Source/FlutterEngineTest.mm

+96-2
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,19 @@
77

88
#include <objc/objc.h>
99

10+
#include <algorithm>
1011
#include <functional>
1112
#include <thread>
13+
#include <vector>
1214

1315
#include "flutter/fml/synchronization/waitable_event.h"
1416
#include "flutter/lib/ui/window/platform_message.h"
1517
#include "flutter/shell/platform/common/accessibility_bridge.h"
1618
#import "flutter/shell/platform/darwin/common/framework/Headers/FlutterChannels.h"
1719
#import "flutter/shell/platform/darwin/common/framework/Source/FlutterBinaryMessengerRelay.h"
1820
#import "flutter/shell/platform/darwin/macos/framework/Headers/FlutterAppDelegate.h"
21+
#import "flutter/shell/platform/darwin/macos/framework/Headers/FlutterAppLifecycleDelegate.h"
22+
#import "flutter/shell/platform/darwin/macos/framework/Headers/FlutterPluginMacOS.h"
1923
#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterEngineTestUtils.h"
2024
#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterViewControllerTestUtils.h"
2125
#include "flutter/shell/platform/embedder/embedder.h"
@@ -59,6 +63,60 @@ - (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication* _Nonnu
5963
}
6064
@end
6165

66+
#pragma mark -
67+
68+
@interface FakeLifecycleProvider : NSObject <FlutterAppLifecycleProvider, NSApplicationDelegate>
69+
70+
@property(nonatomic, strong, readonly) NSPointerArray* registeredDelegates;
71+
72+
// True if the given delegate is currently registered.
73+
- (BOOL)hasDelegate:(nonnull NSObject<FlutterAppLifecycleDelegate>*)delegate;
74+
@end
75+
76+
@implementation FakeLifecycleProvider {
77+
/**
78+
* All currently registered delegates.
79+
*
80+
* This does not use NSPointerArray or any other weak-pointer
81+
* system, because a weak pointer will be nil'd out at the start of dealloc, which will break
82+
* queries. E.g., if a delegate is dealloc'd without being unregistered, a weak pointer array
83+
* would no longer contain that pointer even though removeApplicationLifecycleDelegate: was never
84+
* called, causing tests to pass incorrectly.
85+
*/
86+
std::vector<void*> _delegates;
87+
}
88+
89+
- (void)addApplicationLifecycleDelegate:(nonnull NSObject<FlutterAppLifecycleDelegate>*)delegate {
90+
_delegates.push_back((__bridge void*)delegate);
91+
}
92+
93+
- (void)removeApplicationLifecycleDelegate:
94+
(nonnull NSObject<FlutterAppLifecycleDelegate>*)delegate {
95+
auto delegateIndex = std::find(_delegates.begin(), _delegates.end(), (__bridge void*)delegate);
96+
NSAssert(delegateIndex != _delegates.end(),
97+
@"Attempting to unregister a delegate that was not registered.");
98+
_delegates.erase(delegateIndex);
99+
}
100+
101+
- (BOOL)hasDelegate:(nonnull NSObject<FlutterAppLifecycleDelegate>*)delegate {
102+
return std::find(_delegates.begin(), _delegates.end(), (__bridge void*)delegate) !=
103+
_delegates.end();
104+
}
105+
106+
@end
107+
108+
#pragma mark -
109+
110+
@interface FakeAppDelegatePlugin : NSObject <FlutterPlugin>
111+
@end
112+
113+
@implementation FakeAppDelegatePlugin
114+
+ (void)registerWithRegistrar:(id<FlutterPluginRegistrar>)registrar {
115+
}
116+
@end
117+
118+
#pragma mark -
119+
62120
namespace flutter::testing {
63121

64122
TEST_F(FlutterEngineTest, CanLaunch) {
@@ -515,7 +573,7 @@ - (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication* _Nonnu
515573
ICUDataPath:[fixtures stringByAppendingString:@"/icudtl.dat"]];
516574
FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"test"
517575
project:project
518-
allowHeadlessExecution:true];
576+
allowHeadlessExecution:YES];
519577
weakEngine = engine;
520578
binaryMessenger = engine.binaryMessenger;
521579
}
@@ -539,7 +597,7 @@ - (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication* _Nonnu
539597
ICUDataPath:[fixtures stringByAppendingString:@"/icudtl.dat"]];
540598
FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"test"
541599
project:project
542-
allowHeadlessExecution:true];
600+
allowHeadlessExecution:YES];
543601
id<FlutterPluginRegistrar> registrar = [engine registrarForPlugin:@"MyPlugin"];
544602
textureRegistry = registrar.textures;
545603
}
@@ -956,6 +1014,42 @@ - (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication* _Nonnu
9561014
[mockApplication stopMocking];
9571015
}
9581016

1017+
TEST_F(FlutterEngineTest, ForwardsPluginDelegateRegistration) {
1018+
id<NSApplicationDelegate> previousDelegate = [[NSApplication sharedApplication] delegate];
1019+
FakeLifecycleProvider* fakeAppDelegate = [[FakeLifecycleProvider alloc] init];
1020+
[NSApplication sharedApplication].delegate = fakeAppDelegate;
1021+
1022+
FakeAppDelegatePlugin* plugin = [[FakeAppDelegatePlugin alloc] init];
1023+
FlutterEngine* engine = CreateMockFlutterEngine(nil);
1024+
1025+
[[engine registrarForPlugin:@"TestPlugin"] addApplicationDelegate:plugin];
1026+
1027+
EXPECT_TRUE([fakeAppDelegate hasDelegate:plugin]);
1028+
1029+
[NSApplication sharedApplication].delegate = previousDelegate;
1030+
}
1031+
1032+
TEST_F(FlutterEngineTest, UnregistersPluginsOnEngineDestruction) {
1033+
id<NSApplicationDelegate> previousDelegate = [[NSApplication sharedApplication] delegate];
1034+
FakeLifecycleProvider* fakeAppDelegate = [[FakeLifecycleProvider alloc] init];
1035+
[NSApplication sharedApplication].delegate = fakeAppDelegate;
1036+
1037+
FakeAppDelegatePlugin* plugin = [[FakeAppDelegatePlugin alloc] init];
1038+
1039+
@autoreleasepool {
1040+
FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"test" project:nil];
1041+
1042+
[[engine registrarForPlugin:@"TestPlugin"] addApplicationDelegate:plugin];
1043+
EXPECT_TRUE([fakeAppDelegate hasDelegate:plugin]);
1044+
}
1045+
1046+
// When the engine is released, it should unregister any plugins it had
1047+
// registered on its behalf.
1048+
EXPECT_FALSE([fakeAppDelegate hasDelegate:plugin]);
1049+
1050+
[NSApplication sharedApplication].delegate = previousDelegate;
1051+
}
1052+
9591053
} // namespace flutter::testing
9601054

9611055
// NOLINTEND(clang-analyzer-core.StackAddressEscape)

0 commit comments

Comments
 (0)