1// Copyright 2015-present 650 Industries. All rights reserved. 2 3@import UIKit; 4 5#import "EXAnalytics.h" 6#import "EXAppLoader.h" 7#import "EXAppLoadingView.h" 8#import "EXAppViewController.h" 9#import "EXEnvironment.h" 10#import "EXErrorRecoveryManager.h" 11#import "EXErrorView.h" 12#import "EXFileDownloader.h" 13#import "EXKernel.h" 14#import "EXKernelUtil.h" 15#import "EXReactAppManager.h" 16#import "EXScreenOrientationManager.h" 17#import "EXVersions.h" 18#import "EXUpdatesManager.h" 19#import "EXUtil.h" 20#import <UMCore/UMModuleRegistryProvider.h> 21 22#if __has_include(<EXScreenOrientation/EXScreenOrientationRegistry.h>) 23#import <EXScreenOrientation/EXScreenOrientationRegistry.h> 24#endif 25 26#import <React/RCTUtils.h> 27 28#define EX_INTERFACE_ORIENTATION_USE_MANIFEST 0 29 30// when we encounter an error and auto-refresh, we may actually see a series of errors. 31// we only want to trigger refresh once, so we debounce refresh on a timer. 32const CGFloat kEXAutoReloadDebounceSeconds = 0.1; 33 34// in development only, some errors can happen before we even start loading 35// (e.g. certain packager errors, such as an invalid bundle url) 36// and we want to make sure not to cover the error with a loading view or other chrome. 37const CGFloat kEXDevelopmentErrorCoolDownSeconds = 0.1; 38 39NS_ASSUME_NONNULL_BEGIN 40 41@interface EXAppViewController () 42 <EXReactAppManagerUIDelegate, EXAppLoaderDelegate, EXErrorViewDelegate> 43 44@property (nonatomic, assign) BOOL isLoading; 45@property (nonatomic, assign) BOOL isBridgeAlreadyLoading; 46@property (nonatomic, weak) EXKernelAppRecord *appRecord; 47@property (nonatomic, strong) EXAppLoadingView *loadingView; 48@property (nonatomic, strong) EXErrorView *errorView; 49@property (nonatomic, assign) UIInterfaceOrientationMask supportedInterfaceOrientations; // override super 50@property (nonatomic, strong) NSTimer *tmrAutoReloadDebounce; 51@property (nonatomic, strong) NSDate *dtmLastFatalErrorShown; 52@property (nonatomic, strong) NSMutableArray<UIViewController *> *backgroundedControllers; 53 54@end 55 56@implementation EXAppViewController 57 58@synthesize supportedInterfaceOrientations = _supportedInterfaceOrientations; 59 60#pragma mark - Lifecycle 61 62- (instancetype)initWithAppRecord:(EXKernelAppRecord *)record 63{ 64 if (self = [super init]) { 65 _appRecord = record; 66 _supportedInterfaceOrientations = EX_INTERFACE_ORIENTATION_USE_MANIFEST; 67 } 68 return self; 69} 70 71- (void)dealloc 72{ 73 [self _invalidateRecoveryTimer]; 74 [[NSNotificationCenter defaultCenter] removeObserver:self]; 75} 76 77- (void)viewDidLoad 78{ 79 [super viewDidLoad]; 80 81 // TODO(brentvatne): probably this should not just be UIColor whiteColor? 82 self.view.backgroundColor = [UIColor whiteColor]; 83 84 _loadingView = [[EXAppLoadingView alloc] initWithAppRecord:_appRecord]; 85 [self.view addSubview:_loadingView]; 86 _appRecord.appManager.delegate = self; 87 self.isLoading = YES; 88} 89 90- (void)viewDidAppear:(BOOL)animated 91{ 92 [super viewDidAppear:animated]; 93 if (_appRecord && _appRecord.status == kEXKernelAppRecordStatusNew) { 94 _appRecord.appLoader.delegate = self; 95 _appRecord.appLoader.dataSource = _appRecord.appManager; 96 [self refresh]; 97 } 98} 99 100- (BOOL)shouldAutorotate 101{ 102 return YES; 103} 104 105- (void)viewWillLayoutSubviews 106{ 107 [super viewWillLayoutSubviews]; 108 if (_loadingView) { 109 _loadingView.frame = CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height); 110 } 111 if (_contentView) { 112 _contentView.frame = CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height); 113 } 114} 115 116#pragma mark - Public 117 118- (void)maybeShowError:(NSError *)error 119{ 120 self.isLoading = NO; 121 if ([self _willAutoRecoverFromError:error]) { 122 return; 123 } 124 if (error && ![error isKindOfClass:[NSError class]]) { 125#if DEBUG 126 NSAssert(NO, @"AppViewController error handler was called on an object that isn't an NSError"); 127#endif 128 return; 129 } 130 131 // we don't ever want to show any Expo UI in a production standalone app, so hard crash 132 if ([EXEnvironment sharedEnvironment].isDetached && ![_appRecord.appManager enablesDeveloperTools]) { 133 NSException *e = [NSException exceptionWithName:@"ExpoFatalError" 134 reason:[NSString stringWithFormat:@"Expo encountered a fatal error: %@", [error localizedDescription]] 135 userInfo:@{NSUnderlyingErrorKey: error}]; 136 @throw e; 137 } 138 139 NSString *domain = (error && error.domain) ? error.domain : @""; 140 BOOL isNetworkError = ([domain isEqualToString:(NSString *)kCFErrorDomainCFNetwork] || [domain isEqualToString:EXNetworkErrorDomain]); 141 142 if (isNetworkError) { 143 // show a human-readable reachability error 144 dispatch_async(dispatch_get_main_queue(), ^{ 145 [self _showErrorWithType:kEXFatalErrorTypeLoading error:error]; 146 }); 147 } else if ([domain isEqualToString:@"JSServer"] && [_appRecord.appManager enablesDeveloperTools]) { 148 // RCTRedBox already handled this 149 } else if ([domain rangeOfString:RCTErrorDomain].length > 0 && [_appRecord.appManager enablesDeveloperTools]) { 150 // RCTRedBox already handled this 151 } else { 152 dispatch_async(dispatch_get_main_queue(), ^{ 153 [self _showErrorWithType:kEXFatalErrorTypeException error:error]; 154 }); 155 } 156} 157 158- (void)_rebuildBridge 159{ 160 [self _invalidateRecoveryTimer]; 161 [[EXKernel sharedInstance] logAnalyticsEvent:@"LOAD_EXPERIENCE" forAppRecord:_appRecord]; 162 [_appRecord.appManager rebuildBridge]; 163} 164 165- (void)refresh 166{ 167 self.isLoading = YES; 168 self.isBridgeAlreadyLoading = NO; 169 [self _invalidateRecoveryTimer]; 170 [_appRecord.appLoader request]; 171} 172 173- (void)reloadFromCache 174{ 175 self.isLoading = YES; 176 self.isBridgeAlreadyLoading = NO; 177 [self _invalidateRecoveryTimer]; 178 [_appRecord.appLoader requestFromCache]; 179} 180 181- (void)appStateDidBecomeActive 182{ 183 dispatch_async(dispatch_get_main_queue(), ^{ 184 [self _enforceDesiredDeviceOrientation]; 185 }); 186 [_appRecord.appManager appStateDidBecomeActive]; 187} 188 189- (void)appStateDidBecomeInactive 190{ 191 [_appRecord.appManager appStateDidBecomeInactive]; 192} 193 194- (void)_rebuildBridgeWithLoadingViewManifest:(NSDictionary *)manifest 195{ 196 if (!self.isBridgeAlreadyLoading) { 197 self.isBridgeAlreadyLoading = YES; 198 dispatch_async(dispatch_get_main_queue(), ^{ 199 self->_loadingView.manifest = manifest; 200 [self _overrideUserInterfaceStyle]; 201 [self _enforceDesiredDeviceOrientation]; 202 [self _rebuildBridge]; 203 }); 204 } 205} 206 207- (void)foregroundControllers 208{ 209 if (_backgroundedControllers != nil) { 210 __block UIViewController *parentController = self; 211 212 [_backgroundedControllers enumerateObjectsUsingBlock:^(UIViewController * _Nonnull viewController, NSUInteger idx, BOOL * _Nonnull stop) { 213 [parentController presentViewController:viewController animated:NO completion:nil]; 214 parentController = viewController; 215 }]; 216 217 _backgroundedControllers = nil; 218 } 219} 220 221- (void)backgroundControllers 222{ 223 UIViewController *childController = [self presentedViewController]; 224 225 if (childController != nil) { 226 if (_backgroundedControllers == nil) { 227 _backgroundedControllers = [NSMutableArray new]; 228 } 229 230 while (childController != nil) { 231 [_backgroundedControllers addObject:childController]; 232 childController = childController.presentedViewController; 233 } 234 } 235} 236 237#pragma mark - EXAppLoaderDelegate 238 239- (void)appLoader:(EXAppLoader *)appLoader didLoadOptimisticManifest:(NSDictionary *)manifest 240{ 241 if ([EXKernel sharedInstance].browserController) { 242 [[EXKernel sharedInstance].browserController addHistoryItemWithUrl:appLoader.manifestUrl manifest:manifest]; 243 } 244 [self _rebuildBridgeWithLoadingViewManifest:manifest]; 245} 246 247- (void)appLoader:(EXAppLoader *)appLoader didLoadBundleWithProgress:(EXLoadingProgress *)progress 248{ 249 dispatch_async(dispatch_get_main_queue(), ^{ 250 [self->_loadingView updateStatusWithProgress:progress]; 251 }); 252} 253 254- (void)appLoader:(EXAppLoader *)appLoader didFinishLoadingManifest:(NSDictionary *)manifest bundle:(NSData *)data 255{ 256 [self _rebuildBridgeWithLoadingViewManifest:manifest]; 257 if (self->_appRecord.appManager.status == kEXReactAppManagerStatusBridgeLoading) { 258 [self->_appRecord.appManager appLoaderFinished]; 259 } 260} 261 262- (void)appLoader:(EXAppLoader *)appLoader didFailWithError:(NSError *)error 263{ 264 if (_appRecord.appManager.status == kEXReactAppManagerStatusBridgeLoading) { 265 [_appRecord.appManager appLoaderFailedWithError:error]; 266 } 267 [self maybeShowError:error]; 268} 269 270- (void)appLoader:(EXAppLoader *)appLoader didResolveUpdatedBundleWithManifest:(NSDictionary * _Nullable)manifest isFromCache:(BOOL)isFromCache error:(NSError * _Nullable)error 271{ 272 [[EXKernel sharedInstance].serviceRegistry.updatesManager notifyApp:_appRecord ofDownloadWithManifest:manifest isNew:!isFromCache error:error]; 273} 274 275#pragma mark - EXReactAppManagerDelegate 276 277- (void)reactAppManagerIsReadyForLoad:(EXReactAppManager *)appManager 278{ 279 UIView *reactView = appManager.rootView; 280 reactView.frame = CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height); 281 reactView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; 282 283 [self _setRootViewBackgroundColor:reactView]; 284 285 [_contentView removeFromSuperview]; 286 _contentView = reactView; 287 [self.view addSubview:_contentView]; 288 [self.view sendSubviewToBack:_contentView]; 289 290 [reactView becomeFirstResponder]; 291} 292 293- (void)reactAppManagerStartedLoadingJavaScript:(EXReactAppManager *)appManager 294{ 295 EXAssertMainThread(); 296 self.isLoading = YES; 297} 298 299- (void)reactAppManagerFinishedLoadingJavaScript:(EXReactAppManager *)appManager 300{ 301 EXAssertMainThread(); 302 self.isLoading = NO; 303 if ([EXKernel sharedInstance].browserController) { 304 [[EXKernel sharedInstance].browserController appDidFinishLoadingSuccessfully:_appRecord]; 305 } 306} 307 308- (void)reactAppManager:(EXReactAppManager *)appManager failedToLoadJavaScriptWithError:(NSError *)error 309{ 310 EXAssertMainThread(); 311 [self maybeShowError:error]; 312} 313 314- (void)reactAppManagerDidInvalidate:(EXReactAppManager *)appManager 315{ 316 317} 318 319- (void)errorViewDidSelectRetry:(EXErrorView *)errorView 320{ 321 [self refresh]; 322} 323 324#pragma mark - orientation 325 326- (UIInterfaceOrientationMask)supportedInterfaceOrientations 327{ 328#if __has_include(<EXScreenOrientation/EXScreenOrientationRegistry.h>) 329 EXScreenOrientationRegistry *screenOrientationRegistry = (EXScreenOrientationRegistry *)[UMModuleRegistryProvider getSingletonModuleForClass:[EXScreenOrientationRegistry class]]; 330 if (screenOrientationRegistry && [screenOrientationRegistry requiredOrientationMask] > 0) { 331 return [screenOrientationRegistry requiredOrientationMask]; 332 } 333#endif 334 335 // TODO: Remove once sdk 37 is phased out 336 if (_supportedInterfaceOrientations != EX_INTERFACE_ORIENTATION_USE_MANIFEST) { 337 return _supportedInterfaceOrientations; 338 } 339 340 return [self orientationMaskFromManifestOrDefault]; 341} 342 343- (UIInterfaceOrientationMask)orientationMaskFromManifestOrDefault { 344 if (_appRecord.appLoader.manifest) { 345 NSString *orientationConfig = _appRecord.appLoader.manifest[@"orientation"]; 346 if ([orientationConfig isEqualToString:@"portrait"]) { 347 // lock to portrait 348 return UIInterfaceOrientationMaskPortrait; 349 } else if ([orientationConfig isEqualToString:@"landscape"]) { 350 // lock to landscape 351 return UIInterfaceOrientationMaskLandscape; 352 } 353 } 354 // no config or default value: allow autorotation 355 return UIInterfaceOrientationMaskAllButUpsideDown; 356} 357 358// TODO: Remove once sdk 37 is phased out 359- (void)setSupportedInterfaceOrientations:(UIInterfaceOrientationMask)supportedInterfaceOrientations 360{ 361 _supportedInterfaceOrientations = supportedInterfaceOrientations; 362 [self _enforceDesiredDeviceOrientation]; 363} 364 365- (void)traitCollectionDidChange:(nullable UITraitCollection *)previousTraitCollection { 366 [super traitCollectionDidChange:previousTraitCollection]; 367 if ((self.traitCollection.verticalSizeClass != previousTraitCollection.verticalSizeClass) 368 || (self.traitCollection.horizontalSizeClass != previousTraitCollection.horizontalSizeClass)) { 369 370 #if __has_include(<EXScreenOrientation/EXScreenOrientationRegistry.h>) 371 EXScreenOrientationRegistry *screenOrientationRegistryController = (EXScreenOrientationRegistry *)[UMModuleRegistryProvider getSingletonModuleForClass:[EXScreenOrientationRegistry class]]; 372 [screenOrientationRegistryController traitCollectionDidChangeTo:self.traitCollection]; 373 #endif 374 375 // TODO: Remove once sdk 37 is phased out 376 [[EXKernel sharedInstance].serviceRegistry.screenOrientationManager handleScreenOrientationChange:self.traitCollection]; 377 } 378} 379 380// TODO: Remove once sdk 37 is phased out 381- (void)_enforceDesiredDeviceOrientation 382{ 383 RCTAssertMainQueue(); 384 UIInterfaceOrientationMask mask = [self supportedInterfaceOrientations]; 385 UIDeviceOrientation currentOrientation = [[UIDevice currentDevice] orientation]; 386 UIInterfaceOrientation newOrientation = UIInterfaceOrientationUnknown; 387 switch (mask) { 388 case UIInterfaceOrientationMaskPortrait | UIInterfaceOrientationMaskPortraitUpsideDown: 389 if (!UIDeviceOrientationIsPortrait(currentOrientation)) { 390 newOrientation = UIInterfaceOrientationPortrait; 391 } 392 break; 393 case UIInterfaceOrientationMaskPortrait: 394 newOrientation = UIInterfaceOrientationPortrait; 395 break; 396 case UIInterfaceOrientationMaskPortraitUpsideDown: 397 newOrientation = UIInterfaceOrientationPortraitUpsideDown; 398 break; 399 case UIInterfaceOrientationMaskLandscape: 400 if (!UIDeviceOrientationIsLandscape(currentOrientation)) { 401 newOrientation = UIInterfaceOrientationLandscapeLeft; 402 } 403 break; 404 case UIInterfaceOrientationMaskLandscapeLeft: 405 newOrientation = UIInterfaceOrientationLandscapeLeft; 406 break; 407 case UIInterfaceOrientationMaskLandscapeRight: 408 newOrientation = UIInterfaceOrientationLandscapeRight; 409 break; 410 case UIInterfaceOrientationMaskAllButUpsideDown: 411 if (currentOrientation == UIDeviceOrientationFaceDown) { 412 newOrientation = UIInterfaceOrientationPortrait; 413 } 414 break; 415 default: 416 break; 417 } 418 if (newOrientation != UIInterfaceOrientationUnknown) { 419 [[UIDevice currentDevice] setValue:@(newOrientation) forKey:@"orientation"]; 420 } 421 [UIViewController attemptRotationToDeviceOrientation]; 422} 423 424#pragma mark - user interface style 425 426- (void)_overrideUserInterfaceStyle 427{ 428 if (@available(iOS 13.0, *)) { 429 NSString *userInterfaceStyle = [self _readUserInterfaceStyleFromManifest:_appRecord.appLoader.manifest]; 430 self.overrideUserInterfaceStyle = [self _userInterfaceStyleForString:userInterfaceStyle]; 431 } 432} 433 434- (NSString * _Nullable)_readUserInterfaceStyleFromManifest:(NSDictionary *)manifest 435{ 436 if (manifest[@"ios"] && manifest[@"ios"][@"userInterfaceStyle"]) { 437 return manifest[@"ios"][@"userInterfaceStyle"]; 438 } 439 return manifest[@"userInterfaceStyle"]; 440} 441 442- (UIUserInterfaceStyle)_userInterfaceStyleForString:(NSString *)userInterfaceStyleString API_AVAILABLE(ios(12.0)) { 443 if ([userInterfaceStyleString isEqualToString:@"dark"]) { 444 return UIUserInterfaceStyleDark; 445 } 446 if ([userInterfaceStyleString isEqualToString:@"automatic"]) { 447 return UIUserInterfaceStyleUnspecified; 448 } 449 return UIUserInterfaceStyleLight; 450} 451 452#pragma mark - root view background color 453 454- (void)_setRootViewBackgroundColor:(UIView *)view 455{ 456 NSString *backgroundColorString = [self _readBackgroundColorFromManifest:_appRecord.appLoader.manifest]; 457 UIColor *backgroundColor = [EXUtil colorWithHexString:backgroundColorString]; 458 459 if (backgroundColor) { 460 view.backgroundColor = backgroundColor; 461 } else { 462 view.backgroundColor = [UIColor whiteColor]; 463 464 // NOTE(brentvatne): we may want to default to respecting the default system background color 465 // on iOS13 and higher, but if we do make this choice then we will have to implement it on Android 466 // as well. This would also be a breaking change. Leaaving this here as a placeholder for the future. 467 // if (@available(iOS 13.0, *)) { 468 // view.backgroundColor = [UIColor systemBackgroundColor]; 469 // } else { 470 // view.backgroundColor = [UIColor whiteColor]; 471 // } 472 } 473} 474 475- (NSString * _Nullable)_readBackgroundColorFromManifest:(NSDictionary *)manifest 476{ 477 if (manifest[@"ios"] && manifest[@"ios"][@"backgroundColor"]) { 478 return manifest[@"ios"][@"backgroundColor"]; 479 } 480 return manifest[@"backgroundColor"]; 481} 482 483 484#pragma mark - Internal 485 486- (void)_showErrorWithType:(EXFatalErrorType)type error:(nullable NSError *)error 487{ 488 EXAssertMainThread(); 489 _dtmLastFatalErrorShown = [NSDate date]; 490 if (_errorView && _contentView == _errorView) { 491 // already showing, just update 492 _errorView.type = type; 493 _errorView.error = error; 494 } { 495 [_contentView removeFromSuperview]; 496 if (!_errorView) { 497 _errorView = [[EXErrorView alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height)]; 498 _errorView.delegate = self; 499 _errorView.appRecord = _appRecord; 500 } 501 _errorView.type = type; 502 _errorView.error = error; 503 _contentView = _errorView; 504 [self.view addSubview:_contentView]; 505 [[EXAnalytics sharedInstance] logErrorVisibleEvent]; 506 } 507} 508 509- (void)setIsLoading:(BOOL)isLoading 510{ 511 if ([_appRecord.appManager enablesDeveloperTools] && _dtmLastFatalErrorShown) { 512 if ([_dtmLastFatalErrorShown timeIntervalSinceNow] >= -kEXDevelopmentErrorCoolDownSeconds) { 513 // we just showed a fatal error very recently, do not begin loading. 514 // this can happen in some cases where react native sends the 'started loading' notif 515 // in spite of a packager error. 516 return; 517 } 518 } 519 _isLoading = isLoading; 520 dispatch_async(dispatch_get_main_queue(), ^{ 521 if (isLoading) { 522 self.loadingView.hidden = NO; 523 [self.view bringSubviewToFront:self.loadingView]; 524 } else { 525 self.loadingView.hidden = YES; 526 } 527 }); 528} 529 530#pragma mark - error recovery 531 532- (BOOL)_willAutoRecoverFromError:(NSError *)error 533{ 534 if (![_appRecord.appManager enablesDeveloperTools]) { 535 BOOL shouldRecover = [[EXKernel sharedInstance].serviceRegistry.errorRecoveryManager experienceIdShouldReloadOnError:_appRecord.experienceId]; 536 if (shouldRecover) { 537 [self _invalidateRecoveryTimer]; 538 _tmrAutoReloadDebounce = [NSTimer scheduledTimerWithTimeInterval:kEXAutoReloadDebounceSeconds 539 target:self 540 selector:@selector(refresh) 541 userInfo:nil 542 repeats:NO]; 543 } 544 return shouldRecover; 545 } 546 return NO; 547} 548 549- (void)_invalidateRecoveryTimer 550{ 551 if (_tmrAutoReloadDebounce) { 552 [_tmrAutoReloadDebounce invalidate]; 553 _tmrAutoReloadDebounce = nil; 554 } 555} 556 557@end 558 559NS_ASSUME_NONNULL_END 560