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