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