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