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