xref: /expo/ios/Exponent/Kernel/Core/EXKernel.m (revision dae3fa91)
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#import "EXHomeModule.h"
14
15#import <EXConstants/EXConstantsService.h>
16#import <React/RCTBridge+Private.h>
17#import <React/RCTEventDispatcher.h>
18#import <React/RCTModuleData.h>
19#import <React/RCTUtils.h>
20
21#ifndef EX_DETACHED
22// Kernel is DevMenu's delegate only in non-detached builds.
23#import "EXDevMenuManager.h"
24#import "EXDevMenuDelegateProtocol.h"
25
26@interface EXKernel () <EXDevMenuDelegateProtocol>
27@end
28#endif
29
30NS_ASSUME_NONNULL_BEGIN
31
32NSString *kEXKernelErrorDomain = @"EXKernelErrorDomain";
33NSString *kEXKernelShouldForegroundTaskEvent = @"foregroundTask";
34NSString * const kEXKernelClearJSCacheUserDefaultsKey = @"EXKernelClearJSCacheUserDefaultsKey";
35NSString * const kEXReloadActiveAppRequest = @"EXReloadActiveAppRequest";
36
37@interface EXKernel () <EXKernelAppRegistryDelegate>
38
39@end
40
41// Protocol that should be implemented by all versions of EXAppState class.
42@protocol EXAppStateProtocol
43
44@property (nonatomic, strong, readonly) NSString *lastKnownState;
45
46- (void)setState:(NSString *)state;
47
48@end
49
50@implementation EXKernel
51
52+ (instancetype)sharedInstance
53{
54  static EXKernel *theKernel;
55  static dispatch_once_t once;
56  dispatch_once(&once, ^{
57    if (!theKernel) {
58      theKernel = [[EXKernel alloc] init];
59    }
60  });
61  return theKernel;
62}
63
64- (instancetype)init
65{
66  if (self = [super init]) {
67    // init app registry: keep track of RN bridges we are running
68    _appRegistry = [[EXKernelAppRegistry alloc] init];
69    _appRegistry.delegate = self;
70
71    // init service registry: classes which manage shared resources among all bridges
72    _serviceRegistry = [[EXKernelServiceRegistry alloc] init];
73
74#ifndef EX_DETACHED
75    // Set the delegate of dev menu manager. Maybe it should be a separate class? Will see later once the delegate protocol gets too big.
76    [[EXDevMenuManager sharedInstance] setDelegate:self];
77#endif
78
79    // register for notifications to request reloading the visible app
80    [[NSNotificationCenter defaultCenter] addObserver:self
81                                             selector:@selector(_handleRequestReloadVisibleApp:)
82                                                 name:kEXReloadActiveAppRequest
83                                               object:nil];
84
85    for (NSString *name in @[UIApplicationDidBecomeActiveNotification,
86                             UIApplicationDidEnterBackgroundNotification,
87                             UIApplicationDidFinishLaunchingNotification,
88                             UIApplicationWillResignActiveNotification,
89                             UIApplicationWillEnterForegroundNotification]) {
90
91      [[NSNotificationCenter defaultCenter] addObserver:self
92                                               selector:@selector(_handleAppStateDidChange:)
93                                                   name:name
94                                                 object:nil];
95    }
96    NSLog(@"Expo iOS Runtime Version %@", [EXBuildConstants sharedInstance].expoRuntimeVersion);
97  }
98  return self;
99}
100
101- (void)dealloc
102{
103  [[NSNotificationCenter defaultCenter] removeObserver:self];
104}
105
106#pragma mark - Misc
107
108- (void)logAnalyticsEvent:(NSString *)eventId forAppRecord:(EXKernelAppRecord *)appRecord
109{
110  if (_appRegistry.homeAppRecord && appRecord == _appRegistry.homeAppRecord) {
111    return;
112  }
113  NSString *validatedSdkVersion = [[EXVersions sharedInstance] availableSdkVersionForManifest:appRecord.appLoader.manifest];
114  NSDictionary *props = (validatedSdkVersion) ? @{ @"SDK_VERSION": validatedSdkVersion } : @{};
115  [[EXAnalytics sharedInstance] logEvent:eventId
116                             manifestUrl:appRecord.appLoader.manifestUrl
117                         eventProperties:props];
118}
119
120#pragma mark - bridge registry delegate
121
122- (void)appRegistry:(EXKernelAppRegistry *)registry didRegisterAppRecord:(EXKernelAppRecord *)appRecord
123{
124  // forward to service registry
125  [_serviceRegistry appRegistry:registry didRegisterAppRecord:appRecord];
126}
127
128- (void)appRegistry:(EXKernelAppRegistry *)registry willUnregisterAppRecord:(EXKernelAppRecord *)appRecord
129{
130  // forward to service registry
131  [_serviceRegistry appRegistry:registry willUnregisterAppRecord:appRecord];
132}
133
134#pragma mark - Interfacing with JS
135
136- (void)sendUrl:(NSString *)urlString toAppRecord:(EXKernelAppRecord *)app
137{
138  // fire a Linking url event on this (possibly versioned) bridge
139  EXReactAppManager *appManager = app.appManager;
140  id linkingModule = [self nativeModuleForAppManager:appManager named:@"LinkingManager"];
141  if (!linkingModule) {
142    DDLogError(@"Could not find the Linking module to open URL (%@)", urlString);
143  } else if ([linkingModule respondsToSelector:@selector(dispatchOpenUrlEvent:)]) {
144    [linkingModule dispatchOpenUrlEvent:[NSURL URLWithString:urlString]];
145  } else {
146    DDLogError(@"Linking module doesn't support the API we use to open URL (%@)", urlString);
147  }
148  [self _moveAppToVisible:app];
149}
150
151- (id)nativeModuleForAppManager:(EXReactAppManager *)appManager named:(NSString *)moduleName
152{
153  id destinationBridge = appManager.reactBridge;
154
155  if ([destinationBridge respondsToSelector:@selector(batchedBridge)]) {
156    id batchedBridge = [destinationBridge batchedBridge];
157    id moduleData = [batchedBridge moduleDataForName:moduleName];
158
159    // React Native before SDK 11 didn't strip the "RCT" prefix from module names
160    if (!moduleData && ![moduleName hasPrefix:@"RCT"]) {
161      moduleData = [batchedBridge moduleDataForName:[@"RCT" stringByAppendingString:moduleName]];
162    }
163
164    if (moduleData) {
165      return [moduleData instance];
166    }
167  } else {
168    // bridge can be null if the record is in an error state and never created a bridge.
169    if (destinationBridge) {
170      DDLogError(@"Bridge does not support the API we use to get its underlying batched bridge");
171    }
172  }
173  return nil;
174}
175
176- (BOOL)sendNotification:(EXPendingNotification *)notification
177{
178  EXKernelAppRecord *destinationApp = [_appRegistry standaloneAppRecord] ?: [_appRegistry newestRecordWithScopeKey:notification.scopeKey];
179
180  // This allows home app record to receive notification events as well.
181  if (!destinationApp && [_appRegistry.homeAppRecord.scopeKey isEqualToString:notification.scopeKey]) {
182    destinationApp = _appRegistry.homeAppRecord;
183  }
184
185  if (destinationApp) {
186    // send the body to the already-open experience
187    BOOL success = [self _dispatchJSEvent:@"Exponent.notification" body:notification.properties toApp:destinationApp];
188    [self _moveAppToVisible:destinationApp];
189    return success;
190  } else {
191    // no app is currently running for this experience id.
192    // if we're Expo Go, we can query Home for a past experience in the user's history, and route the notification there.
193    if (_browserController) {
194      __weak typeof(self) weakSelf = self;
195      [_browserController getHistoryUrlForScopeKey:notification.scopeKey completion:^(NSString *urlString) {
196        if (urlString) {
197          NSURL *url = [NSURL URLWithString:urlString];
198          if (url) {
199            [weakSelf createNewAppWithUrl:url initialProps:@{ @"notification": notification.properties }];
200          }
201        }
202      }];
203      // If we're here, there's no active app in appRegistry matching notification.experienceId
204      // and we are in Expo Go, since _browserController is not nil.
205      // If so, we can return YES (meaning "notification has been successfully dispatched")
206      // because we pass the notification as initialProps in completion handler
207      // of getHistoryUrlForExperienceId:. If urlString passed to completion handler is empty,
208      // the notification is forgotten (this is the expected behavior).
209      return YES;
210    }
211  }
212
213  return NO;
214}
215
216/**
217 *  If the bridge has a batchedBridge or parentBridge selector, posts the notification on that object as well.
218 */
219- (void)_postNotificationName: (NSNotificationName)name onAbstractBridge: (id)bridge
220{
221  [[NSNotificationCenter defaultCenter] postNotificationName:name object:bridge];
222  if ([bridge respondsToSelector:@selector(batchedBridge)]) {
223    [[NSNotificationCenter defaultCenter] postNotificationName:name object:[bridge batchedBridge]];
224  } else if ([bridge respondsToSelector:@selector(parentBridge)]) {
225    [[NSNotificationCenter defaultCenter] postNotificationName:name object:[bridge parentBridge]];
226  }
227}
228
229- (BOOL)_dispatchJSEvent:(NSString *)eventName body:(NSDictionary *)eventBody toApp:(EXKernelAppRecord *)appRecord
230{
231  if (!appRecord.appManager.reactBridge) {
232    return NO;
233  }
234  [appRecord.appManager.reactBridge enqueueJSCall:@"RCTDeviceEventEmitter.emit"
235                                             args:eventBody ? @[eventName, eventBody] : @[eventName]];
236  return YES;
237}
238
239#pragma mark - App props
240
241- (nullable NSDictionary *)initialAppPropsFromLaunchOptions:(NSDictionary *)launchOptions
242{
243  return nil;
244}
245
246#pragma mark - App State
247
248- (EXKernelAppRecord *)createNewAppWithUrl:(NSURL *)url initialProps:(nullable NSDictionary *)initialProps
249{
250  NSString *recordId = [_appRegistry registerAppWithManifestUrl:url initialProps:initialProps];
251  EXKernelAppRecord *record = [_appRegistry recordForId:recordId];
252  [self _moveAppToVisible:record];
253  return record;
254}
255
256
257- (void)reloadVisibleApp
258{
259  if (_browserController) {
260    [EXUtil performSynchronouslyOnMainThread:^{
261      [self->_browserController reloadVisibleApp];
262    }];
263  }
264}
265
266- (void)switchTasks
267{
268  if (!_browserController) {
269    return;
270  }
271
272  if (_visibleApp != _appRegistry.homeAppRecord) {
273#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.
274    [EXUtil performSynchronouslyOnMainThread:^{
275      [[EXDevMenuManager sharedInstance] toggle];
276    }];
277#endif
278  } else {
279    EXKernelAppRegistry *appRegistry = [EXKernel sharedInstance].appRegistry;
280    for (NSString *recordId in appRegistry.appEnumerator) {
281      EXKernelAppRecord *record = [appRegistry recordForId:recordId];
282      // foreground the first thing we find
283      [self _moveAppToVisible:record];
284    }
285  }
286}
287
288- (void)reloadAppWithScopeKey:(NSString *)scopeKey
289{
290  EXKernelAppRecord *appRecord = [_appRegistry newestRecordWithScopeKey:scopeKey];
291  if (_browserController) {
292    [self createNewAppWithUrl:appRecord.appLoader.manifestUrl initialProps:nil];
293  } else if (_appRegistry.standaloneAppRecord && appRecord == _appRegistry.standaloneAppRecord) {
294    [appRecord.viewController refresh];
295  }
296}
297
298- (void)reloadAppFromCacheWithScopeKey:(NSString *)scopeKey
299{
300  EXKernelAppRecord *appRecord = [_appRegistry newestRecordWithScopeKey:scopeKey];
301  [appRecord.viewController reloadFromCache];
302}
303
304- (void)viewController:(__unused EXViewController *)vc didNavigateAppToVisible:(EXKernelAppRecord *)appRecord
305{
306  EXKernelAppRecord *appRecordPreviouslyVisible = _visibleApp;
307  if (appRecord != appRecordPreviouslyVisible) {
308    if (appRecordPreviouslyVisible) {
309      [appRecordPreviouslyVisible.viewController appStateDidBecomeInactive];
310      [self _postNotificationName:kEXKernelBridgeDidBackgroundNotification onAbstractBridge:appRecordPreviouslyVisible.appManager.reactBridge];
311      id<EXAppStateProtocol> appStateModule = [self nativeModuleForAppManager:appRecordPreviouslyVisible.appManager named:@"AppState"];
312      if (appStateModule != nil) {
313        [appStateModule setState:@"background"];
314      }
315    }
316    if (appRecord) {
317      [appRecord.viewController appStateDidBecomeActive];
318      [self _postNotificationName:kEXKernelBridgeDidForegroundNotification onAbstractBridge:appRecord.appManager.reactBridge];
319      id<EXAppStateProtocol> appStateModule = [self nativeModuleForAppManager:appRecord.appManager named:@"AppState"];
320      if (appStateModule != nil) {
321        [appStateModule setState:@"active"];
322      }
323      _visibleApp = appRecord;
324      [[EXAnalytics sharedInstance] logAppVisibleEvent];
325    } else {
326      _visibleApp = nil;
327    }
328
329    if (_visibleApp && _visibleApp != _appRegistry.homeAppRecord) {
330      [self _unregisterUnusedAppRecords];
331    }
332  }
333}
334
335- (void)_unregisterUnusedAppRecords
336{
337  for (NSString *recordId in _appRegistry.appEnumerator) {
338    EXKernelAppRecord *record = [_appRegistry recordForId:recordId];
339    if (record && record != _visibleApp) {
340      [_appRegistry unregisterAppWithRecordId:recordId];
341      break;
342    }
343  }
344}
345
346- (void)_handleAppStateDidChange:(NSNotification *)notification
347{
348  NSString *newState;
349
350  if ([notification.name isEqualToString:UIApplicationWillResignActiveNotification]) {
351    newState = @"inactive";
352  } else if ([notification.name isEqualToString:UIApplicationWillEnterForegroundNotification]) {
353    newState = @"background";
354  } else {
355    switch (RCTSharedApplication().applicationState) {
356      case UIApplicationStateActive:
357        newState = @"active";
358        break;
359      case UIApplicationStateBackground: {
360        newState = @"background";
361        break;
362      }
363      default: {
364        newState = @"unknown";
365        break;
366      }
367    }
368  }
369
370  if (_visibleApp) {
371    EXReactAppManager *appManager = _visibleApp.appManager;
372    id<EXAppStateProtocol> appStateModule = [self nativeModuleForAppManager:appManager named:@"AppState"];
373    NSString *lastKnownState;
374    if (appStateModule != nil) {
375      lastKnownState = [appStateModule lastKnownState];
376      [appStateModule setState:newState];
377    }
378    if (!lastKnownState || ![newState isEqualToString:lastKnownState]) {
379      if ([newState isEqualToString:@"active"]) {
380        [_visibleApp.viewController appStateDidBecomeActive];
381        [self _postNotificationName:kEXKernelBridgeDidForegroundNotification onAbstractBridge:appManager.reactBridge];
382      } else if ([newState isEqualToString:@"background"]) {
383        [_visibleApp.viewController appStateDidBecomeInactive];
384        [self _postNotificationName:kEXKernelBridgeDidBackgroundNotification onAbstractBridge:appManager.reactBridge];
385      }
386    }
387  }
388}
389
390
391- (void)_handleRequestReloadVisibleApp:(NSNotification *)notification
392{
393  [self reloadVisibleApp];
394}
395
396- (void)_moveAppToVisible:(EXKernelAppRecord *)appRecord
397{
398  if (_browserController) {
399    [EXUtil performSynchronouslyOnMainThread:^{
400      [self->_browserController moveAppToVisible:appRecord];
401    }];
402  }
403}
404
405#ifndef EX_DETACHED
406#pragma mark - EXDevMenuDelegateProtocol
407
408- (RCTBridge *)mainBridgeForDevMenuManager:(EXDevMenuManager *)manager
409{
410  return _appRegistry.homeAppRecord.appManager.reactBridge;
411}
412
413- (nullable RCTBridge *)appBridgeForDevMenuManager:(EXDevMenuManager *)manager
414{
415  if (_visibleApp == _appRegistry.homeAppRecord) {
416    return nil;
417  }
418  return _visibleApp.appManager.reactBridge;
419}
420
421- (BOOL)devMenuManager:(EXDevMenuManager *)manager canChangeVisibility:(BOOL)visibility
422{
423  return !visibility || _visibleApp != _appRegistry.homeAppRecord;
424}
425
426#endif
427
428@end
429
430NS_ASSUME_NONNULL_END
431