1// Copyright 2015-present 650 Industries. All rights reserved. 2 3#import "EXAppState.h" 4#import "EXAppViewController.h" 5#import "EXBuildConstants.h" 6#import "EXKernel.h" 7#import "EXAbstractLoader.h" 8#import "EXKernelAppRecord.h" 9#import "EXKernelLinkingManager.h" 10#import "EXLinkingManager.h" 11#import "EXVersions.h" 12#import "EXHomeModule.h" 13 14#import <EXConstants/EXConstantsService.h> 15#import <React/RCTBridge+Private.h> 16#import <React/RCTEventDispatcher.h> 17#import <React/RCTModuleData.h> 18#import <React/RCTUtils.h> 19 20#ifndef EX_DETACHED 21// Kernel is DevMenu's delegate only in non-detached builds. 22#import "EXDevMenuManager.h" 23#import "EXDevMenuDelegateProtocol.h" 24 25@interface EXKernel () <EXDevMenuDelegateProtocol> 26@end 27#endif 28 29NS_ASSUME_NONNULL_BEGIN 30 31NSString *kEXKernelErrorDomain = @"EXKernelErrorDomain"; 32NSString *kEXKernelShouldForegroundTaskEvent = @"foregroundTask"; 33NSString * const kEXKernelClearJSCacheUserDefaultsKey = @"EXKernelClearJSCacheUserDefaultsKey"; 34NSString * const kEXReloadActiveAppRequest = @"EXReloadActiveAppRequest"; 35 36@interface EXKernel () <EXKernelAppRegistryDelegate> 37 38@end 39 40// Protocol that should be implemented by all versions of EXAppState class. 41@protocol EXAppStateProtocol 42 43@property (nonatomic, strong, readonly) NSString *lastKnownState; 44 45- (void)setState:(NSString *)state; 46 47@end 48 49@implementation EXKernel 50 51+ (instancetype)sharedInstance 52{ 53 static EXKernel *theKernel; 54 static dispatch_once_t once; 55 dispatch_once(&once, ^{ 56 if (!theKernel) { 57 theKernel = [[EXKernel alloc] init]; 58 } 59 }); 60 return theKernel; 61} 62 63- (instancetype)init 64{ 65 if (self = [super init]) { 66 // init app registry: keep track of RN bridges we are running 67 _appRegistry = [[EXKernelAppRegistry alloc] init]; 68 _appRegistry.delegate = self; 69 70 // init service registry: classes which manage shared resources among all bridges 71 _serviceRegistry = [[EXKernelServiceRegistry alloc] init]; 72 73#ifndef EX_DETACHED 74 // Set the delegate of dev menu manager. Maybe it should be a separate class? Will see later once the delegate protocol gets too big. 75 [[EXDevMenuManager sharedInstance] setDelegate:self]; 76#endif 77 78 // register for notifications to request reloading the visible app 79 [[NSNotificationCenter defaultCenter] addObserver:self 80 selector:@selector(_handleRequestReloadVisibleApp:) 81 name:kEXReloadActiveAppRequest 82 object:nil]; 83 84 for (NSString *name in @[UIApplicationDidBecomeActiveNotification, 85 UIApplicationDidEnterBackgroundNotification, 86 UIApplicationDidFinishLaunchingNotification, 87 UIApplicationWillResignActiveNotification, 88 UIApplicationWillEnterForegroundNotification]) { 89 90 [[NSNotificationCenter defaultCenter] addObserver:self 91 selector:@selector(_handleAppStateDidChange:) 92 name:name 93 object:nil]; 94 } 95 NSLog(@"Expo iOS Runtime Version %@", [EXBuildConstants sharedInstance].expoRuntimeVersion); 96 } 97 return self; 98} 99 100- (void)dealloc 101{ 102 [[NSNotificationCenter defaultCenter] removeObserver:self]; 103} 104 105#pragma mark - bridge registry delegate 106 107- (void)appRegistry:(EXKernelAppRegistry *)registry didRegisterAppRecord:(EXKernelAppRecord *)appRecord 108{ 109 // forward to service registry 110 [_serviceRegistry appRegistry:registry didRegisterAppRecord:appRecord]; 111} 112 113- (void)appRegistry:(EXKernelAppRegistry *)registry willUnregisterAppRecord:(EXKernelAppRecord *)appRecord 114{ 115 // forward to service registry 116 [_serviceRegistry appRegistry:registry willUnregisterAppRecord:appRecord]; 117} 118 119#pragma mark - Interfacing with JS 120 121- (void)sendUrl:(NSString *)urlString toAppRecord:(EXKernelAppRecord *)app 122{ 123 // fire a Linking url event on this (possibly versioned) bridge 124 EXReactAppManager *appManager = app.appManager; 125 id linkingModule = [self nativeModuleForAppManager:appManager named:@"LinkingManager"]; 126 if (!linkingModule) { 127 DDLogError(@"Could not find the Linking module to open URL (%@)", urlString); 128 } else if ([linkingModule respondsToSelector:@selector(dispatchOpenUrlEvent:)]) { 129 [linkingModule dispatchOpenUrlEvent:[NSURL URLWithString:urlString]]; 130 } else { 131 DDLogError(@"Linking module doesn't support the API we use to open URL (%@)", urlString); 132 } 133 [self _moveAppToVisible:app]; 134} 135 136- (id)nativeModuleForAppManager:(EXReactAppManager *)appManager named:(NSString *)moduleName 137{ 138 id destinationBridge = appManager.reactBridge; 139 140 if ([destinationBridge respondsToSelector:@selector(batchedBridge)]) { 141 id batchedBridge = [destinationBridge batchedBridge]; 142 id moduleData = [batchedBridge moduleDataForName:moduleName]; 143 144 if (moduleData) { 145 return [moduleData instance]; 146 } 147 } else { 148 // bridge can be null if the record is in an error state and never created a bridge. 149 if (destinationBridge) { 150 DDLogError(@"Bridge does not support the API we use to get its underlying batched bridge"); 151 } 152 } 153 return nil; 154} 155 156- (BOOL)sendNotification:(EXPendingNotification *)notification 157{ 158 EXKernelAppRecord *destinationApp = [_appRegistry standaloneAppRecord] ?: [_appRegistry newestRecordWithScopeKey:notification.scopeKey]; 159 160 // This allows home app record to receive notification events as well. 161 if (!destinationApp && [_appRegistry.homeAppRecord.scopeKey isEqualToString:notification.scopeKey]) { 162 destinationApp = _appRegistry.homeAppRecord; 163 } 164 165 if (destinationApp) { 166 // send the body to the already-open experience 167 BOOL success = [self _dispatchJSEvent:@"Exponent.notification" body:notification.properties toApp:destinationApp]; 168 [self _moveAppToVisible:destinationApp]; 169 return success; 170 } else { 171 // no app is currently running for this experience id. 172 // if we're Expo Go, we can query Home for a past experience in the user's history, and route the notification there. 173 if (_browserController) { 174 __weak typeof(self) weakSelf = self; 175 [_browserController getHistoryUrlForScopeKey:notification.scopeKey completion:^(NSString *urlString) { 176 if (urlString) { 177 NSURL *url = [NSURL URLWithString:urlString]; 178 if (url) { 179 [weakSelf createNewAppWithUrl:url initialProps:@{ @"notification": notification.properties }]; 180 } 181 } 182 }]; 183 // If we're here, there's no active app in appRegistry matching notification.experienceId 184 // and we are in Expo Go, since _browserController is not nil. 185 // If so, we can return YES (meaning "notification has been successfully dispatched") 186 // because we pass the notification as initialProps in completion handler 187 // of getHistoryUrlForExperienceId:. If urlString passed to completion handler is empty, 188 // the notification is forgotten (this is the expected behavior). 189 return YES; 190 } 191 } 192 193 return NO; 194} 195 196/** 197 * If the bridge has a batchedBridge or parentBridge selector, posts the notification on that object as well. 198 */ 199- (void)_postNotificationName: (NSNotificationName)name onAbstractBridge: (id)bridge 200{ 201 [[NSNotificationCenter defaultCenter] postNotificationName:name object:bridge]; 202 if ([bridge respondsToSelector:@selector(batchedBridge)]) { 203 [[NSNotificationCenter defaultCenter] postNotificationName:name object:[bridge batchedBridge]]; 204 } else if ([bridge respondsToSelector:@selector(parentBridge)]) { 205 [[NSNotificationCenter defaultCenter] postNotificationName:name object:[bridge parentBridge]]; 206 } 207} 208 209- (BOOL)_dispatchJSEvent:(NSString *)eventName body:(NSDictionary *)eventBody toApp:(EXKernelAppRecord *)appRecord 210{ 211 if (!appRecord.appManager.reactBridge) { 212 return NO; 213 } 214 [appRecord.appManager.reactBridge enqueueJSCall:@"RCTDeviceEventEmitter.emit" 215 args:eventBody ? @[eventName, eventBody] : @[eventName]]; 216 return YES; 217} 218 219#pragma mark - App props 220 221- (nullable NSDictionary *)initialAppPropsFromLaunchOptions:(NSDictionary *)launchOptions 222{ 223 return nil; 224} 225 226#pragma mark - App State 227 228- (EXKernelAppRecord *)createNewAppWithUrl:(NSURL *)url initialProps:(nullable NSDictionary *)initialProps 229{ 230 NSString *recordId = [_appRegistry registerAppWithManifestUrl:url initialProps:initialProps]; 231 EXKernelAppRecord *record = [_appRegistry recordForId:recordId]; 232 [self _moveAppToVisible:record]; 233 return record; 234} 235 236 237- (void)reloadVisibleApp 238{ 239 if (_browserController) { 240 [EXUtil performSynchronouslyOnMainThread:^{ 241 [self->_browserController reloadVisibleApp]; 242 }]; 243 } 244} 245 246- (void)switchTasks 247{ 248 if (!_browserController) { 249 return; 250 } 251 252 if (_visibleApp != _appRegistry.homeAppRecord) { 253#ifndef EX_DETACHED // Just to compile without access to EXDevMenuManager, we wouldn't get here either way because browser controller is unset in this case. 254 [EXUtil performSynchronouslyOnMainThread:^{ 255 [[EXDevMenuManager sharedInstance] toggle]; 256 }]; 257#endif 258 } else { 259 EXKernelAppRegistry *appRegistry = [EXKernel sharedInstance].appRegistry; 260 for (NSString *recordId in appRegistry.appEnumerator) { 261 EXKernelAppRecord *record = [appRegistry recordForId:recordId]; 262 // foreground the first thing we find 263 [self _moveAppToVisible:record]; 264 } 265 } 266} 267 268- (void)reloadAppWithScopeKey:(NSString *)scopeKey 269{ 270 EXKernelAppRecord *appRecord = [_appRegistry newestRecordWithScopeKey:scopeKey]; 271 if (_browserController) { 272 [self createNewAppWithUrl:appRecord.appLoader.manifestUrl initialProps:nil]; 273 } else if (_appRegistry.standaloneAppRecord && appRecord == _appRegistry.standaloneAppRecord) { 274 [appRecord.viewController refresh]; 275 } 276} 277 278- (void)reloadAppFromCacheWithScopeKey:(NSString *)scopeKey 279{ 280 EXKernelAppRecord *appRecord = [_appRegistry newestRecordWithScopeKey:scopeKey]; 281 [appRecord.viewController reloadFromCache]; 282} 283 284- (void)viewController:(__unused EXViewController *)vc didNavigateAppToVisible:(EXKernelAppRecord *)appRecord 285{ 286 EXKernelAppRecord *appRecordPreviouslyVisible = _visibleApp; 287 if (appRecord != appRecordPreviouslyVisible) { 288 if (appRecordPreviouslyVisible) { 289 [appRecordPreviouslyVisible.viewController appStateDidBecomeInactive]; 290 [self _postNotificationName:kEXKernelBridgeDidBackgroundNotification onAbstractBridge:appRecordPreviouslyVisible.appManager.reactBridge]; 291 id<EXAppStateProtocol> appStateModule = [self nativeModuleForAppManager:appRecordPreviouslyVisible.appManager named:@"AppState"]; 292 if (appStateModule != nil) { 293 [appStateModule setState:@"background"]; 294 } 295 } 296 if (appRecord) { 297 [appRecord.viewController appStateDidBecomeActive]; 298 [self _postNotificationName:kEXKernelBridgeDidForegroundNotification onAbstractBridge:appRecord.appManager.reactBridge]; 299 id<EXAppStateProtocol> appStateModule = [self nativeModuleForAppManager:appRecord.appManager named:@"AppState"]; 300 if (appStateModule != nil) { 301 [appStateModule setState:@"active"]; 302 } 303 _visibleApp = appRecord; 304 } else { 305 _visibleApp = nil; 306 } 307 308 if (_visibleApp && _visibleApp != _appRegistry.homeAppRecord) { 309 [self _unregisterUnusedAppRecords]; 310 } 311 } 312} 313 314- (void)_unregisterUnusedAppRecords 315{ 316 for (NSString *recordId in _appRegistry.appEnumerator) { 317 EXKernelAppRecord *record = [_appRegistry recordForId:recordId]; 318 if (record && record != _visibleApp) { 319 [_appRegistry unregisterAppWithRecordId:recordId]; 320 break; 321 } 322 } 323} 324 325- (void)_handleAppStateDidChange:(NSNotification *)notification 326{ 327 NSString *newState; 328 329 if ([notification.name isEqualToString:UIApplicationWillResignActiveNotification]) { 330 newState = @"inactive"; 331 } else if ([notification.name isEqualToString:UIApplicationWillEnterForegroundNotification]) { 332 newState = @"background"; 333 } else { 334 switch (RCTSharedApplication().applicationState) { 335 case UIApplicationStateActive: 336 newState = @"active"; 337 break; 338 case UIApplicationStateBackground: { 339 newState = @"background"; 340 break; 341 } 342 default: { 343 newState = @"unknown"; 344 break; 345 } 346 } 347 } 348 349 if (_visibleApp) { 350 EXReactAppManager *appManager = _visibleApp.appManager; 351 id<EXAppStateProtocol> appStateModule = [self nativeModuleForAppManager:appManager named:@"AppState"]; 352 NSString *lastKnownState; 353 if (appStateModule != nil) { 354 lastKnownState = [appStateModule lastKnownState]; 355 [appStateModule setState:newState]; 356 } 357 if (!lastKnownState || ![newState isEqualToString:lastKnownState]) { 358 if ([newState isEqualToString:@"active"]) { 359 [_visibleApp.viewController appStateDidBecomeActive]; 360 [self _postNotificationName:kEXKernelBridgeDidForegroundNotification onAbstractBridge:appManager.reactBridge]; 361 } else if ([newState isEqualToString:@"background"]) { 362 [_visibleApp.viewController appStateDidBecomeInactive]; 363 [self _postNotificationName:kEXKernelBridgeDidBackgroundNotification onAbstractBridge:appManager.reactBridge]; 364 } 365 } 366 } 367} 368 369 370- (void)_handleRequestReloadVisibleApp:(NSNotification *)notification 371{ 372 [self reloadVisibleApp]; 373} 374 375- (void)_moveAppToVisible:(EXKernelAppRecord *)appRecord 376{ 377 if (_browserController) { 378 [EXUtil performSynchronouslyOnMainThread:^{ 379 [self->_browserController moveAppToVisible:appRecord]; 380 }]; 381 } 382} 383 384#ifndef EX_DETACHED 385#pragma mark - EXDevMenuDelegateProtocol 386 387- (RCTBridge *)mainBridgeForDevMenuManager:(EXDevMenuManager *)manager 388{ 389 return _appRegistry.homeAppRecord.appManager.reactBridge; 390} 391 392- (nullable RCTBridge *)appBridgeForDevMenuManager:(EXDevMenuManager *)manager 393{ 394 if (_visibleApp == _appRegistry.homeAppRecord) { 395 return nil; 396 } 397 return _visibleApp.appManager.reactBridge; 398} 399 400- (BOOL)devMenuManager:(EXDevMenuManager *)manager canChangeVisibility:(BOOL)visibility 401{ 402 return !visibility || _visibleApp != _appRegistry.homeAppRecord; 403} 404 405#endif 406 407@end 408 409NS_ASSUME_NONNULL_END 410