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