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