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 |