// _____ _ // |_ _| |_ _ _ ___ ___ _ __ __ _ // | | | ' \| '_/ -_) -_) ' \/ _` |_ // |_| |_||_|_| \___\___|_|_|_\__,_(_) // // Threema iOS Client // Copyright (c) 2014-2020 Threema GmbH // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License, version 3, // as published by the Free Software Foundation. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . #import "QRScannerViewController.h" #import #import "MBProgressHUD.h" #import "Threema-Swift.h" #ifdef DEBUG static const DDLogLevel ddLogLevel = DDLogLevelVerbose; #else static const DDLogLevel ddLogLevel = DDLogLevelWarning; #endif #define MEDIA_TYPE AVMediaTypeVideo @interface Barcode : NSObject @property (nonatomic, strong) AVMetadataMachineReadableCodeObject *metadataObject; @property (nonatomic, strong) UIBezierPath *cornersPath; @property (nonatomic, strong) UIBezierPath *boundingBoxPath; @end @implementation Barcode @end @implementation QRScannerViewController { AVCaptureSession *_captureSession; AVCaptureDevice *_videoDevice; AVCaptureDeviceInput *_videoInput; AVCaptureVideoPreviewLayer *_previewLayer; AVCaptureMetadataOutput *_metadataOutput; BOOL _running; NSMutableDictionary *_barcodes; CGFloat _initialPinchZoom; } #pragma mark - - (instancetype)init { self = [super init]; if (self) { self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemCancel target:self action:@selector(cancelScan)]; } return self; } - (void)loadView { [super loadView]; self.view.backgroundColor = [UIColor blackColor]; self.previewView = [[UIView alloc] initWithFrame:self.view.bounds]; [MBProgressHUD showHUDAddedTo:self.view animated:YES]; [self.view addSubview:self.previewView]; } - (void)viewDidLoad { [super viewDidLoad]; if ([self hasCameraAccess]) { [self setupCaptureSession]; } _barcodes = [NSMutableDictionary new]; } - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; } - (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; [self startRunning]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationWillEnterForeground:) name:UIApplicationWillEnterForegroundNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidEnterBackground:) name:UIApplicationDidEnterBackgroundNotification object:nil]; } - (void)viewWillDisappear:(BOOL)animated { [super viewWillDisappear:animated]; [self stopRunning]; [[NSNotificationCenter defaultCenter] removeObserver:self]; if (_barcodes.count == 0) { [self.delegate qrScannerViewControllerDidCancel:self]; } } #pragma mark - Notifications - (void)applicationWillEnterForeground:(NSNotification*)note { [self startRunning]; } - (void)applicationDidEnterBackground:(NSNotification*)note { [self stopRunning]; } #pragma mark - Actions - (void)cancelScan { [self.delegate qrScannerViewControllerDidCancel:self]; } - (void)pinchDetected:(UIPinchGestureRecognizer*)recogniser { if (!_videoDevice) return; if (recogniser.state == UIGestureRecognizerStateBegan) { _initialPinchZoom = _videoDevice.videoZoomFactor; } NSError *error = nil; [_videoDevice lockForConfiguration:&error]; if (!error) { CGFloat zoomFactor; CGFloat scale = recogniser.scale; if (scale < 1.0f) { zoomFactor = _initialPinchZoom - pow(_videoDevice.activeFormat.videoMaxZoomFactor, 1.0f - recogniser.scale); } else { zoomFactor = _initialPinchZoom + pow(_videoDevice.activeFormat.videoMaxZoomFactor, (recogniser.scale - 1.0f) / 2.0f); } zoomFactor = MIN(10.0f, zoomFactor); zoomFactor = MAX(1.0f, zoomFactor); _videoDevice.videoZoomFactor = zoomFactor; [_videoDevice unlockForConfiguration]; } } #pragma mark - Video stuff - (void)startRunning { if (_captureSession) { if (_running) return; [_captureSession startRunning]; _metadataOutput.metadataObjectTypes = _metadataOutput.availableMetadataObjectTypes; if ([[VoIPCallStateManager shared] currentCallState] == CallStateIdle) { [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryAmbient withOptions:0 error:nil]; [[AVAudioSession sharedInstance] setActive:YES error:nil]; } _running = YES; } } - (void)stopRunning { if (_captureSession) { if (!_running) return; [_captureSession stopRunning]; if ([[VoIPCallStateManager shared] currentCallState] == CallStateIdle) { [[AVAudioSession sharedInstance] setActive:NO withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation error:nil]; } _running = NO; } } - (BOOL)hasCameraAccess { AVAuthorizationStatus authStatus = [AVCaptureDevice authorizationStatusForMediaType:MEDIA_TYPE]; if (authStatus == AVAuthorizationStatusAuthorized) { return YES; } else if(authStatus == AVAuthorizationStatusDenied || authStatus == AVAuthorizationStatusRestricted){ [self showCameraAccessAlert]; } else if(authStatus == AVAuthorizationStatusNotDetermined){ [AVCaptureDevice requestAccessForMediaType:MEDIA_TYPE completionHandler:^(BOOL granted) { if(granted){ dispatch_async(dispatch_get_main_queue(), ^{ [self setupCaptureSession]; [self startRunning]; }); } else { DDLogError(@"Camera access not granted"); } }]; } return NO; } - (void)showCameraAccessAlert { [UIAlertTemplate showAlertWithOwner:self title:NSLocalizedString(@"camera_disabled_title", nil) message:NSLocalizedString(@"camera_disabled_message", nil) actionOk:nil]; } - (void)setupCaptureSession { if (_captureSession) return; _videoDevice = [AVCaptureDevice defaultDeviceWithMediaType:MEDIA_TYPE]; if (!_videoDevice) { DDLogError(@"No video camera on this device!"); return; } _captureSession = [[AVCaptureSession alloc] init]; _videoInput = [[AVCaptureDeviceInput alloc] initWithDevice:_videoDevice error:nil]; if ([_captureSession canAddInput:_videoInput]) { [_captureSession addInput:_videoInput]; } _previewLayer = [[AVCaptureVideoPreviewLayer alloc] initWithSession:_captureSession]; _previewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill; _previewLayer.frame = _previewView.bounds; [_previewView.layer addSublayer:_previewLayer]; [_previewView addGestureRecognizer:[[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(pinchDetected:)]]; _metadataOutput = [[AVCaptureMetadataOutput alloc] init]; dispatch_queue_t metadataQueue = dispatch_queue_create("ch.threema.app.qrmetadata", 0); [_metadataOutput setMetadataObjectsDelegate:self queue:metadataQueue]; if ([_captureSession canAddOutput:_metadataOutput]) { [_captureSession addOutput:_metadataOutput]; } } #pragma mark - - (Barcode*)processMetadataObject:(AVMetadataMachineReadableCodeObject*)code { if (code.stringValue == nil) return nil; /* e.g. when scanning binary data */ Barcode *barcode = _barcodes[code.stringValue]; if (!barcode) { barcode = [Barcode new]; _barcodes[code.stringValue] = barcode; } barcode.metadataObject = code; // Create the path joining code's corners CGMutablePathRef cornersPath = CGPathCreateMutable(); CGPoint point; CGPointMakeWithDictionaryRepresentation((CFDictionaryRef)code.corners[0], &point); CGPathMoveToPoint(cornersPath, nil, point.x, point.y); for (int i = 1; i < code.corners.count; i++) { CGPointMakeWithDictionaryRepresentation((CFDictionaryRef)code.corners[i], &point); CGPathAddLineToPoint(cornersPath, nil, point.x, point.y); } CGPathCloseSubpath(cornersPath); barcode.cornersPath = [UIBezierPath bezierPathWithCGPath:cornersPath]; CGPathRelease(cornersPath); barcode.boundingBoxPath = [UIBezierPath bezierPathWithRect:code.bounds]; return barcode; } #pragma mark - AVCaptureMetadataOutputObjectsDelegate - (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputMetadataObjects:(NSArray *)metadataObjects fromConnection:(AVCaptureConnection *)connection { NSSet *originalBarcodes = [NSSet setWithArray:_barcodes.allValues]; NSMutableSet *foundBarcodes = [NSMutableSet new]; [metadataObjects enumerateObjectsUsingBlock:^(AVMetadataObject *obj, NSUInteger idx, BOOL *stop) { DDLogVerbose(@"Metadata: %@", obj); if ([obj isKindOfClass:[AVMetadataMachineReadableCodeObject class]]) { AVMetadataMachineReadableCodeObject *code = (AVMetadataMachineReadableCodeObject*)[_previewLayer transformedMetadataObjectForMetadataObject:obj]; Barcode *barcode = [self processMetadataObject:code]; if (barcode != nil) [foundBarcodes addObject:barcode]; } }]; NSMutableSet *newBarcodes = [foundBarcodes mutableCopy]; [newBarcodes minusSet:originalBarcodes]; NSMutableSet *goneBarcodes = [originalBarcodes mutableCopy]; [goneBarcodes minusSet:foundBarcodes]; [goneBarcodes enumerateObjectsUsingBlock:^(Barcode *barcode, BOOL *stop) { [_barcodes removeObjectForKey:barcode.metadataObject.stringValue]; }]; dispatch_sync(dispatch_get_main_queue(), ^{ // Remove all old layers NSArray *allSublayers = [_previewView.layer.sublayers copy]; [allSublayers enumerateObjectsUsingBlock:^(CALayer *layer, NSUInteger idx, BOOL *stop) { if (layer != _previewLayer) { [layer removeFromSuperlayer]; } }]; // Add new layers [foundBarcodes enumerateObjectsUsingBlock:^(Barcode *barcode, BOOL *stop) { CAShapeLayer *cornersPathLayer = [CAShapeLayer new]; cornersPathLayer.path = barcode.cornersPath.CGPath; cornersPathLayer.lineWidth = 2.0f; cornersPathLayer.strokeColor = [UIColor blueColor].CGColor; cornersPathLayer.fillColor = [UIColor colorWithRed:0.0f green:0.0f blue:1.0f alpha:0.5f].CGColor; [_previewView.layer addSublayer:cornersPathLayer]; }]; [newBarcodes enumerateObjectsUsingBlock:^(Barcode *barcode, BOOL *stop) { // call delegate with slight delay so that user can see our fancy box [self stopRunning]; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ [self.delegate qrScannerViewController:self didScanResult:barcode.metadataObject.stringValue]; }); }]; }); } @end