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