xref: /expo/ios/Exponent/Kernel/Core/EXKernel.m (revision 13308734)
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    [self _dispatchJSEvent:@"Exponent.notification" body:notification.properties toApp:destinationApp];
177    [self _moveAppToVisible:destinationApp];
178    return YES;
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- (void)_dispatchJSEvent:(NSString *)eventName body:(NSDictionary *)eventBody toApp:(EXKernelAppRecord *)appRecord
219{
220  [appRecord.appManager.reactBridge enqueueJSCall:@"RCTDeviceEventEmitter.emit"
221                                             args:eventBody ? @[eventName, eventBody] : @[eventName]];
222}
223
224#pragma mark - App props
225
226- (nullable NSDictionary *)initialAppPropsFromLaunchOptions:(NSDictionary *)launchOptions
227{
228  return nil;
229}
230
231#pragma mark - App State
232
233- (EXKernelAppRecord *)createNewAppWithUrl:(NSURL *)url initialProps:(nullable NSDictionary *)initialProps
234{
235  NSString *recordId = [_appRegistry registerAppWithManifestUrl:url initialProps:initialProps];
236  EXKernelAppRecord *record = [_appRegistry recordForId:recordId];
237  [self _moveAppToVisible:record];
238  return record;
239}
240
241- (void)switchTasks
242{
243  if (!_browserController) {
244    return;
245  }
246
247  if (_visibleApp != _appRegistry.homeAppRecord) {
248    [EXUtil performSynchronouslyOnMainThread:^{
249      [self->_browserController toggleMenuWithCompletion:nil];
250    }];
251  } else {
252    EXKernelAppRegistry *appRegistry = [EXKernel sharedInstance].appRegistry;
253    for (NSString *recordId in appRegistry.appEnumerator) {
254      EXKernelAppRecord *record = [appRegistry recordForId:recordId];
255      // foreground the first thing we find
256      [self _moveAppToVisible:record];
257    }
258  }
259}
260
261- (void)reloadAppWithExperienceId:(NSString *)experienceId
262{
263  EXKernelAppRecord *appRecord = [_appRegistry newestRecordWithExperienceId:experienceId];
264  if (_browserController) {
265    [self createNewAppWithUrl:appRecord.appLoader.manifestUrl initialProps:nil];
266  } else if (_appRegistry.standaloneAppRecord && appRecord == _appRegistry.standaloneAppRecord) {
267    [appRecord.viewController refresh];
268  }
269}
270
271- (void)reloadAppFromCacheWithExperienceId:(NSString *)experienceId
272{
273  EXKernelAppRecord *appRecord = [_appRegistry newestRecordWithExperienceId:experienceId];
274  [appRecord.viewController reloadFromCache];
275}
276
277- (void)viewController:(__unused EXViewController *)vc didNavigateAppToVisible:(EXKernelAppRecord *)appRecord
278{
279  EXKernelAppRecord *appRecordPreviouslyVisible = _visibleApp;
280  if (appRecord != appRecordPreviouslyVisible) {
281    if (appRecordPreviouslyVisible) {
282      [appRecordPreviouslyVisible.viewController appStateDidBecomeInactive];
283      [self _postNotificationName:kEXKernelBridgeDidBackgroundNotification onAbstractBridge:appRecordPreviouslyVisible.appManager.reactBridge];
284      id<EXAppStateProtocol> appStateModule = [self nativeModuleForAppManager:appRecordPreviouslyVisible.appManager named:@"AppState"];
285      if (appStateModule != nil) {
286        [appStateModule setState:@"background"];
287      }
288    }
289    if (appRecord) {
290      [appRecord.viewController appStateDidBecomeActive];
291      [self _postNotificationName:kEXKernelBridgeDidForegroundNotification onAbstractBridge:appRecord.appManager.reactBridge];
292      id<EXAppStateProtocol> appStateModule = [self nativeModuleForAppManager:appRecord.appManager named:@"AppState"];
293      if (appStateModule != nil) {
294        [appStateModule setState:@"active"];
295      }
296      _visibleApp = appRecord;
297      [[EXAnalytics sharedInstance] logAppVisibleEvent];
298    } else {
299      _visibleApp = nil;
300    }
301
302    if (_visibleApp && _visibleApp != _appRegistry.homeAppRecord) {
303      [self _unregisterUnusedAppRecords];
304    }
305  }
306}
307
308- (void)_unregisterUnusedAppRecords
309{
310  for (NSString *recordId in _appRegistry.appEnumerator) {
311    EXKernelAppRecord *record = [_appRegistry recordForId:recordId];
312    if (record && record != _visibleApp) {
313      [_appRegistry unregisterAppWithRecordId:recordId];
314      break;
315    }
316  }
317}
318
319- (void)_handleAppStateDidChange:(NSNotification *)notification
320{
321  NSString *newState;
322
323  if ([notification.name isEqualToString:UIApplicationWillResignActiveNotification]) {
324    newState = @"inactive";
325  } else if ([notification.name isEqualToString:UIApplicationWillEnterForegroundNotification]) {
326    newState = @"background";
327  } else {
328    switch (RCTSharedApplication().applicationState) {
329      case UIApplicationStateActive:
330        newState = @"active";
331        break;
332      case UIApplicationStateBackground: {
333        newState = @"background";
334        break;
335      }
336      default: {
337        newState = @"unknown";
338        break;
339      }
340    }
341  }
342
343  if (_visibleApp) {
344    EXReactAppManager *appManager = _visibleApp.appManager;
345    id<EXAppStateProtocol> appStateModule = [self nativeModuleForAppManager:appManager named:@"AppState"];
346    NSString *lastKnownState;
347    if (appStateModule != nil) {
348      lastKnownState = [appStateModule lastKnownState];
349      [appStateModule setState:newState];
350    }
351    if (!lastKnownState || ![newState isEqualToString:lastKnownState]) {
352      if ([newState isEqualToString:@"active"]) {
353        [_visibleApp.viewController appStateDidBecomeActive];
354        [self _postNotificationName:kEXKernelBridgeDidForegroundNotification onAbstractBridge:appManager.reactBridge];
355      } else if ([newState isEqualToString:@"background"]) {
356        [_visibleApp.viewController appStateDidBecomeInactive];
357        [self _postNotificationName:kEXKernelBridgeDidBackgroundNotification onAbstractBridge:appManager.reactBridge];
358      }
359    }
360  }
361}
362
363- (void)_moveAppToVisible:(EXKernelAppRecord *)appRecord
364{
365  if (_browserController) {
366    [EXUtil performSynchronouslyOnMainThread:^{
367      [self->_browserController moveAppToVisible:appRecord];
368    }];
369  }
370}
371
372@end
373
374NS_ASSUME_NONNULL_END
375