• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2013 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#define FML_USED_ON_EMBEDDER
6
7#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h"
8#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterViewController_Internal.h"
9
10#include <memory>
11
12#include "flutter/fml/memory/weak_ptr.h"
13#include "flutter/fml/message_loop.h"
14#include "flutter/fml/platform/darwin/platform_version.h"
15#include "flutter/fml/platform/darwin/scoped_nsobject.h"
16#include "flutter/shell/common/thread_host.h"
17#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterBinaryMessengerRelay.h"
18#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine_Internal.h"
19#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformPlugin.h"
20#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputDelegate.h"
21#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.h"
22#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterView.h"
23#import "flutter/shell/platform/darwin/ios/framework/Source/platform_message_response_darwin.h"
24#import "flutter/shell/platform/darwin/ios/platform_view_ios.h"
25
26NSNotificationName const FlutterSemanticsUpdateNotification = @"FlutterSemanticsUpdate";
27
28// This is left a FlutterBinaryMessenger privately for now to give people a chance to notice the
29// change. Unfortunately unless you have Werror turned on, incompatible pointers as arguments are
30// just a warning.
31@interface FlutterViewController () <FlutterBinaryMessenger>
32@property(nonatomic, readwrite, getter=isDisplayingFlutterUI) BOOL displayingFlutterUI;
33@end
34
35// The following conditional compilation defines an API 13 concept on earlier API targets so that
36// a compiler compiling against API 12 or below does not blow up due to non-existent members.
37#if __IPHONE_OS_VERSION_MAX_ALLOWED < 130000
38typedef enum UIAccessibilityContrast : NSInteger {
39  UIAccessibilityContrastUnspecified = 0,
40  UIAccessibilityContrastNormal = 1,
41  UIAccessibilityContrastHigh = 2
42} UIAccessibilityContrast;
43
44@interface UITraitCollection (MethodsFromNewerSDK)
45- (UIAccessibilityContrast)accessibilityContrast;
46@end
47#endif
48
49@implementation FlutterViewController {
50  std::unique_ptr<fml::WeakPtrFactory<FlutterViewController>> _weakFactory;
51  fml::scoped_nsobject<FlutterEngine> _engine;
52
53  // We keep a separate reference to this and create it ahead of time because we want to be able to
54  // setup a shell along with its platform view before the view has to appear.
55  fml::scoped_nsobject<FlutterView> _flutterView;
56  fml::scoped_nsobject<UIView> _splashScreenView;
57  fml::ScopedBlock<void (^)(void)> _flutterViewRenderedCallback;
58  UIInterfaceOrientationMask _orientationPreferences;
59  UIStatusBarStyle _statusBarStyle;
60  flutter::ViewportMetrics _viewportMetrics;
61  BOOL _initialized;
62  BOOL _viewOpaque;
63  BOOL _engineNeedsLaunch;
64  NSMutableSet<NSNumber*>* _ongoingTouches;
65}
66
67@synthesize displayingFlutterUI = _displayingFlutterUI;
68
69#pragma mark - Manage and override all designated initializers
70
71- (instancetype)initWithEngine:(FlutterEngine*)engine
72                       nibName:(NSString*)nibNameOrNil
73                        bundle:(NSBundle*)nibBundleOrNil {
74  NSAssert(engine != nil, @"Engine is required");
75  self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
76  if (self) {
77    _viewOpaque = YES;
78    _engine.reset([engine retain]);
79    _engineNeedsLaunch = NO;
80    _flutterView.reset([[FlutterView alloc] initWithDelegate:_engine opaque:self.isViewOpaque]);
81    _weakFactory = std::make_unique<fml::WeakPtrFactory<FlutterViewController>>(self);
82    _ongoingTouches = [[NSMutableSet alloc] init];
83
84    [self performCommonViewControllerInitialization];
85    [engine setViewController:self];
86  }
87
88  return self;
89}
90
91- (instancetype)initWithName:(NSString*)nibNameOrNil
92                      bundle:(NSBundle*)nibBundleOrNil {
93  self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
94  if (self) {
95    _viewOpaque = YES;
96    _weakFactory = std::make_unique<fml::WeakPtrFactory<FlutterViewController>>(self);
97    _engine.reset([[FlutterEngine alloc] initWithName:@"io.flutter"
98//                                              project:projectOrNil
99                               allowHeadlessExecution:NO]);
100    _flutterView.reset([[FlutterView alloc] initWithDelegate:_engine opaque:self.isViewOpaque]);
101    [_engine.get() createShell:nil libraryURI:nil];
102    _engineNeedsLaunch = YES;
103    _ongoingTouches = [[NSMutableSet alloc] init];
104    [self loadDefaultSplashScreenView];
105    [self performCommonViewControllerInitialization];
106  }
107
108  return self;
109}
110
111- (void) setIdleCallBack:(std::function<void(int64_t)>)idleCallback {
112// void setIdleCallBack(IdleCallback idleCallback){
113  flutter::Shell& shell = [_engine.get() shell];
114
115
116  auto platform_view = shell.GetPlatformView();
117  if (!platform_view) {
118    return;
119  }
120  platform_view->SetIdleNotificationCallback(std::move(idleCallback));
121}
122
123- (instancetype)initWithNibName:(NSString*)nibNameOrNil bundle:(NSBundle*)nibBundleOrNil {
124  return [self initWithName:nil bundle:nil];
125}
126
127- (instancetype)initWithCoder:(NSCoder*)aDecoder {
128  return [self initWithName:nil bundle:nil];
129}
130
131- (instancetype)init {
132  return [self initWithName:nil bundle:nil];
133}
134
135- (BOOL)isViewOpaque {
136  return _viewOpaque;
137}
138
139- (void)setViewOpaque:(BOOL)value {
140  _viewOpaque = value;
141  if (_flutterView.get().layer.opaque != value) {
142    _flutterView.get().layer.opaque = value;
143    [_flutterView.get().layer setNeedsLayout];
144  }
145}
146
147#pragma mark - Common view controller initialization tasks
148
149- (void)performCommonViewControllerInitialization {
150  if (_initialized)
151    return;
152
153  _initialized = YES;
154
155  _orientationPreferences = UIInterfaceOrientationMaskAll;
156  _statusBarStyle = UIStatusBarStyleDefault;
157
158  [self setupNotificationCenterObservers];
159}
160
161- (FlutterEngine*)engine {
162  return _engine.get();
163}
164
165- (fml::WeakPtr<FlutterViewController>)getWeakPtr {
166  return _weakFactory->GetWeakPtr();
167}
168
169- (void)setupNotificationCenterObservers {
170  NSNotificationCenter* center = [NSNotificationCenter defaultCenter];
171  [center addObserver:self
172             selector:@selector(onOrientationPreferencesUpdated:)
173                 name:@(flutter::kOrientationUpdateNotificationName)
174               object:nil];
175
176  [center addObserver:self
177             selector:@selector(onPreferredStatusBarStyleUpdated:)
178                 name:@(flutter::kOverlayStyleUpdateNotificationName)
179               object:nil];
180
181  [center addObserver:self
182             selector:@selector(applicationBecameActive:)
183                 name:UIApplicationDidBecomeActiveNotification
184               object:nil];
185
186  [center addObserver:self
187             selector:@selector(applicationWillResignActive:)
188                 name:UIApplicationWillResignActiveNotification
189               object:nil];
190
191  [center addObserver:self
192             selector:@selector(applicationDidEnterBackground:)
193                 name:UIApplicationDidEnterBackgroundNotification
194               object:nil];
195
196  [center addObserver:self
197             selector:@selector(applicationWillEnterForeground:)
198                 name:UIApplicationWillEnterForegroundNotification
199               object:nil];
200
201  [center addObserver:self
202             selector:@selector(keyboardWillChangeFrame:)
203                 name:UIKeyboardWillChangeFrameNotification
204               object:nil];
205
206  [center addObserver:self
207             selector:@selector(keyboardWillBeHidden:)
208                 name:UIKeyboardWillHideNotification
209               object:nil];
210
211  [center addObserver:self
212             selector:@selector(onLocaleUpdated:)
213                 name:NSCurrentLocaleDidChangeNotification
214               object:nil];
215
216  [center addObserver:self
217             selector:@selector(onUserSettingsChanged:)
218                 name:UIContentSizeCategoryDidChangeNotification
219               object:nil];
220}
221
222- (void)setInitialRoute:(NSString*)route {
223  [[_engine.get() navigationChannel] invokeMethod:@"setInitialRoute" arguments:route];
224}
225
226- (void)popRoute {
227  [[_engine.get() navigationChannel] invokeMethod:@"popRoute" arguments:nil];
228}
229
230- (void)pushRoute:(NSString*)route {
231  [[_engine.get() navigationChannel] invokeMethod:@"pushRoute" arguments:route];
232}
233
234#pragma mark - Loading the view
235
236- (void)loadView {
237  self.view = _flutterView.get();
238  self.view.multipleTouchEnabled = YES;
239  self.view.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
240
241  [self installSplashScreenViewIfNecessary];
242}
243
244#pragma mark - Managing launch views
245
246- (void)installSplashScreenViewIfNecessary {
247  // Show the launch screen view again on top of the FlutterView if available.
248  // This launch screen view will be removed once the first Flutter frame is rendered.
249  if (_splashScreenView && (self.isBeingPresented || self.isMovingToParentViewController)) {
250    [_splashScreenView.get() removeFromSuperview];
251    _splashScreenView.reset();
252    return;
253  }
254
255  // Use the property getter to initialize the default value.
256  UIView* splashScreenView = self.splashScreenView;
257  if (splashScreenView == nil) {
258    return;
259  }
260  splashScreenView.frame = self.view.bounds;
261  [self.view addSubview:splashScreenView];
262}
263
264+ (BOOL)automaticallyNotifiesObserversOfDisplayingFlutterUI {
265  return NO;
266}
267
268- (void)setDisplayingFlutterUI:(BOOL)displayingFlutterUI {
269  if (_displayingFlutterUI != displayingFlutterUI) {
270    if (displayingFlutterUI == YES) {
271      if (!self.isViewLoaded || !self.view.window) {
272        return;
273      }
274    }
275    [self willChangeValueForKey:@"displayingFlutterUI"];
276    _displayingFlutterUI = displayingFlutterUI;
277    [self didChangeValueForKey:@"displayingFlutterUI"];
278  }
279}
280
281- (void)callViewRenderedCallback {
282  self.displayingFlutterUI = YES;
283  if (_flutterViewRenderedCallback != nil) {
284    _flutterViewRenderedCallback.get()();
285    _flutterViewRenderedCallback.reset();
286  }
287}
288
289- (void)removeSplashScreenView:(dispatch_block_t _Nullable)onComplete {
290  NSAssert(_splashScreenView, @"The splash screen view must not be null");
291  UIView* splashScreen = _splashScreenView.get();
292  _splashScreenView.reset();
293  [UIView animateWithDuration:0.2
294      animations:^{
295        splashScreen.alpha = 0;
296      }
297      completion:^(BOOL finished) {
298        [splashScreen removeFromSuperview];
299        if (onComplete) {
300          onComplete();
301        }
302      }];
303}
304
305- (void)installFirstFrameCallback {
306  fml::WeakPtr<flutter::PlatformViewIOS> weakPlatformView = [_engine.get() platformView];
307  if (!weakPlatformView) {
308    return;
309  }
310
311  // Start on the platform thread.
312  weakPlatformView->SetNextFrameCallback([weakSelf = [self getWeakPtr],
313                                          platformTaskRunner = [_engine.get() platformTaskRunner],
314                                          gpuTaskRunner = [_engine.get() GPUTaskRunner]]() {
315    FML_DCHECK(gpuTaskRunner->RunsTasksOnCurrentThread());
316    // Get callback on GPU thread and jump back to platform thread.
317    platformTaskRunner->PostTask([weakSelf]() {
318      fml::scoped_nsobject<FlutterViewController> flutterViewController(
319          [(FlutterViewController*)weakSelf.get() retain]);
320      if (flutterViewController) {
321        if (flutterViewController.get()->_splashScreenView) {
322          [flutterViewController removeSplashScreenView:^{
323            [flutterViewController callViewRenderedCallback];
324          }];
325        } else {
326          [flutterViewController callViewRenderedCallback];
327        }
328      }
329    });
330  });
331}
332
333#pragma mark - Properties
334
335- (FlutterView*)flutterView {
336  return _flutterView;
337}
338
339- (UIView*)splashScreenView {
340  if (!_splashScreenView) {
341    return nil;
342  }
343  return _splashScreenView.get();
344}
345
346- (BOOL)loadDefaultSplashScreenView {
347  NSString* launchscreenName =
348      [[[NSBundle mainBundle] infoDictionary] objectForKey:@"UILaunchStoryboardName"];
349  if (launchscreenName == nil) {
350    return NO;
351  }
352  UIView* splashView = [self splashScreenFromStoryboard:launchscreenName];
353  if (!splashView) {
354    splashView = [self splashScreenFromXib:launchscreenName];
355  }
356  if (!splashView) {
357    return NO;
358  }
359  self.splashScreenView = splashView;
360  return YES;
361}
362
363- (UIView*)splashScreenFromStoryboard:(NSString*)name {
364  UIStoryboard* storyboard = nil;
365  @try {
366    storyboard = [UIStoryboard storyboardWithName:name bundle:nil];
367  } @catch (NSException* exception) {
368    return nil;
369  }
370  if (storyboard) {
371    UIViewController* splashScreenViewController = [storyboard instantiateInitialViewController];
372    return splashScreenViewController.view;
373  }
374  return nil;
375}
376
377- (UIView*)splashScreenFromXib:(NSString*)name {
378  NSArray* objects = [[NSBundle mainBundle] loadNibNamed:name owner:self options:nil];
379  if ([objects count] != 0) {
380    UIView* view = [objects objectAtIndex:0];
381    return view;
382  }
383  return nil;
384}
385
386- (void)setSplashScreenView:(UIView*)view {
387  if (!view) {
388    // Special case: user wants to remove the splash screen view.
389    if (_splashScreenView) {
390      [self removeSplashScreenView:nil];
391    }
392    return;
393  }
394
395  _splashScreenView.reset([view retain]);
396  _splashScreenView.get().autoresizingMask =
397      UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
398}
399
400- (void)setFlutterViewDidRenderCallback:(void (^)(void))callback {
401  _flutterViewRenderedCallback.reset(callback, fml::OwnershipPolicy::Retain);
402}
403
404#pragma mark - Surface creation and teardown updates
405
406- (void)surfaceUpdated:(BOOL)appeared {
407  // NotifyCreated/NotifyDestroyed are synchronous and require hops between the UI and GPU thread.
408  if (appeared) {
409    [self installFirstFrameCallback];
410    [_engine.get() platformViewsController] -> SetFlutterView(_flutterView.get());
411    [_engine.get() platformViewsController] -> SetFlutterViewController(self);
412    [_engine.get() platformView] -> NotifyCreated();
413  } else {
414    self.displayingFlutterUI = NO;
415    [_engine.get() platformView] -> NotifyDestroyed();
416    [_engine.get() platformViewsController] -> SetFlutterView(nullptr);
417    [_engine.get() platformViewsController] -> SetFlutterViewController(nullptr);
418  }
419}
420
421#pragma mark - UIViewController lifecycle notifications
422
423- (void)viewWillAppear:(BOOL)animated {
424  TRACE_EVENT0("flutter", "viewWillAppear");
425
426  if (_engineNeedsLaunch) {
427    [_engine.get() launchEngine:nil libraryURI:nil];
428    [_engine.get() setViewController:self];
429    _engineNeedsLaunch = NO;
430  }
431
432  // Send platform settings to Flutter, e.g., platform brightness.
433  [self onUserSettingsChanged:nil];
434
435  // Only recreate surface on subsequent appearances when viewport metrics are known.
436  // First time surface creation is done on viewDidLayoutSubviews.
437  if (_viewportMetrics.physical_width) {
438    [self surfaceUpdated:YES];
439  }
440  [[_engine.get() lifecycleChannel] sendMessage:@"AppLifecycleState.inactive"];
441
442  [super viewWillAppear:animated];
443}
444
445- (void)viewDidAppear:(BOOL)animated {
446  TRACE_EVENT0("flutter", "viewDidAppear");
447  [self onLocaleUpdated:nil];
448  [self onUserSettingsChanged:nil];
449  [[_engine.get() lifecycleChannel] sendMessage:@"AppLifecycleState.resumed"];
450
451  [super viewDidAppear:animated];
452}
453
454- (void)viewWillDisappear:(BOOL)animated {
455  TRACE_EVENT0("flutter", "viewWillDisappear");
456  [[_engine.get() lifecycleChannel] sendMessage:@"AppLifecycleState.inactive"];
457
458  [super viewWillDisappear:animated];
459}
460
461- (void)viewDidDisappear:(BOOL)animated {
462  TRACE_EVENT0("flutter", "viewDidDisappear");
463  [self surfaceUpdated:NO];
464  [[_engine.get() lifecycleChannel] sendMessage:@"AppLifecycleState.paused"];
465  [self flushOngoingTouches];
466
467  [super viewDidDisappear:animated];
468}
469
470- (void)flushOngoingTouches {
471  if (_ongoingTouches.count > 0) {
472    auto packet = std::make_unique<flutter::PointerDataPacket>(_ongoingTouches.count);
473    size_t pointer_index = 0;
474    // If the view controller is going away, we want to flush cancel all the ongoing
475    // touches to the framework so nothing gets orphaned.
476    for (NSNumber* device in _ongoingTouches) {
477      // Create fake PointerData to balance out each previously started one for the framework.
478      flutter::PointerData pointer_data;
479      pointer_data.Clear();
480
481      constexpr int kMicrosecondsPerSecond = 1000 * 1000;
482      // Use current time.
483      pointer_data.time_stamp = [[NSDate date] timeIntervalSince1970] * kMicrosecondsPerSecond;
484
485      pointer_data.change = flutter::PointerData::Change::kCancel;
486      pointer_data.kind = flutter::PointerData::DeviceKind::kTouch;
487      pointer_data.device = device.longLongValue;
488
489      // Anything we put here will be arbitrary since there are no touches.
490      pointer_data.physical_x = 0;
491      pointer_data.physical_y = 0;
492      pointer_data.pressure = 1.0;
493      pointer_data.pressure_max = 1.0;
494
495      packet->SetPointerData(pointer_index++, pointer_data);
496    }
497
498    [_ongoingTouches removeAllObjects];
499    [_engine.get() dispatchPointerDataPacket:std::move(packet)];
500  }
501}
502
503- (void)dealloc {
504  [_engine.get() notifyViewControllerDeallocated];
505  [[NSNotificationCenter defaultCenter] removeObserver:self];
506  [super dealloc];
507}
508
509#pragma mark - Application lifecycle notifications
510
511- (void)applicationBecameActive:(NSNotification*)notification {
512  TRACE_EVENT0("flutter", "applicationBecameActive");
513  if (_viewportMetrics.physical_width)
514    [self surfaceUpdated:YES];
515  [[_engine.get() lifecycleChannel] sendMessage:@"AppLifecycleState.resumed"];
516}
517
518- (void)applicationWillResignActive:(NSNotification*)notification {
519  TRACE_EVENT0("flutter", "applicationWillResignActive");
520  [self surfaceUpdated:NO];
521  [[_engine.get() lifecycleChannel] sendMessage:@"AppLifecycleState.inactive"];
522}
523
524- (void)applicationDidEnterBackground:(NSNotification*)notification {
525  TRACE_EVENT0("flutter", "applicationDidEnterBackground");
526  [[_engine.get() lifecycleChannel] sendMessage:@"AppLifecycleState.paused"];
527}
528
529- (void)applicationWillEnterForeground:(NSNotification*)notification {
530  TRACE_EVENT0("flutter", "applicationWillEnterForeground");
531  [[_engine.get() lifecycleChannel] sendMessage:@"AppLifecycleState.inactive"];
532}
533
534#pragma mark - Touch event handling
535
536static flutter::PointerData::Change PointerDataChangeFromUITouchPhase(UITouchPhase phase) {
537  switch (phase) {
538    case UITouchPhaseBegan:
539      return flutter::PointerData::Change::kDown;
540    case UITouchPhaseMoved:
541    case UITouchPhaseStationary:
542      // There is no EVENT_TYPE_POINTER_STATIONARY. So we just pass a move type
543      // with the same coordinates
544      return flutter::PointerData::Change::kMove;
545    case UITouchPhaseEnded:
546      return flutter::PointerData::Change::kUp;
547    case UITouchPhaseCancelled:
548      return flutter::PointerData::Change::kCancel;
549  }
550
551  return flutter::PointerData::Change::kCancel;
552}
553
554static flutter::PointerData::DeviceKind DeviceKindFromTouchType(UITouch* touch) {
555  if (@available(iOS 9, *)) {
556    switch (touch.type) {
557      case UITouchTypeDirect:
558      case UITouchTypeIndirect:
559        return flutter::PointerData::DeviceKind::kTouch;
560      case UITouchTypeStylus:
561        return flutter::PointerData::DeviceKind::kStylus;
562    }
563  } else {
564    return flutter::PointerData::DeviceKind::kTouch;
565  }
566
567  return flutter::PointerData::DeviceKind::kTouch;
568}
569
570// Dispatches the UITouches to the engine. Usually, the type of change of the touch is determined
571// from the UITouch's phase. However, FlutterAppDelegate fakes touches to ensure that touch events
572// in the status bar area are available to framework code. The change type (optional) of the faked
573// touch is specified in the second argument.
574- (void)dispatchTouches:(NSSet*)touches
575    pointerDataChangeOverride:(flutter::PointerData::Change*)overridden_change {
576  const CGFloat scale = [UIScreen mainScreen].scale;
577  auto packet = std::make_unique<flutter::PointerDataPacket>(touches.count);
578
579  size_t pointer_index = 0;
580
581  for (UITouch* touch in touches) {
582    CGPoint windowCoordinates = [touch locationInView:self.view];
583
584    flutter::PointerData pointer_data;
585    pointer_data.Clear();
586
587    constexpr int kMicrosecondsPerSecond = 1000 * 1000;
588    pointer_data.time_stamp = touch.timestamp * kMicrosecondsPerSecond;
589
590    pointer_data.change = overridden_change != nullptr
591                              ? *overridden_change
592                              : PointerDataChangeFromUITouchPhase(touch.phase);
593
594    pointer_data.kind = DeviceKindFromTouchType(touch);
595
596    pointer_data.device = reinterpret_cast<int64_t>(touch);
597
598    pointer_data.physical_x = windowCoordinates.x * scale;
599    pointer_data.physical_y = windowCoordinates.y * scale;
600
601    NSNumber* deviceKey = [NSNumber numberWithLongLong:pointer_data.device];
602    // Track touches that began and not yet stopped so we can flush them
603    // if the view controller goes away.
604    switch (pointer_data.change) {
605      case flutter::PointerData::Change::kDown:
606        [_ongoingTouches addObject:deviceKey];
607        break;
608      case flutter::PointerData::Change::kCancel:
609      case flutter::PointerData::Change::kUp:
610        [_ongoingTouches removeObject:deviceKey];
611        break;
612      case flutter::PointerData::Change::kHover:
613      case flutter::PointerData::Change::kMove:
614        // We're only tracking starts and stops.
615        break;
616      case flutter::PointerData::Change::kAdd:
617      case flutter::PointerData::Change::kRemove:
618        // We don't use kAdd/kRemove.
619        break;
620    }
621
622    // pressure_min is always 0.0
623    if (@available(iOS 9, *)) {
624      // These properties were introduced in iOS 9.0.
625      pointer_data.pressure = touch.force;
626      pointer_data.pressure_max = touch.maximumPossibleForce;
627    } else {
628      pointer_data.pressure = 1.0;
629      pointer_data.pressure_max = 1.0;
630    }
631
632    // These properties were introduced in iOS 8.0
633    pointer_data.radius_major = touch.majorRadius;
634    pointer_data.radius_min = touch.majorRadius - touch.majorRadiusTolerance;
635    pointer_data.radius_max = touch.majorRadius + touch.majorRadiusTolerance;
636
637    // These properties were introduced in iOS 9.1
638    if (@available(iOS 9.1, *)) {
639      // iOS Documentation: altitudeAngle
640      // A value of 0 radians indicates that the stylus is parallel to the surface. The value of
641      // this property is Pi/2 when the stylus is perpendicular to the surface.
642      //
643      // PointerData Documentation: tilt
644      // The angle of the stylus, in radians in the range:
645      //    0 <= tilt <= pi/2
646      // giving the angle of the axis of the stylus, relative to the axis perpendicular to the input
647      // surface (thus 0.0 indicates the stylus is orthogonal to the plane of the input surface,
648      // while pi/2 indicates that the stylus is flat on that surface).
649      //
650      // Discussion:
651      // The ranges are the same. Origins are swapped.
652      pointer_data.tilt = M_PI_2 - touch.altitudeAngle;
653
654      // iOS Documentation: azimuthAngleInView:
655      // With the tip of the stylus touching the screen, the value of this property is 0 radians
656      // when the cap end of the stylus (that is, the end opposite of the tip) points along the
657      // positive x axis of the device's screen. The azimuth angle increases as the user swings the
658      // cap end of the stylus in a clockwise direction around the tip.
659      //
660      // PointerData Documentation: orientation
661      // The angle of the stylus, in radians in the range:
662      //    -pi < orientation <= pi
663      // giving the angle of the axis of the stylus projected onto the input surface, relative to
664      // the positive y-axis of that surface (thus 0.0 indicates the stylus, if projected onto that
665      // surface, would go from the contact point vertically up in the positive y-axis direction, pi
666      // would indicate that the stylus would go down in the negative y-axis direction; pi/4 would
667      // indicate that the stylus goes up and to the right, -pi/2 would indicate that the stylus
668      // goes to the left, etc).
669      //
670      // Discussion:
671      // Sweep direction is the same. Phase of M_PI_2.
672      pointer_data.orientation = [touch azimuthAngleInView:nil] - M_PI_2;
673    }
674
675    packet->SetPointerData(pointer_index++, pointer_data);
676  }
677
678  [_engine.get() dispatchPointerDataPacket:std::move(packet)];
679}
680
681#pragma mark - Handle view resizing
682
683- (void)updateViewportMetrics {
684  [_engine.get() updateViewportMetrics:_viewportMetrics];
685}
686
687- (CGFloat)statusBarPadding {
688  UIScreen* screen = self.view.window.screen;
689  CGRect statusFrame = [UIApplication sharedApplication].statusBarFrame;
690  CGRect viewFrame = [self.view convertRect:self.view.bounds
691                          toCoordinateSpace:screen.coordinateSpace];
692  CGRect intersection = CGRectIntersection(statusFrame, viewFrame);
693  return CGRectIsNull(intersection) ? 0.0 : intersection.size.height;
694}
695
696- (void)viewDidLayoutSubviews {
697  CGSize viewSize = self.view.bounds.size;
698  CGFloat scale = [UIScreen mainScreen].scale;
699
700  // First time since creation that the dimensions of its view is known.
701  bool firstViewBoundsUpdate = !_viewportMetrics.physical_width;
702  _viewportMetrics.device_pixel_ratio = scale;
703  _viewportMetrics.physical_width = viewSize.width * scale;
704  _viewportMetrics.physical_height = viewSize.height * scale;
705
706  [self updateViewportPadding];
707  [self updateViewportMetrics];
708
709  // This must run after updateViewportMetrics so that the surface creation tasks are queued after
710  // the viewport metrics update tasks.
711  if (firstViewBoundsUpdate) {
712    [self surfaceUpdated:YES];
713
714    flutter::Shell& shell = [_engine.get() shell];
715    fml::TimeDelta waitTime =
716#if FLUTTER_RUNTIME_MODE == FLUTTER_RUNTIME_MODE_DEBUG
717        fml::TimeDelta::FromMilliseconds(200);
718#else
719        fml::TimeDelta::FromMilliseconds(100);
720#endif
721    if (shell.WaitForFirstFrame(waitTime).code() == fml::StatusCode::kDeadlineExceeded) {
722      FML_LOG(INFO) << "Timeout waiting for the first frame to render.  This may happen in "
723                    << "unoptimized builds.  If this is a release build, you should load a less "
724                    << "complex frame to avoid the timeout.";
725    }
726  }
727}
728
729- (void)viewSafeAreaInsetsDidChange {
730  [self updateViewportPadding];
731  [self updateViewportMetrics];
732  [super viewSafeAreaInsetsDidChange];
733}
734
735// Updates _viewportMetrics physical padding.
736//
737// Viewport padding represents the iOS safe area insets.
738- (void)updateViewportPadding {
739  CGFloat scale = [UIScreen mainScreen].scale;
740  if (@available(iOS 11, *)) {
741    _viewportMetrics.physical_padding_top = self.view.safeAreaInsets.top * scale;
742    _viewportMetrics.physical_padding_left = self.view.safeAreaInsets.left * scale;
743    _viewportMetrics.physical_padding_right = self.view.safeAreaInsets.right * scale;
744    _viewportMetrics.physical_padding_bottom = self.view.safeAreaInsets.bottom * scale;
745  } else {
746    _viewportMetrics.physical_padding_top = [self statusBarPadding] * scale;
747  }
748}
749
750#pragma mark - Keyboard events
751
752- (void)keyboardWillChangeFrame:(NSNotification*)notification {
753  NSDictionary* info = [notification userInfo];
754  CGFloat bottom = CGRectGetHeight([[info objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue]);
755  CGFloat scale = [UIScreen mainScreen].scale;
756
757  // The keyboard is treated as an inset since we want to effectively reduce the window size by the
758  // keyboard height. The Dart side will compute a value accounting for the keyboard-consuming
759  // bottom padding.
760  _viewportMetrics.physical_view_inset_bottom = bottom * scale;
761  [self updateViewportMetrics];
762}
763
764- (void)keyboardWillBeHidden:(NSNotification*)notification {
765  _viewportMetrics.physical_view_inset_bottom = 0;
766  [self updateViewportMetrics];
767}
768
769#pragma mark - Orientation updates
770
771- (void)onOrientationPreferencesUpdated:(NSNotification*)notification {
772  // Notifications may not be on the iOS UI thread
773  dispatch_async(dispatch_get_main_queue(), ^{
774    NSDictionary* info = notification.userInfo;
775
776    NSNumber* update = info[@(flutter::kOrientationUpdateNotificationKey)];
777
778    if (update == nil) {
779      return;
780    }
781
782    NSUInteger new_preferences = update.unsignedIntegerValue;
783
784    if (new_preferences != _orientationPreferences) {
785      _orientationPreferences = new_preferences;
786      [UIViewController attemptRotationToDeviceOrientation];
787    }
788  });
789}
790
791- (BOOL)shouldAutorotate {
792  return YES;
793}
794
795- (NSUInteger)supportedInterfaceOrientations {
796  return _orientationPreferences;
797}
798
799#pragma mark - Locale updates
800
801- (void)onLocaleUpdated:(NSNotification*)notification {
802  NSArray<NSString*>* preferredLocales = [NSLocale preferredLanguages];
803  NSMutableArray<NSString*>* data = [[NSMutableArray new] autorelease];
804
805  // Force prepend the [NSLocale currentLocale] to the front of the list
806  // to ensure we are including the full default locale. preferredLocales
807  // is not guaranteed to include anything beyond the languageCode.
808  NSLocale* currentLocale = [NSLocale currentLocale];
809  NSString* languageCode = [currentLocale objectForKey:NSLocaleLanguageCode];
810  NSString* countryCode = [currentLocale objectForKey:NSLocaleCountryCode];
811  NSString* scriptCode = [currentLocale objectForKey:NSLocaleScriptCode];
812  NSString* variantCode = [currentLocale objectForKey:NSLocaleVariantCode];
813  if (languageCode) {
814    [data addObject:languageCode];
815    [data addObject:(countryCode ? countryCode : @"")];
816    [data addObject:(scriptCode ? scriptCode : @"")];
817    [data addObject:(variantCode ? variantCode : @"")];
818  }
819
820  // Add any secondary locales/languages to the list.
821  for (NSString* localeID in preferredLocales) {
822    NSLocale* currentLocale = [[[NSLocale alloc] initWithLocaleIdentifier:localeID] autorelease];
823    NSString* languageCode = [currentLocale objectForKey:NSLocaleLanguageCode];
824    NSString* countryCode = [currentLocale objectForKey:NSLocaleCountryCode];
825    NSString* scriptCode = [currentLocale objectForKey:NSLocaleScriptCode];
826    NSString* variantCode = [currentLocale objectForKey:NSLocaleVariantCode];
827    if (!languageCode) {
828      continue;
829    }
830    [data addObject:languageCode];
831    [data addObject:(countryCode ? countryCode : @"")];
832    [data addObject:(scriptCode ? scriptCode : @"")];
833    [data addObject:(variantCode ? variantCode : @"")];
834  }
835  if (data.count == 0) {
836    return;
837  }
838  [[_engine.get() localizationChannel] invokeMethod:@"setLocale" arguments:data];
839}
840
841#pragma mark - Set user settings
842
843- (void)traitCollectionDidChange:(UITraitCollection*)previousTraitCollection {
844  [super traitCollectionDidChange:previousTraitCollection];
845  [self onUserSettingsChanged:nil];
846}
847
848- (void)onUserSettingsChanged:(NSNotification*)notification {
849  [[_engine.get() settingsChannel] sendMessage:@{
850    @"textScaleFactor" : @([self textScaleFactor]),
851    @"alwaysUse24HourFormat" : @([self isAlwaysUse24HourFormat]),
852    @"platformBrightness" : [self brightnessMode],
853    @"platformContrast" : [self contrastMode]
854  }];
855}
856
857- (CGFloat)textScaleFactor {
858  UIContentSizeCategory category = [UIApplication sharedApplication].preferredContentSizeCategory;
859  // The delta is computed by approximating Apple's typography guidelines:
860  // https://developer.apple.com/ios/human-interface-guidelines/visual-design/typography/
861  //
862  // Specifically:
863  // Non-accessibility sizes for "body" text are:
864  const CGFloat xs = 14;
865  const CGFloat s = 15;
866  const CGFloat m = 16;
867  const CGFloat l = 17;
868  const CGFloat xl = 19;
869  const CGFloat xxl = 21;
870  const CGFloat xxxl = 23;
871
872  // Accessibility sizes for "body" text are:
873  const CGFloat ax1 = 28;
874  const CGFloat ax2 = 33;
875  const CGFloat ax3 = 40;
876  const CGFloat ax4 = 47;
877  const CGFloat ax5 = 53;
878
879  // We compute the scale as relative difference from size L (large, the default size), where
880  // L is assumed to have scale 1.0.
881  if ([category isEqualToString:UIContentSizeCategoryExtraSmall])
882    return xs / l;
883  else if ([category isEqualToString:UIContentSizeCategorySmall])
884    return s / l;
885  else if ([category isEqualToString:UIContentSizeCategoryMedium])
886    return m / l;
887  else if ([category isEqualToString:UIContentSizeCategoryLarge])
888    return 1.0;
889  else if ([category isEqualToString:UIContentSizeCategoryExtraLarge])
890    return xl / l;
891  else if ([category isEqualToString:UIContentSizeCategoryExtraExtraLarge])
892    return xxl / l;
893  else if ([category isEqualToString:UIContentSizeCategoryExtraExtraExtraLarge])
894    return xxxl / l;
895  else if ([category isEqualToString:UIContentSizeCategoryAccessibilityMedium])
896    return ax1 / l;
897  else if ([category isEqualToString:UIContentSizeCategoryAccessibilityLarge])
898    return ax2 / l;
899  else if ([category isEqualToString:UIContentSizeCategoryAccessibilityExtraLarge])
900    return ax3 / l;
901  else if ([category isEqualToString:UIContentSizeCategoryAccessibilityExtraExtraLarge])
902    return ax4 / l;
903  else if ([category isEqualToString:UIContentSizeCategoryAccessibilityExtraExtraExtraLarge])
904    return ax5 / l;
905  else
906    return 1.0;
907}
908
909- (BOOL)isAlwaysUse24HourFormat {
910  // iOS does not report its "24-Hour Time" user setting in the API. Instead, it applies
911  // it automatically to NSDateFormatter when used with [NSLocale currentLocale]. It is
912  // essential that [NSLocale currentLocale] is used. Any custom locale, even the one
913  // that's the same as [NSLocale currentLocale] will ignore the 24-hour option (there
914  // must be some internal field that's not exposed to developers).
915  //
916  // Therefore this option behaves differently across Android and iOS. On Android this
917  // setting is exposed standalone, and can therefore be applied to all locales, whether
918  // the "current system locale" or a custom one. On iOS it only applies to the current
919  // system locale. Widget implementors must take this into account in order to provide
920  // platform-idiomatic behavior in their widgets.
921  NSString* dateFormat = [NSDateFormatter dateFormatFromTemplate:@"j"
922                                                         options:0
923                                                          locale:[NSLocale currentLocale]];
924  return [dateFormat rangeOfString:@"a"].location == NSNotFound;
925}
926
927// The brightness mode of the platform, e.g., light or dark, expressed as a string that
928// is understood by the Flutter framework. See the settings system channel for more
929// information.
930- (NSString*)brightnessMode {
931  if (@available(iOS 13, *)) {
932    UIUserInterfaceStyle style = self.traitCollection.userInterfaceStyle;
933
934    if (style == UIUserInterfaceStyleDark) {
935      return @"dark";
936    } else {
937      return @"light";
938    }
939  } else {
940    return @"light";
941  }
942}
943
944// The contrast mode of the platform, e.g., normal or high, expressed as a string that is
945// understood by the Flutter framework. See the settings system channel for more
946// information.
947- (NSString*)contrastMode {
948  if (@available(iOS 13, *)) {
949    UIAccessibilityContrast contrast = self.traitCollection.accessibilityContrast;
950
951    if (contrast == UIAccessibilityContrastHigh) {
952      return @"high";
953    } else {
954      return @"normal";
955    }
956  } else {
957    return @"normal";
958  }
959}
960
961#pragma mark - Status Bar touch event handling
962
963// Standard iOS status bar height in pixels.
964constexpr CGFloat kStandardStatusBarHeight = 20.0;
965
966- (void)handleStatusBarTouches:(UIEvent*)event {
967  CGFloat standardStatusBarHeight = kStandardStatusBarHeight;
968  if (@available(iOS 11, *)) {
969    standardStatusBarHeight = self.view.safeAreaInsets.top;
970  }
971
972  // If the status bar is double-height, don't handle status bar taps. iOS
973  // should open the app associated with the status bar.
974  CGRect statusBarFrame = [UIApplication sharedApplication].statusBarFrame;
975  if (statusBarFrame.size.height != standardStatusBarHeight) {
976    return;
977  }
978
979  // If we detect a touch in the status bar, synthesize a fake touch begin/end.
980  for (UITouch* touch in event.allTouches) {
981    if (touch.phase == UITouchPhaseBegan && touch.tapCount > 0) {
982      CGPoint windowLoc = [touch locationInView:nil];
983      CGPoint screenLoc = [touch.window convertPoint:windowLoc toWindow:nil];
984      if (CGRectContainsPoint(statusBarFrame, screenLoc)) {
985        NSSet* statusbarTouches = [NSSet setWithObject:touch];
986
987        flutter::PointerData::Change change = flutter::PointerData::Change::kDown;
988        [self dispatchTouches:statusbarTouches pointerDataChangeOverride:&change];
989        change = flutter::PointerData::Change::kUp;
990        [self dispatchTouches:statusbarTouches pointerDataChangeOverride:&change];
991        return;
992      }
993    }
994  }
995}
996
997#pragma mark - Status bar style
998
999- (UIStatusBarStyle)preferredStatusBarStyle {
1000  return _statusBarStyle;
1001}
1002
1003- (void)onPreferredStatusBarStyleUpdated:(NSNotification*)notification {
1004  // Notifications may not be on the iOS UI thread
1005  dispatch_async(dispatch_get_main_queue(), ^{
1006    NSDictionary* info = notification.userInfo;
1007
1008    NSNumber* update = info[@(flutter::kOverlayStyleUpdateNotificationKey)];
1009
1010    if (update == nil) {
1011      return;
1012    }
1013
1014    NSInteger style = update.integerValue;
1015
1016    if (style != _statusBarStyle) {
1017      _statusBarStyle = static_cast<UIStatusBarStyle>(style);
1018      [self setNeedsStatusBarAppearanceUpdate];
1019    }
1020  });
1021}
1022
1023#pragma mark - Platform views
1024
1025- (flutter::FlutterPlatformViewsController*)platformViewsController {
1026  return [_engine.get() platformViewsController];
1027}
1028
1029- (NSObject<FlutterBinaryMessenger>*)binaryMessenger {
1030  return _engine.get().binaryMessenger;
1031}
1032
1033#pragma mark - FlutterBinaryMessenger
1034
1035- (void)sendOnChannel:(NSString*)channel message:(NSData*)message {
1036  [_engine.get().binaryMessenger sendOnChannel:channel message:message];
1037}
1038
1039- (void)sendOnChannel:(NSString*)channel
1040              message:(NSData*)message
1041          binaryReply:(FlutterBinaryReply)callback {
1042  NSAssert(channel, @"The channel must not be null");
1043  [_engine.get().binaryMessenger sendOnChannel:channel message:message binaryReply:callback];
1044}
1045
1046- (void)setMessageHandlerOnChannel:(NSString*)channel
1047              binaryMessageHandler:(FlutterBinaryMessageHandler)handler {
1048  NSAssert(channel, @"The channel must not be null");
1049  [_engine.get().binaryMessenger setMessageHandlerOnChannel:channel binaryMessageHandler:handler];
1050}
1051
1052#pragma mark - FlutterTextureRegistry
1053
1054- (int64_t)registerTexture:(NSObject<FlutterTexture>*)texture {
1055  return [_engine.get() registerTexture:texture];
1056}
1057
1058- (void)unregisterTexture:(int64_t)textureId {
1059  [_engine.get() unregisterTexture:textureId];
1060}
1061
1062- (void)textureFrameAvailable:(int64_t)textureId {
1063  [_engine.get() textureFrameAvailable:textureId];
1064}
1065
1066- (NSString*)lookupKeyForAsset:(NSString*)asset {
1067    return nil;
1068}
1069
1070- (NSString*)lookupKeyForAsset:(NSString*)asset fromPackage:(NSString*)package {
1071    return nil;
1072}
1073
1074- (id<FlutterPluginRegistry>)pluginRegistry {
1075  return _engine;
1076}
1077
1078#pragma mark - FlutterPluginRegistry
1079
1080- (NSObject<FlutterPluginRegistrar>*)registrarForPlugin:(NSString*)pluginKey {
1081  return [_engine.get() registrarForPlugin:pluginKey];
1082}
1083
1084- (BOOL)hasPlugin:(NSString*)pluginKey {
1085  return [_engine.get() hasPlugin:pluginKey];
1086}
1087
1088- (NSObject*)valuePublishedByPlugin:(NSString*)pluginKey {
1089  return [_engine.get() valuePublishedByPlugin:pluginKey];
1090}
1091
1092@end
1093