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 "EXAbstractLoader.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 if (moduleData) { 160 return [moduleData instance]; 161 } 162 } else { 163 // bridge can be null if the record is in an error state and never created a bridge. 164 if (destinationBridge) { 165 DDLogError(@"Bridge does not support the API we use to get its underlying batched bridge"); 166 } 167 } 168 return nil; 169} 170 171- (BOOL)sendNotification:(EXPendingNotification *)notification 172{ 173 EXKernelAppRecord *destinationApp = [_appRegistry standaloneAppRecord] ?: [_appRegistry newestRecordWithScopeKey:notification.scopeKey]; 174 175 // This allows home app record to receive notification events as well. 176 if (!destinationApp && [_appRegistry.homeAppRecord.scopeKey isEqualToString:notification.scopeKey]) { 177 destinationApp = _appRegistry.homeAppRecord; 178 } 179 180 if (destinationApp) { 181 // send the body to the already-open experience 182 BOOL success = [self _dispatchJSEvent:@"Exponent.notification" body:notification.properties toApp:destinationApp]; 183 [self _moveAppToVisible:destinationApp]; 184 return success; 185 } else { 186 // no app is currently running for this experience id. 187 // if we're Expo Go, we can query Home for a past experience in the user's history, and route the notification there. 188 if (_browserController) { 189 __weak typeof(self) weakSelf = self; 190 [_browserController getHistoryUrlForScopeKey:notification.scopeKey completion:^(NSString *urlString) { 191 if (urlString) { 192 NSURL *url = [NSURL URLWithString:urlString]; 193 if (url) { 194 [weakSelf createNewAppWithUrl:url initialProps:@{ @"notification": notification.properties }]; 195 } 196 } 197 }]; 198 // If we're here, there's no active app in appRegistry matching notification.experienceId 199 // and we are in Expo Go, since _browserController is not nil. 200 // If so, we can return YES (meaning "notification has been successfully dispatched") 201 // because we pass the notification as initialProps in completion handler 202 // of getHistoryUrlForExperienceId:. If urlString passed to completion handler is empty, 203 // the notification is forgotten (this is the expected behavior). 204 return YES; 205 } 206 } 207 208 return NO; 209} 210 211/** 212 * If the bridge has a batchedBridge or parentBridge selector, posts the notification on that object as well. 213 */ 214- (void)_postNotificationName: (NSNotificationName)name onAbstractBridge: (id)bridge 215{ 216 [[NSNotificationCenter defaultCenter] postNotificationName:name object:bridge]; 217 if ([bridge respondsToSelector:@selector(batchedBridge)]) { 218 [[NSNotificationCenter defaultCenter] postNotificationName:name object:[bridge batchedBridge]]; 219 } else if ([bridge respondsToSelector:@selector(parentBridge)]) { 220 [[NSNotificationCenter defaultCenter] postNotificationName:name object:[bridge parentBridge]]; 221 } 222} 223 224- (BOOL)_dispatchJSEvent:(NSString *)eventName body:(NSDictionary *)eventBody toApp:(EXKernelAppRecord *)appRecord 225{ 226 if (!appRecord.appManager.reactBridge) { 227 return NO; 228 } 229 [appRecord.appManager.reactBridge enqueueJSCall:@"RCTDeviceEventEmitter.emit" 230 args:eventBody ? @[eventName, eventBody] : @[eventName]]; 231 return YES; 232} 233 234#pragma mark - App props 235 236- (nullable NSDictionary *)initialAppPropsFromLaunchOptions:(NSDictionary *)launchOptions 237{ 238 return nil; 239} 240 241#pragma mark - App State 242 243- (EXKernelAppRecord *)createNewAppWithUrl:(NSURL *)url initialProps:(nullable NSDictionary *)initialProps 244{ 245 NSString *recordId = [_appRegistry registerAppWithManifestUrl:url initialProps:initialProps]; 246 EXKernelAppRecord *record = [_appRegistry recordForId:recordId]; 247 [self _moveAppToVisible:record]; 248 return record; 249} 250 251 252- (void)reloadVisibleApp 253{ 254 if (_browserController) { 255 [EXUtil performSynchronouslyOnMainThread:^{ 256 [self->_browserController reloadVisibleApp]; 257 }]; 258 } 259} 260 261- (void)switchTasks 262{ 263 if (!_browserController) { 264 return; 265 } 266 267 if (_visibleApp != _appRegistry.homeAppRecord) { 268#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. 269 [EXUtil performSynchronouslyOnMainThread:^{ 270 [[EXDevMenuManager sharedInstance] toggle]; 271 }]; 272#endif 273 } else { 274 EXKernelAppRegistry *appRegistry = [EXKernel sharedInstance].appRegistry; 275 for (NSString *recordId in appRegistry.appEnumerator) { 276 EXKernelAppRecord *record = [appRegistry recordForId:recordId]; 277 // foreground the first thing we find 278 [self _moveAppToVisible:record]; 279 } 280 } 281} 282 283- (void)reloadAppWithScopeKey:(NSString *)scopeKey 284{ 285 EXKernelAppRecord *appRecord = [_appRegistry newestRecordWithScopeKey:scopeKey]; 286 if (_browserController) { 287 [self createNewAppWithUrl:appRecord.appLoader.manifestUrl initialProps:nil]; 288 } else if (_appRegistry.standaloneAppRecord && appRecord == _appRegistry.standaloneAppRecord) { 289 [appRecord.viewController refresh]; 290 } 291} 292 293- (void)reloadAppFromCacheWithScopeKey:(NSString *)scopeKey 294{ 295 EXKernelAppRecord *appRecord = [_appRegistry newestRecordWithScopeKey:scopeKey]; 296 [appRecord.viewController reloadFromCache]; 297} 298 299- (void)viewController:(__unused EXViewController *)vc didNavigateAppToVisible:(EXKernelAppRecord *)appRecord 300{ 301 EXKernelAppRecord *appRecordPreviouslyVisible = _visibleApp; 302 if (appRecord != appRecordPreviouslyVisible) { 303 if (appRecordPreviouslyVisible) { 304 [appRecordPreviouslyVisible.viewController appStateDidBecomeInactive]; 305 [self _postNotificationName:kEXKernelBridgeDidBackgroundNotification onAbstractBridge:appRecordPreviouslyVisible.appManager.reactBridge]; 306 id<EXAppStateProtocol> appStateModule = [self nativeModuleForAppManager:appRecordPreviouslyVisible.appManager named:@"AppState"]; 307 if (appStateModule != nil) { 308 [appStateModule setState:@"background"]; 309 } 310 } 311 if (appRecord) { 312 [appRecord.viewController appStateDidBecomeActive]; 313 [self _postNotificationName:kEXKernelBridgeDidForegroundNotification onAbstractBridge:appRecord.appManager.reactBridge]; 314 id<EXAppStateProtocol> appStateModule = [self nativeModuleForAppManager:appRecord.appManager named:@"AppState"]; 315 if (appStateModule != nil) { 316 [appStateModule setState:@"active"]; 317 } 318 _visibleApp = appRecord; 319 [[EXAnalytics sharedInstance] logAppVisibleEvent]; 320 } else { 321 _visibleApp = nil; 322 } 323 324 if (_visibleApp && _visibleApp != _appRegistry.homeAppRecord) { 325 [self _unregisterUnusedAppRecords]; 326 } 327 } 328} 329 330- (void)_unregisterUnusedAppRecords 331{ 332 for (NSString *recordId in _appRegistry.appEnumerator) { 333 EXKernelAppRecord *record = [_appRegistry recordForId:recordId]; 334 if (record && record != _visibleApp) { 335 [_appRegistry unregisterAppWithRecordId:recordId]; 336 break; 337 } 338 } 339} 340 341- (void)_handleAppStateDidChange:(NSNotification *)notification 342{ 343 NSString *newState; 344 345 if ([notification.name isEqualToString:UIApplicationWillResignActiveNotification]) { 346 newState = @"inactive"; 347 } else if ([notification.name isEqualToString:UIApplicationWillEnterForegroundNotification]) { 348 newState = @"background"; 349 } else { 350 switch (RCTSharedApplication().applicationState) { 351 case UIApplicationStateActive: 352 newState = @"active"; 353 break; 354 case UIApplicationStateBackground: { 355 newState = @"background"; 356 break; 357 } 358 default: { 359 newState = @"unknown"; 360 break; 361 } 362 } 363 } 364 365 if (_visibleApp) { 366 EXReactAppManager *appManager = _visibleApp.appManager; 367 id<EXAppStateProtocol> appStateModule = [self nativeModuleForAppManager:appManager named:@"AppState"]; 368 NSString *lastKnownState; 369 if (appStateModule != nil) { 370 lastKnownState = [appStateModule lastKnownState]; 371 [appStateModule setState:newState]; 372 } 373 if (!lastKnownState || ![newState isEqualToString:lastKnownState]) { 374 if ([newState isEqualToString:@"active"]) { 375 [_visibleApp.viewController appStateDidBecomeActive]; 376 [self _postNotificationName:kEXKernelBridgeDidForegroundNotification onAbstractBridge:appManager.reactBridge]; 377 } else if ([newState isEqualToString:@"background"]) { 378 [_visibleApp.viewController appStateDidBecomeInactive]; 379 [self _postNotificationName:kEXKernelBridgeDidBackgroundNotification onAbstractBridge:appManager.reactBridge]; 380 } 381 } 382 } 383} 384 385 386- (void)_handleRequestReloadVisibleApp:(NSNotification *)notification 387{ 388 [self reloadVisibleApp]; 389} 390 391- (void)_moveAppToVisible:(EXKernelAppRecord *)appRecord 392{ 393 if (_browserController) { 394 [EXUtil performSynchronouslyOnMainThread:^{ 395 [self->_browserController moveAppToVisible:appRecord]; 396 }]; 397 } 398} 399 400#ifndef EX_DETACHED 401#pragma mark - EXDevMenuDelegateProtocol 402 403- (RCTBridge *)mainBridgeForDevMenuManager:(EXDevMenuManager *)manager 404{ 405 return _appRegistry.homeAppRecord.appManager.reactBridge; 406} 407 408- (nullable RCTBridge *)appBridgeForDevMenuManager:(EXDevMenuManager *)manager 409{ 410 if (_visibleApp == _appRegistry.homeAppRecord) { 411 return nil; 412 } 413 return _visibleApp.appManager.reactBridge; 414} 415 416- (BOOL)devMenuManager:(EXDevMenuManager *)manager canChangeVisibility:(BOOL)visibility 417{ 418 return !visibility || _visibleApp != _appRegistry.homeAppRecord; 419} 420 421#endif 422 423@end 424 425NS_ASSUME_NONNULL_END 426