1// Copyright 2015-present 650 Industries. All rights reserved.
2
3@import UIKit;
4
5#import "EXAnalytics.h"
6#import "EXAppLoadingView.h"
7#import "EXErrorRecoveryManager.h"
8#import "EXFileDownloader.h"
9#import "EXAppViewController.h"
10#import "EXReactAppManager.h"
11#import "EXErrorView.h"
12#import "EXKernel.h"
13#import "EXKernelAppLoader.h"
14#import "EXKernelUtil.h"
15#import "EXScreenOrientationManager.h"
16#import "EXShellManager.h"
17
18#import <React/RCTUtils.h>
19
20#define EX_INTERFACE_ORIENTATION_USE_MANIFEST 0
21
22const CGFloat kEXAutoReloadDebounceSeconds = 0.1;
23
24NS_ASSUME_NONNULL_BEGIN
25
26@interface EXAppViewController () <EXReactAppManagerUIDelegate, EXKernelAppLoaderDelegate, EXErrorViewDelegate>
27
28@property (nonatomic, assign) BOOL isLoading;
29@property (nonatomic, weak) EXKernelAppRecord *appRecord;
30@property (nonatomic, strong) EXAppLoadingView *loadingView;
31@property (nonatomic, strong) EXErrorView *errorView;
32@property (nonatomic, assign) UIInterfaceOrientationMask supportedInterfaceOrientations; // override super
33@property (nonatomic, strong) NSTimer *tmrAutoReloadDebounce;
34
35@end
36
37@implementation EXAppViewController
38
39@synthesize supportedInterfaceOrientations = _supportedInterfaceOrientations;
40
41#pragma mark - Lifecycle
42
43- (instancetype)initWithAppRecord:(EXKernelAppRecord *)record
44{
45  if (self = [super init]) {
46    _appRecord = record;
47    _supportedInterfaceOrientations = EX_INTERFACE_ORIENTATION_USE_MANIFEST;
48  }
49  return self;
50}
51
52- (void)dealloc
53{
54  [self _invalidateRecoveryTimer];
55  [[NSNotificationCenter defaultCenter] removeObserver:self];
56}
57
58- (void)viewDidLoad
59{
60  [super viewDidLoad];
61  _loadingView = [[EXAppLoadingView alloc] initWithAppRecord:_appRecord];
62  [self.view addSubview:_loadingView];
63  _appRecord.appManager.delegate = self;
64  self.isLoading = YES;
65}
66
67- (void)viewDidAppear:(BOOL)animated
68{
69  [super viewDidAppear:animated];
70  if (_appRecord && _appRecord.status == kEXKernelAppRecordStatusNew) {
71    _appRecord.appLoader.delegate = self;
72    [self refresh];
73  }
74}
75
76- (BOOL)shouldAutorotate
77{
78  return YES;
79}
80
81- (void)viewWillLayoutSubviews
82{
83  [super viewWillLayoutSubviews];
84  if (_loadingView) {
85    _loadingView.frame = CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height);
86  }
87  if (_contentView) {
88    _contentView.frame = CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height);
89  }
90}
91
92#pragma mark - Public
93
94- (void)maybeShowError:(NSError *)error
95{
96  self.isLoading = NO;
97  if ([self _willAutoRecoverFromError:error]) {
98    return;
99  }
100  BOOL isNetworkError = ([error.domain isEqualToString:(NSString *)kCFErrorDomainCFNetwork] ||
101                         [error.domain isEqualToString:EXNetworkErrorDomain]);
102  if (isNetworkError) {
103    // show a human-readable reachability error
104    dispatch_async(dispatch_get_main_queue(), ^{
105      [self _showErrorWithType:kEXFatalErrorTypeLoading error:error];
106    });
107  } else if ([error.domain isEqualToString:@"JSServer"] && [_appRecord.appManager enablesDeveloperTools]) {
108    // RCTRedBox already handled this
109  } else if ([error.domain rangeOfString:RCTErrorDomain].length > 0 && [_appRecord.appManager enablesDeveloperTools]) {
110    // RCTRedBox already handled this
111  } else {
112    dispatch_async(dispatch_get_main_queue(), ^{
113      [self _showErrorWithType:kEXFatalErrorTypeException error:error];
114    });
115  }
116}
117
118- (void)_rebuildBridge
119{
120  [self _invalidateRecoveryTimer];
121  [[EXKernel sharedInstance] logAnalyticsEvent:@"LOAD_EXPERIENCE" forAppRecord:_appRecord];
122  [_appRecord.appManager rebuildBridge];
123}
124
125- (void)refresh
126{
127  self.isLoading = YES;
128  [self _invalidateRecoveryTimer];
129  [_appRecord.appLoader request];
130}
131
132- (void)appStateDidBecomeActive
133{
134  dispatch_async(dispatch_get_main_queue(), ^{
135    [self _enforceDesiredDeviceOrientation];
136  });
137  [_appRecord.appManager appStateDidBecomeActive];
138}
139
140- (void)appStateDidBecomeInactive
141{
142  [_appRecord.appManager appStateDidBecomeInactive];
143}
144
145#pragma mark - EXKernelAppLoaderDelegate
146
147- (void)appLoader:(EXKernelAppLoader *)appLoader didLoadOptimisticManifest:(NSDictionary *)manifest
148{
149  if ([EXKernel sharedInstance].browserController) {
150    [[EXKernel sharedInstance].browserController addHistoryItemWithUrl:appLoader.manifestUrl manifest:manifest];
151  }
152  dispatch_async(dispatch_get_main_queue(), ^{
153    _loadingView.manifest = manifest;
154    [self _enforceDesiredDeviceOrientation];
155    [self _rebuildBridge];
156  });
157}
158
159- (void)appLoader:(EXKernelAppLoader *)appLoader didLoadBundleWithProgress:(EXLoadingProgress *)progress
160{
161  dispatch_async(dispatch_get_main_queue(), ^{
162    [_loadingView updateStatusWithProgress:progress];
163  });
164}
165
166- (void)appLoader:(EXKernelAppLoader *)appLoader didFinishLoadingManifest:(NSDictionary *)manifest bundle:(NSData *)data
167{
168  if (_appRecord.appManager.status == kEXReactAppManagerStatusBridgeLoading) {
169    [_appRecord.appManager appLoaderFinished];
170  }
171}
172
173- (void)appLoader:(EXKernelAppLoader *)appLoader didFailWithError:(NSError *)error
174{
175  if (_appRecord.appManager.status == kEXReactAppManagerStatusBridgeLoading) {
176    [_appRecord.appManager appLoaderFailedWithError:error];
177  }
178  [self maybeShowError:error];
179}
180
181#pragma mark - EXReactAppManagerDelegate
182
183- (void)reactAppManagerIsReadyForLoad:(EXReactAppManager *)appManager
184{
185  UIView *reactView = appManager.rootView;
186  reactView.frame = CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height);
187  reactView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
188  reactView.backgroundColor = [UIColor clearColor];
189
190  [_contentView removeFromSuperview];
191  _contentView = reactView;
192  [self.view addSubview:_contentView];
193  [self.view sendSubviewToBack:_contentView];
194
195  [reactView becomeFirstResponder];
196}
197
198- (void)reactAppManagerStartedLoadingJavaScript:(EXReactAppManager *)appManager
199{
200  EXAssertMainThread();
201  self.isLoading = YES;
202}
203
204- (void)reactAppManagerFinishedLoadingJavaScript:(EXReactAppManager *)appManager
205{
206  EXAssertMainThread();
207  self.isLoading = NO;
208  if ([EXKernel sharedInstance].browserController) {
209    [[EXKernel sharedInstance].browserController appDidFinishLoadingSuccessfully:_appRecord];
210  }
211}
212
213- (void)reactAppManager:(EXReactAppManager *)appManager failedToLoadJavaScriptWithError:(NSError *)error
214{
215  EXAssertMainThread();
216  [self maybeShowError:error];
217}
218
219- (void)reactAppManagerDidInvalidate:(EXReactAppManager *)appManager
220{
221
222}
223
224- (void)errorViewDidSelectRetry:(EXErrorView *)errorView
225{
226  [self refresh];
227}
228
229#pragma mark - orientation
230
231- (UIInterfaceOrientationMask)supportedInterfaceOrientations
232{
233  if (_supportedInterfaceOrientations != EX_INTERFACE_ORIENTATION_USE_MANIFEST) {
234    return _supportedInterfaceOrientations;
235  }
236  if (_appRecord.appLoader.manifest) {
237    NSString *orientationConfig = _appRecord.appLoader.manifest[@"orientation"];
238    if ([orientationConfig isEqualToString:@"portrait"]) {
239      // lock to portrait
240      return UIInterfaceOrientationMaskPortrait;
241    } else if ([orientationConfig isEqualToString:@"landscape"]) {
242      // lock to landscape
243      return UIInterfaceOrientationMaskLandscape;
244    }
245  }
246  // no config or default value: allow autorotation
247  return UIInterfaceOrientationMaskAllButUpsideDown;
248}
249
250- (void)setSupportedInterfaceOrientations:(UIInterfaceOrientationMask)supportedInterfaceOrientations
251{
252  _supportedInterfaceOrientations = supportedInterfaceOrientations;
253  [self _enforceDesiredDeviceOrientation];
254}
255
256- (void)_enforceDesiredDeviceOrientation
257{
258  RCTAssertMainQueue();
259  UIInterfaceOrientationMask mask = [self supportedInterfaceOrientations];
260  UIDeviceOrientation currentOrientation = [[UIDevice currentDevice] orientation];
261  UIInterfaceOrientation newOrientation = UIInterfaceOrientationUnknown;
262  switch (mask) {
263    case UIInterfaceOrientationMaskPortrait:
264      if (!UIDeviceOrientationIsPortrait(currentOrientation)) {
265        newOrientation = UIInterfaceOrientationPortrait;
266      }
267      break;
268    case UIInterfaceOrientationMaskPortraitUpsideDown:
269      newOrientation = UIInterfaceOrientationPortraitUpsideDown;
270      break;
271    case UIInterfaceOrientationMaskLandscape:
272      if (!UIDeviceOrientationIsLandscape(currentOrientation)) {
273        newOrientation = UIInterfaceOrientationLandscapeLeft;
274      }
275      break;
276    case UIInterfaceOrientationMaskLandscapeLeft:
277      newOrientation = UIInterfaceOrientationLandscapeLeft;
278      break;
279    case UIInterfaceOrientationMaskLandscapeRight:
280      newOrientation = UIInterfaceOrientationLandscapeRight;
281      break;
282    case UIInterfaceOrientationMaskAllButUpsideDown:
283      if (currentOrientation == UIDeviceOrientationFaceDown) {
284        newOrientation = UIInterfaceOrientationPortrait;
285      }
286      break;
287    default:
288      break;
289  }
290  if (newOrientation != UIInterfaceOrientationUnknown) {
291    [[UIDevice currentDevice] setValue:@(newOrientation) forKey:@"orientation"];
292  }
293  [UIViewController attemptRotationToDeviceOrientation];
294}
295
296#pragma mark - Internal
297
298- (void)_showErrorWithType:(EXFatalErrorType)type error:(nullable NSError *)error
299{
300  EXAssertMainThread();
301  if (_errorView && _contentView == _errorView) {
302    // already showing, just update
303    _errorView.type = type;
304    _errorView.error = error;
305  } {
306    [_contentView removeFromSuperview];
307    if (!_errorView) {
308      _errorView = [[EXErrorView alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height)];
309      _errorView.delegate = self;
310      _errorView.appRecord = _appRecord;
311    }
312    _errorView.type = type;
313    _errorView.error = error;
314    _contentView = _errorView;
315    [self.view addSubview:_contentView];
316    [[EXAnalytics sharedInstance] logErrorVisibleEvent];
317  }
318}
319
320- (void)setIsLoading:(BOOL)isLoading
321{
322  _isLoading = isLoading;
323  dispatch_async(dispatch_get_main_queue(), ^{
324    if (isLoading) {
325      self.loadingView.hidden = NO;
326      [self.view bringSubviewToFront:self.loadingView];
327    } else {
328      self.loadingView.hidden = YES;
329    }
330  });
331}
332
333#pragma mark - error recovery
334
335- (BOOL)_willAutoRecoverFromError:(NSError *)error
336{
337  if (![_appRecord.appManager enablesDeveloperTools]) {
338    BOOL shouldRecover = [[EXKernel sharedInstance].serviceRegistry.errorRecoveryManager experienceIdShouldReloadOnError:_appRecord.experienceId];
339    if (shouldRecover) {
340      [self _invalidateRecoveryTimer];
341      _tmrAutoReloadDebounce = [NSTimer scheduledTimerWithTimeInterval:kEXAutoReloadDebounceSeconds
342                                                                target:self
343                                                              selector:@selector(refresh)
344                                                              userInfo:nil
345                                                               repeats:NO];
346    }
347    return shouldRecover;
348  }
349  return NO;
350}
351
352- (void)_invalidateRecoveryTimer
353{
354  if (_tmrAutoReloadDebounce) {
355    [_tmrAutoReloadDebounce invalidate];
356    _tmrAutoReloadDebounce = nil;
357  }
358}
359
360@end
361
362NS_ASSUME_NONNULL_END
363