// Copyright 2016 The Chromium 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/ios/framework/Headers/FlutterViewController.h" #include #include "base/mac/scoped_block.h" #include "base/mac/scoped_nsobject.h" #include "base/strings/sys_string_conversions.h" #include "flutter/common/threads.h" #include "flutter/services/platform/ios/system_chrome_impl.h" #include "flutter/shell/gpu/gpu_rasterizer.h" #include "flutter/shell/gpu/gpu_surface_gl.h" #include "flutter/shell/platform/darwin/common/platform_mac.h" #include "flutter/shell/platform/darwin/ios/framework/Source/FlutterDartProject_Internal.h" #include "flutter/shell/platform/darwin/ios/framework/Source/flutter_touch_mapper.h" #include "flutter/shell/platform/darwin/ios/platform_view_ios.h" #include "lib/ftl/functional/make_copyable.h" #include "lib/ftl/time/time_delta.h" @interface FlutterViewController () @end void FlutterInit(int argc, const char* argv[]) { NSBundle* bundle = [NSBundle bundleForClass:[FlutterViewController class]]; NSString* icuDataPath = [bundle pathForResource:@"icudtl" ofType:@"dat"]; shell::PlatformMacMain(argc, argv, icuDataPath.UTF8String); } @implementation FlutterViewController { base::scoped_nsprotocol _dartProject; UIInterfaceOrientationMask _orientationPreferences; UIStatusBarStyle _statusBarStyle; sky::ViewportMetricsPtr _viewportMetrics; shell::TouchMapper _touchMapper; std::unique_ptr _platformView; BOOL _initialized; } #pragma mark - Manage and override all designated initializers - (instancetype)initWithProject:(FlutterDartProject*)project nibName:(NSString*)nibNameOrNil bundle:(NSBundle*)nibBundleOrNil { self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]; if (self) { if (project == nil) _dartProject.reset( [[FlutterDartProject alloc] initFromDefaultSourceForConfiguration]); else _dartProject.reset([project retain]); [self performCommonViewControllerInitialization]; } return self; } - (instancetype)initWithNibName:(NSString*)nibNameOrNil bundle:(NSBundle*)nibBundleOrNil { return [self initWithProject:nil nibName:nil bundle:nil]; } - (instancetype)initWithCoder:(NSCoder*)aDecoder { return [self initWithProject:nil nibName:nil bundle:nil]; } #pragma mark - Common view controller initialization tasks - (void)performCommonViewControllerInitialization { if (_initialized) return; _initialized = YES; _orientationPreferences = UIInterfaceOrientationMaskAll; _statusBarStyle = UIStatusBarStyleDefault; _viewportMetrics = sky::ViewportMetrics::New(); _platformView = std::make_unique( reinterpret_cast(self.view.layer)); _platformView->SetupResourceContextOnIOThread(); [self setupNotificationCenterObservers]; [self connectToEngineAndLoad]; } - (void)setupNotificationCenterObservers { NSNotificationCenter* center = [NSNotificationCenter defaultCenter]; [center addObserver:self selector:@selector(onOrientationPreferencesUpdated:) name:@(flutter::platform::kOrientationUpdateNotificationName) object:nil]; [center addObserver:self selector:@selector(onPreferredStatusBarStyleUpdated:) name:@(flutter::platform::kOverlayStyleUpdateNotificationName) object:nil]; [center addObserver:self selector:@selector(applicationBecameActive:) name:UIApplicationDidBecomeActiveNotification object:nil]; [center addObserver:self selector:@selector(applicationWillResignActive:) name:UIApplicationWillResignActiveNotification object:nil]; [center addObserver:self selector:@selector(keyboardWasShown:) name:UIKeyboardDidShowNotification object:nil]; [center addObserver:self selector:@selector(keyboardWillBeHidden:) name:UIKeyboardWillHideNotification object:nil]; [center addObserver:self selector:@selector(onLocaleUpdated:) name:NSCurrentLocaleDidChangeNotification object:nil]; [center addObserver:self selector:@selector(onVoiceOverChanged:) name:UIAccessibilityVoiceOverStatusChanged object:nil]; } #pragma mark - Initializing the engine - (void)alertView:(UIAlertView*)alertView clickedButtonAtIndex:(NSInteger)buttonIndex { exit(0); } - (void)connectToEngineAndLoad { TRACE_EVENT0("flutter", "connectToEngineAndLoad"); _platformView->ConnectToEngineAndSetupServices(); // We ask the VM to check what it supports. const enum VMType type = Dart_IsPrecompiledRuntime() ? VMTypePrecompilation : VMTypeInterpreter; [_dartProject launchInEngine:_platformView->engineProxy() embedderVMType:type result:^(BOOL success, NSString* message) { if (!success) { UIAlertView* alert = [[UIAlertView alloc] initWithTitle:@"Launch Error" message:message delegate:self cancelButtonTitle:@"OK" otherButtonTitles:nil]; [alert show]; [alert release]; } }]; } #pragma mark - Loading the view - (void)loadView { FlutterView* surface = [[FlutterView alloc] init]; self.view = surface; self.view.multipleTouchEnabled = YES; self.view.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; [surface release]; } #pragma mark - Application lifecycle notifications - (void)applicationBecameActive:(NSNotification*)notification { auto& engine = _platformView->engineProxy(); if (engine) { engine->OnAppLifecycleStateChanged(sky::AppLifecycleState::RESUMED); } } - (void)applicationWillResignActive:(NSNotification*)notification { auto& engine = _platformView->engineProxy(); if (engine) { engine->OnAppLifecycleStateChanged(sky::AppLifecycleState::PAUSED); } } #pragma mark - Touch event handling enum MapperPhase { Accessed, Added, Removed, }; using PointerChangeMapperPhase = std::pair; static inline PointerChangeMapperPhase PointerChangePhaseFromUITouchPhase( UITouchPhase phase) { switch (phase) { case UITouchPhaseBegan: return PointerChangeMapperPhase(blink::PointerData::Change::kDown, MapperPhase::Added); case UITouchPhaseMoved: case UITouchPhaseStationary: // There is no EVENT_TYPE_POINTER_STATIONARY. So we just pass a move type // with the same coordinates return PointerChangeMapperPhase(blink::PointerData::Change::kMove, MapperPhase::Accessed); case UITouchPhaseEnded: return PointerChangeMapperPhase(blink::PointerData::Change::kUp, MapperPhase::Removed); case UITouchPhaseCancelled: return PointerChangeMapperPhase(blink::PointerData::Change::kCancel, MapperPhase::Removed); } return PointerChangeMapperPhase(blink::PointerData::Change::kCancel, MapperPhase::Accessed); } - (void)dispatchTouches:(NSSet*)touches phase:(UITouchPhase)phase { auto eventTypePhase = PointerChangePhaseFromUITouchPhase(phase); const CGFloat scale = [UIScreen mainScreen].scale; auto packet = std::make_unique(touches.count); int i = 0; for (UITouch* touch in touches) { int touch_identifier = 0; switch (eventTypePhase.second) { case Accessed: touch_identifier = _touchMapper.identifierOf(touch); break; case Added: touch_identifier = _touchMapper.registerTouch(touch); break; case Removed: touch_identifier = _touchMapper.unregisterTouch(touch); break; } DCHECK(touch_identifier != 0); CGPoint windowCoordinates = [touch locationInView:nil]; auto pointer_time = ftl::TimeDelta::FromSeconds(touch.timestamp).ToMicroseconds(); blink::PointerData pointer_data; pointer_data.Clear(); pointer_data.time_stamp = pointer_time; pointer_data.change = eventTypePhase.first; pointer_data.kind = blink::PointerData::DeviceKind::kTouch; pointer_data.pointer = touch_identifier; pointer_data.physical_x = windowCoordinates.x * scale; pointer_data.physical_y = windowCoordinates.y * scale; pointer_data.pressure = 1.0; pointer_data.pressure_max = 1.0; packet->SetPointerData(i++, pointer_data); } blink::Threads::UI()->PostTask(ftl::MakeCopyable([ engine = _platformView->engine().GetWeakPtr(), packet = std::move(packet) ] { if (engine.get()) engine->DispatchPointerDataPacket(*packet); })); } - (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event { [self dispatchTouches:touches phase:UITouchPhaseBegan]; } - (void)touchesMoved:(NSSet*)touches withEvent:(UIEvent*)event { [self dispatchTouches:touches phase:UITouchPhaseMoved]; } - (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event { [self dispatchTouches:touches phase:UITouchPhaseEnded]; } - (void)touchesCancelled:(NSSet*)touches withEvent:(UIEvent*)event { [self dispatchTouches:touches phase:UITouchPhaseCancelled]; } #pragma mark - Handle view resizing - (void)viewDidLayoutSubviews { CGSize size = self.view.bounds.size; CGFloat scale = [UIScreen mainScreen].scale; _viewportMetrics->device_pixel_ratio = scale; _viewportMetrics->physical_width = size.width * scale; _viewportMetrics->physical_height = size.height * scale; _viewportMetrics->physical_padding_top = [UIApplication sharedApplication].statusBarFrame.size.height * scale; _platformView->engineProxy()->OnViewportMetricsChanged( _viewportMetrics.Clone()); } #pragma mark - Keyboard events - (void)keyboardWasShown:(NSNotification*)notification { NSDictionary* info = [notification userInfo]; CGFloat bottom = CGRectGetHeight( [[info objectForKey:UIKeyboardFrameBeginUserInfoKey] CGRectValue]); CGFloat scale = [UIScreen mainScreen].scale; _viewportMetrics->physical_padding_bottom = bottom * scale; _platformView->engineProxy()->OnViewportMetricsChanged( _viewportMetrics.Clone()); } - (void)keyboardWillBeHidden:(NSNotification*)notification { _viewportMetrics->physical_padding_bottom = 0.0; _platformView->engineProxy()->OnViewportMetricsChanged( _viewportMetrics.Clone()); } #pragma mark - Orientation updates - (void)onOrientationPreferencesUpdated:(NSNotification*)notification { // Notifications may not be on the iOS UI thread dispatch_async(dispatch_get_main_queue(), ^{ NSDictionary* info = notification.userInfo; NSNumber* update = info[@(flutter::platform::kOrientationUpdateNotificationKey)]; if (update == nil) { return; } NSUInteger new_preferences = update.unsignedIntegerValue; if (new_preferences != _orientationPreferences) { _orientationPreferences = new_preferences; [UIViewController attemptRotationToDeviceOrientation]; } }); } - (BOOL)shouldAutorotate { return YES; } - (NSUInteger)supportedInterfaceOrientations { return _orientationPreferences; } #pragma mark - Accessibility - (void)onVoiceOverChanged:(NSNotification*)notification { #if TARGET_OS_SIMULATOR // There doesn't appear to be any way to determine whether the accessibility // inspector is enabled on the simulator. We conservatively always turn on the // accessibility bridge in the simulator. bool enabled = true; #else bool enabled = UIAccessibilityIsVoiceOverRunning(); #endif _platformView->ToggleAccessibility(self.view, enabled); } #pragma mark - Locale updates - (void)onLocaleUpdated:(NSNotification*)notification { NSLocale* currentLocale = [NSLocale currentLocale]; NSString* languageCode = [currentLocale objectForKey:NSLocaleLanguageCode]; NSString* countryCode = [currentLocale objectForKey:NSLocaleCountryCode]; _platformView->engineProxy()->OnLocaleChanged(languageCode.UTF8String, countryCode.UTF8String); } #pragma mark - Surface creation and teardown updates - (void)surfaceUpdated:(BOOL)appeared { CHECK(_platformView != nullptr); if (appeared) { _platformView->NotifyCreated( std::make_unique(_platformView.get())); } else { _platformView->NotifyDestroyed(); } } - (void)viewDidAppear:(BOOL)animated { [self surfaceUpdated:YES]; [self onLocaleUpdated:nil]; [self onVoiceOverChanged:nil]; [super viewWillAppear:animated]; } - (void)viewWillDisappear:(BOOL)animated { [self surfaceUpdated:NO]; [super viewWillDisappear:animated]; } - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; [super dealloc]; } #pragma mark - Status bar style - (UIStatusBarStyle)preferredStatusBarStyle { return _statusBarStyle; } - (void)onPreferredStatusBarStyleUpdated:(NSNotification*)notification { // Notifications may not be on the iOS UI thread dispatch_async(dispatch_get_main_queue(), ^{ NSDictionary* info = notification.userInfo; NSNumber* update = info[@(flutter::platform::kOverlayStyleUpdateNotificationKey)]; if (update == nil) { return; } NSInteger style = update.integerValue; if (style != _statusBarStyle) { _statusBarStyle = static_cast(style); [self setNeedsStatusBarAppearanceUpdate]; } }); } #pragma mark - Application Messages - (void)sendString:(NSString*)message withMessageName:(NSString*)messageName { NSAssert(message, @"The message must not be null"); NSAssert(messageName, @"The messageName must not be null"); _platformView->AppMessageSender()->SendString( messageName.UTF8String, message.UTF8String, [](const mojo::String& response) {}); } - (void)sendString:(NSString*)message withMessageName:(NSString*)messageName callback:(void (^)(NSString*))callback { NSAssert(message, @"The message must not be null"); NSAssert(messageName, @"The messageName must not be null"); NSAssert(callback, @"The callback must not be null"); base::mac::ScopedBlock callback_ptr( callback, base::scoped_policy::RETAIN); _platformView->AppMessageSender()->SendString( messageName.UTF8String, message.UTF8String, [callback_ptr](const mojo::String& response) { callback_ptr.get()(base::SysUTF8ToNSString(response)); }); } - (void)addMessageListener:(NSObject*)listener { NSAssert(listener, @"The listener must not be null"); NSString* messageName = listener.messageName; NSAssert(messageName, @"The messageName must not be null"); _platformView->AppMessageReceiver().SetMessageListener(messageName.UTF8String, listener); } - (void)removeMessageListener:(NSObject*)listener { NSAssert(listener, @"The listener must not be null"); NSString* messageName = listener.messageName; NSAssert(messageName, @"The messageName must not be null"); _platformView->AppMessageReceiver().SetMessageListener(messageName.UTF8String, nil); } - (void)addAsyncMessageListener: (NSObject*)listener { NSAssert(listener, @"The listener must not be null"); NSString* messageName = listener.messageName; NSAssert(messageName, @"The messageName must not be null"); _platformView->AppMessageReceiver().SetAsyncMessageListener( messageName.UTF8String, listener); } - (void)removeAsyncMessageListener: (NSObject*)listener { NSAssert(listener, @"The listener must not be null"); NSString* messageName = listener.messageName; NSAssert(messageName, @"The messageName must not be null"); _platformView->AppMessageReceiver().SetAsyncMessageListener( messageName.UTF8String, nil); } @end