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