OLD | NEW |
(Empty) | |
| 1 // Copyright 2016 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. |
| 4 |
| 5 #import "ios/chrome/browser/ui/qr_scanner/qr_scanner_view_controller.h" |
| 6 |
| 7 #import <AVFoundation/AVFoundation.h> |
| 8 |
| 9 #include "base/logging.h" |
| 10 #include "base/mac/scoped_nsobject.h" |
| 11 #include "base/metrics/user_metrics.h" |
| 12 #include "base/metrics/user_metrics_action.h" |
| 13 #include "ios/chrome/browser/ui/qr_scanner/qr_scanner_alerts.h" |
| 14 #include "ios/chrome/browser/ui/qr_scanner/qr_scanner_transitioning_delegate.h" |
| 15 #include "ios/chrome/browser/ui/qr_scanner/qr_scanner_view.h" |
| 16 #include "ios/chrome/grit/ios_strings.h" |
| 17 #include "ui/base/l10n/l10n_util.h" |
| 18 |
| 19 using base::UserMetricsAction; |
| 20 |
| 21 namespace { |
| 22 |
| 23 // The reason why the QRScannerViewController was dismissed. Used for collecting |
| 24 // metrics. |
| 25 enum DismissalReason { |
| 26 CLOSE_BUTTON, |
| 27 ERROR_DIALOG, |
| 28 SCANNED_CODE, |
| 29 // Not reported. Should be kept last of enum. |
| 30 IMPOSSIBLY_UNLIKELY_AUTHORIZATION_CHANGE |
| 31 }; |
| 32 |
| 33 } // namespace |
| 34 |
| 35 @interface QRScannerViewController ()<QRScannerViewDelegate> { |
| 36 // The CameraController managing the camera connection. |
| 37 base::scoped_nsobject<CameraController> _cameraController; |
| 38 // The view displaying the QR scanner. |
| 39 base::scoped_nsobject<QRScannerView> _qrScannerView; |
| 40 // The scanned result. |
| 41 base::scoped_nsobject<NSString> _result; |
| 42 // Whether the scanned result should be immediately loaded. |
| 43 BOOL _loadResultImmediately; |
| 44 // The transitioning delegate used for presenting and dismissing the QR |
| 45 // scanner. |
| 46 base::scoped_nsobject<QRScannerTransitioningDelegate> _transitioningDelegate; |
| 47 } |
| 48 |
| 49 // Dismisses the QRScannerViewController and runs |completion| on completion. |
| 50 // Logs metrics according to the |reason| for dismissal. |
| 51 - (void)dismissForReason:(DismissalReason)reason |
| 52 withCompletion:(void (^)(void))completion; |
| 53 // Starts receiving notifications about the UIApplication going to background. |
| 54 - (void)startReceivingNotifications; |
| 55 // Stops receiving all notificatins. |
| 56 - (void)stopReceivingNotifications; |
| 57 // Requests the torch mode to be set to |mode| by the |_cameraController| and |
| 58 // the icon of the torch button to be changed by the |_qrScannerView|. |
| 59 - (void)setTorchMode:(AVCaptureTorchMode)mode; |
| 60 |
| 61 // Stops recording when the application resigns active. |
| 62 - (void)handleUIApplicationWillResignActiveNotification; |
| 63 // Dismisses the QR scanner and passes the scanned result to the delegate when |
| 64 // the accessibility announcement for scanned QR code finishes. |
| 65 - (void)handleUIAccessibilityAnnouncementDidFinishNotification: |
| 66 (NSNotification*)notification; |
| 67 |
| 68 @end |
| 69 |
| 70 @implementation QRScannerViewController |
| 71 |
| 72 @synthesize delegate = _delegate; |
| 73 |
| 74 #pragma mark lifecycle |
| 75 |
| 76 - (instancetype)initWithDelegate:(id<QRScannerViewControllerDelegate>)delegate { |
| 77 self = [super initWithNibName:nil bundle:nil]; |
| 78 if (self) { |
| 79 DCHECK(delegate); |
| 80 _delegate = delegate; |
| 81 _cameraController.reset([[CameraController alloc] initWithDelegate:self]); |
| 82 } |
| 83 return self; |
| 84 } |
| 85 |
| 86 - (instancetype)initWithNibName:(NSString*)name bundle:(NSBundle*)bundle { |
| 87 NOTREACHED(); |
| 88 return nil; |
| 89 } |
| 90 |
| 91 - (instancetype)initWithCoder:(NSCoder*)coder { |
| 92 NOTREACHED(); |
| 93 return nil; |
| 94 } |
| 95 |
| 96 #pragma mark UIAccessibilityAction |
| 97 |
| 98 - (BOOL)accessibilityPerformEscape { |
| 99 [self dismissForReason:CLOSE_BUTTON withCompletion:nil]; |
| 100 return YES; |
| 101 } |
| 102 |
| 103 #pragma mark UIViewController |
| 104 |
| 105 - (void)viewDidLoad { |
| 106 [super viewDidLoad]; |
| 107 DCHECK(_cameraController); |
| 108 |
| 109 _qrScannerView.reset( |
| 110 [[QRScannerView alloc] initWithFrame:self.view.frame delegate:self]); |
| 111 [self.view addSubview:_qrScannerView]; |
| 112 |
| 113 // Constraints for |_qrScannerView|. |
| 114 [_qrScannerView setTranslatesAutoresizingMaskIntoConstraints:NO]; |
| 115 [NSLayoutConstraint activateConstraints:@[ |
| 116 [[_qrScannerView leadingAnchor] |
| 117 constraintEqualToAnchor:[self.view leadingAnchor]], |
| 118 [[_qrScannerView trailingAnchor] |
| 119 constraintEqualToAnchor:[self.view trailingAnchor]], |
| 120 [[_qrScannerView topAnchor] constraintEqualToAnchor:[self.view topAnchor]], |
| 121 [[_qrScannerView bottomAnchor] |
| 122 constraintEqualToAnchor:[self.view bottomAnchor]], |
| 123 ]]; |
| 124 |
| 125 AVCaptureVideoPreviewLayer* previewLayer = [_qrScannerView getPreviewLayer]; |
| 126 switch ([_cameraController getAuthorizationStatus]) { |
| 127 case AVAuthorizationStatusNotDetermined: |
| 128 [_cameraController |
| 129 requestAuthorizationAndLoadCaptureSession:previewLayer]; |
| 130 break; |
| 131 case AVAuthorizationStatusAuthorized: |
| 132 [_cameraController loadCaptureSession:previewLayer]; |
| 133 break; |
| 134 case AVAuthorizationStatusRestricted: |
| 135 case AVAuthorizationStatusDenied: |
| 136 // If this happens, then the user is really unlucky: |
| 137 // The authorization status changed in between the moment this VC was |
| 138 // instantiated and presented, and the moment viewDidLoad was called. |
| 139 [self dismissForReason:IMPOSSIBLY_UNLIKELY_AUTHORIZATION_CHANGE |
| 140 withCompletion:nil]; |
| 141 break; |
| 142 } |
| 143 } |
| 144 |
| 145 - (void)viewWillAppear:(BOOL)animated { |
| 146 [super viewWillAppear:animated]; |
| 147 [self startReceivingNotifications]; |
| 148 [_cameraController startRecording]; |
| 149 |
| 150 // Reset torch. |
| 151 [self setTorchMode:AVCaptureTorchModeOff]; |
| 152 } |
| 153 |
| 154 - (void)viewWillTransitionToSize:(CGSize)size |
| 155 withTransitionCoordinator: |
| 156 (id<UIViewControllerTransitionCoordinator>)coordinator { |
| 157 [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator]; |
| 158 CGFloat epsilon = 0.0001; |
| 159 // Note: targetTransform is always either identity or a 90, -90, or 180 degree |
| 160 // rotation. |
| 161 CGAffineTransform targetTransform = coordinator.targetTransform; |
| 162 CGFloat angle = atan2f(targetTransform.b, targetTransform.a); |
| 163 if (fabs(angle) > epsilon) { |
| 164 // Rotate the preview in the opposite direction of the interface rotation |
| 165 // and add a small value to the angle to force the rotation to occur in the |
| 166 // correct direction when rotating by 180 degrees. |
| 167 void (^animationBlock)(id<UIViewControllerTransitionCoordinatorContext>) = |
| 168 ^void(id<UIViewControllerTransitionCoordinatorContext> context) { |
| 169 [_qrScannerView rotatePreviewByAngle:(epsilon - angle)]; |
| 170 }; |
| 171 // Note: The completion block is called even if the animation is |
| 172 // interrupted, for example by pressing the home button, with the same |
| 173 // target transform as the animation block. |
| 174 void (^completionBlock)(id<UIViewControllerTransitionCoordinatorContext>) = |
| 175 ^void(id<UIViewControllerTransitionCoordinatorContext> context) { |
| 176 [_qrScannerView finishPreviewRotation]; |
| 177 }; |
| 178 [coordinator animateAlongsideTransition:animationBlock |
| 179 completion:completionBlock]; |
| 180 } else if (!CGSizeEqualToSize(self.view.frame.size, size)) { |
| 181 // Reset the size of the preview if the bounds of the view controller |
| 182 // changed. This can happen if entering or leaving Split View mode on iPad. |
| 183 [_qrScannerView resetPreviewFrame:size]; |
| 184 [_cameraController resetVideoOrientation:[_qrScannerView getPreviewLayer]]; |
| 185 } |
| 186 } |
| 187 |
| 188 - (void)viewDidDisappear:(BOOL)animated { |
| 189 [super viewDidDisappear:animated]; |
| 190 [_cameraController stopRecording]; |
| 191 [self stopReceivingNotifications]; |
| 192 |
| 193 // Reset torch. |
| 194 [self setTorchMode:AVCaptureTorchModeOff]; |
| 195 } |
| 196 |
| 197 - (BOOL)prefersStatusBarHidden { |
| 198 return YES; |
| 199 } |
| 200 |
| 201 #pragma mark public methods |
| 202 |
| 203 - (UIViewController*)getViewControllerToPresent { |
| 204 DCHECK(_cameraController); |
| 205 switch ([_cameraController getAuthorizationStatus]) { |
| 206 case AVAuthorizationStatusNotDetermined: |
| 207 case AVAuthorizationStatusAuthorized: |
| 208 _transitioningDelegate.reset( |
| 209 [[QRScannerTransitioningDelegate alloc] init]); |
| 210 [self setTransitioningDelegate:_transitioningDelegate]; |
| 211 return self; |
| 212 case AVAuthorizationStatusRestricted: |
| 213 case AVAuthorizationStatusDenied: |
| 214 return qr_scanner::DialogForCameraState( |
| 215 qr_scanner::CAMERA_PERMISSION_DENIED, nil); |
| 216 } |
| 217 } |
| 218 |
| 219 #pragma mark private methods |
| 220 |
| 221 - (void)dismissForReason:(DismissalReason)reason |
| 222 withCompletion:(void (^)(void))completion { |
| 223 switch (reason) { |
| 224 case CLOSE_BUTTON: |
| 225 base::RecordAction(UserMetricsAction("MobileQRScannerClose")); |
| 226 break; |
| 227 case ERROR_DIALOG: |
| 228 base::RecordAction(UserMetricsAction("MobileQRScannerError")); |
| 229 break; |
| 230 case SCANNED_CODE: |
| 231 base::RecordAction(UserMetricsAction("MobileQRScannerScannedCode")); |
| 232 break; |
| 233 case IMPOSSIBLY_UNLIKELY_AUTHORIZATION_CHANGE: |
| 234 break; |
| 235 } |
| 236 [[self presentingViewController] dismissViewControllerAnimated:YES |
| 237 completion:completion]; |
| 238 } |
| 239 |
| 240 - (void)startReceivingNotifications { |
| 241 [[NSNotificationCenter defaultCenter] |
| 242 addObserver:self |
| 243 selector:@selector(handleUIApplicationWillResignActiveNotification) |
| 244 name:UIApplicationWillResignActiveNotification |
| 245 object:nil]; |
| 246 |
| 247 [[NSNotificationCenter defaultCenter] |
| 248 addObserver:self |
| 249 selector:@selector( |
| 250 handleUIAccessibilityAnnouncementDidFinishNotification:) |
| 251 name:UIAccessibilityAnnouncementDidFinishNotification |
| 252 object:nil]; |
| 253 } |
| 254 |
| 255 - (void)stopReceivingNotifications { |
| 256 [[NSNotificationCenter defaultCenter] removeObserver:self]; |
| 257 } |
| 258 |
| 259 - (void)setTorchMode:(AVCaptureTorchMode)mode { |
| 260 [_cameraController setTorchMode:mode]; |
| 261 } |
| 262 |
| 263 #pragma mark notification handlers |
| 264 |
| 265 - (void)handleUIApplicationWillResignActiveNotification { |
| 266 [self setTorchMode:AVCaptureTorchModeOff]; |
| 267 } |
| 268 |
| 269 - (void)handleUIAccessibilityAnnouncementDidFinishNotification: |
| 270 (NSNotification*)notification { |
| 271 NSString* announcement = [[notification userInfo] |
| 272 valueForKey:UIAccessibilityAnnouncementKeyStringValue]; |
| 273 if ([announcement |
| 274 isEqualToString: |
| 275 l10n_util::GetNSString( |
| 276 IDS_IOS_QR_SCANNER_CODE_SCANNED_ACCESSIBILITY_ANNOUNCEMENT)])
{ |
| 277 DCHECK(_result); |
| 278 [self dismissForReason:SCANNED_CODE |
| 279 withCompletion:^{ |
| 280 [[self delegate] receiveQRScannerResult:_result |
| 281 loadImmediately:_loadResultImmediately]; |
| 282 }]; |
| 283 } |
| 284 } |
| 285 |
| 286 #pragma mark CameraControllerDelegate |
| 287 |
| 288 - (void)captureSessionIsConnected { |
| 289 [_cameraController setViewport:[_qrScannerView viewportRectOfInterest]]; |
| 290 } |
| 291 |
| 292 - (void)cameraStateChanged:(qr_scanner::CameraState)state { |
| 293 switch (state) { |
| 294 case qr_scanner::CAMERA_AVAILABLE: |
| 295 // Dismiss any presented alerts. |
| 296 if ([self presentedViewController]) { |
| 297 [self dismissViewControllerAnimated:YES completion:nil]; |
| 298 } |
| 299 break; |
| 300 case qr_scanner::CAMERA_IN_USE_BY_ANOTHER_APPLICATION: |
| 301 case qr_scanner::MULTIPLE_FOREGROUND_APPS: |
| 302 case qr_scanner::CAMERA_PERMISSION_DENIED: |
| 303 case qr_scanner::CAMERA_UNAVAILABLE: |
| 304 // Dismiss any presented alerts. |
| 305 if ([self presentedViewController]) { |
| 306 [self dismissViewControllerAnimated:YES completion:nil]; |
| 307 } |
| 308 [self presentViewController:qr_scanner::DialogForCameraState( |
| 309 state, |
| 310 ^(UIAlertAction*) { |
| 311 [self dismissForReason:ERROR_DIALOG |
| 312 withCompletion:nil]; |
| 313 }) |
| 314 animated:YES |
| 315 completion:nil]; |
| 316 break; |
| 317 case qr_scanner::CAMERA_NOT_LOADED: |
| 318 NOTREACHED(); |
| 319 break; |
| 320 } |
| 321 } |
| 322 |
| 323 - (void)torchStateChanged:(BOOL)torchIsOn { |
| 324 [_qrScannerView setTorchButtonTo:torchIsOn]; |
| 325 } |
| 326 |
| 327 - (void)torchAvailabilityChanged:(BOOL)torchIsAvailable { |
| 328 [_qrScannerView enableTorchButton:torchIsAvailable]; |
| 329 } |
| 330 |
| 331 - (void)receiveQRScannerResult:(NSString*)result loadImmediately:(BOOL)load { |
| 332 if (UIAccessibilityIsVoiceOverRunning()) { |
| 333 // Post a notification announcing that a code was scanned. QR scanner will |
| 334 // be dismissed when the UIAccessibilityAnnouncementDidFinishNotification is |
| 335 // received. |
| 336 _result.reset([result copy]); |
| 337 _loadResultImmediately = load; |
| 338 UIAccessibilityPostNotification( |
| 339 UIAccessibilityAnnouncementNotification, |
| 340 l10n_util::GetNSString( |
| 341 IDS_IOS_QR_SCANNER_CODE_SCANNED_ACCESSIBILITY_ANNOUNCEMENT)); |
| 342 } else { |
| 343 [_qrScannerView animateScanningResultWithCompletion:^void(void) { |
| 344 [self dismissForReason:SCANNED_CODE |
| 345 withCompletion:^{ |
| 346 [[self delegate] receiveQRScannerResult:result |
| 347 loadImmediately:load]; |
| 348 }]; |
| 349 }]; |
| 350 } |
| 351 } |
| 352 |
| 353 #pragma mark QRScannerViewDelegate |
| 354 |
| 355 - (void)dismissQRScannerView:(id)sender { |
| 356 [self dismissForReason:CLOSE_BUTTON withCompletion:nil]; |
| 357 } |
| 358 |
| 359 - (void)toggleTorch:(id)sender { |
| 360 if ([_cameraController isTorchActive]) { |
| 361 [self setTorchMode:AVCaptureTorchModeOff]; |
| 362 } else { |
| 363 base::RecordAction(UserMetricsAction("MobileQRScannerTorchOn")); |
| 364 [self setTorchMode:AVCaptureTorchModeOn]; |
| 365 } |
| 366 } |
| 367 |
| 368 @end |
OLD | NEW |