blob: fab9b0bc469900857b7ee9a27e8b40a5677b59a2 [file] [log] [blame]
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "ios/chrome/browser/ui/scanner/scanner_view_controller.h"
#import <AVFoundation/AVFoundation.h>
#include "base/logging.h"
#include "base/metrics/user_metrics.h"
#include "base/metrics/user_metrics_action.h"
#include "ios/chrome/browser/ui/commands/load_query_commands.h"
#include "ios/chrome/browser/ui/scanner/scanner_alerts.h"
#include "ios/chrome/browser/ui/scanner/scanner_presenting.h"
#include "ios/chrome/browser/ui/scanner/scanner_transitioning_delegate.h"
#include "ios/chrome/browser/ui/scanner/scanner_view.h"
#import "ios/chrome/browser/ui/util/ui_util.h"
#include "ios/chrome/grit/ios_strings.h"
#include "ui/base/l10n/l10n_util.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
using base::UserMetricsAction;
@interface ScannerViewController ()
@property(nonatomic, readwrite, weak) id<LoadQueryCommands> queryLoader;
@end
@implementation ScannerViewController
#pragma mark - lifecycle
- (instancetype)initWithPresentationProvider:
(id<ScannerPresenting>)presentationProvider {
self = [super initWithNibName:nil bundle:nil];
if (self) {
_presentationProvider = presentationProvider;
}
return self;
}
#pragma mark - UIAccessibilityAction
- (BOOL)accessibilityPerformEscape {
[self dismissForReason:scannerViewController::CLOSE_BUTTON
withCompletion:nil];
return YES;
}
#pragma mark - UIViewController
- (void)viewDidLoad {
[super viewDidLoad];
DCHECK(self.cameraController);
[self.view addSubview:self.scannerView];
// Constraints for |self.scannerView|.
[self.scannerView setTranslatesAutoresizingMaskIntoConstraints:NO];
[NSLayoutConstraint activateConstraints:@[
[[self.scannerView leadingAnchor]
constraintEqualToAnchor:[self.view leadingAnchor]],
[[self.scannerView trailingAnchor]
constraintEqualToAnchor:[self.view trailingAnchor]],
[[self.scannerView topAnchor]
constraintEqualToAnchor:[self.view topAnchor]],
[[self.scannerView bottomAnchor]
constraintEqualToAnchor:[self.view bottomAnchor]],
]];
AVCaptureVideoPreviewLayer* previewLayer = [self.scannerView getPreviewLayer];
switch ([self.cameraController getAuthorizationStatus]) {
case AVAuthorizationStatusNotDetermined:
[self.cameraController
requestAuthorizationAndLoadCaptureSession:previewLayer];
break;
case AVAuthorizationStatusAuthorized:
[self.cameraController loadCaptureSession:previewLayer];
break;
case AVAuthorizationStatusRestricted:
case AVAuthorizationStatusDenied:
// If this happens, then the user is really unlucky:
// The authorization status changed in between the moment this VC was
// instantiated and presented, and the moment viewDidLoad was called.
[self dismissForReason:scannerViewController::
IMPOSSIBLY_UNLIKELY_AUTHORIZATION_CHANGE
withCompletion:nil];
break;
}
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
[self startReceivingNotifications];
[self.cameraController startRecording];
// Reset torch.
[self setTorchMode:AVCaptureTorchModeOff];
}
- (void)viewWillTransitionToSize:(CGSize)size
withTransitionCoordinator:
(id<UIViewControllerTransitionCoordinator>)coordinator {
[super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
CGFloat epsilon = 0.0001;
// Note: targetTransform is always either identity or a 90, -90, or 180 degree
// rotation.
CGAffineTransform targetTransform = coordinator.targetTransform;
CGFloat angle = atan2f(targetTransform.b, targetTransform.a);
if (fabs(angle) > epsilon) {
// Rotate the preview in the opposite direction of the interface rotation
// and add a small value to the angle to force the rotation to occur in the
// correct direction when rotating by 180 degrees.
void (^animationBlock)(id<UIViewControllerTransitionCoordinatorContext>) =
^void(id<UIViewControllerTransitionCoordinatorContext> context) {
[self.scannerView rotatePreviewByAngle:(epsilon - angle)];
};
// Note: The completion block is called even if the animation is
// interrupted, for example by pressing the home button, with the same
// target transform as the animation block.
void (^completionBlock)(id<UIViewControllerTransitionCoordinatorContext>) =
^void(id<UIViewControllerTransitionCoordinatorContext> context) {
[self.scannerView finishPreviewRotation];
};
[coordinator animateAlongsideTransition:animationBlock
completion:completionBlock];
} else if (!CGSizeEqualToSize(self.view.frame.size, size)) {
// Reset the size of the preview if the bounds of the view controller
// changed. This can happen if entering or leaving Split View mode on iPad.
[self.scannerView resetPreviewFrame:size];
[self.cameraController
resetVideoOrientation:[self.scannerView getPreviewLayer]];
}
}
- (void)viewDidDisappear:(BOOL)animated {
[super viewDidDisappear:animated];
[self.cameraController stopRecording];
[self stopReceivingNotifications];
// Reset torch.
[self setTorchMode:AVCaptureTorchModeOff];
}
- (BOOL)prefersStatusBarHidden {
return YES;
}
#pragma mark - public methods
- (void)dismissForReason:(scannerViewController::DismissalReason)reason
withCompletion:(void (^)(void))completion {
[self.presentationProvider dismissScannerViewController:self
completion:completion];
}
- (ScannerView*)buildScannerView {
NOTIMPLEMENTED();
return nil;
}
- (CameraController*)buildCameraController {
NOTIMPLEMENTED();
return nil;
}
#pragma mark - private methods
// Starts receiving notifications about the UIApplication going to background.
- (void)startReceivingNotifications {
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(handleUIApplicationWillResignActiveNotification)
name:UIApplicationWillResignActiveNotification
object:nil];
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector
(handleUIAccessibilityAnnouncementDidFinishNotification:)
name:UIAccessibilityAnnouncementDidFinishNotification
object:nil];
}
// Stops receiving all notifications.
- (void)stopReceivingNotifications {
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
// Requests the torch mode to be set to |mode| by the |self.cameraController|
// and the icon of the torch button to be changed by the |self.scannerView|.
- (void)setTorchMode:(AVCaptureTorchMode)mode {
[self.cameraController setTorchMode:mode];
}
- (ScannerView*)scannerView {
if (!_scannerView) {
_scannerView = [self buildScannerView];
}
return _scannerView;
}
- (CameraController*)cameraController {
if (!_cameraController) {
_cameraController = [self buildCameraController];
}
return _cameraController;
}
#pragma mark - notification handlers
- (void)handleUIApplicationWillResignActiveNotification {
[self setTorchMode:AVCaptureTorchModeOff];
}
- (void)handleUIAccessibilityAnnouncementDidFinishNotification:
(NSNotification*)notification {
NSString* announcement = [[notification userInfo]
valueForKey:UIAccessibilityAnnouncementKeyStringValue];
if ([announcement
isEqualToString:
l10n_util::GetNSString(
IDS_IOS_SCANNER_SCANNED_ACCESSIBILITY_ANNOUNCEMENT)]) {
DCHECK(_result);
[self dismissForReason:scannerViewController::SCAN_COMPLETE
withCompletion:^{
[self.queryLoader loadQuery:_result
immediately:self.loadResultImmediately];
}];
}
}
#pragma mark - CameraControllerDelegate
- (void)captureSessionIsConnected {
[self.cameraController setViewport:[self.scannerView viewportRectOfInterest]];
}
- (void)cameraStateChanged:(scanner::CameraState)state {
switch (state) {
case scanner::CAMERA_AVAILABLE:
// Dismiss any presented alerts.
if ([self presentedViewController]) {
[self dismissViewControllerAnimated:YES completion:nil];
}
break;
case scanner::CAMERA_IN_USE_BY_ANOTHER_APPLICATION:
case scanner::MULTIPLE_FOREGROUND_APPS:
case scanner::CAMERA_PERMISSION_DENIED:
case scanner::CAMERA_UNAVAILABLE_DUE_TO_SYSTEM_PRESSURE:
case scanner::CAMERA_UNAVAILABLE: {
// Dismiss any presented alerts.
if ([self presentedViewController]) {
[self dismissViewControllerAnimated:YES completion:nil];
}
[self presentViewController:
scanner::DialogForCameraState(
state,
^(UIAlertAction*) {
[self dismissForReason:scannerViewController::ERROR_DIALOG
withCompletion:nil];
})
animated:YES
completion:nil];
break;
}
case scanner::CAMERA_NOT_LOADED:
NOTREACHED();
break;
}
}
- (void)torchStateChanged:(BOOL)torchIsOn {
[self.scannerView setTorchButtonTo:torchIsOn];
}
- (void)torchAvailabilityChanged:(BOOL)torchIsAvailable {
[self.scannerView enableTorchButton:torchIsAvailable];
}
#pragma mark - ScannerViewDelegate
- (void)dismissScannerView:(id)sender {
[self dismissForReason:scannerViewController::CLOSE_BUTTON
withCompletion:nil];
}
- (void)toggleTorch:(id)sender {
if ([self.cameraController isTorchActive]) {
[self setTorchMode:AVCaptureTorchModeOff];
} else {
base::RecordAction(UserMetricsAction("MobileQRScannerTorchOn"));
[self setTorchMode:AVCaptureTorchModeOn];
}
}
@end