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 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 NSDictionary *bodyWithOrigin = [self _notificationPropsWithBody:notifBody isFromBackground:isFromBackground isRemote:isRemote]; 165 if (destinationApp) { 166 // send the body to the already-open experience 167 [self _dispatchJSEvent:@"Exponent.notification" body:bodyWithOrigin toApp:destinationApp]; 168 [self _moveAppToVisible:destinationApp]; 169 } else { 170 // no app is currently running for this experience id. 171 // if we're Expo Client, we can query Home for a past experience in the user's history, and route the notification there. 172 if (_browserController) { 173 __weak typeof(self) weakSelf = self; 174 [_browserController getHistoryUrlForExperienceId:destinationExperienceId completion:^(NSString *urlString) { 175 if (urlString) { 176 NSURL *url = [NSURL URLWithString:urlString]; 177 if (url) { 178 [weakSelf createNewAppWithUrl:url initialProps:@{ @"notification": bodyWithOrigin }]; 179 } 180 } 181 }]; 182 } 183 } 184} 185 186/** 187 * If the bridge has a batchedBridge or parentBridge selector, posts the notification on that object as well. 188 */ 189- (void)_postNotificationName: (NSNotificationName)name onAbstractBridge: (id)bridge 190{ 191 [[NSNotificationCenter defaultCenter] postNotificationName:name object:bridge]; 192 if ([bridge respondsToSelector:@selector(batchedBridge)]) { 193 [[NSNotificationCenter defaultCenter] postNotificationName:name object:[bridge batchedBridge]]; 194 } else if ([bridge respondsToSelector:@selector(parentBridge)]) { 195 [[NSNotificationCenter defaultCenter] postNotificationName:name object:[bridge parentBridge]]; 196 } 197} 198 199- (void)_dispatchJSEvent:(NSString *)eventName body:(NSDictionary *)eventBody toApp:(EXKernelAppRecord *)appRecord 200{ 201 [appRecord.appManager.reactBridge enqueueJSCall:@"RCTDeviceEventEmitter.emit" 202 args:eventBody ? @[eventName, eventBody] : @[eventName]]; 203} 204 205#pragma mark - App props 206 207- (NSDictionary *)initialAppPropsFromLaunchOptions:(NSDictionary *)launchOptions 208{ 209 NSMutableDictionary *initialProps = [NSMutableDictionary dictionary]; 210 211 NSDictionary *remoteNotification = [launchOptions objectForKey:UIApplicationLaunchOptionsRemoteNotificationKey]; 212 if (remoteNotification) { 213 initialProps[@"notification"] = [self _notificationPropsWithBody:remoteNotification[@"body"] isFromBackground:YES isRemote:YES]; 214 } 215 UILocalNotification *localNotification = [launchOptions objectForKey:UIApplicationLaunchOptionsLocalNotificationKey]; 216 if (localNotification) { 217 initialProps[@"notification"] = [self _notificationPropsWithBody:localNotification.userInfo[@"body"] isFromBackground:YES isRemote:NO]; 218 } 219 return initialProps; 220} 221 222- (NSDictionary *)_notificationPropsWithBody:(NSDictionary *)notifBody isFromBackground:(BOOL)isFromBackground isRemote:(BOOL)isRemote 223{ 224 // if the notification came from the background, in most but not all cases, this means the user acted on an iOS notification 225 // and caused the app to launch. 226 // From SO: 227 // > Note that "App opened from Notification" will be a false positive if the notification is sent while the user is on a different 228 // > screen (for example, if they pull down the status bar and then receive a notification from your app). 229 if (!notifBody) { 230 notifBody = @{}; 231 } 232 return @{ 233 @"origin": (isFromBackground) ? @"selected" : @"received", 234 @"remote": @(isRemote), 235 @"data": notifBody, 236 }; 237} 238 239#pragma mark - App State 240 241- (EXKernelAppRecord *)createNewAppWithUrl:(NSURL *)url initialProps:(nullable NSDictionary *)initialProps 242{ 243 NSString *recordId = [_appRegistry registerAppWithManifestUrl:url initialProps:initialProps]; 244 EXKernelAppRecord *record = [_appRegistry recordForId:recordId]; 245 [self _moveAppToVisible:record]; 246 return record; 247} 248 249- (void)switchTasks 250{ 251 if (!_browserController) { 252 return; 253 } 254 255 if (_visibleApp != _appRegistry.homeAppRecord) { 256 [EXUtil performSynchronouslyOnMainThread:^{ 257 [_browserController toggleMenuWithCompletion:nil]; 258 }]; 259 } else { 260 EXKernelAppRegistry *appRegistry = [EXKernel sharedInstance].appRegistry; 261 for (NSString *recordId in appRegistry.appEnumerator) { 262 EXKernelAppRecord *record = [appRegistry recordForId:recordId]; 263 // foreground the first thing we find 264 [self _moveAppToVisible:record]; 265 } 266 } 267} 268 269- (void)reloadAppWithExperienceId:(NSString *)experienceId 270{ 271 EXKernelAppRecord *appRecord = [_appRegistry newestRecordWithExperienceId:experienceId]; 272 if (_browserController) { 273 [self createNewAppWithUrl:appRecord.appLoader.manifestUrl initialProps:nil]; 274 } else if (_appRegistry.standaloneAppRecord && appRecord == _appRegistry.standaloneAppRecord) { 275 [appRecord.viewController refresh]; 276 } 277} 278 279- (void)reloadAppFromCacheWithExperienceId:(NSString *)experienceId 280{ 281 EXKernelAppRecord *appRecord = [_appRegistry newestRecordWithExperienceId:experienceId]; 282 [appRecord.viewController reloadFromCache]; 283} 284 285- (void)viewController:(__unused EXViewController *)vc didNavigateAppToVisible:(EXKernelAppRecord *)appRecord 286{ 287 EXKernelAppRecord *appRecordPreviouslyVisible = _visibleApp; 288 if (appRecord != appRecordPreviouslyVisible) { 289 if (appRecordPreviouslyVisible) { 290 [appRecordPreviouslyVisible.viewController appStateDidBecomeInactive]; 291 [self _postNotificationName:kEXKernelBridgeDidBackgroundNotification onAbstractBridge:appRecordPreviouslyVisible.appManager.reactBridge]; 292 id appStateModule = [self nativeModuleForAppManager:appRecordPreviouslyVisible.appManager named:@"AppState"]; 293 if ([appStateModule respondsToSelector:@selector(setState:)]) { 294 [appStateModule setState:@"background"]; 295 } 296 } 297 if (appRecord) { 298 [appRecord.viewController appStateDidBecomeActive]; 299 [self _postNotificationName:kEXKernelBridgeDidForegroundNotification onAbstractBridge:appRecord.appManager.reactBridge]; 300 id appStateModule = [self nativeModuleForAppManager:appRecord.appManager named:@"AppState"]; 301 if ([appStateModule respondsToSelector:@selector(setState:)]) { 302 [appStateModule setState:@"active"]; 303 } 304 _visibleApp = appRecord; 305 [[EXAnalytics sharedInstance] logAppVisibleEvent]; 306 } else { 307 _visibleApp = nil; 308 } 309 310 if (_visibleApp && _visibleApp != _appRegistry.homeAppRecord) { 311 [self _unregisterUnusedAppRecords]; 312 } 313 } 314} 315 316- (void)_unregisterUnusedAppRecords 317{ 318 for (NSString *recordId in _appRegistry.appEnumerator) { 319 EXKernelAppRecord *record = [_appRegistry recordForId:recordId]; 320 if (record && record != _visibleApp) { 321 [_appRegistry unregisterAppWithRecordId:recordId]; 322 break; 323 } 324 } 325} 326 327- (void)_handleAppStateDidChange:(NSNotification *)notification 328{ 329 NSString *newState; 330 331 if ([notification.name isEqualToString:UIApplicationWillResignActiveNotification]) { 332 newState = @"inactive"; 333 } else if ([notification.name isEqualToString:UIApplicationWillEnterForegroundNotification]) { 334 newState = @"background"; 335 } else { 336 switch (RCTSharedApplication().applicationState) { 337 case UIApplicationStateActive: 338 newState = @"active"; 339 break; 340 case UIApplicationStateBackground: { 341 newState = @"background"; 342 break; 343 } 344 default: { 345 newState = @"unknown"; 346 break; 347 } 348 } 349 } 350 351 if (_visibleApp) { 352 EXReactAppManager *appManager = _visibleApp.appManager; 353 id appStateModule = [self nativeModuleForAppManager:appManager named:@"AppState"]; 354 NSString *lastKnownState; 355 if ([appStateModule respondsToSelector:@selector(lastKnownState)]) { 356 lastKnownState = [appStateModule lastKnownState]; 357 } 358 if ([appStateModule respondsToSelector:@selector(setState:)]) { 359 [appStateModule setState:newState]; 360 } 361 if (!lastKnownState || ![newState isEqualToString:lastKnownState]) { 362 if ([newState isEqualToString:@"active"]) { 363 [_visibleApp.viewController appStateDidBecomeActive]; 364 [self _postNotificationName:kEXKernelBridgeDidForegroundNotification onAbstractBridge:appManager.reactBridge]; 365 } else if ([newState isEqualToString:@"background"]) { 366 [_visibleApp.viewController appStateDidBecomeInactive]; 367 [self _postNotificationName:kEXKernelBridgeDidBackgroundNotification onAbstractBridge:appManager.reactBridge]; 368 } 369 } 370 } 371} 372 373- (void)_moveAppToVisible:(EXKernelAppRecord *)appRecord 374{ 375 if (_browserController) { 376 [EXUtil performSynchronouslyOnMainThread:^{ 377 [_browserController moveAppToVisible:appRecord]; 378 }]; 379 } 380} 381 382@end 383 384NS_ASSUME_NONNULL_END 385