| // 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/FlutterEngine.h" |
| #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterEngine_Internal.h" |
| |
| #include <algorithm> |
| #include <iostream> |
| #include <vector> |
| |
| #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/Source/FlutterAppDelegate_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" |
| #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterMouseCursorPlugin.h" |
| #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterPlatformViewController.h" |
| #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" |
| |
| NSString* const kFlutterPlatformChannel = @"flutter/platform"; |
| |
| /** |
| * Constructs and returns a FlutterLocale struct corresponding to |locale|, which must outlive |
| * the returned struct. |
| */ |
| static FlutterLocale FlutterLocaleFromNSLocale(NSLocale* locale) { |
| FlutterLocale flutterLocale = {}; |
| flutterLocale.struct_size = sizeof(FlutterLocale); |
| flutterLocale.language_code = [[locale objectForKey:NSLocaleLanguageCode] UTF8String]; |
| flutterLocale.country_code = [[locale objectForKey:NSLocaleCountryCode] UTF8String]; |
| flutterLocale.script_code = [[locale objectForKey:NSLocaleScriptCode] UTF8String]; |
| flutterLocale.variant_code = [[locale objectForKey:NSLocaleVariantCode] UTF8String]; |
| return flutterLocale; |
| } |
| |
| /// The private notification for voice over. |
| static NSString* const kEnhancedUserInterfaceNotification = |
| @"NSApplicationDidChangeAccessibilityEnhancedUserInterfaceNotification"; |
| static NSString* const kEnhancedUserInterfaceKey = @"AXEnhancedUserInterface"; |
| |
| /// Clipboard plain text format. |
| constexpr char kTextPlainFormat[] = "text/plain"; |
| |
| #pragma mark - |
| |
| // Records an active handler of the messenger (FlutterEngine) that listens to |
| // platform messages on a given channel. |
| @interface FlutterEngineHandlerInfo : NSObject |
| |
| - (instancetype)initWithConnection:(NSNumber*)connection |
| handler:(FlutterBinaryMessageHandler)handler; |
| |
| @property(nonatomic, readonly) FlutterBinaryMessageHandler handler; |
| @property(nonatomic, readonly) NSNumber* connection; |
| |
| @end |
| |
| @implementation FlutterEngineHandlerInfo |
| - (instancetype)initWithConnection:(NSNumber*)connection |
| handler:(FlutterBinaryMessageHandler)handler { |
| self = [super init]; |
| NSAssert(self, @"Super init cannot be nil"); |
| _connection = connection; |
| _handler = handler; |
| return self; |
| } |
| @end |
| |
| #pragma mark - |
| |
| /** |
| * Private interface declaration for FlutterEngine. |
| */ |
| @interface FlutterEngine () <FlutterBinaryMessenger> |
| |
| /** |
| * A mutable array that holds one bool value that determines if responses to platform messages are |
| * clear to execute. This value should be read or written only inside of a synchronized block and |
| * will return `NO` after the FlutterEngine has been dealloc'd. |
| */ |
| @property(nonatomic, strong) NSMutableArray<NSNumber*>* isResponseValid; |
| |
| - (nullable FlutterViewController*)viewControllerForId:(FlutterViewId)viewId; |
| |
| /** |
| * An internal method that adds the view controller with the given ID. |
| * |
| * This method assigns the controller with the ID, puts the controller into the |
| * map, and does assertions related to the default view ID. |
| */ |
| - (void)registerViewController:(FlutterViewController*)controller forId:(FlutterViewId)viewId; |
| |
| /** |
| * An internal method that removes the view controller with the given ID. |
| * |
| * This method clears the ID of the controller, removes the controller from the |
| * map. This is an no-op if the view ID is not associated with any view |
| * controllers. |
| */ |
| - (void)deregisterViewControllerForId:(FlutterViewId)viewId; |
| |
| /** |
| * Shuts down the engine if view requirement is not met, and headless execution |
| * is not allowed. |
| */ |
| - (void)shutDownIfNeeded; |
| |
| /** |
| * Sends the list of user-preferred locales to the Flutter engine. |
| */ |
| - (void)sendUserLocales; |
| |
| /** |
| * Handles a platform message from the engine. |
| */ |
| - (void)engineCallbackOnPlatformMessage:(const FlutterPlatformMessage*)message; |
| |
| /** |
| * Invoked right before the engine is restarted. |
| * |
| * This should reset states to as if the application has just started. It |
| * usually indicates a hot restart (Shift-R in Flutter CLI.) |
| */ |
| - (void)engineCallbackOnPreEngineRestart; |
| |
| /** |
| * Requests that the task be posted back the to the Flutter engine at the target time. The target |
| * time is in the clock used by the Flutter engine. |
| */ |
| - (void)postMainThreadTask:(FlutterTask)task targetTimeInNanoseconds:(uint64_t)targetTime; |
| |
| /** |
| * Loads the AOT snapshots and instructions from the elf bundle (app_elf_snapshot.so) into _aotData, |
| * if it is present in the assets directory. |
| */ |
| - (void)loadAOTData:(NSString*)assetsDir; |
| |
| /** |
| * Creates a platform view channel and sets up the method handler. |
| */ |
| - (void)setUpPlatformViewChannel; |
| |
| /** |
| * Creates an accessibility channel and sets up the message handler. |
| */ |
| - (void)setUpAccessibilityChannel; |
| |
| /** |
| * Handles messages received from the Flutter engine on the _*Channel channels. |
| */ |
| - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result; |
| |
| @end |
| |
| #pragma mark - |
| |
| @implementation FlutterEngineTerminationHandler { |
| FlutterEngine* _engine; |
| FlutterTerminationCallback _terminator; |
| } |
| |
| - (instancetype)initWithEngine:(FlutterEngine*)engine |
| terminator:(FlutterTerminationCallback)terminator { |
| self = [super init]; |
| _acceptingRequests = NO; |
| _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. |
| NSApplication* flutterApp = [NSApplication sharedApplication]; |
| [flutterApp terminate:sender]; |
| }; |
| FlutterAppDelegate* appDelegate = |
| (FlutterAppDelegate*)[[NSApplication 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<NSString*, id>*)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:[NSApplication 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 { |
| _shouldTerminate = YES; |
| if (![self acceptingRequests]) { |
| // Until the Dart application has signaled that it is ready to handle |
| // termination requests, the app will just terminate when asked. |
| type = kFlutterAppExitTypeRequired; |
| } |
| 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) { |
| NSAssert(_terminator, @"terminator shouldn't be nil"); |
| id decoded_reply = [codec decodeEnvelope:reply]; |
| if ([decoded_reply isKindOfClass:[FlutterError class]]) { |
| FlutterError* error = (FlutterError*)decoded_reply; |
| NSLog(@"Method call returned error[%@]: %@ %@", [error code], [error message], |
| [error details]); |
| _terminator(sender); |
| return; |
| } |
| if (![decoded_reply isKindOfClass:[NSDictionary class]]) { |
| NSLog(@"Call to System.requestAppExit returned an unexpected object: %@", |
| decoded_reply); |
| _terminator(sender); |
| return; |
| } |
| 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); |
| } |
| }]; |
| break; |
| } |
| case kFlutterAppExitTypeRequired: |
| NSAssert(_terminator, @"terminator shouldn't be nil"); |
| _terminator(sender); |
| break; |
| } |
| } |
| |
| @end |
| |
| #pragma mark - |
| |
| /** |
| * `FlutterPluginRegistrar` implementation handling a single plugin. |
| */ |
| @interface FlutterEngineRegistrar : NSObject <FlutterPluginRegistrar> |
| - (instancetype)initWithPlugin:(nonnull NSString*)pluginKey |
| flutterEngine:(nonnull FlutterEngine*)flutterEngine; |
| |
| - (NSView*)viewForId:(FlutterViewId)viewId; |
| @end |
| |
| @implementation FlutterEngineRegistrar { |
| NSString* _pluginKey; |
| FlutterEngine* _flutterEngine; |
| } |
| |
| @dynamic view; |
| |
| - (instancetype)initWithPlugin:(NSString*)pluginKey flutterEngine:(FlutterEngine*)flutterEngine { |
| self = [super init]; |
| if (self) { |
| _pluginKey = [pluginKey copy]; |
| _flutterEngine = flutterEngine; |
| } |
| return self; |
| } |
| |
| #pragma mark - FlutterPluginRegistrar |
| |
| - (id<FlutterBinaryMessenger>)messenger { |
| return _flutterEngine.binaryMessenger; |
| } |
| |
| - (id<FlutterTextureRegistry>)textures { |
| return _flutterEngine.renderer; |
| } |
| |
| - (NSView*)view { |
| return [self viewForId:kFlutterDefaultViewId]; |
| } |
| |
| - (NSView*)viewForId:(FlutterViewId)viewId { |
| FlutterViewController* controller = [_flutterEngine viewControllerForId:viewId]; |
| if (controller == nil) { |
| return nil; |
| } |
| if (!controller.viewLoaded) { |
| [controller loadView]; |
| } |
| return controller.flutterView; |
| } |
| |
| - (void)addMethodCallDelegate:(nonnull id<FlutterPlugin>)delegate |
| channel:(nonnull FlutterMethodChannel*)channel { |
| [channel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) { |
| [delegate handleMethodCall:call result:result]; |
| }]; |
| } |
| |
| - (void)registerViewFactory:(nonnull NSObject<FlutterPlatformViewFactory>*)factory |
| withId:(nonnull NSString*)factoryId { |
| [[_flutterEngine platformViewController] registerViewFactory:factory withId:factoryId]; |
| } |
| |
| - (NSString*)lookupKeyForAsset:(NSString*)asset { |
| return [FlutterDartProject lookupKeyForAsset:asset]; |
| } |
| |
| - (NSString*)lookupKeyForAsset:(NSString*)asset fromPackage:(NSString*)package { |
| return [FlutterDartProject lookupKeyForAsset:asset fromPackage:package]; |
| } |
| |
| @end |
| |
| // Callbacks provided to the engine. See the called methods for documentation. |
| #pragma mark - Static methods provided to engine configuration |
| |
| static void OnPlatformMessage(const FlutterPlatformMessage* message, FlutterEngine* engine) { |
| [engine engineCallbackOnPlatformMessage:message]; |
| } |
| |
| #pragma mark - |
| |
| @implementation FlutterEngine { |
| // The embedding-API-level engine object. |
| FLUTTER_API_SYMBOL(FlutterEngine) _engine; |
| |
| // The project being run by this engine. |
| FlutterDartProject* _project; |
| |
| // A mapping of channel names to the registered information for those channels. |
| NSMutableDictionary<NSString*, FlutterEngineHandlerInfo*>* _messengerHandlers; |
| |
| // A self-incremental integer to assign to newly assigned channels as |
| // identification. |
| FlutterBinaryMessengerConnection _currentMessengerConnection; |
| |
| // Whether the engine can continue running after the view controller is removed. |
| BOOL _allowHeadlessExecution; |
| |
| // Pointer to the Dart AOT snapshot and instruction data. |
| _FlutterEngineAOTData* _aotData; |
| |
| // _macOSCompositor is created when the engine is created and its destruction is handled by ARC |
| // when the engine is destroyed. |
| std::unique_ptr<flutter::FlutterCompositor> _macOSCompositor; |
| |
| // The information of all views attached to this engine mapped from IDs. |
| // |
| // It can't use NSDictionary, because the values need to be weak references. |
| NSMapTable* _viewControllers; |
| |
| // FlutterCompositor is copied and used in embedder.cc. |
| FlutterCompositor _compositor; |
| |
| // Method channel for platform view functions. These functions include creating, disposing and |
| // mutating a platform view. |
| FlutterMethodChannel* _platformViewsChannel; |
| |
| // Used to support creation and deletion of platform views and registering platform view |
| // factories. Lifecycle is tied to the engine. |
| FlutterPlatformViewController* _platformViewController; |
| |
| // A message channel for sending user settings to the flutter engine. |
| FlutterBasicMessageChannel* _settingsChannel; |
| |
| // A message channel for accessibility. |
| FlutterBasicMessageChannel* _accessibilityChannel; |
| |
| // A method channel for miscellaneous platform functionality. |
| FlutterMethodChannel* _platformChannel; |
| |
| int _nextViewId; |
| } |
| |
| - (instancetype)initWithName:(NSString*)labelPrefix project:(FlutterDartProject*)project { |
| return [self initWithName:labelPrefix project:project allowHeadlessExecution:YES]; |
| } |
| |
| - (instancetype)initWithName:(NSString*)labelPrefix |
| project:(FlutterDartProject*)project |
| allowHeadlessExecution:(BOOL)allowHeadlessExecution { |
| self = [super init]; |
| NSAssert(self, @"Super init cannot be nil"); |
| |
| _project = project ?: [[FlutterDartProject alloc] init]; |
| _messengerHandlers = [[NSMutableDictionary alloc] init]; |
| _currentMessengerConnection = 1; |
| _allowHeadlessExecution = allowHeadlessExecution; |
| _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; |
| |
| _embedderAPI.struct_size = sizeof(FlutterEngineProcTable); |
| FlutterEngineGetProcAddresses(&_embedderAPI); |
| |
| _viewControllers = [NSMapTable weakToWeakObjectsMapTable]; |
| _renderer = [[FlutterRenderer alloc] initWithFlutterEngine:self]; |
| |
| NSNotificationCenter* notificationCenter = [NSNotificationCenter defaultCenter]; |
| [notificationCenter addObserver:self |
| selector:@selector(sendUserLocales) |
| name:NSCurrentLocaleDidChangeNotification |
| object:nil]; |
| |
| _platformViewController = [[FlutterPlatformViewController alloc] init]; |
| [self setUpPlatformViewChannel]; |
| [self setUpAccessibilityChannel]; |
| [self setUpNotificationCenterListeners]; |
| |
| return self; |
| } |
| |
| - (void)dealloc { |
| @synchronized(_isResponseValid) { |
| [_isResponseValid removeAllObjects]; |
| [_isResponseValid addObject:@NO]; |
| } |
| [self shutDownEngine]; |
| if (_aotData) { |
| _embedderAPI.CollectAOTData(_aotData); |
| } |
| } |
| |
| - (BOOL)runWithEntrypoint:(NSString*)entrypoint { |
| if (self.running) { |
| return NO; |
| } |
| |
| if (!_allowHeadlessExecution && [_viewControllers count] == 0) { |
| NSLog(@"Attempted to run an engine with no view controller without headless mode enabled."); |
| return NO; |
| } |
| |
| [self addInternalPlugins]; |
| |
| // The first argument of argv is required to be the executable name. |
| std::vector<const char*> argv = {[self.executableName UTF8String]}; |
| std::vector<std::string> switches = self.switches; |
| std::transform(switches.begin(), switches.end(), std::back_inserter(argv), |
| [](const std::string& arg) -> const char* { return arg.c_str(); }); |
| |
| std::vector<const char*> dartEntrypointArgs; |
| for (NSString* argument in [_project dartEntrypointArguments]) { |
| dartEntrypointArgs.push_back([argument UTF8String]); |
| } |
| |
| FlutterProjectArgs flutterArguments = {}; |
| flutterArguments.struct_size = sizeof(FlutterProjectArgs); |
| flutterArguments.assets_path = _project.assetsPath.UTF8String; |
| flutterArguments.icu_data_path = _project.ICUDataPath.UTF8String; |
| flutterArguments.command_line_argc = static_cast<int>(argv.size()); |
| flutterArguments.command_line_argv = argv.empty() ? nullptr : argv.data(); |
| flutterArguments.platform_message_callback = (FlutterPlatformMessageCallback)OnPlatformMessage; |
| flutterArguments.update_semantics_callback2 = [](const FlutterSemanticsUpdate2* update, |
| void* user_data) { |
| // TODO(dkwingsmt): This callback only supports single-view, therefore it |
| // only operates on the default view. To support multi-view, we need a |
| // way to pass in the ID (probably through FlutterSemanticsUpdate). |
| FlutterEngine* engine = (__bridge FlutterEngine*)user_data; |
| [[engine viewControllerForId:kFlutterDefaultViewId] updateSemantics:update]; |
| }; |
| flutterArguments.custom_dart_entrypoint = entrypoint.UTF8String; |
| flutterArguments.shutdown_dart_vm_when_done = true; |
| flutterArguments.dart_entrypoint_argc = dartEntrypointArgs.size(); |
| flutterArguments.dart_entrypoint_argv = dartEntrypointArgs.data(); |
| flutterArguments.root_isolate_create_callback = _project.rootIsolateCreateCallback; |
| flutterArguments.log_message_callback = [](const char* tag, const char* message, |
| void* user_data) { |
| if (tag && tag[0]) { |
| std::cout << tag << ": "; |
| } |
| std::cout << message << std::endl; |
| }; |
| |
| static size_t sTaskRunnerIdentifiers = 0; |
| const FlutterTaskRunnerDescription cocoa_task_runner_description = { |
| .struct_size = sizeof(FlutterTaskRunnerDescription), |
| .user_data = (void*)CFBridgingRetain(self), |
| .runs_task_on_current_thread_callback = [](void* user_data) -> bool { |
| return [[NSThread currentThread] isMainThread]; |
| }, |
| .post_task_callback = [](FlutterTask task, uint64_t target_time_nanos, |
| void* user_data) -> void { |
| [((__bridge FlutterEngine*)(user_data)) postMainThreadTask:task |
| targetTimeInNanoseconds:target_time_nanos]; |
| }, |
| .identifier = ++sTaskRunnerIdentifiers, |
| }; |
| const FlutterCustomTaskRunners custom_task_runners = { |
| .struct_size = sizeof(FlutterCustomTaskRunners), |
| .platform_task_runner = &cocoa_task_runner_description, |
| }; |
| flutterArguments.custom_task_runners = &custom_task_runners; |
| |
| [self loadAOTData:_project.assetsPath]; |
| if (_aotData) { |
| flutterArguments.aot_data = _aotData; |
| } |
| |
| flutterArguments.compositor = [self createFlutterCompositor]; |
| |
| flutterArguments.on_pre_engine_restart_callback = [](void* user_data) { |
| FlutterEngine* engine = (__bridge FlutterEngine*)user_data; |
| [engine engineCallbackOnPreEngineRestart]; |
| }; |
| |
| FlutterRendererConfig rendererConfig = [_renderer createRendererConfig]; |
| FlutterEngineResult result = _embedderAPI.Initialize( |
| FLUTTER_ENGINE_VERSION, &rendererConfig, &flutterArguments, (__bridge void*)(self), &_engine); |
| if (result != kSuccess) { |
| NSLog(@"Failed to initialize Flutter engine: error %d", result); |
| return NO; |
| } |
| |
| result = _embedderAPI.RunInitialized(_engine); |
| if (result != kSuccess) { |
| NSLog(@"Failed to run an initialized engine: error %d", result); |
| return NO; |
| } |
| |
| [self sendUserLocales]; |
| |
| // Update window metric for all view controllers. |
| NSEnumerator* viewControllerEnumerator = [_viewControllers objectEnumerator]; |
| FlutterViewController* nextViewController; |
| while ((nextViewController = [viewControllerEnumerator nextObject])) { |
| [self updateWindowMetricsForViewController:nextViewController]; |
| } |
| |
| [self updateDisplayConfig]; |
| // Send the initial user settings such as brightness and text scale factor |
| // to the engine. |
| [self sendInitialSettings]; |
| return YES; |
| } |
| |
| - (void)loadAOTData:(NSString*)assetsDir { |
| if (!_embedderAPI.RunsAOTCompiledDartCode()) { |
| return; |
| } |
| |
| BOOL isDirOut = false; // required for NSFileManager fileExistsAtPath. |
| NSFileManager* fileManager = [NSFileManager defaultManager]; |
| |
| // This is the location where the test fixture places the snapshot file. |
| // For applications built by Flutter tool, this is in "App.framework". |
| NSString* elfPath = [NSString pathWithComponents:@[ assetsDir, @"app_elf_snapshot.so" ]]; |
| |
| if (![fileManager fileExistsAtPath:elfPath isDirectory:&isDirOut]) { |
| return; |
| } |
| |
| FlutterEngineAOTDataSource source = {}; |
| source.type = kFlutterEngineAOTDataSourceTypeElfPath; |
| source.elf_path = [elfPath cStringUsingEncoding:NSUTF8StringEncoding]; |
| |
| auto result = _embedderAPI.CreateAOTData(&source, &_aotData); |
| if (result != kSuccess) { |
| NSLog(@"Failed to load AOT data from: %@", elfPath); |
| } |
| } |
| |
| - (void)registerViewController:(FlutterViewController*)controller forId:(FlutterViewId)viewId { |
| NSAssert(controller != nil, @"The controller must not be nil."); |
| NSAssert(![controller attached], |
| @"The incoming view controller is already attached to an engine."); |
| NSAssert([_viewControllers objectForKey:@(viewId)] == nil, @"The requested view ID is occupied."); |
| [controller attachToEngine:self withId:viewId]; |
| NSAssert(controller.viewId == viewId, @"Failed to assign view ID."); |
| [_viewControllers setObject:controller forKey:@(viewId)]; |
| } |
| |
| - (void)deregisterViewControllerForId:(FlutterViewId)viewId { |
| FlutterViewController* oldController = [self viewControllerForId:viewId]; |
| if (oldController != nil) { |
| [oldController detachFromEngine]; |
| [_viewControllers removeObjectForKey:@(viewId)]; |
| } |
| } |
| |
| - (void)shutDownIfNeeded { |
| if ([_viewControllers count] == 0 && !_allowHeadlessExecution) { |
| [self shutDownEngine]; |
| } |
| } |
| |
| - (FlutterViewController*)viewControllerForId:(FlutterViewId)viewId { |
| FlutterViewController* controller = [_viewControllers objectForKey:@(viewId)]; |
| NSAssert(controller == nil || controller.viewId == viewId, |
| @"The stored controller has unexpected view ID."); |
| return controller; |
| } |
| |
| - (void)setViewController:(FlutterViewController*)controller { |
| FlutterViewController* currentController = |
| [_viewControllers objectForKey:@(kFlutterDefaultViewId)]; |
| if (currentController == controller) { |
| // From nil to nil, or from non-nil to the same controller. |
| return; |
| } |
| if (currentController == nil && controller != nil) { |
| // From nil to non-nil. |
| NSAssert(controller.engine == nil, |
| @"Failed to set view controller to the engine: " |
| @"The given FlutterViewController is already attached to an engine %@. " |
| @"If you wanted to create an FlutterViewController and set it to an existing engine, " |
| @"you should use FlutterViewController#init(engine:, nibName, bundle:) instead.", |
| controller.engine); |
| [self registerViewController:controller forId:kFlutterDefaultViewId]; |
| } else if (currentController != nil && controller == nil) { |
| NSAssert(currentController.viewId == kFlutterDefaultViewId, |
| @"The default controller has an unexpected ID %llu", currentController.viewId); |
| // From non-nil to nil. |
| [self deregisterViewControllerForId:kFlutterDefaultViewId]; |
| [self shutDownIfNeeded]; |
| } else { |
| // From non-nil to a different non-nil view controller. |
| NSAssert(NO, |
| @"Failed to set view controller to the engine: " |
| @"The engine already has a default view controller %@. " |
| @"If you wanted to make the default view render in a different window, " |
| @"you should attach the current view controller to the window instead.", |
| [_viewControllers objectForKey:@(kFlutterDefaultViewId)]); |
| } |
| } |
| |
| - (FlutterViewController*)viewController { |
| return [self viewControllerForId:kFlutterDefaultViewId]; |
| } |
| |
| - (FlutterCompositor*)createFlutterCompositor { |
| _macOSCompositor = std::make_unique<flutter::FlutterCompositor>( |
| [[FlutterViewEngineProvider alloc] initWithEngine:self], _platformViewController); |
| |
| _compositor = {}; |
| _compositor.struct_size = sizeof(FlutterCompositor); |
| _compositor.user_data = _macOSCompositor.get(); |
| |
| _compositor.create_backing_store_callback = [](const FlutterBackingStoreConfig* config, // |
| FlutterBackingStore* backing_store_out, // |
| void* user_data // |
| ) { |
| return reinterpret_cast<flutter::FlutterCompositor*>(user_data)->CreateBackingStore( |
| config, backing_store_out); |
| }; |
| |
| _compositor.collect_backing_store_callback = [](const FlutterBackingStore* backing_store, // |
| void* user_data // |
| ) { return true; }; |
| |
| _compositor.present_layers_callback = [](const FlutterLayer** layers, // |
| size_t layers_count, // |
| void* user_data // |
| ) { |
| // TODO(dkwingsmt): This callback only supports single-view, therefore it |
| // only operates on the default view. To support multi-view, we need a new |
| // callback that also receives a view ID. |
| return reinterpret_cast<flutter::FlutterCompositor*>(user_data)->Present(kFlutterDefaultViewId, |
| layers, layers_count); |
| }; |
| |
| _compositor.avoid_backing_store_cache = true; |
| |
| return &_compositor; |
| } |
| |
| - (id<FlutterBinaryMessenger>)binaryMessenger { |
| // TODO(stuartmorgan): Switch to FlutterBinaryMessengerRelay to avoid plugins |
| // keeping the engine alive. |
| return self; |
| } |
| |
| #pragma mark - Framework-internal methods |
| |
| - (void)addViewController:(FlutterViewController*)controller { |
| [self registerViewController:controller forId:kFlutterDefaultViewId]; |
| } |
| |
| - (void)removeViewController:(nonnull FlutterViewController*)viewController { |
| NSAssert([viewController attached] && viewController.engine == self, |
| @"The given view controller is not associated with this engine."); |
| [self deregisterViewControllerForId:viewController.viewId]; |
| [self shutDownIfNeeded]; |
| } |
| |
| - (BOOL)running { |
| return _engine != nullptr; |
| } |
| |
| - (void)updateDisplayConfig:(NSNotification*)notification { |
| [self updateDisplayConfig]; |
| } |
| |
| - (void)updateDisplayConfig { |
| if (!_engine) { |
| return; |
| } |
| |
| std::vector<FlutterEngineDisplay> displays; |
| for (NSScreen* screen : [NSScreen screens]) { |
| CGDirectDisplayID displayID = |
| static_cast<CGDirectDisplayID>([screen.deviceDescription[@"NSScreenNumber"] integerValue]); |
| |
| FlutterEngineDisplay display; |
| display.struct_size = sizeof(display); |
| display.display_id = displayID; |
| display.single_display = false; |
| display.width = static_cast<size_t>(screen.frame.size.width); |
| display.height = static_cast<size_t>(screen.frame.size.height); |
| display.device_pixel_ratio = screen.backingScaleFactor; |
| |
| CVDisplayLinkRef displayLinkRef = nil; |
| CVReturn error = CVDisplayLinkCreateWithCGDisplay(displayID, &displayLinkRef); |
| |
| if (error == 0) { |
| CVTime nominal = CVDisplayLinkGetNominalOutputVideoRefreshPeriod(displayLinkRef); |
| if (!(nominal.flags & kCVTimeIsIndefinite)) { |
| double refreshRate = static_cast<double>(nominal.timeScale) / nominal.timeValue; |
| display.refresh_rate = round(refreshRate); |
| } |
| CVDisplayLinkRelease(displayLinkRef); |
| } else { |
| display.refresh_rate = 0; |
| } |
| |
| displays.push_back(display); |
| } |
| _embedderAPI.NotifyDisplayUpdate(_engine, kFlutterEngineDisplaysUpdateTypeStartup, |
| displays.data(), displays.size()); |
| } |
| |
| - (void)onSettingsChanged:(NSNotification*)notification { |
| // TODO(jonahwilliams): https://github.com/flutter/flutter/issues/32015. |
| NSString* brightness = |
| [[NSUserDefaults standardUserDefaults] stringForKey:@"AppleInterfaceStyle"]; |
| [_settingsChannel sendMessage:@{ |
| @"platformBrightness" : [brightness isEqualToString:@"Dark"] ? @"dark" : @"light", |
| // TODO(jonahwilliams): https://github.com/flutter/flutter/issues/32006. |
| @"textScaleFactor" : @1.0, |
| @"alwaysUse24HourFormat" : @false |
| }]; |
| } |
| |
| - (void)sendInitialSettings { |
| // TODO(jonahwilliams): https://github.com/flutter/flutter/issues/32015. |
| [[NSDistributedNotificationCenter defaultCenter] |
| addObserver:self |
| selector:@selector(onSettingsChanged:) |
| name:@"AppleInterfaceThemeChangedNotification" |
| object:nil]; |
| [self onSettingsChanged:nil]; |
| } |
| |
| - (FlutterEngineProcTable&)embedderAPI { |
| return _embedderAPI; |
| } |
| |
| - (nonnull NSString*)executableName { |
| return [[[NSProcessInfo processInfo] arguments] firstObject] ?: @"Flutter"; |
| } |
| |
| - (void)updateWindowMetricsForViewController:(FlutterViewController*)viewController { |
| if (viewController.viewId != kFlutterDefaultViewId) { |
| // TODO(dkwingsmt): The embedder API only supports single-view for now. As |
| // embedder APIs are converted to multi-view, this method should support any |
| // views. |
| return; |
| } |
| if (!_engine || !viewController || !viewController.viewLoaded) { |
| return; |
| } |
| NSAssert([self viewControllerForId:viewController.viewId] == viewController, |
| @"The provided view controller is not attached to this engine."); |
| NSView* view = viewController.flutterView; |
| CGRect scaledBounds = [view convertRectToBacking:view.bounds]; |
| CGSize scaledSize = scaledBounds.size; |
| double pixelRatio = view.bounds.size.width == 0 ? 1 : scaledSize.width / view.bounds.size.width; |
| auto displayId = [view.window.screen.deviceDescription[@"NSScreenNumber"] integerValue]; |
| const FlutterWindowMetricsEvent windowMetricsEvent = { |
| .struct_size = sizeof(windowMetricsEvent), |
| .width = static_cast<size_t>(scaledSize.width), |
| .height = static_cast<size_t>(scaledSize.height), |
| .pixel_ratio = pixelRatio, |
| .left = static_cast<size_t>(scaledBounds.origin.x), |
| .top = static_cast<size_t>(scaledBounds.origin.y), |
| .display_id = static_cast<uint64_t>(displayId), |
| }; |
| _embedderAPI.SendWindowMetricsEvent(_engine, &windowMetricsEvent); |
| } |
| |
| - (void)sendPointerEvent:(const FlutterPointerEvent&)event { |
| _embedderAPI.SendPointerEvent(_engine, &event, 1); |
| } |
| |
| - (void)sendKeyEvent:(const FlutterKeyEvent&)event |
| callback:(FlutterKeyEventCallback)callback |
| userData:(void*)userData { |
| _embedderAPI.SendKeyEvent(_engine, &event, callback, userData); |
| } |
| |
| - (void)setSemanticsEnabled:(BOOL)enabled { |
| if (_semanticsEnabled == enabled) { |
| return; |
| } |
| _semanticsEnabled = enabled; |
| |
| // Update all view controllers' bridges. |
| NSEnumerator* viewControllerEnumerator = [_viewControllers objectEnumerator]; |
| FlutterViewController* nextViewController; |
| while ((nextViewController = [viewControllerEnumerator nextObject])) { |
| [nextViewController notifySemanticsEnabledChanged]; |
| } |
| |
| _embedderAPI.UpdateSemanticsEnabled(_engine, _semanticsEnabled); |
| } |
| |
| - (void)dispatchSemanticsAction:(FlutterSemanticsAction)action |
| toTarget:(uint16_t)target |
| withData:(fml::MallocMapping)data { |
| _embedderAPI.DispatchSemanticsAction(_engine, target, action, data.GetMapping(), data.GetSize()); |
| } |
| |
| - (FlutterPlatformViewController*)platformViewController { |
| return _platformViewController; |
| } |
| |
| #pragma mark - Private methods |
| |
| - (void)sendUserLocales { |
| if (!self.running) { |
| return; |
| } |
| |
| // Create a list of FlutterLocales corresponding to the preferred languages. |
| NSMutableArray<NSLocale*>* locales = [NSMutableArray array]; |
| std::vector<FlutterLocale> flutterLocales; |
| flutterLocales.reserve(locales.count); |
| for (NSString* localeID in [NSLocale preferredLanguages]) { |
| NSLocale* locale = [[NSLocale alloc] initWithLocaleIdentifier:localeID]; |
| [locales addObject:locale]; |
| flutterLocales.push_back(FlutterLocaleFromNSLocale(locale)); |
| } |
| // Convert to a list of pointers, and send to the engine. |
| std::vector<const FlutterLocale*> flutterLocaleList; |
| flutterLocaleList.reserve(flutterLocales.size()); |
| std::transform(flutterLocales.begin(), flutterLocales.end(), |
| std::back_inserter(flutterLocaleList), |
| [](const auto& arg) -> const auto* { return &arg; }); |
| _embedderAPI.UpdateLocales(_engine, flutterLocaleList.data(), flutterLocaleList.size()); |
| } |
| |
| - (void)engineCallbackOnPlatformMessage:(const FlutterPlatformMessage*)message { |
| NSData* messageData = nil; |
| if (message->message_size > 0) { |
| messageData = [NSData dataWithBytesNoCopy:(void*)message->message |
| length:message->message_size |
| freeWhenDone:NO]; |
| } |
| NSString* channel = @(message->channel); |
| __block const FlutterPlatformMessageResponseHandle* responseHandle = message->response_handle; |
| __block FlutterEngine* weakSelf = self; |
| NSMutableArray* isResponseValid = self.isResponseValid; |
| FlutterEngineSendPlatformMessageResponseFnPtr sendPlatformMessageResponse = |
| _embedderAPI.SendPlatformMessageResponse; |
| FlutterBinaryReply binaryResponseHandler = ^(NSData* response) { |
| @synchronized(isResponseValid) { |
| if (![isResponseValid[0] boolValue]) { |
| // Ignore, engine was killed. |
| return; |
| } |
| if (responseHandle) { |
| sendPlatformMessageResponse(weakSelf->_engine, responseHandle, |
| static_cast<const uint8_t*>(response.bytes), response.length); |
| responseHandle = NULL; |
| } else { |
| NSLog(@"Error: Message responses can be sent only once. Ignoring duplicate response " |
| "on channel '%@'.", |
| channel); |
| } |
| } |
| }; |
| |
| FlutterEngineHandlerInfo* handlerInfo = _messengerHandlers[channel]; |
| if (handlerInfo) { |
| handlerInfo.handler(messageData, binaryResponseHandler); |
| } else { |
| binaryResponseHandler(nil); |
| } |
| } |
| |
| - (void)engineCallbackOnPreEngineRestart { |
| NSEnumerator* viewControllerEnumerator = [_viewControllers objectEnumerator]; |
| FlutterViewController* nextViewController; |
| while ((nextViewController = [viewControllerEnumerator nextObject])) { |
| [nextViewController onPreEngineRestart]; |
| } |
| } |
| |
| /** |
| * Note: Called from dealloc. Should not use accessors or other methods. |
| */ |
| - (void)shutDownEngine { |
| if (_engine == nullptr) { |
| return; |
| } |
| |
| NSEnumerator* viewControllerEnumerator = [_viewControllers objectEnumerator]; |
| FlutterViewController* nextViewController; |
| while ((nextViewController = [viewControllerEnumerator nextObject])) { |
| [nextViewController.flutterView shutdown]; |
| } |
| |
| FlutterEngineResult result = _embedderAPI.Deinitialize(_engine); |
| if (result != kSuccess) { |
| NSLog(@"Could not de-initialize the Flutter engine: error %d", result); |
| } |
| |
| // Balancing release for the retain in the task runner dispatch table. |
| CFRelease((CFTypeRef)self); |
| |
| result = _embedderAPI.Shutdown(_engine); |
| if (result != kSuccess) { |
| NSLog(@"Failed to shut down Flutter engine: error %d", result); |
| } |
| _engine = nullptr; |
| } |
| |
| - (void)setUpPlatformViewChannel { |
| _platformViewsChannel = |
| [FlutterMethodChannel methodChannelWithName:@"flutter/platform_views" |
| binaryMessenger:self.binaryMessenger |
| codec:[FlutterStandardMethodCodec sharedInstance]]; |
| |
| __weak FlutterEngine* weakSelf = self; |
| [_platformViewsChannel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) { |
| [[weakSelf platformViewController] handleMethodCall:call result:result]; |
| }]; |
| } |
| |
| - (void)setUpAccessibilityChannel { |
| _accessibilityChannel = [FlutterBasicMessageChannel |
| messageChannelWithName:@"flutter/accessibility" |
| binaryMessenger:self.binaryMessenger |
| codec:[FlutterStandardMessageCodec sharedInstance]]; |
| __weak FlutterEngine* weakSelf = self; |
| [_accessibilityChannel setMessageHandler:^(id message, FlutterReply reply) { |
| [weakSelf handleAccessibilityEvent:message]; |
| }]; |
| } |
| - (void)setUpNotificationCenterListeners { |
| NSNotificationCenter* center = [NSNotificationCenter defaultCenter]; |
| // macOS fires this private message when VoiceOver turns on or off. |
| [center addObserver:self |
| selector:@selector(onAccessibilityStatusChanged:) |
| name:kEnhancedUserInterfaceNotification |
| object:nil]; |
| [center addObserver:self |
| selector:@selector(applicationWillTerminate:) |
| name:NSApplicationWillTerminateNotification |
| object:nil]; |
| [center addObserver:self |
| selector:@selector(windowDidChangeScreen:) |
| name:NSWindowDidChangeScreenNotification |
| object:nil]; |
| [center addObserver:self |
| selector:@selector(updateDisplayConfig:) |
| name:NSApplicationDidChangeScreenParametersNotification |
| object:nil]; |
| } |
| |
| - (void)addInternalPlugins { |
| __weak FlutterEngine* weakSelf = self; |
| [FlutterMouseCursorPlugin registerWithRegistrar:[self registrarForPlugin:@"mousecursor"]]; |
| [FlutterMenuPlugin registerWithRegistrar:[self registrarForPlugin:@"menu"]]; |
| _settingsChannel = |
| [FlutterBasicMessageChannel messageChannelWithName:@"flutter/settings" |
| binaryMessenger:self.binaryMessenger |
| codec:[FlutterJSONMessageCodec sharedInstance]]; |
| _platformChannel = |
| [FlutterMethodChannel methodChannelWithName:@"flutter/platform" |
| binaryMessenger:self.binaryMessenger |
| codec:[FlutterJSONMethodCodec sharedInstance]]; |
| [_platformChannel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) { |
| [weakSelf handleMethodCall:call result:result]; |
| }]; |
| } |
| |
| - (void)applicationWillTerminate:(NSNotification*)notification { |
| [self shutDownEngine]; |
| } |
| |
| - (void)windowDidChangeScreen:(NSNotification*)notification { |
| // Update window metric for all view controllers since the display_id has |
| // changed. |
| NSEnumerator* viewControllerEnumerator = [_viewControllers objectEnumerator]; |
| FlutterViewController* nextViewController; |
| while ((nextViewController = [viewControllerEnumerator nextObject])) { |
| [self updateWindowMetricsForViewController:nextViewController]; |
| } |
| } |
| |
| - (void)onAccessibilityStatusChanged:(NSNotification*)notification { |
| BOOL enabled = [notification.userInfo[kEnhancedUserInterfaceKey] boolValue]; |
| NSEnumerator* viewControllerEnumerator = [_viewControllers objectEnumerator]; |
| FlutterViewController* nextViewController; |
| while ((nextViewController = [viewControllerEnumerator nextObject])) { |
| [nextViewController onAccessibilityStatusChanged:enabled]; |
| } |
| |
| self.semanticsEnabled = enabled; |
| } |
| - (void)handleAccessibilityEvent:(NSDictionary<NSString*, id>*)annotatedEvent { |
| NSString* type = annotatedEvent[@"type"]; |
| if ([type isEqualToString:@"announce"]) { |
| NSString* message = annotatedEvent[@"data"][@"message"]; |
| NSNumber* assertiveness = annotatedEvent[@"data"][@"assertiveness"]; |
| if (message == nil) { |
| return; |
| } |
| |
| NSAccessibilityPriorityLevel priority = [assertiveness isEqualToNumber:@1] |
| ? NSAccessibilityPriorityHigh |
| : NSAccessibilityPriorityMedium; |
| |
| [self announceAccessibilityMessage:message withPriority:priority]; |
| } |
| } |
| |
| - (void)announceAccessibilityMessage:(NSString*)message |
| withPriority:(NSAccessibilityPriorityLevel)priority { |
| NSAccessibilityPostNotificationWithUserInfo( |
| [self viewControllerForId:kFlutterDefaultViewId].flutterView, |
| NSAccessibilityAnnouncementRequestedNotification, |
| @{NSAccessibilityAnnouncementKey : message, NSAccessibilityPriorityKey : @(priority)}); |
| } |
| - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { |
| if ([call.method isEqualToString:@"SystemNavigator.pop"]) { |
| [NSApp terminate:self]; |
| result(nil); |
| } else if ([call.method isEqualToString:@"SystemSound.play"]) { |
| [self playSystemSound:call.arguments]; |
| result(nil); |
| } else if ([call.method isEqualToString:@"Clipboard.getData"]) { |
| result([self getClipboardData:call.arguments]); |
| } else if ([call.method isEqualToString:@"Clipboard.setData"]) { |
| [self setClipboardData:call.arguments]; |
| 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 if ([call.method isEqualToString:@"System.initializationComplete"]) { |
| [self terminationHandler].acceptingRequests = YES; |
| result(nil); |
| } else { |
| result(FlutterMethodNotImplemented); |
| } |
| } |
| |
| - (void)playSystemSound:(NSString*)soundType { |
| if ([soundType isEqualToString:@"SystemSoundType.alert"]) { |
| NSBeep(); |
| } |
| } |
| |
| - (NSDictionary*)getClipboardData:(NSString*)format { |
| NSPasteboard* pasteboard = self.pasteboard; |
| if ([format isEqualToString:@(kTextPlainFormat)]) { |
| NSString* stringInPasteboard = [pasteboard stringForType:NSPasteboardTypeString]; |
| return stringInPasteboard == nil ? nil : @{@"text" : stringInPasteboard}; |
| } |
| return nil; |
| } |
| |
| - (void)setClipboardData:(NSDictionary*)data { |
| NSPasteboard* pasteboard = self.pasteboard; |
| NSString* text = data[@"text"]; |
| [pasteboard clearContents]; |
| if (text && ![text isEqual:[NSNull null]]) { |
| [pasteboard setString:text forType:NSPasteboardTypeString]; |
| } |
| } |
| |
| - (BOOL)clipboardHasStrings { |
| return [self.pasteboard stringForType:NSPasteboardTypeString].length > 0; |
| } |
| |
| - (NSPasteboard*)pasteboard { |
| return [NSPasteboard generalPasteboard]; |
| } |
| |
| - (std::vector<std::string>)switches { |
| return flutter::GetSwitchesFromEnvironment(); |
| } |
| |
| #pragma mark - FlutterBinaryMessenger |
| |
| - (void)sendOnChannel:(nonnull NSString*)channel message:(nullable NSData*)message { |
| [self sendOnChannel:channel message:message binaryReply:nil]; |
| } |
| |
| - (void)sendOnChannel:(NSString*)channel |
| message:(NSData* _Nullable)message |
| binaryReply:(FlutterBinaryReply _Nullable)callback { |
| FlutterPlatformMessageResponseHandle* response_handle = nullptr; |
| if (callback) { |
| struct Captures { |
| FlutterBinaryReply reply; |
| }; |
| auto captures = std::make_unique<Captures>(); |
| captures->reply = callback; |
| auto message_reply = [](const uint8_t* data, size_t data_size, void* user_data) { |
| auto captures = reinterpret_cast<Captures*>(user_data); |
| NSData* reply_data = nil; |
| if (data != nullptr && data_size > 0) { |
| reply_data = [NSData dataWithBytes:static_cast<const void*>(data) length:data_size]; |
| } |
| captures->reply(reply_data); |
| delete captures; |
| }; |
| |
| FlutterEngineResult create_result = _embedderAPI.PlatformMessageCreateResponseHandle( |
| _engine, message_reply, captures.get(), &response_handle); |
| if (create_result != kSuccess) { |
| NSLog(@"Failed to create a FlutterPlatformMessageResponseHandle (%d)", create_result); |
| return; |
| } |
| captures.release(); |
| } |
| |
| FlutterPlatformMessage platformMessage = { |
| .struct_size = sizeof(FlutterPlatformMessage), |
| .channel = [channel UTF8String], |
| .message = static_cast<const uint8_t*>(message.bytes), |
| .message_size = message.length, |
| .response_handle = response_handle, |
| }; |
| |
| FlutterEngineResult message_result = _embedderAPI.SendPlatformMessage(_engine, &platformMessage); |
| if (message_result != kSuccess) { |
| NSLog(@"Failed to send message to Flutter engine on channel '%@' (%d).", channel, |
| message_result); |
| } |
| |
| if (response_handle != nullptr) { |
| FlutterEngineResult release_result = |
| _embedderAPI.PlatformMessageReleaseResponseHandle(_engine, response_handle); |
| if (release_result != kSuccess) { |
| NSLog(@"Failed to release the response handle (%d).", release_result); |
| }; |
| } |
| } |
| |
| - (FlutterBinaryMessengerConnection)setMessageHandlerOnChannel:(nonnull NSString*)channel |
| binaryMessageHandler: |
| (nullable FlutterBinaryMessageHandler)handler { |
| _currentMessengerConnection += 1; |
| _messengerHandlers[channel] = |
| [[FlutterEngineHandlerInfo alloc] initWithConnection:@(_currentMessengerConnection) |
| handler:[handler copy]]; |
| return _currentMessengerConnection; |
| } |
| |
| - (void)cleanUpConnection:(FlutterBinaryMessengerConnection)connection { |
| // Find the _messengerHandlers that has the required connection, and record its |
| // channel. |
| NSString* foundChannel = nil; |
| for (NSString* key in [_messengerHandlers allKeys]) { |
| FlutterEngineHandlerInfo* handlerInfo = [_messengerHandlers objectForKey:key]; |
| if ([handlerInfo.connection isEqual:@(connection)]) { |
| foundChannel = key; |
| break; |
| } |
| } |
| if (foundChannel) { |
| [_messengerHandlers removeObjectForKey:foundChannel]; |
| } |
| } |
| |
| #pragma mark - FlutterPluginRegistry |
| |
| - (id<FlutterPluginRegistrar>)registrarForPlugin:(NSString*)pluginName { |
| return [[FlutterEngineRegistrar alloc] initWithPlugin:pluginName flutterEngine:self]; |
| } |
| |
| #pragma mark - FlutterTextureRegistrar |
| |
| - (int64_t)registerTexture:(id<FlutterTexture>)texture { |
| return [_renderer registerTexture:texture]; |
| } |
| |
| - (BOOL)registerTextureWithID:(int64_t)textureId { |
| return _embedderAPI.RegisterExternalTexture(_engine, textureId) == kSuccess; |
| } |
| |
| - (void)textureFrameAvailable:(int64_t)textureID { |
| [_renderer textureFrameAvailable:textureID]; |
| } |
| |
| - (BOOL)markTextureFrameAvailable:(int64_t)textureID { |
| return _embedderAPI.MarkExternalTextureFrameAvailable(_engine, textureID) == kSuccess; |
| } |
| |
| - (void)unregisterTexture:(int64_t)textureID { |
| [_renderer unregisterTexture:textureID]; |
| } |
| |
| - (BOOL)unregisterTextureWithID:(int64_t)textureID { |
| return _embedderAPI.UnregisterExternalTexture(_engine, textureID) == kSuccess; |
| } |
| |
| #pragma mark - Task runner integration |
| |
| - (void)runTaskOnEmbedder:(FlutterTask)task { |
| if (_engine) { |
| auto result = _embedderAPI.RunTask(_engine, &task); |
| if (result != kSuccess) { |
| NSLog(@"Could not post a task to the Flutter engine."); |
| } |
| } |
| } |
| |
| - (void)postMainThreadTask:(FlutterTask)task targetTimeInNanoseconds:(uint64_t)targetTime { |
| __weak FlutterEngine* weakSelf = self; |
| auto worker = ^{ |
| [weakSelf runTaskOnEmbedder:task]; |
| }; |
| |
| const auto engine_time = _embedderAPI.GetCurrentTime(); |
| if (targetTime <= engine_time) { |
| dispatch_async(dispatch_get_main_queue(), worker); |
| |
| } else { |
| dispatch_after(dispatch_time(DISPATCH_TIME_NOW, targetTime - engine_time), |
| dispatch_get_main_queue(), worker); |
| } |
| } |
| |
| // Getter used by test harness, only exposed through the FlutterEngine(Test) category |
| - (flutter::FlutterCompositor*)macOSCompositor { |
| return _macOSCompositor.get(); |
| } |
| |
| @end |