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