1// Copyright 2015-present 650 Industries. All rights reserved. 2 3#import "EXAppState.h" 4#import "EXAppViewController.h" 5#import "EXBuildConstants.h" 6#import "EXKernel.h" 7#import "EXAbstractLoader.h" 8#import "EXKernelAppRecord.h" 9#import "EXKernelLinkingManager.h" 10#import "EXLinkingManager.h" 11#import "EXVersions.h" 12#import "EXHomeModule.h" 13 14#import <EXConstants/EXConstantsService.h> 15#import <React/RCTBridge+Private.h> 16#import <React/RCTEventDispatcher.h> 17#import <React/RCTModuleData.h> 18#import <React/RCTUtils.h> 19 20#ifndef EX_DETACHED 21// Kernel is DevMenu's delegate only in non-detached builds. 22#import "EXDevMenuManager.h" 23#import "EXDevMenuDelegateProtocol.h" 24 25@interface EXKernel () <EXDevMenuDelegateProtocol> 26@end 27#endif 28 29NS_ASSUME_NONNULL_BEGIN 30 31NSString *kEXKernelErrorDomain = @"EXKernelErrorDomain"; 32NSString *kEXKernelShouldForegroundTaskEvent = @"foregroundTask"; 33NSString * const kEXKernelClearJSCacheUserDefaultsKey = @"EXKernelClearJSCacheUserDefaultsKey"; 34NSString * const kEXReloadActiveAppRequest = @"EXReloadActiveAppRequest"; 35 36@interface EXKernel () <EXKernelAppRegistryDelegate> 37 38@end 39 40// Protocol that should be implemented by all versions of EXAppState class. 41@protocol EXAppStateProtocol 42 43@property (nonatomic, strong, readonly) NSString *lastKnownState; 44 45- (void)setState:(NSString *)state; 46 47@end 48 49@implementation EXKernel 50 51+ (instancetype)sharedInstance 52{ 53 static EXKernel *theKernel; 54 static dispatch_once_t once; 55 dispatch_once(&once, ^{ 56 if (!theKernel) { 57 theKernel = [[EXKernel alloc] init]; 58 } 59 }); 60 return theKernel; 61} 62 63- (instancetype)init 64{ 65 if (self = [super init]) { 66 // init app registry: keep track of RN bridges we are running 67 _appRegistry = [[EXKernelAppRegistry alloc] init]; 68 _appRegistry.delegate = self; 69 70 // init service registry: classes which manage shared resources among all bridges 71 _serviceRegistry = [[EXKernelServiceRegistry alloc] init]; 72 73#ifndef EX_DETACHED 74 // Set the delegate of dev menu manager. Maybe it should be a separate class? Will see later once the delegate protocol gets too big. 75 [[EXDevMenuManager sharedInstance] setDelegate:self]; 76#endif 77 78 // register for notifications to request reloading the visible app 79 [[NSNotificationCenter defaultCenter] addObserver:self 80 selector:@selector(_handleRequestReloadVisibleApp:) 81 name:kEXReloadActiveAppRequest 82 object:nil]; 83 84 for (NSString *name in @[UIApplicationDidBecomeActiveNotification, 85 UIApplicationDidEnterBackgroundNotification, 86 UIApplicationDidFinishLaunchingNotification, 87 UIApplicationWillResignActiveNotification, 88 UIApplicationWillEnterForegroundNotification]) { 89 90 [[NSNotificationCenter defaultCenter] addObserver:self 91 selector:@selector(_handleAppStateDidChange:) 92 name:name 93 object:nil]; 94 } 95 NSLog(@"Expo iOS Runtime Version %@", [EXBuildConstants sharedInstance].expoRuntimeVersion); 96 } 97 return self; 98} 99 100- (void)dealloc 101{ 102 [[NSNotificationCenter defaultCenter] removeObserver:self]; 103} 104 105#pragma mark - bridge registry delegate 106 107- (void)appRegistry:(EXKernelAppRegistry *)registry didRegisterAppRecord:(EXKernelAppRecord *)appRecord 108{ 109 // forward to service registry 110 [_serviceRegistry appRegistry:registry didRegisterAppRecord:appRecord]; 111} 112 113- (void)appRegistry:(EXKernelAppRegistry *)registry willUnregisterAppRecord:(EXKernelAppRecord *)appRecord 114{ 115 // forward to service registry 116 [_serviceRegistry appRegistry:registry willUnregisterAppRecord:appRecord]; 117} 118 119#pragma mark - Interfacing with JS 120 121- (void)sendUrl:(NSString *)urlString toAppRecord:(EXKernelAppRecord *)app 122{ 123 // fire a Linking url event on this (possibly versioned) bridge 124 EXReactAppManager *appManager = app.appManager; 125 id linkingModule = [self nativeModuleForAppManager:appManager named:@"LinkingManager"]; 126 if (!linkingModule) { 127 DDLogError(@"Could not find the Linking module to open URL (%@)", urlString); 128 } else if ([linkingModule respondsToSelector:@selector(dispatchOpenUrlEvent:)]) { 129 [linkingModule dispatchOpenUrlEvent:[NSURL URLWithString:urlString]]; 130 } else { 131 DDLogError(@"Linking module doesn't support the API we use to open URL (%@)", urlString); 132 } 133 [self _moveAppToVisible:app]; 134} 135 136- (id)nativeModuleForAppManager:(EXReactAppManager *)appManager named:(NSString *)moduleName 137{ 138 id destinationBridge = appManager.reactBridge; 139 140 if ([destinationBridge respondsToSelector:@selector(batchedBridge)]) { 141 id batchedBridge = [destinationBridge batchedBridge]; 142 id moduleData = [batchedBridge moduleDataForName:moduleName]; 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/** 157 * If the bridge has a batchedBridge or parentBridge selector, posts the notification on that object as well. 158 */ 159- (void)_postNotificationName: (NSNotificationName)name onAbstractBridge: (id)bridge 160{ 161 [[NSNotificationCenter defaultCenter] postNotificationName:name object:bridge]; 162 if ([bridge respondsToSelector:@selector(batchedBridge)]) { 163 [[NSNotificationCenter defaultCenter] postNotificationName:name object:[bridge batchedBridge]]; 164 } else if ([bridge respondsToSelector:@selector(parentBridge)]) { 165 [[NSNotificationCenter defaultCenter] postNotificationName:name object:[bridge parentBridge]]; 166 } 167} 168 169- (BOOL)_dispatchJSEvent:(NSString *)eventName body:(NSDictionary *)eventBody toApp:(EXKernelAppRecord *)appRecord 170{ 171 if (!appRecord.appManager.reactBridge) { 172 return NO; 173 } 174 [appRecord.appManager.reactBridge enqueueJSCall:@"RCTDeviceEventEmitter.emit" 175 args:eventBody ? @[eventName, eventBody] : @[eventName]]; 176 return YES; 177} 178 179#pragma mark - App props 180 181- (nullable NSDictionary *)initialAppPropsFromLaunchOptions:(NSDictionary *)launchOptions 182{ 183 return nil; 184} 185 186#pragma mark - App State 187 188- (EXKernelAppRecord *)createNewAppWithUrl:(NSURL *)url initialProps:(nullable NSDictionary *)initialProps 189{ 190 NSString *recordId = [_appRegistry registerAppWithManifestUrl:url initialProps:initialProps]; 191 EXKernelAppRecord *record = [_appRegistry recordForId:recordId]; 192 [self _moveAppToVisible:record]; 193 return record; 194} 195 196 197- (void)reloadVisibleApp 198{ 199 if (_browserController) { 200 [EXUtil performSynchronouslyOnMainThread:^{ 201 [self->_browserController reloadVisibleApp]; 202 }]; 203 } 204} 205 206- (void)switchTasks 207{ 208 if (!_browserController) { 209 return; 210 } 211 212 if (_visibleApp != _appRegistry.homeAppRecord) { 213#ifndef EX_DETACHED // Just to compile without access to EXDevMenuManager, we wouldn't get here either way because browser controller is unset in this case. 214 [EXUtil performSynchronouslyOnMainThread:^{ 215 [[EXDevMenuManager sharedInstance] toggle]; 216 }]; 217#endif 218 } else { 219 EXKernelAppRegistry *appRegistry = [EXKernel sharedInstance].appRegistry; 220 for (NSString *recordId in appRegistry.appEnumerator) { 221 EXKernelAppRecord *record = [appRegistry recordForId:recordId]; 222 // foreground the first thing we find 223 [self _moveAppToVisible:record]; 224 } 225 } 226} 227 228- (void)reloadAppWithScopeKey:(NSString *)scopeKey 229{ 230 EXKernelAppRecord *appRecord = [_appRegistry newestRecordWithScopeKey:scopeKey]; 231 if (_browserController) { 232 [self createNewAppWithUrl:appRecord.appLoader.manifestUrl initialProps:nil]; 233 } else if (_appRegistry.standaloneAppRecord && appRecord == _appRegistry.standaloneAppRecord) { 234 [appRecord.viewController refresh]; 235 } 236} 237 238- (void)reloadAppFromCacheWithScopeKey:(NSString *)scopeKey 239{ 240 EXKernelAppRecord *appRecord = [_appRegistry newestRecordWithScopeKey:scopeKey]; 241 [appRecord.viewController reloadFromCache]; 242} 243 244- (void)viewController:(__unused EXViewController *)vc didNavigateAppToVisible:(EXKernelAppRecord *)appRecord 245{ 246 EXKernelAppRecord *appRecordPreviouslyVisible = _visibleApp; 247 if (appRecord != appRecordPreviouslyVisible) { 248 if (appRecordPreviouslyVisible) { 249 [appRecordPreviouslyVisible.viewController appStateDidBecomeInactive]; 250 [self _postNotificationName:kEXKernelBridgeDidBackgroundNotification onAbstractBridge:appRecordPreviouslyVisible.appManager.reactBridge]; 251 id<EXAppStateProtocol> appStateModule = [self nativeModuleForAppManager:appRecordPreviouslyVisible.appManager named:@"AppState"]; 252 if (appStateModule != nil) { 253 [appStateModule setState:@"background"]; 254 } 255 } 256 if (appRecord) { 257 [appRecord.viewController appStateDidBecomeActive]; 258 [self _postNotificationName:kEXKernelBridgeDidForegroundNotification onAbstractBridge:appRecord.appManager.reactBridge]; 259 id<EXAppStateProtocol> appStateModule = [self nativeModuleForAppManager:appRecord.appManager named:@"AppState"]; 260 if (appStateModule != nil) { 261 [appStateModule setState:@"active"]; 262 } 263 _visibleApp = appRecord; 264 } else { 265 _visibleApp = nil; 266 } 267 268 if (_visibleApp && _visibleApp != _appRegistry.homeAppRecord) { 269 [self _unregisterUnusedAppRecords]; 270 } 271 } 272} 273 274- (void)_unregisterUnusedAppRecords 275{ 276 for (NSString *recordId in _appRegistry.appEnumerator) { 277 EXKernelAppRecord *record = [_appRegistry recordForId:recordId]; 278 if (record && record != _visibleApp) { 279 [_appRegistry unregisterAppWithRecordId:recordId]; 280 break; 281 } 282 } 283} 284 285- (void)_handleAppStateDidChange:(NSNotification *)notification 286{ 287 NSString *newState; 288 289 if ([notification.name isEqualToString:UIApplicationWillResignActiveNotification]) { 290 newState = @"inactive"; 291 } else if ([notification.name isEqualToString:UIApplicationWillEnterForegroundNotification]) { 292 newState = @"background"; 293 } else { 294 switch (RCTSharedApplication().applicationState) { 295 case UIApplicationStateActive: 296 newState = @"active"; 297 break; 298 case UIApplicationStateBackground: { 299 newState = @"background"; 300 break; 301 } 302 default: { 303 newState = @"unknown"; 304 break; 305 } 306 } 307 } 308 309 if (_visibleApp) { 310 EXReactAppManager *appManager = _visibleApp.appManager; 311 id<EXAppStateProtocol> appStateModule = [self nativeModuleForAppManager:appManager named:@"AppState"]; 312 NSString *lastKnownState; 313 if (appStateModule != nil) { 314 lastKnownState = [appStateModule lastKnownState]; 315 [appStateModule setState:newState]; 316 } 317 if (!lastKnownState || ![newState isEqualToString:lastKnownState]) { 318 if ([newState isEqualToString:@"active"]) { 319 [_visibleApp.viewController appStateDidBecomeActive]; 320 [self _postNotificationName:kEXKernelBridgeDidForegroundNotification onAbstractBridge:appManager.reactBridge]; 321 } else if ([newState isEqualToString:@"background"]) { 322 [_visibleApp.viewController appStateDidBecomeInactive]; 323 [self _postNotificationName:kEXKernelBridgeDidBackgroundNotification onAbstractBridge:appManager.reactBridge]; 324 } 325 } 326 } 327} 328 329 330- (void)_handleRequestReloadVisibleApp:(NSNotification *)notification 331{ 332 [self reloadVisibleApp]; 333} 334 335- (void)_moveAppToVisible:(EXKernelAppRecord *)appRecord 336{ 337 if (_browserController) { 338 [EXUtil performSynchronouslyOnMainThread:^{ 339 [self->_browserController moveAppToVisible:appRecord]; 340 }]; 341 } 342} 343 344#ifndef EX_DETACHED 345#pragma mark - EXDevMenuDelegateProtocol 346 347- (RCTBridge *)mainBridgeForDevMenuManager:(EXDevMenuManager *)manager 348{ 349 return _appRegistry.homeAppRecord.appManager.reactBridge; 350} 351 352- (nullable RCTBridge *)appBridgeForDevMenuManager:(EXDevMenuManager *)manager 353{ 354 if (_visibleApp == _appRegistry.homeAppRecord) { 355 return nil; 356 } 357 return _visibleApp.appManager.reactBridge; 358} 359 360- (BOOL)devMenuManager:(EXDevMenuManager *)manager canChangeVisibility:(BOOL)visibility 361{ 362 return !visibility || _visibleApp != _appRegistry.homeAppRecord; 363} 364 365#endif 366 367@end 368 369NS_ASSUME_NONNULL_END 370