1// Copyright 2018-present 650 Industries. All rights reserved.
2
3#import <EXBarCodeScanner/EXBarCodeScannerView.h>
4#import <EXBarCodeScanner/EXBarCodeScanner.h>
5#import <EXBarCodeScanner/EXBarCodeScannerUtils.h>
6#import <EXPermissionsInterface/EXPermissionsInterface.h>
7#import <EXCore/EXAppLifecycleService.h>
8#import <EXCore/EXUtilities.h>
9
10@interface EXBarCodeScannerView ()
11
12@property (nonatomic, strong) dispatch_queue_t sessionQueue;
13@property (nonatomic, strong) AVCaptureSession *session;
14@property (nonatomic, strong) AVCaptureDeviceInput *videoCaptureDeviceInput;
15@property (nonatomic, strong) AVCaptureMetadataOutput *metadataOutput;
16@property (nonatomic, strong) AVCaptureVideoPreviewLayer *previewLayer;
17@property (nonatomic, strong) id runtimeErrorHandlingObserver;
18@property (nonatomic, strong) EXBarCodeScanner *barCodeScanner;
19
20@property (nonatomic, weak) EXModuleRegistry *moduleRegistry;
21@property (nonatomic, weak) id<EXPermissionsInterface> permissionsManager;
22@property (nonatomic, weak) id<EXAppLifecycleService> lifecycleManager;
23
24@property (nonatomic, assign, getter=isSessionPaused) BOOL paused;
25
26@property (nonatomic, copy) EXDirectEventBlock onCameraReady;
27@property (nonatomic, copy) EXDirectEventBlock onMountError;
28@property (nonatomic, copy) EXDirectEventBlock onBarCodeScanned;
29
30@end
31
32@implementation EXBarCodeScannerView
33
34- (instancetype)initWithModuleRegistry:(EXModuleRegistry *)moduleRegistry
35{
36  if ((self = [super init])) {
37    _presetCamera = AVCaptureDevicePositionBack;
38    _moduleRegistry = moduleRegistry;
39    _session = [AVCaptureSession new];
40    _sessionQueue = dispatch_queue_create("barCodeScannerQueue", DISPATCH_QUEUE_SERIAL);
41    _lifecycleManager = [moduleRegistry getModuleImplementingProtocol:@protocol(EXAppLifecycleListener)];
42    _permissionsManager = [moduleRegistry getModuleImplementingProtocol:@protocol(EXPermissionsInterface)];
43    _barCodeScanner = [self createBarCodeScanner];
44
45#if !(TARGET_IPHONE_SIMULATOR)
46    _previewLayer = [AVCaptureVideoPreviewLayer layerWithSession:_session];
47    _previewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill;
48    _previewLayer.needsDisplayOnBoundsChange = YES;
49#endif
50    _paused = NO;
51
52    [_lifecycleManager registerAppLifecycleListener:self];
53    [[NSNotificationCenter defaultCenter] addObserver:self
54                                             selector:@selector(orientationChanged:)
55                                                 name:UIDeviceOrientationDidChangeNotification
56                                               object:nil];
57
58    [self changePreviewOrientation:[UIApplication sharedApplication].statusBarOrientation];
59    [self initializeSession];
60  }
61  return self;
62}
63
64# pragma mark - events
65
66- (void)onReady
67{
68  if (_onCameraReady) {
69    _onCameraReady(nil);
70  }
71}
72
73- (void)onMountingError:(NSDictionary *)event
74{
75  if (_onMountError) {
76    _onMountError(event);
77  }
78}
79
80- (void)onBarCodeScanned:(NSDictionary *)event
81{
82  if (_onBarCodeScanned) {
83    _onBarCodeScanned(event);
84  }
85}
86
87# pragma mark - JS properties setters
88
89- (void)setPresetCamera:(NSInteger)presetCamera
90{
91  if (_presetCamera == presetCamera) {
92    return;
93  }
94  _presetCamera = presetCamera;
95  EX_WEAKIFY(self);
96  dispatch_async(_sessionQueue, ^{
97    EX_ENSURE_STRONGIFY(self);
98    [self initializeSession];
99  });
100}
101
102- (void)setBarCodeTypes:(NSArray *)barCodeTypes
103{
104  _barCodeTypes = barCodeTypes;
105  [_barCodeScanner setSettings:@{
106                                 @"barCodeTypes": barCodeTypes,
107                                 }];
108}
109
110# pragma mark - lifecycle
111
112- (void)layoutSubviews
113{
114  [super layoutSubviews];
115  _previewLayer.frame = self.bounds;
116  [self setBackgroundColor:[UIColor blackColor]];
117  [self.layer insertSublayer:_previewLayer atIndex:0];
118}
119
120- (void)removeFromSuperview
121{
122  [_lifecycleManager unregisterAppLifecycleListener:self];
123  [self stopSession];
124  [super removeFromSuperview];
125  [[NSNotificationCenter defaultCenter] removeObserver:self
126                                                  name:UIDeviceOrientationDidChangeNotification
127                                                object:nil];
128}
129
130- (void)onAppForegrounded
131{
132  if (![_session isRunning] && [self isSessionPaused]) {
133    _paused = NO;
134    EX_WEAKIFY(self);
135    dispatch_async(_sessionQueue, ^{
136      EX_ENSURE_STRONGIFY(self);
137      [self.session startRunning];
138    });
139  }
140}
141
142- (void)onAppBackgrounded
143{
144  if ([_session isRunning] && ![self isSessionPaused]) {
145    _paused = YES;
146    EX_WEAKIFY(self);
147    dispatch_async(_sessionQueue, ^{
148      EX_ENSURE_STRONGIFY(self);
149      [self.session stopRunning];
150    });
151  }
152}
153
154# pragma mark - orientation
155
156- (void)orientationChanged:(NSNotification *)notification
157{
158  UIInterfaceOrientation orientation = [[UIApplication sharedApplication] statusBarOrientation];
159  [self changePreviewOrientation:orientation];
160}
161
162- (void)changePreviewOrientation:(UIInterfaceOrientation)orientation
163{
164  EX_WEAKIFY(self);
165  AVCaptureVideoOrientation videoOrientation = [EXBarCodeScannerUtils videoOrientationForInterfaceOrientation:orientation];
166  [EXUtilities performSynchronouslyOnMainThread:^{
167    EX_ENSURE_STRONGIFY(self);
168    if (self.previewLayer.connection.isVideoOrientationSupported) {
169      [self.previewLayer.connection setVideoOrientation:videoOrientation];
170    }
171  }];
172}
173
174# pragma mark - session
175
176- (BOOL)ensurePermissionsGranted
177{
178  NSDictionary *cameraPermissions = [_permissionsManager getPermissionsForResource:@"camera"];
179  if (![cameraPermissions[@"status"] isEqualToString:@"granted"]) {
180    [self onMountingError:@{@"message": @"Camera permissions not granted - component could not be rendered."}];
181    return FALSE;
182  }
183  return TRUE;
184}
185
186- (void)initializeSession
187{
188  if (_videoCaptureDeviceInput.device.position == _presetCamera) {
189    return;
190  }
191
192  __block UIInterfaceOrientation interfaceOrientation;
193  [EXUtilities performSynchronouslyOnMainThread:^{
194    interfaceOrientation = [[UIApplication sharedApplication] statusBarOrientation];
195  }];
196  AVCaptureVideoOrientation orientation = [EXBarCodeScannerUtils videoOrientationForInterfaceOrientation:interfaceOrientation];
197
198  EX_WEAKIFY(self);
199  dispatch_async(_sessionQueue, ^{
200    EX_ENSURE_STRONGIFY(self);
201
202    [self.session beginConfiguration];
203
204    NSError *error = nil;
205    AVCaptureDevice *captureDevice = [EXBarCodeScannerUtils deviceWithMediaType:AVMediaTypeVideo
206                                                             preferringPosition:self.presetCamera];
207    AVCaptureDeviceInput *captureDeviceInput = [AVCaptureDeviceInput deviceInputWithDevice:captureDevice error:&error];
208
209    if (error || captureDeviceInput == nil) {
210      NSString *errorMessage = @"Camera could not be started - ";
211      if (error) {
212        errorMessage = [errorMessage stringByAppendingString:[error description]];
213      } else {
214        errorMessage = [errorMessage stringByAppendingString:@"there's no captureDeviceInput available"];
215      }
216      [self onMountingError:@{@"message": errorMessage}];
217      return;
218    }
219
220    [self.session removeInput:self.videoCaptureDeviceInput];
221    if ([self.session canAddInput:captureDeviceInput]) {
222      [self.session addInput:captureDeviceInput];
223
224      self.videoCaptureDeviceInput = captureDeviceInput;
225      [self.previewLayer.connection setVideoOrientation:orientation];
226    }
227
228    [self.session commitConfiguration];
229    if (!self.session.isRunning) {
230      [self startSession];
231    }
232  });
233}
234
235- (void)startSession
236{
237#if TARGET_IPHONE_SIMULATOR
238  return;
239#endif
240  if (![self ensurePermissionsGranted]) {
241    return;
242  };
243
244  EX_WEAKIFY(self);
245  dispatch_async(_sessionQueue, ^{
246    EX_ENSURE_STRONGIFY(self);
247
248    if (self.presetCamera == AVCaptureDevicePositionUnspecified) {
249      return;
250    }
251
252    [self setRuntimeErrorHandlingObserver:
253     [[NSNotificationCenter defaultCenter] addObserverForName:AVCaptureSessionRuntimeErrorNotification
254                                                       object:self.session
255                                                        queue:nil
256                                                   usingBlock:^(NSNotification *note) {
257      EX_ENSURE_STRONGIFY(self);
258      dispatch_async(self.sessionQueue, ^{
259        EX_ENSURE_STRONGIFY(self);
260        // Manually restarting the session since it must have been stopped due to an error.
261        [self.session startRunning];
262        [self onReady];
263      });
264    }]];
265
266    [self.barCodeScanner maybeStartBarCodeScanning];
267
268    [self.session startRunning];
269    [self onReady];
270  });
271}
272
273- (void)stopSession
274{
275#if TARGET_IPHONE_SIMULATOR
276  return;
277#endif
278  EX_WEAKIFY(self);
279  dispatch_async(_sessionQueue, ^{
280    EX_ENSURE_STRONGIFY(self);
281
282    [self.barCodeScanner stopBarCodeScanning];
283
284    [self.previewLayer removeFromSuperlayer];
285    [self.session commitConfiguration];
286    [self.session stopRunning];
287    for (AVCaptureInput *input in self.session.inputs) {
288      [self.session removeInput:input];
289    }
290
291    for (AVCaptureOutput *output in self.session.outputs) {
292      [self.session removeOutput:output];
293    }
294  });
295}
296
297# pragma mark - BarCode scanner
298
299- (EXBarCodeScanner *)createBarCodeScanner
300{
301  EXBarCodeScanner *barCodeScanner = [EXBarCodeScanner new];
302  [barCodeScanner setSession:_session];
303  [barCodeScanner setSessionQueue:_sessionQueue];
304  EX_WEAKIFY(self);
305  [barCodeScanner setOnBarCodeScanned:^(NSDictionary *body) {
306    EX_ENSURE_STRONGIFY(self);
307    [self onBarCodeScanned:body];
308  }];
309  [barCodeScanner setIsEnabled:true];
310  return barCodeScanner;
311}
312
313@end
314