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