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