| Index: ios/chrome/browser/ui/qr_scanner/qr_scanner_view_controller.mm
|
| diff --git a/ios/chrome/browser/ui/qr_scanner/qr_scanner_view_controller.mm b/ios/chrome/browser/ui/qr_scanner/qr_scanner_view_controller.mm
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..b98f7d11c193a59f4920eaa05b571bc01a162eb1
|
| --- /dev/null
|
| +++ b/ios/chrome/browser/ui/qr_scanner/qr_scanner_view_controller.mm
|
| @@ -0,0 +1,368 @@
|
| +// Copyright 2016 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/qr_scanner/qr_scanner_view_controller.h"
|
| +
|
| +#import <AVFoundation/AVFoundation.h>
|
| +
|
| +#include "base/logging.h"
|
| +#include "base/mac/scoped_nsobject.h"
|
| +#include "base/metrics/user_metrics.h"
|
| +#include "base/metrics/user_metrics_action.h"
|
| +#include "ios/chrome/browser/ui/qr_scanner/qr_scanner_alerts.h"
|
| +#include "ios/chrome/browser/ui/qr_scanner/qr_scanner_transitioning_delegate.h"
|
| +#include "ios/chrome/browser/ui/qr_scanner/qr_scanner_view.h"
|
| +#include "ios/chrome/grit/ios_strings.h"
|
| +#include "ui/base/l10n/l10n_util.h"
|
| +
|
| +using base::UserMetricsAction;
|
| +
|
| +namespace {
|
| +
|
| +// The reason why the QRScannerViewController was dismissed. Used for collecting
|
| +// metrics.
|
| +enum DismissalReason {
|
| + CLOSE_BUTTON,
|
| + ERROR_DIALOG,
|
| + SCANNED_CODE,
|
| + // Not reported. Should be kept last of enum.
|
| + IMPOSSIBLY_UNLIKELY_AUTHORIZATION_CHANGE
|
| +};
|
| +
|
| +} // namespace
|
| +
|
| +@interface QRScannerViewController ()<QRScannerViewDelegate> {
|
| + // The CameraController managing the camera connection.
|
| + base::scoped_nsobject<CameraController> _cameraController;
|
| + // The view displaying the QR scanner.
|
| + base::scoped_nsobject<QRScannerView> _qrScannerView;
|
| + // The scanned result.
|
| + base::scoped_nsobject<NSString> _result;
|
| + // Whether the scanned result should be immediately loaded.
|
| + BOOL _loadResultImmediately;
|
| + // The transitioning delegate used for presenting and dismissing the QR
|
| + // scanner.
|
| + base::scoped_nsobject<QRScannerTransitioningDelegate> _transitioningDelegate;
|
| +}
|
| +
|
| +// Dismisses the QRScannerViewController and runs |completion| on completion.
|
| +// Logs metrics according to the |reason| for dismissal.
|
| +- (void)dismissForReason:(DismissalReason)reason
|
| + withCompletion:(void (^)(void))completion;
|
| +// Starts receiving notifications about the UIApplication going to background.
|
| +- (void)startReceivingNotifications;
|
| +// Stops receiving all notificatins.
|
| +- (void)stopReceivingNotifications;
|
| +// Requests the torch mode to be set to |mode| by the |_cameraController| and
|
| +// the icon of the torch button to be changed by the |_qrScannerView|.
|
| +- (void)setTorchMode:(AVCaptureTorchMode)mode;
|
| +
|
| +// Stops recording when the application resigns active.
|
| +- (void)handleUIApplicationWillResignActiveNotification;
|
| +// Dismisses the QR scanner and passes the scanned result to the delegate when
|
| +// the accessibility announcement for scanned QR code finishes.
|
| +- (void)handleUIAccessibilityAnnouncementDidFinishNotification:
|
| + (NSNotification*)notification;
|
| +
|
| +@end
|
| +
|
| +@implementation QRScannerViewController
|
| +
|
| +@synthesize delegate = _delegate;
|
| +
|
| +#pragma mark lifecycle
|
| +
|
| +- (instancetype)initWithDelegate:(id<QRScannerViewControllerDelegate>)delegate {
|
| + self = [super initWithNibName:nil bundle:nil];
|
| + if (self) {
|
| + DCHECK(delegate);
|
| + _delegate = delegate;
|
| + _cameraController.reset([[CameraController alloc] initWithDelegate:self]);
|
| + }
|
| + return self;
|
| +}
|
| +
|
| +- (instancetype)initWithNibName:(NSString*)name bundle:(NSBundle*)bundle {
|
| + NOTREACHED();
|
| + return nil;
|
| +}
|
| +
|
| +- (instancetype)initWithCoder:(NSCoder*)coder {
|
| + NOTREACHED();
|
| + return nil;
|
| +}
|
| +
|
| +#pragma mark UIAccessibilityAction
|
| +
|
| +- (BOOL)accessibilityPerformEscape {
|
| + [self dismissForReason:CLOSE_BUTTON withCompletion:nil];
|
| + return YES;
|
| +}
|
| +
|
| +#pragma mark UIViewController
|
| +
|
| +- (void)viewDidLoad {
|
| + [super viewDidLoad];
|
| + DCHECK(_cameraController);
|
| +
|
| + _qrScannerView.reset(
|
| + [[QRScannerView alloc] initWithFrame:self.view.frame delegate:self]);
|
| + [self.view addSubview:_qrScannerView];
|
| +
|
| + // Constraints for |_qrScannerView|.
|
| + [_qrScannerView setTranslatesAutoresizingMaskIntoConstraints:NO];
|
| + [NSLayoutConstraint activateConstraints:@[
|
| + [[_qrScannerView leadingAnchor]
|
| + constraintEqualToAnchor:[self.view leadingAnchor]],
|
| + [[_qrScannerView trailingAnchor]
|
| + constraintEqualToAnchor:[self.view trailingAnchor]],
|
| + [[_qrScannerView topAnchor] constraintEqualToAnchor:[self.view topAnchor]],
|
| + [[_qrScannerView bottomAnchor]
|
| + constraintEqualToAnchor:[self.view bottomAnchor]],
|
| + ]];
|
| +
|
| + AVCaptureVideoPreviewLayer* previewLayer = [_qrScannerView getPreviewLayer];
|
| + switch ([_cameraController getAuthorizationStatus]) {
|
| + case AVAuthorizationStatusNotDetermined:
|
| + [_cameraController
|
| + requestAuthorizationAndLoadCaptureSession:previewLayer];
|
| + break;
|
| + case AVAuthorizationStatusAuthorized:
|
| + [_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:IMPOSSIBLY_UNLIKELY_AUTHORIZATION_CHANGE
|
| + withCompletion:nil];
|
| + break;
|
| + }
|
| +}
|
| +
|
| +- (void)viewWillAppear:(BOOL)animated {
|
| + [super viewWillAppear:animated];
|
| + [self startReceivingNotifications];
|
| + [_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) {
|
| + [_qrScannerView 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) {
|
| + [_qrScannerView 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.
|
| + [_qrScannerView resetPreviewFrame:size];
|
| + [_cameraController resetVideoOrientation:[_qrScannerView getPreviewLayer]];
|
| + }
|
| +}
|
| +
|
| +- (void)viewDidDisappear:(BOOL)animated {
|
| + [super viewDidDisappear:animated];
|
| + [_cameraController stopRecording];
|
| + [self stopReceivingNotifications];
|
| +
|
| + // Reset torch.
|
| + [self setTorchMode:AVCaptureTorchModeOff];
|
| +}
|
| +
|
| +- (BOOL)prefersStatusBarHidden {
|
| + return YES;
|
| +}
|
| +
|
| +#pragma mark public methods
|
| +
|
| +- (UIViewController*)getViewControllerToPresent {
|
| + DCHECK(_cameraController);
|
| + switch ([_cameraController getAuthorizationStatus]) {
|
| + case AVAuthorizationStatusNotDetermined:
|
| + case AVAuthorizationStatusAuthorized:
|
| + _transitioningDelegate.reset(
|
| + [[QRScannerTransitioningDelegate alloc] init]);
|
| + [self setTransitioningDelegate:_transitioningDelegate];
|
| + return self;
|
| + case AVAuthorizationStatusRestricted:
|
| + case AVAuthorizationStatusDenied:
|
| + return qr_scanner::DialogForCameraState(
|
| + qr_scanner::CAMERA_PERMISSION_DENIED, nil);
|
| + }
|
| +}
|
| +
|
| +#pragma mark private methods
|
| +
|
| +- (void)dismissForReason:(DismissalReason)reason
|
| + withCompletion:(void (^)(void))completion {
|
| + switch (reason) {
|
| + case CLOSE_BUTTON:
|
| + base::RecordAction(UserMetricsAction("MobileQRScannerClose"));
|
| + break;
|
| + case ERROR_DIALOG:
|
| + base::RecordAction(UserMetricsAction("MobileQRScannerError"));
|
| + break;
|
| + case SCANNED_CODE:
|
| + base::RecordAction(UserMetricsAction("MobileQRScannerScannedCode"));
|
| + break;
|
| + case IMPOSSIBLY_UNLIKELY_AUTHORIZATION_CHANGE:
|
| + break;
|
| + }
|
| + [[self presentingViewController] dismissViewControllerAnimated:YES
|
| + completion:completion];
|
| +}
|
| +
|
| +- (void)startReceivingNotifications {
|
| + [[NSNotificationCenter defaultCenter]
|
| + addObserver:self
|
| + selector:@selector(handleUIApplicationWillResignActiveNotification)
|
| + name:UIApplicationWillResignActiveNotification
|
| + object:nil];
|
| +
|
| + [[NSNotificationCenter defaultCenter]
|
| + addObserver:self
|
| + selector:@selector(
|
| + handleUIAccessibilityAnnouncementDidFinishNotification:)
|
| + name:UIAccessibilityAnnouncementDidFinishNotification
|
| + object:nil];
|
| +}
|
| +
|
| +- (void)stopReceivingNotifications {
|
| + [[NSNotificationCenter defaultCenter] removeObserver:self];
|
| +}
|
| +
|
| +- (void)setTorchMode:(AVCaptureTorchMode)mode {
|
| + [_cameraController setTorchMode:mode];
|
| +}
|
| +
|
| +#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_QR_SCANNER_CODE_SCANNED_ACCESSIBILITY_ANNOUNCEMENT)]) {
|
| + DCHECK(_result);
|
| + [self dismissForReason:SCANNED_CODE
|
| + withCompletion:^{
|
| + [[self delegate] receiveQRScannerResult:_result
|
| + loadImmediately:_loadResultImmediately];
|
| + }];
|
| + }
|
| +}
|
| +
|
| +#pragma mark CameraControllerDelegate
|
| +
|
| +- (void)captureSessionIsConnected {
|
| + [_cameraController setViewport:[_qrScannerView viewportRectOfInterest]];
|
| +}
|
| +
|
| +- (void)cameraStateChanged:(qr_scanner::CameraState)state {
|
| + switch (state) {
|
| + case qr_scanner::CAMERA_AVAILABLE:
|
| + // Dismiss any presented alerts.
|
| + if ([self presentedViewController]) {
|
| + [self dismissViewControllerAnimated:YES completion:nil];
|
| + }
|
| + break;
|
| + case qr_scanner::CAMERA_IN_USE_BY_ANOTHER_APPLICATION:
|
| + case qr_scanner::MULTIPLE_FOREGROUND_APPS:
|
| + case qr_scanner::CAMERA_PERMISSION_DENIED:
|
| + case qr_scanner::CAMERA_UNAVAILABLE:
|
| + // Dismiss any presented alerts.
|
| + if ([self presentedViewController]) {
|
| + [self dismissViewControllerAnimated:YES completion:nil];
|
| + }
|
| + [self presentViewController:qr_scanner::DialogForCameraState(
|
| + state,
|
| + ^(UIAlertAction*) {
|
| + [self dismissForReason:ERROR_DIALOG
|
| + withCompletion:nil];
|
| + })
|
| + animated:YES
|
| + completion:nil];
|
| + break;
|
| + case qr_scanner::CAMERA_NOT_LOADED:
|
| + NOTREACHED();
|
| + break;
|
| + }
|
| +}
|
| +
|
| +- (void)torchStateChanged:(BOOL)torchIsOn {
|
| + [_qrScannerView setTorchButtonTo:torchIsOn];
|
| +}
|
| +
|
| +- (void)torchAvailabilityChanged:(BOOL)torchIsAvailable {
|
| + [_qrScannerView enableTorchButton:torchIsAvailable];
|
| +}
|
| +
|
| +- (void)receiveQRScannerResult:(NSString*)result loadImmediately:(BOOL)load {
|
| + if (UIAccessibilityIsVoiceOverRunning()) {
|
| + // Post a notification announcing that a code was scanned. QR scanner will
|
| + // be dismissed when the UIAccessibilityAnnouncementDidFinishNotification is
|
| + // received.
|
| + _result.reset([result copy]);
|
| + _loadResultImmediately = load;
|
| + UIAccessibilityPostNotification(
|
| + UIAccessibilityAnnouncementNotification,
|
| + l10n_util::GetNSString(
|
| + IDS_IOS_QR_SCANNER_CODE_SCANNED_ACCESSIBILITY_ANNOUNCEMENT));
|
| + } else {
|
| + [_qrScannerView animateScanningResultWithCompletion:^void(void) {
|
| + [self dismissForReason:SCANNED_CODE
|
| + withCompletion:^{
|
| + [[self delegate] receiveQRScannerResult:result
|
| + loadImmediately:load];
|
| + }];
|
| + }];
|
| + }
|
| +}
|
| +
|
| +#pragma mark QRScannerViewDelegate
|
| +
|
| +- (void)dismissQRScannerView:(id)sender {
|
| + [self dismissForReason:CLOSE_BUTTON withCompletion:nil];
|
| +}
|
| +
|
| +- (void)toggleTorch:(id)sender {
|
| + if ([_cameraController isTorchActive]) {
|
| + [self setTorchMode:AVCaptureTorchModeOff];
|
| + } else {
|
| + base::RecordAction(UserMetricsAction("MobileQRScannerTorchOn"));
|
| + [self setTorchMode:AVCaptureTorchModeOn];
|
| + }
|
| +}
|
| +
|
| +@end
|
|
|