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