1// Copyright 2015-present 650 Industries. All rights reserved. 2 3@import UIKit; 4 5#import "EXAnalytics.h" 6#import "EXAppLoadingView.h" 7#import "EXErrorRecoveryManager.h" 8#import "EXFileDownloader.h" 9#import "EXAppViewController.h" 10#import "EXReactAppManager.h" 11#import "EXErrorView.h" 12#import "EXKernel.h" 13#import "EXKernelAppLoader.h" 14#import "EXKernelUtil.h" 15#import "EXScreenOrientationManager.h" 16#import "EXShellManager.h" 17 18#import <React/RCTUtils.h> 19 20#define EX_INTERFACE_ORIENTATION_USE_MANIFEST 0 21 22const CGFloat kEXAutoReloadDebounceSeconds = 0.1; 23 24NS_ASSUME_NONNULL_BEGIN 25 26@interface EXAppViewController () <EXReactAppManagerUIDelegate, EXKernelAppLoaderDelegate, EXErrorViewDelegate> 27 28@property (nonatomic, assign) BOOL isLoading; 29@property (nonatomic, weak) EXKernelAppRecord *appRecord; 30@property (nonatomic, strong) EXAppLoadingView *loadingView; 31@property (nonatomic, strong) EXErrorView *errorView; 32@property (nonatomic, assign) UIInterfaceOrientationMask supportedInterfaceOrientations; // override super 33@property (nonatomic, strong) NSTimer *tmrAutoReloadDebounce; 34 35@end 36 37@implementation EXAppViewController 38 39@synthesize supportedInterfaceOrientations = _supportedInterfaceOrientations; 40 41#pragma mark - Lifecycle 42 43- (instancetype)initWithAppRecord:(EXKernelAppRecord *)record 44{ 45 if (self = [super init]) { 46 _appRecord = record; 47 _supportedInterfaceOrientations = EX_INTERFACE_ORIENTATION_USE_MANIFEST; 48 } 49 return self; 50} 51 52- (void)dealloc 53{ 54 [self _invalidateRecoveryTimer]; 55 [[NSNotificationCenter defaultCenter] removeObserver:self]; 56} 57 58- (void)viewDidLoad 59{ 60 [super viewDidLoad]; 61 _loadingView = [[EXAppLoadingView alloc] initWithAppRecord:_appRecord]; 62 [self.view addSubview:_loadingView]; 63 _appRecord.appManager.delegate = self; 64 self.isLoading = YES; 65} 66 67- (void)viewDidAppear:(BOOL)animated 68{ 69 [super viewDidAppear:animated]; 70 if (_appRecord && _appRecord.status == kEXKernelAppRecordStatusNew) { 71 _appRecord.appLoader.delegate = self; 72 [self refresh]; 73 } 74} 75 76- (BOOL)shouldAutorotate 77{ 78 return YES; 79} 80 81- (void)viewWillLayoutSubviews 82{ 83 [super viewWillLayoutSubviews]; 84 if (_loadingView) { 85 _loadingView.frame = CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height); 86 } 87 if (_contentView) { 88 _contentView.frame = CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height); 89 } 90} 91 92#pragma mark - Public 93 94- (void)maybeShowError:(NSError *)error 95{ 96 self.isLoading = NO; 97 if ([self _willAutoRecoverFromError:error]) { 98 return; 99 } 100 BOOL isNetworkError = ([error.domain isEqualToString:(NSString *)kCFErrorDomainCFNetwork] || 101 [error.domain isEqualToString:EXNetworkErrorDomain]); 102 if (isNetworkError) { 103 // show a human-readable reachability error 104 dispatch_async(dispatch_get_main_queue(), ^{ 105 [self _showErrorWithType:kEXFatalErrorTypeLoading error:error]; 106 }); 107 } else if ([error.domain isEqualToString:@"JSServer"] && [_appRecord.appManager enablesDeveloperTools]) { 108 // RCTRedBox already handled this 109 } else if ([error.domain rangeOfString:RCTErrorDomain].length > 0 && [_appRecord.appManager enablesDeveloperTools]) { 110 // RCTRedBox already handled this 111 } else { 112 dispatch_async(dispatch_get_main_queue(), ^{ 113 [self _showErrorWithType:kEXFatalErrorTypeException error:error]; 114 }); 115 } 116} 117 118- (void)_rebuildBridge 119{ 120 [self _invalidateRecoveryTimer]; 121 [[EXKernel sharedInstance] logAnalyticsEvent:@"LOAD_EXPERIENCE" forAppRecord:_appRecord]; 122 [_appRecord.appManager rebuildBridge]; 123} 124 125- (void)refresh 126{ 127 self.isLoading = YES; 128 [self _invalidateRecoveryTimer]; 129 [_appRecord.appLoader request]; 130} 131 132- (void)appStateDidBecomeActive 133{ 134 dispatch_async(dispatch_get_main_queue(), ^{ 135 [self _enforceDesiredDeviceOrientation]; 136 }); 137 [_appRecord.appManager appStateDidBecomeActive]; 138} 139 140- (void)appStateDidBecomeInactive 141{ 142 [_appRecord.appManager appStateDidBecomeInactive]; 143} 144 145#pragma mark - EXKernelAppLoaderDelegate 146 147- (void)appLoader:(EXKernelAppLoader *)appLoader didLoadOptimisticManifest:(NSDictionary *)manifest 148{ 149 if ([EXKernel sharedInstance].browserController) { 150 [[EXKernel sharedInstance].browserController addHistoryItemWithUrl:appLoader.manifestUrl manifest:manifest]; 151 } 152 dispatch_async(dispatch_get_main_queue(), ^{ 153 _loadingView.manifest = manifest; 154 [self _enforceDesiredDeviceOrientation]; 155 [self _rebuildBridge]; 156 }); 157} 158 159- (void)appLoader:(EXKernelAppLoader *)appLoader didLoadBundleWithProgress:(EXLoadingProgress *)progress 160{ 161 dispatch_async(dispatch_get_main_queue(), ^{ 162 [_loadingView updateStatusWithProgress:progress]; 163 }); 164} 165 166- (void)appLoader:(EXKernelAppLoader *)appLoader didFinishLoadingManifest:(NSDictionary *)manifest bundle:(NSData *)data 167{ 168 if (_appRecord.appManager.status == kEXReactAppManagerStatusBridgeLoading) { 169 [_appRecord.appManager appLoaderFinished]; 170 } 171} 172 173- (void)appLoader:(EXKernelAppLoader *)appLoader didFailWithError:(NSError *)error 174{ 175 if (_appRecord.appManager.status == kEXReactAppManagerStatusBridgeLoading) { 176 [_appRecord.appManager appLoaderFailedWithError:error]; 177 } 178 [self maybeShowError:error]; 179} 180 181#pragma mark - EXReactAppManagerDelegate 182 183- (void)reactAppManagerIsReadyForLoad:(EXReactAppManager *)appManager 184{ 185 UIView *reactView = appManager.rootView; 186 reactView.frame = CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height); 187 reactView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; 188 reactView.backgroundColor = [UIColor clearColor]; 189 190 [_contentView removeFromSuperview]; 191 _contentView = reactView; 192 [self.view addSubview:_contentView]; 193 [self.view sendSubviewToBack:_contentView]; 194 195 [reactView becomeFirstResponder]; 196} 197 198- (void)reactAppManagerStartedLoadingJavaScript:(EXReactAppManager *)appManager 199{ 200 EXAssertMainThread(); 201 self.isLoading = YES; 202} 203 204- (void)reactAppManagerFinishedLoadingJavaScript:(EXReactAppManager *)appManager 205{ 206 EXAssertMainThread(); 207 self.isLoading = NO; 208 if ([EXKernel sharedInstance].browserController) { 209 [[EXKernel sharedInstance].browserController appDidFinishLoadingSuccessfully:_appRecord]; 210 } 211} 212 213- (void)reactAppManager:(EXReactAppManager *)appManager failedToLoadJavaScriptWithError:(NSError *)error 214{ 215 EXAssertMainThread(); 216 [self maybeShowError:error]; 217} 218 219- (void)reactAppManagerDidInvalidate:(EXReactAppManager *)appManager 220{ 221 222} 223 224- (void)errorViewDidSelectRetry:(EXErrorView *)errorView 225{ 226 [self refresh]; 227} 228 229#pragma mark - orientation 230 231- (UIInterfaceOrientationMask)supportedInterfaceOrientations 232{ 233 if (_supportedInterfaceOrientations != EX_INTERFACE_ORIENTATION_USE_MANIFEST) { 234 return _supportedInterfaceOrientations; 235 } 236 if (_appRecord.appLoader.manifest) { 237 NSString *orientationConfig = _appRecord.appLoader.manifest[@"orientation"]; 238 if ([orientationConfig isEqualToString:@"portrait"]) { 239 // lock to portrait 240 return UIInterfaceOrientationMaskPortrait; 241 } else if ([orientationConfig isEqualToString:@"landscape"]) { 242 // lock to landscape 243 return UIInterfaceOrientationMaskLandscape; 244 } 245 } 246 // no config or default value: allow autorotation 247 return UIInterfaceOrientationMaskAllButUpsideDown; 248} 249 250- (void)setSupportedInterfaceOrientations:(UIInterfaceOrientationMask)supportedInterfaceOrientations 251{ 252 _supportedInterfaceOrientations = supportedInterfaceOrientations; 253 [self _enforceDesiredDeviceOrientation]; 254} 255 256- (void)_enforceDesiredDeviceOrientation 257{ 258 RCTAssertMainQueue(); 259 UIInterfaceOrientationMask mask = [self supportedInterfaceOrientations]; 260 UIDeviceOrientation currentOrientation = [[UIDevice currentDevice] orientation]; 261 UIInterfaceOrientation newOrientation = UIInterfaceOrientationUnknown; 262 switch (mask) { 263 case UIInterfaceOrientationMaskPortrait: 264 if (!UIDeviceOrientationIsPortrait(currentOrientation)) { 265 newOrientation = UIInterfaceOrientationPortrait; 266 } 267 break; 268 case UIInterfaceOrientationMaskPortraitUpsideDown: 269 newOrientation = UIInterfaceOrientationPortraitUpsideDown; 270 break; 271 case UIInterfaceOrientationMaskLandscape: 272 if (!UIDeviceOrientationIsLandscape(currentOrientation)) { 273 newOrientation = UIInterfaceOrientationLandscapeLeft; 274 } 275 break; 276 case UIInterfaceOrientationMaskLandscapeLeft: 277 newOrientation = UIInterfaceOrientationLandscapeLeft; 278 break; 279 case UIInterfaceOrientationMaskLandscapeRight: 280 newOrientation = UIInterfaceOrientationLandscapeRight; 281 break; 282 case UIInterfaceOrientationMaskAllButUpsideDown: 283 if (currentOrientation == UIDeviceOrientationFaceDown) { 284 newOrientation = UIInterfaceOrientationPortrait; 285 } 286 break; 287 default: 288 break; 289 } 290 if (newOrientation != UIInterfaceOrientationUnknown) { 291 [[UIDevice currentDevice] setValue:@(newOrientation) forKey:@"orientation"]; 292 } 293 [UIViewController attemptRotationToDeviceOrientation]; 294} 295 296#pragma mark - Internal 297 298- (void)_showErrorWithType:(EXFatalErrorType)type error:(nullable NSError *)error 299{ 300 EXAssertMainThread(); 301 if (_errorView && _contentView == _errorView) { 302 // already showing, just update 303 _errorView.type = type; 304 _errorView.error = error; 305 } { 306 [_contentView removeFromSuperview]; 307 if (!_errorView) { 308 _errorView = [[EXErrorView alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height)]; 309 _errorView.delegate = self; 310 _errorView.appRecord = _appRecord; 311 } 312 _errorView.type = type; 313 _errorView.error = error; 314 _contentView = _errorView; 315 [self.view addSubview:_contentView]; 316 [[EXAnalytics sharedInstance] logErrorVisibleEvent]; 317 } 318} 319 320- (void)setIsLoading:(BOOL)isLoading 321{ 322 _isLoading = isLoading; 323 dispatch_async(dispatch_get_main_queue(), ^{ 324 if (isLoading) { 325 self.loadingView.hidden = NO; 326 [self.view bringSubviewToFront:self.loadingView]; 327 } else { 328 self.loadingView.hidden = YES; 329 } 330 }); 331} 332 333#pragma mark - error recovery 334 335- (BOOL)_willAutoRecoverFromError:(NSError *)error 336{ 337 if (![_appRecord.appManager enablesDeveloperTools]) { 338 BOOL shouldRecover = [[EXKernel sharedInstance].serviceRegistry.errorRecoveryManager experienceIdShouldReloadOnError:_appRecord.experienceId]; 339 if (shouldRecover) { 340 [self _invalidateRecoveryTimer]; 341 _tmrAutoReloadDebounce = [NSTimer scheduledTimerWithTimeInterval:kEXAutoReloadDebounceSeconds 342 target:self 343 selector:@selector(refresh) 344 userInfo:nil 345 repeats:NO]; 346 } 347 return shouldRecover; 348 } 349 return NO; 350} 351 352- (void)_invalidateRecoveryTimer 353{ 354 if (_tmrAutoReloadDebounce) { 355 [_tmrAutoReloadDebounce invalidate]; 356 _tmrAutoReloadDebounce = nil; 357 } 358} 359 360@end 361 362NS_ASSUME_NONNULL_END 363