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