// Copyright © 2019 650 Industries. All rights reserved. #import #import #import #import #import #import #import NS_ASSUME_NONNULL_BEGIN static NSString * const EXUpdatesAppControllerErrorDomain = @"EXUpdatesAppController"; static NSString * const EXUpdatesConfigPlistName = @"Expo"; static NSString * const EXUpdatesUpdateAvailableEventName = @"updateAvailable"; static NSString * const EXUpdatesNoUpdateAvailableEventName = @"noUpdateAvailable"; static NSString * const EXUpdatesErrorEventName = @"error"; @interface EXUpdatesAppController () @property (nonatomic, readwrite, strong) EXUpdatesConfig *config; @property (nonatomic, readwrite, strong) id launcher; @property (nonatomic, readwrite, strong) EXUpdatesDatabase *database; @property (nonatomic, readwrite, strong) id selectionPolicy; @property (nonatomic, readwrite, strong) dispatch_queue_t assetFilesQueue; @property (nonatomic, readwrite, strong) NSURL *updatesDirectory; @property (nonatomic, strong) id candidateLauncher; @property (nonatomic, assign) BOOL hasLaunched; @property (nonatomic, strong) dispatch_queue_t controllerQueue; @property (nonatomic, assign) BOOL isStarted; @property (nonatomic, assign) BOOL isEmergencyLaunch; @end @implementation EXUpdatesAppController + (instancetype)sharedInstance { static EXUpdatesAppController *theController; static dispatch_once_t once; dispatch_once(&once, ^{ if (!theController) { theController = [[EXUpdatesAppController alloc] init]; } }); return theController; } - (instancetype)init { if (self = [super init]) { _config = [self _loadConfigFromExpoPlist]; _database = [[EXUpdatesDatabase alloc] init]; _selectionPolicy = [[EXUpdatesSelectionPolicyNewest alloc] initWithRuntimeVersion:[EXUpdatesUtils getRuntimeVersionWithConfig:_config]]; _assetFilesQueue = dispatch_queue_create("expo.controller.AssetFilesQueue", DISPATCH_QUEUE_SERIAL); _controllerQueue = dispatch_queue_create("expo.controller.ControllerQueue", DISPATCH_QUEUE_SERIAL); _isStarted = NO; } return self; } - (void)setConfiguration:(NSDictionary *)configuration { if (_updatesDirectory) { @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"EXUpdatesAppController:setConfiguration should not be called after start" userInfo:@{}]; } [_config loadConfigFromDictionary:configuration]; _selectionPolicy = [[EXUpdatesSelectionPolicyNewest alloc] initWithRuntimeVersion:[EXUpdatesUtils getRuntimeVersionWithConfig:_config]]; } - (void)start { NSAssert(!_updatesDirectory, @"EXUpdatesAppController:start should only be called once per instance"); if (!_config.isEnabled) { EXUpdatesAppLauncherNoDatabase *launcher = [[EXUpdatesAppLauncherNoDatabase alloc] init]; _launcher = launcher; [launcher launchUpdateWithConfig:_config]; if (_delegate) { [_delegate appController:self didStartWithSuccess:self.launchAssetUrl != nil]; } return; } if (!_config.updateUrl) { @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"expo-updates is enabled, but no valid URL is configured under EXUpdatesURL. If you are making a release build for the first time, make sure you have run `expo publish` at least once." userInfo:@{}]; } _isStarted = YES; NSError *fsError; _updatesDirectory = [EXUpdatesUtils initializeUpdatesDirectoryWithError:&fsError]; if (fsError) { [self _emergencyLaunchWithFatalError:fsError]; return; } __block BOOL dbSuccess; __block NSError *dbError; dispatch_semaphore_t dbSemaphore = dispatch_semaphore_create(0); dispatch_async(_database.databaseQueue, ^{ dbSuccess = [self->_database openDatabaseInDirectory:self->_updatesDirectory withError:&dbError]; dispatch_semaphore_signal(dbSemaphore); }); dispatch_semaphore_wait(dbSemaphore, DISPATCH_TIME_FOREVER); if (!dbSuccess) { [self _emergencyLaunchWithFatalError:dbError]; return; } EXUpdatesAppLoaderTask *loaderTask = [[EXUpdatesAppLoaderTask alloc] initWithConfig:_config database:_database directory:_updatesDirectory selectionPolicy:_selectionPolicy delegateQueue:_controllerQueue]; loaderTask.delegate = self; [loaderTask start]; } - (void)startAndShowLaunchScreen:(UIWindow *)window { NSBundle *mainBundle = [NSBundle mainBundle]; UIViewController *rootViewController = [UIViewController new]; NSString *launchScreen = (NSString *)[mainBundle objectForInfoDictionaryKey:@"UILaunchStoryboardName"] ?: @"LaunchScreen"; if ([mainBundle pathForResource:launchScreen ofType:@"nib"] != nil) { NSArray *views = [mainBundle loadNibNamed:launchScreen owner:self options:nil]; rootViewController.view = views.firstObject; rootViewController.view.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; } else if ([mainBundle pathForResource:launchScreen ofType:@"storyboard"] != nil || [mainBundle pathForResource:launchScreen ofType:@"storyboardc"] != nil) { UIStoryboard *launchScreenStoryboard = [UIStoryboard storyboardWithName:launchScreen bundle:nil]; rootViewController = [launchScreenStoryboard instantiateInitialViewController]; } else { NSLog(@"Launch screen could not be loaded from a .xib or .storyboard. Unexpected loading behavior may occur."); UIView *view = [UIView new]; view.backgroundColor = [UIColor whiteColor]; rootViewController.view = view; } window.rootViewController = rootViewController; [window makeKeyAndVisible]; [self start]; } - (void)requestRelaunchWithCompletion:(EXUpdatesAppRelaunchCompletionBlock)completion { if (_bridge) { EXUpdatesAppLauncherWithDatabase *launcher = [[EXUpdatesAppLauncherWithDatabase alloc] initWithConfig:_config database:_database directory:_updatesDirectory completionQueue:_controllerQueue]; _candidateLauncher = launcher; [launcher launchUpdateWithSelectionPolicy:self->_selectionPolicy completion:^(NSError * _Nullable error, BOOL success) { if (success) { self->_launcher = self->_candidateLauncher; completion(YES); [self->_bridge reload]; [self _runReaper]; } else { NSLog(@"Failed to relaunch: %@", error.localizedDescription); completion(NO); } }]; } else { NSLog(@"EXUpdatesAppController: Failed to reload because bridge was nil. Did you set the bridge property on the controller singleton?"); completion(NO); } } - (nullable EXUpdatesUpdate *)launchedUpdate { return _launcher.launchedUpdate ?: nil; } - (nullable NSURL *)launchAssetUrl { return _launcher.launchAssetUrl ?: nil; } - (nullable NSDictionary *)assetFilesMap { return _launcher.assetFilesMap ?: nil; } - (BOOL)isUsingEmbeddedAssets { if (!_launcher) { return YES; } return _launcher.isUsingEmbeddedAssets; } # pragma mark - EXUpdatesAppLoaderTaskDelegate - (BOOL)appLoaderTask:(EXUpdatesAppLoaderTask *)appLoaderTask didLoadCachedUpdate:(nonnull EXUpdatesUpdate *)update { return YES; } - (void)appLoaderTask:(EXUpdatesAppLoaderTask *)appLoaderTask didStartLoadingUpdate:(EXUpdatesUpdate *)update { // do nothing here for now } - (void)appLoaderTask:(EXUpdatesAppLoaderTask *)appLoaderTask didFinishWithLauncher:(id)launcher isUpToDate:(BOOL)isUpToDate { _launcher = launcher; if (self->_delegate) { [EXUpdatesUtils runBlockOnMainThread:^{ [self->_delegate appController:self didStartWithSuccess:YES]; }]; } } - (void)appLoaderTask:(EXUpdatesAppLoaderTask *)appLoaderTask didFinishWithError:(NSError *)error { [self _emergencyLaunchWithFatalError:error]; } - (void)appLoaderTask:(EXUpdatesAppLoaderTask *)appLoaderTask didFinishBackgroundUpdateWithStatus:(EXUpdatesBackgroundUpdateStatus)status update:(nullable EXUpdatesUpdate *)update error:(nullable NSError *)error { if (status == EXUpdatesBackgroundUpdateStatusError) { NSAssert(error != nil, @"Background update with error status must have a nonnull error object"); [EXUpdatesUtils sendEventToBridge:_bridge withType:EXUpdatesErrorEventName body:@{@"message": error.localizedDescription}]; } else if (status == EXUpdatesBackgroundUpdateStatusUpdateAvailable) { NSAssert(update != nil, @"Background update with error status must have a nonnull update object"); [EXUpdatesUtils sendEventToBridge:_bridge withType:EXUpdatesUpdateAvailableEventName body:@{@"manifest": update.rawManifest}]; } else if (status == EXUpdatesBackgroundUpdateStatusNoUpdateAvailable) { [EXUpdatesUtils sendEventToBridge:_bridge withType:EXUpdatesNoUpdateAvailableEventName body:@{}]; } } # pragma mark - internal - (EXUpdatesConfig *)_loadConfigFromExpoPlist { NSString *configPath = [[NSBundle mainBundle] pathForResource:EXUpdatesConfigPlistName ofType:@"plist"]; if (!configPath) { @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"Cannot load configuration from Expo.plist. Please ensure you've followed the setup and installation instructions for expo-updates to create Expo.plist and add it to your Xcode project." userInfo:@{}]; } return [EXUpdatesConfig configWithDictionary:[NSDictionary dictionaryWithContentsOfFile:configPath]]; } - (void)_runReaper { if (_launcher.launchedUpdate) { [EXUpdatesReaper reapUnusedUpdatesWithConfig:_config database:_database directory:_updatesDirectory selectionPolicy:_selectionPolicy launchedUpdate:_launcher.launchedUpdate]; } } - (void)_emergencyLaunchWithFatalError:(NSError *)error { _isEmergencyLaunch = YES; EXUpdatesAppLauncherNoDatabase *launcher = [[EXUpdatesAppLauncherNoDatabase alloc] init]; _launcher = launcher; [launcher launchUpdateWithConfig:_config fatalError:error]; if (_delegate) { [EXUpdatesUtils runBlockOnMainThread:^{ [self->_delegate appController:self didStartWithSuccess:self.launchAssetUrl != nil]; }]; } } @end NS_ASSUME_NONNULL_END