Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(103)

Unified Diff: ios/chrome/browser/ui/qr_scanner/qr_scanner_view_controller.mm

Issue 2589583003: Upstream Chrome on iOS source code [7/11]. (Closed)
Patch Set: Created 4 years ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
« no previous file with comments | « no previous file | ios/chrome/browser/ui/qr_scanner/qr_scanner_view_controller_egtest.mm » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
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
« no previous file with comments | « no previous file | ios/chrome/browser/ui/qr_scanner/qr_scanner_view_controller_egtest.mm » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698