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