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