xref: /expo/ios/Exponent/Kernel/Core/EXKernel.m (revision 604792ab)
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- (BOOL)sendNotification:(EXPendingNotification *)notification
157{
158  EXKernelAppRecord *destinationApp = [_appRegistry standaloneAppRecord] ?: [_appRegistry newestRecordWithScopeKey:notification.scopeKey];
159
160  // This allows home app record to receive notification events as well.
161  if (!destinationApp && [_appRegistry.homeAppRecord.scopeKey isEqualToString:notification.scopeKey]) {
162    destinationApp = _appRegistry.homeAppRecord;
163  }
164
165  if (destinationApp) {
166    // send the body to the already-open experience
167    BOOL success = [self _dispatchJSEvent:@"Exponent.notification" body:notification.properties toApp:destinationApp];
168    [self _moveAppToVisible:destinationApp];
169    return success;
170  } else {
171    // no app is currently running for this experience id.
172    // if we're Expo Go, we can query Home for a past experience in the user's history, and route the notification there.
173    if (_browserController) {
174      __weak typeof(self) weakSelf = self;
175      [_browserController getHistoryUrlForScopeKey:notification.scopeKey completion:^(NSString *urlString) {
176        if (urlString) {
177          NSURL *url = [NSURL URLWithString:urlString];
178          if (url) {
179            [weakSelf createNewAppWithUrl:url initialProps:@{ @"notification": notification.properties }];
180          }
181        }
182      }];
183      // If we're here, there's no active app in appRegistry matching notification.experienceId
184      // and we are in Expo Go, since _browserController is not nil.
185      // If so, we can return YES (meaning "notification has been successfully dispatched")
186      // because we pass the notification as initialProps in completion handler
187      // of getHistoryUrlForExperienceId:. If urlString passed to completion handler is empty,
188      // the notification is forgotten (this is the expected behavior).
189      return YES;
190    }
191  }
192
193  return NO;
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- (BOOL)_dispatchJSEvent:(NSString *)eventName body:(NSDictionary *)eventBody toApp:(EXKernelAppRecord *)appRecord
210{
211  if (!appRecord.appManager.reactBridge) {
212    return NO;
213  }
214  [appRecord.appManager.reactBridge enqueueJSCall:@"RCTDeviceEventEmitter.emit"
215                                             args:eventBody ? @[eventName, eventBody] : @[eventName]];
216  return YES;
217}
218
219#pragma mark - App props
220
221- (nullable NSDictionary *)initialAppPropsFromLaunchOptions:(NSDictionary *)launchOptions
222{
223  return nil;
224}
225
226#pragma mark - App State
227
228- (EXKernelAppRecord *)createNewAppWithUrl:(NSURL *)url initialProps:(nullable NSDictionary *)initialProps
229{
230  NSString *recordId = [_appRegistry registerAppWithManifestUrl:url initialProps:initialProps];
231  EXKernelAppRecord *record = [_appRegistry recordForId:recordId];
232  [self _moveAppToVisible:record];
233  return record;
234}
235
236
237- (void)reloadVisibleApp
238{
239  if (_browserController) {
240    [EXUtil performSynchronouslyOnMainThread:^{
241      [self->_browserController reloadVisibleApp];
242    }];
243  }
244}
245
246- (void)switchTasks
247{
248  if (!_browserController) {
249    return;
250  }
251
252  if (_visibleApp != _appRegistry.homeAppRecord) {
253#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.
254    [EXUtil performSynchronouslyOnMainThread:^{
255      [[EXDevMenuManager sharedInstance] toggle];
256    }];
257#endif
258  } else {
259    EXKernelAppRegistry *appRegistry = [EXKernel sharedInstance].appRegistry;
260    for (NSString *recordId in appRegistry.appEnumerator) {
261      EXKernelAppRecord *record = [appRegistry recordForId:recordId];
262      // foreground the first thing we find
263      [self _moveAppToVisible:record];
264    }
265  }
266}
267
268- (void)reloadAppWithScopeKey:(NSString *)scopeKey
269{
270  EXKernelAppRecord *appRecord = [_appRegistry newestRecordWithScopeKey:scopeKey];
271  if (_browserController) {
272    [self createNewAppWithUrl:appRecord.appLoader.manifestUrl initialProps:nil];
273  } else if (_appRegistry.standaloneAppRecord && appRecord == _appRegistry.standaloneAppRecord) {
274    [appRecord.viewController refresh];
275  }
276}
277
278- (void)reloadAppFromCacheWithScopeKey:(NSString *)scopeKey
279{
280  EXKernelAppRecord *appRecord = [_appRegistry newestRecordWithScopeKey:scopeKey];
281  [appRecord.viewController reloadFromCache];
282}
283
284- (void)viewController:(__unused EXViewController *)vc didNavigateAppToVisible:(EXKernelAppRecord *)appRecord
285{
286  EXKernelAppRecord *appRecordPreviouslyVisible = _visibleApp;
287  if (appRecord != appRecordPreviouslyVisible) {
288    if (appRecordPreviouslyVisible) {
289      [appRecordPreviouslyVisible.viewController appStateDidBecomeInactive];
290      [self _postNotificationName:kEXKernelBridgeDidBackgroundNotification onAbstractBridge:appRecordPreviouslyVisible.appManager.reactBridge];
291      id<EXAppStateProtocol> appStateModule = [self nativeModuleForAppManager:appRecordPreviouslyVisible.appManager named:@"AppState"];
292      if (appStateModule != nil) {
293        [appStateModule setState:@"background"];
294      }
295    }
296    if (appRecord) {
297      [appRecord.viewController appStateDidBecomeActive];
298      [self _postNotificationName:kEXKernelBridgeDidForegroundNotification onAbstractBridge:appRecord.appManager.reactBridge];
299      id<EXAppStateProtocol> appStateModule = [self nativeModuleForAppManager:appRecord.appManager named:@"AppState"];
300      if (appStateModule != nil) {
301        [appStateModule setState:@"active"];
302      }
303      _visibleApp = appRecord;
304    } else {
305      _visibleApp = nil;
306    }
307
308    if (_visibleApp && _visibleApp != _appRegistry.homeAppRecord) {
309      [self _unregisterUnusedAppRecords];
310    }
311  }
312}
313
314- (void)_unregisterUnusedAppRecords
315{
316  for (NSString *recordId in _appRegistry.appEnumerator) {
317    EXKernelAppRecord *record = [_appRegistry recordForId:recordId];
318    if (record && record != _visibleApp) {
319      [_appRegistry unregisterAppWithRecordId:recordId];
320      break;
321    }
322  }
323}
324
325- (void)_handleAppStateDidChange:(NSNotification *)notification
326{
327  NSString *newState;
328
329  if ([notification.name isEqualToString:UIApplicationWillResignActiveNotification]) {
330    newState = @"inactive";
331  } else if ([notification.name isEqualToString:UIApplicationWillEnterForegroundNotification]) {
332    newState = @"background";
333  } else {
334    switch (RCTSharedApplication().applicationState) {
335      case UIApplicationStateActive:
336        newState = @"active";
337        break;
338      case UIApplicationStateBackground: {
339        newState = @"background";
340        break;
341      }
342      default: {
343        newState = @"unknown";
344        break;
345      }
346    }
347  }
348
349  if (_visibleApp) {
350    EXReactAppManager *appManager = _visibleApp.appManager;
351    id<EXAppStateProtocol> appStateModule = [self nativeModuleForAppManager:appManager named:@"AppState"];
352    NSString *lastKnownState;
353    if (appStateModule != nil) {
354      lastKnownState = [appStateModule lastKnownState];
355      [appStateModule setState:newState];
356    }
357    if (!lastKnownState || ![newState isEqualToString:lastKnownState]) {
358      if ([newState isEqualToString:@"active"]) {
359        [_visibleApp.viewController appStateDidBecomeActive];
360        [self _postNotificationName:kEXKernelBridgeDidForegroundNotification onAbstractBridge:appManager.reactBridge];
361      } else if ([newState isEqualToString:@"background"]) {
362        [_visibleApp.viewController appStateDidBecomeInactive];
363        [self _postNotificationName:kEXKernelBridgeDidBackgroundNotification onAbstractBridge:appManager.reactBridge];
364      }
365    }
366  }
367}
368
369
370- (void)_handleRequestReloadVisibleApp:(NSNotification *)notification
371{
372  [self reloadVisibleApp];
373}
374
375- (void)_moveAppToVisible:(EXKernelAppRecord *)appRecord
376{
377  if (_browserController) {
378    [EXUtil performSynchronouslyOnMainThread:^{
379      [self->_browserController moveAppToVisible:appRecord];
380    }];
381  }
382}
383
384#ifndef EX_DETACHED
385#pragma mark - EXDevMenuDelegateProtocol
386
387- (RCTBridge *)mainBridgeForDevMenuManager:(EXDevMenuManager *)manager
388{
389  return _appRegistry.homeAppRecord.appManager.reactBridge;
390}
391
392- (nullable RCTBridge *)appBridgeForDevMenuManager:(EXDevMenuManager *)manager
393{
394  if (_visibleApp == _appRegistry.homeAppRecord) {
395    return nil;
396  }
397  return _visibleApp.appManager.reactBridge;
398}
399
400- (BOOL)devMenuManager:(EXDevMenuManager *)manager canChangeVisibility:(BOOL)visibility
401{
402  return !visibility || _visibleApp != _appRegistry.homeAppRecord;
403}
404
405#endif
406
407@end
408
409NS_ASSUME_NONNULL_END
410