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