From 3976c1bbd8ffba10007e5beeaddff37bfbaa2ae1 Mon Sep 17 00:00:00 2001 From: Marcin Chudy Date: Mon, 30 Dec 2024 21:22:28 +0100 Subject: [PATCH] [camera] Remove OCMock from permission tests (#8350) Smaller part extracted from https://github.com/flutter/packages/pull/8342 - Removes OCMock from `CameraPermissionTests` - Wraps permission methods into a new interface `FLTCameraPermissionManager` - Introduces new protocol which wraps framework methods and can be mocked directly `FLTPermissionService` --- .../camera/camera_avfoundation/CHANGELOG.md | 3 +- .../ios/RunnerTests/CameraPermissionTests.m | 188 +++++++++++------- .../CameraPermissionUtils.m | 87 -------- .../camera_avfoundation/CameraPlugin.m | 37 ++-- .../FLTCameraPermissionManager.m | 101 ++++++++++ .../FLTPermissionServicing.m | 16 ++ .../include/CameraPlugin.modulemap | 3 +- ...onUtils.h => FLTCameraPermissionManager.h} | 23 ++- .../FLTPermissionServicing.h | 19 ++ .../camera/camera_avfoundation/pubspec.yaml | 2 +- 10 files changed, 294 insertions(+), 185 deletions(-) delete mode 100644 packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/CameraPermissionUtils.m create mode 100644 packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/FLTCameraPermissionManager.m create mode 100644 packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/FLTPermissionServicing.m rename packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/camera_avfoundation/{CameraPermissionUtils.h => FLTCameraPermissionManager.h} (71%) create mode 100644 packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/camera_avfoundation/FLTPermissionServicing.h diff --git a/packages/camera/camera_avfoundation/CHANGELOG.md b/packages/camera/camera_avfoundation/CHANGELOG.md index 34de3bf88e46..f0f4a302ae5a 100644 --- a/packages/camera/camera_avfoundation/CHANGELOG.md +++ b/packages/camera/camera_avfoundation/CHANGELOG.md @@ -1,6 +1,7 @@ -## NEXT +## 0.9.17+6 * Updates minimum supported SDK version to Flutter 3.22/Dart 3.4. +* Removes OCMock usage from permission tests ## 0.9.17+5 diff --git a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraPermissionTests.m b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraPermissionTests.m index 02a610affaa5..ec7530381023 100644 --- a/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraPermissionTests.m +++ b/packages/camera/camera_avfoundation/example/ios/RunnerTests/CameraPermissionTests.m @@ -8,31 +8,58 @@ #endif @import AVFoundation; @import XCTest; -#import + #import "CameraTestUtils.h" -@interface CameraPermissionTests : XCTestCase +@interface MockPermissionService : NSObject +@property(nonatomic, copy) AVAuthorizationStatus (^authorizationStatusStub)(AVMediaType mediaType); +@property(nonatomic, copy) void (^requestAccessStub)(AVMediaType mediaType, void (^handler)(BOOL)); +@end +@implementation MockPermissionService +- (AVAuthorizationStatus)authorizationStatusForMediaType:(AVMediaType)mediaType { + return self.authorizationStatusStub ? self.authorizationStatusStub(mediaType) + : AVAuthorizationStatusNotDetermined; +} + +- (void)requestAccessForMediaType:(AVMediaType)mediaType completionHandler:(void (^)(BOOL))handler { + if (self.requestAccessStub) { + self.requestAccessStub(mediaType, handler); + } +} @end -@implementation CameraPermissionTests +@interface FLTCameraPermissionManagerTests : XCTestCase +@property(nonatomic, strong) FLTCameraPermissionManager *permissionManager; +@property(nonatomic, strong) MockPermissionService *mockService; +@end + +@implementation FLTCameraPermissionManagerTests + +- (void)setUp { + [super setUp]; + self.mockService = [[MockPermissionService alloc] init]; + self.permissionManager = + [[FLTCameraPermissionManager alloc] initWithPermissionService:self.mockService]; +} #pragma mark - camera permissions -- (void)testRequestCameraPermission_completeWithoutErrorIfPrevoiuslyAuthorized { +- (void)testRequestCameraPermission_completeWithoutErrorIfPreviouslyAuthorized { XCTestExpectation *expectation = [self expectationWithDescription: @"Must copmlete without error if camera access was previously authorized."]; - id mockDevice = OCMClassMock([AVCaptureDevice class]); - OCMStub([mockDevice authorizationStatusForMediaType:AVMediaTypeVideo]) - .andReturn(AVAuthorizationStatusAuthorized); + self.mockService.authorizationStatusStub = ^AVAuthorizationStatus(AVMediaType mediaType) { + XCTAssertEqualObjects(mediaType, AVMediaTypeVideo); + return AVAuthorizationStatusAuthorized; + }; - FLTRequestCameraPermissionWithCompletionHandler(^(FlutterError *error) { + [self.permissionManager requestCameraPermissionWithCompletionHandler:^(FlutterError *error) { if (error == nil) { [expectation fulfill]; } - }); + }]; [self waitForExpectationsWithTimeout:1 handler:nil]; } - (void)testRequestCameraPermission_completeWithErrorIfPreviouslyDenied { @@ -45,14 +72,16 @@ - (void)testRequestCameraPermission_completeWithErrorIfPreviouslyDenied { @"Settings to enable camera access." details:nil]; - id mockDevice = OCMClassMock([AVCaptureDevice class]); - OCMStub([mockDevice authorizationStatusForMediaType:AVMediaTypeVideo]) - .andReturn(AVAuthorizationStatusDenied); - FLTRequestCameraPermissionWithCompletionHandler(^(FlutterError *error) { + self.mockService.authorizationStatusStub = ^AVAuthorizationStatus(AVMediaType mediaType) { + XCTAssertEqualObjects(mediaType, AVMediaTypeVideo); + return AVAuthorizationStatusDenied; + }; + + [self.permissionManager requestCameraPermissionWithCompletionHandler:^(FlutterError *error) { if ([error isEqual:expectedError]) { [expectation fulfill]; } - }); + }]; [self waitForExpectationsWithTimeout:1 handler:nil]; } @@ -63,15 +92,16 @@ - (void)testRequestCameraPermission_completeWithErrorIfRestricted { message:@"Camera access is restricted. " details:nil]; - id mockDevice = OCMClassMock([AVCaptureDevice class]); - OCMStub([mockDevice authorizationStatusForMediaType:AVMediaTypeVideo]) - .andReturn(AVAuthorizationStatusRestricted); + self.mockService.authorizationStatusStub = ^AVAuthorizationStatus(AVMediaType mediaType) { + XCTAssertEqualObjects(mediaType, AVMediaTypeVideo); + return AVAuthorizationStatusRestricted; + }; - FLTRequestCameraPermissionWithCompletionHandler(^(FlutterError *error) { + [self.permissionManager requestCameraPermissionWithCompletionHandler:^(FlutterError *error) { if ([error isEqual:expectedError]) { [expectation fulfill]; } - }); + }]; [self waitForExpectationsWithTimeout:1 handler:nil]; } @@ -79,21 +109,22 @@ - (void)testRequestCameraPermission_completeWithoutErrorIfUserGrantAccess { XCTestExpectation *grantedExpectation = [self expectationWithDescription:@"Must complete without error if user choose to grant access"]; - id mockDevice = OCMClassMock([AVCaptureDevice class]); - OCMStub([mockDevice authorizationStatusForMediaType:AVMediaTypeVideo]) - .andReturn(AVAuthorizationStatusNotDetermined); + self.mockService.authorizationStatusStub = ^AVAuthorizationStatus(AVMediaType mediaType) { + XCTAssertEqualObjects(mediaType, AVMediaTypeVideo); + return AVAuthorizationStatusNotDetermined; + }; + // Mimic user choosing "allow" in permission dialog. - OCMStub([mockDevice requestAccessForMediaType:AVMediaTypeVideo - completionHandler:[OCMArg checkWithBlock:^BOOL(void (^block)(BOOL)) { - block(YES); - return YES; - }]]); + self.mockService.requestAccessStub = ^(AVMediaType mediaType, void (^handler)(BOOL)) { + XCTAssertEqualObjects(mediaType, AVMediaTypeVideo); + handler(YES); + }; - FLTRequestCameraPermissionWithCompletionHandler(^(FlutterError *error) { + [self.permissionManager requestCameraPermissionWithCompletionHandler:^(FlutterError *error) { if (error == nil) { [grantedExpectation fulfill]; } - }); + }]; [self waitForExpectationsWithTimeout:1 handler:nil]; } @@ -105,21 +136,22 @@ - (void)testRequestCameraPermission_completeWithErrorIfUserDenyAccess { message:@"User denied the camera access request." details:nil]; - id mockDevice = OCMClassMock([AVCaptureDevice class]); - OCMStub([mockDevice authorizationStatusForMediaType:AVMediaTypeVideo]) - .andReturn(AVAuthorizationStatusNotDetermined); + self.mockService.authorizationStatusStub = ^AVAuthorizationStatus(AVMediaType mediaType) { + XCTAssertEqualObjects(mediaType, AVMediaTypeVideo); + return AVAuthorizationStatusNotDetermined; + }; // Mimic user choosing "deny" in permission dialog. - OCMStub([mockDevice requestAccessForMediaType:AVMediaTypeVideo - completionHandler:[OCMArg checkWithBlock:^BOOL(void (^block)(BOOL)) { - block(NO); - return YES; - }]]); - FLTRequestCameraPermissionWithCompletionHandler(^(FlutterError *error) { + self.mockService.requestAccessStub = ^(AVMediaType mediaType, void (^handler)(BOOL)) { + XCTAssertEqualObjects(mediaType, AVMediaTypeVideo); + handler(NO); + }; + + [self.permissionManager requestCameraPermissionWithCompletionHandler:^(FlutterError *error) { if ([error isEqual:expectedError]) { [expectation fulfill]; } - }); + }]; [self waitForExpectationsWithTimeout:1 handler:nil]; } @@ -131,17 +163,19 @@ - (void)testRequestAudioPermission_completeWithoutErrorIfPrevoiuslyAuthorized { [self expectationWithDescription: @"Must copmlete without error if audio access was previously authorized."]; - id mockDevice = OCMClassMock([AVCaptureDevice class]); - OCMStub([mockDevice authorizationStatusForMediaType:AVMediaTypeAudio]) - .andReturn(AVAuthorizationStatusAuthorized); + self.mockService.authorizationStatusStub = ^AVAuthorizationStatus(AVMediaType mediaType) { + XCTAssertEqualObjects(mediaType, AVMediaTypeAudio); + return AVAuthorizationStatusAuthorized; + }; - FLTRequestAudioPermissionWithCompletionHandler(^(FlutterError *error) { + [self.permissionManager requestAudioPermissionWithCompletionHandler:^(FlutterError *error) { if (error == nil) { [expectation fulfill]; } - }); + }]; [self waitForExpectationsWithTimeout:1 handler:nil]; } + - (void)testRequestAudioPermission_completeWithErrorIfPreviouslyDenied { XCTestExpectation *expectation = [self expectationWithDescription: @@ -152,14 +186,16 @@ - (void)testRequestAudioPermission_completeWithErrorIfPreviouslyDenied { @"Settings to enable audio access." details:nil]; - id mockDevice = OCMClassMock([AVCaptureDevice class]); - OCMStub([mockDevice authorizationStatusForMediaType:AVMediaTypeAudio]) - .andReturn(AVAuthorizationStatusDenied); - FLTRequestAudioPermissionWithCompletionHandler(^(FlutterError *error) { + self.mockService.authorizationStatusStub = ^AVAuthorizationStatus(AVMediaType mediaType) { + XCTAssertEqualObjects(mediaType, AVMediaTypeAudio); + return AVAuthorizationStatusDenied; + }; + + [self.permissionManager requestAudioPermissionWithCompletionHandler:^(FlutterError *error) { if ([error isEqual:expectedError]) { [expectation fulfill]; } - }); + }]; [self waitForExpectationsWithTimeout:1 handler:nil]; } @@ -170,15 +206,16 @@ - (void)testRequestAudioPermission_completeWithErrorIfRestricted { message:@"Audio access is restricted. " details:nil]; - id mockDevice = OCMClassMock([AVCaptureDevice class]); - OCMStub([mockDevice authorizationStatusForMediaType:AVMediaTypeAudio]) - .andReturn(AVAuthorizationStatusRestricted); + self.mockService.authorizationStatusStub = ^AVAuthorizationStatus(AVMediaType mediaType) { + XCTAssertEqualObjects(mediaType, AVMediaTypeAudio); + return AVAuthorizationStatusRestricted; + }; - FLTRequestAudioPermissionWithCompletionHandler(^(FlutterError *error) { + [self.permissionManager requestAudioPermissionWithCompletionHandler:^(FlutterError *error) { if ([error isEqual:expectedError]) { [expectation fulfill]; } - }); + }]; [self waitForExpectationsWithTimeout:1 handler:nil]; } @@ -186,21 +223,22 @@ - (void)testRequestAudioPermission_completeWithoutErrorIfUserGrantAccess { XCTestExpectation *grantedExpectation = [self expectationWithDescription:@"Must complete without error if user choose to grant access"]; - id mockDevice = OCMClassMock([AVCaptureDevice class]); - OCMStub([mockDevice authorizationStatusForMediaType:AVMediaTypeAudio]) - .andReturn(AVAuthorizationStatusNotDetermined); + self.mockService.authorizationStatusStub = ^AVAuthorizationStatus(AVMediaType mediaType) { + XCTAssertEqualObjects(mediaType, AVMediaTypeAudio); + return AVAuthorizationStatusNotDetermined; + }; + // Mimic user choosing "allow" in permission dialog. - OCMStub([mockDevice requestAccessForMediaType:AVMediaTypeAudio - completionHandler:[OCMArg checkWithBlock:^BOOL(void (^block)(BOOL)) { - block(YES); - return YES; - }]]); + self.mockService.requestAccessStub = ^(AVMediaType mediaType, void (^handler)(BOOL)) { + XCTAssertEqualObjects(mediaType, AVMediaTypeAudio); + handler(YES); + }; - FLTRequestAudioPermissionWithCompletionHandler(^(FlutterError *error) { + [self.permissionManager requestAudioPermissionWithCompletionHandler:^(FlutterError *error) { if (error == nil) { [grantedExpectation fulfill]; } - }); + }]; [self waitForExpectationsWithTimeout:1 handler:nil]; } @@ -211,22 +249,22 @@ - (void)testRequestAudioPermission_completeWithErrorIfUserDenyAccess { message:@"User denied the audio access request." details:nil]; - id mockDevice = OCMClassMock([AVCaptureDevice class]); - OCMStub([mockDevice authorizationStatusForMediaType:AVMediaTypeAudio]) - .andReturn(AVAuthorizationStatusNotDetermined); + self.mockService.authorizationStatusStub = ^AVAuthorizationStatus(AVMediaType mediaType) { + XCTAssertEqualObjects(mediaType, AVMediaTypeAudio); + return AVAuthorizationStatusNotDetermined; + }; // Mimic user choosing "deny" in permission dialog. - OCMStub([mockDevice requestAccessForMediaType:AVMediaTypeAudio - completionHandler:[OCMArg checkWithBlock:^BOOL(void (^block)(BOOL)) { - block(NO); - return YES; - }]]); - FLTRequestAudioPermissionWithCompletionHandler(^(FlutterError *error) { + self.mockService.requestAccessStub = ^(AVMediaType mediaType, void (^handler)(BOOL)) { + XCTAssertEqualObjects(mediaType, AVMediaTypeAudio); + handler(NO); + }; + + [self.permissionManager requestAudioPermissionWithCompletionHandler:^(FlutterError *error) { if ([error isEqual:expectedError]) { [expectation fulfill]; } - }); - + }]; [self waitForExpectationsWithTimeout:1 handler:nil]; } diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/CameraPermissionUtils.m b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/CameraPermissionUtils.m deleted file mode 100644 index b63a1d684e00..000000000000 --- a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/CameraPermissionUtils.m +++ /dev/null @@ -1,87 +0,0 @@ -// Copyright 2013 The Flutter 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 AVFoundation; -#import "./include/camera_avfoundation/CameraPermissionUtils.h" - -void FLTRequestPermission(BOOL forAudio, FLTCameraPermissionRequestCompletionHandler handler) { - AVMediaType mediaType; - if (forAudio) { - mediaType = AVMediaTypeAudio; - } else { - mediaType = AVMediaTypeVideo; - } - - switch ([AVCaptureDevice authorizationStatusForMediaType:mediaType]) { - case AVAuthorizationStatusAuthorized: - handler(nil); - break; - case AVAuthorizationStatusDenied: { - FlutterError *flutterError; - if (forAudio) { - flutterError = - [FlutterError errorWithCode:@"AudioAccessDeniedWithoutPrompt" - message:@"User has previously denied the audio access request. " - @"Go to Settings to enable audio access." - details:nil]; - } else { - flutterError = - [FlutterError errorWithCode:@"CameraAccessDeniedWithoutPrompt" - message:@"User has previously denied the camera access request. " - @"Go to Settings to enable camera access." - details:nil]; - } - handler(flutterError); - break; - } - case AVAuthorizationStatusRestricted: { - FlutterError *flutterError; - if (forAudio) { - flutterError = [FlutterError errorWithCode:@"AudioAccessRestricted" - message:@"Audio access is restricted. " - details:nil]; - } else { - flutterError = [FlutterError errorWithCode:@"CameraAccessRestricted" - message:@"Camera access is restricted. " - details:nil]; - } - handler(flutterError); - break; - } - case AVAuthorizationStatusNotDetermined: { - [AVCaptureDevice requestAccessForMediaType:mediaType - completionHandler:^(BOOL granted) { - // handler can be invoked on an arbitrary dispatch queue. - if (granted) { - handler(nil); - } else { - FlutterError *flutterError; - if (forAudio) { - flutterError = [FlutterError - errorWithCode:@"AudioAccessDenied" - message:@"User denied the audio access request." - details:nil]; - } else { - flutterError = [FlutterError - errorWithCode:@"CameraAccessDenied" - message:@"User denied the camera access request." - details:nil]; - } - handler(flutterError); - } - }]; - break; - } - } -} - -void FLTRequestCameraPermissionWithCompletionHandler( - FLTCameraPermissionRequestCompletionHandler handler) { - FLTRequestPermission(/*forAudio*/ NO, handler); -} - -void FLTRequestAudioPermissionWithCompletionHandler( - FLTCameraPermissionRequestCompletionHandler handler) { - FLTRequestPermission(/*forAudio*/ YES, handler); -} diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/CameraPlugin.m b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/CameraPlugin.m index de208fd560ef..63a49025231a 100644 --- a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/CameraPlugin.m +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/CameraPlugin.m @@ -8,9 +8,9 @@ @import AVFoundation; @import Flutter; -#import "./include/camera_avfoundation/CameraPermissionUtils.h" #import "./include/camera_avfoundation/CameraProperties.h" #import "./include/camera_avfoundation/FLTCam.h" +#import "./include/camera_avfoundation/FLTCameraPermissionManager.h" #import "./include/camera_avfoundation/FLTThreadSafeEventChannel.h" #import "./include/camera_avfoundation/QueueUtils.h" #import "./include/camera_avfoundation/messages.g.h" @@ -25,6 +25,7 @@ @interface CameraPlugin () @property(readonly, nonatomic) id registry; @property(readonly, nonatomic) NSObject *messenger; @property(nonatomic) FCPCameraGlobalEventApi *globalEventAPI; +@property(readonly, nonatomic) FLTCameraPermissionManager *permissionManager; @end @implementation CameraPlugin @@ -52,6 +53,11 @@ - (instancetype)initWithRegistry:(NSObject *)registry _messenger = messenger; _globalEventAPI = globalAPI; _captureSessionQueue = dispatch_queue_create("io.flutter.camera.captureSessionQueue", NULL); + + id permissionService = [[FLTDefaultPermissionService alloc] init]; + _permissionManager = + [[FLTCameraPermissionManager alloc] initWithPermissionService:permissionService]; + dispatch_queue_set_specific(_captureSessionQueue, FLTCaptureSessionQueueSpecific, (void *)FLTCaptureSessionQueueSpecific, NULL); @@ -145,7 +151,7 @@ - (void)createCameraWithName:(nonnull NSString *)cameraName // Create FLTCam only if granted camera access (and audio access if audio is enabled) __weak typeof(self) weakSelf = self; dispatch_async(self.captureSessionQueue, ^{ - FLTRequestCameraPermissionWithCompletionHandler(^(FlutterError *error) { + [self->_permissionManager requestCameraPermissionWithCompletionHandler:^(FlutterError *error) { typeof(self) strongSelf = weakSelf; if (!strongSelf) return; @@ -157,25 +163,26 @@ - (void)createCameraWithName:(nonnull NSString *)cameraName // optional, and used as a workaround to fix a missing frame issue on iOS. if (settings.enableAudio) { // Setup audio capture session only if granted audio access. - FLTRequestAudioPermissionWithCompletionHandler(^(FlutterError *error) { - // cannot use the outter `strongSelf` - typeof(self) strongSelf = weakSelf; - if (!strongSelf) return; - if (error) { - completion(nil, error); - } else { - [strongSelf createCameraOnSessionQueueWithName:cameraName - settings:settings - completion:completion]; - } - }); + [self->_permissionManager + requestAudioPermissionWithCompletionHandler:^(FlutterError *error) { + // cannot use the outter `strongSelf` + typeof(self) strongSelf = weakSelf; + if (!strongSelf) return; + if (error) { + completion(nil, error); + } else { + [strongSelf createCameraOnSessionQueueWithName:cameraName + settings:settings + completion:completion]; + } + }]; } else { [strongSelf createCameraOnSessionQueueWithName:cameraName settings:settings completion:completion]; } } - }); + }]; }); } diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/FLTCameraPermissionManager.m b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/FLTCameraPermissionManager.m new file mode 100644 index 000000000000..013326a0de32 --- /dev/null +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/FLTCameraPermissionManager.m @@ -0,0 +1,101 @@ +// Copyright 2013 The Flutter 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 AVFoundation; +#import "./include/camera_avfoundation/FLTCameraPermissionManager.h" +#import "./include/camera_avfoundation/FLTPermissionServicing.h" + +@implementation FLTCameraPermissionManager + +- (instancetype)initWithPermissionService:(id)service { + self = [super init]; + if (self) { + _permissionService = service ?: [[FLTDefaultPermissionService alloc] init]; + } + return self; +} + +- (void)requestAudioPermissionWithCompletionHandler: + (__strong FLTCameraPermissionRequestCompletionHandler)handler { + [self requestPermissionForAudio:YES handler:handler]; +} + +- (void)requestCameraPermissionWithCompletionHandler: + (__strong FLTCameraPermissionRequestCompletionHandler)handler { + [self requestPermissionForAudio:NO handler:handler]; +} + +- (void)requestPermissionForAudio:(BOOL)forAudio + handler:(FLTCameraPermissionRequestCompletionHandler)handler { + AVMediaType mediaType; + if (forAudio) { + mediaType = AVMediaTypeAudio; + } else { + mediaType = AVMediaTypeVideo; + } + + switch ([_permissionService authorizationStatusForMediaType:mediaType]) { + case AVAuthorizationStatusAuthorized: + handler(nil); + break; + case AVAuthorizationStatusDenied: { + FlutterError *flutterError; + if (forAudio) { + flutterError = + [FlutterError errorWithCode:@"AudioAccessDeniedWithoutPrompt" + message:@"User has previously denied the audio access request. " + @"Go to Settings to enable audio access." + details:nil]; + } else { + flutterError = + [FlutterError errorWithCode:@"CameraAccessDeniedWithoutPrompt" + message:@"User has previously denied the camera access request. " + @"Go to Settings to enable camera access." + details:nil]; + } + handler(flutterError); + break; + } + case AVAuthorizationStatusRestricted: { + FlutterError *flutterError; + if (forAudio) { + flutterError = [FlutterError errorWithCode:@"AudioAccessRestricted" + message:@"Audio access is restricted. " + details:nil]; + } else { + flutterError = [FlutterError errorWithCode:@"CameraAccessRestricted" + message:@"Camera access is restricted. " + details:nil]; + } + handler(flutterError); + break; + } + case AVAuthorizationStatusNotDetermined: { + [_permissionService requestAccessForMediaType:mediaType + completionHandler:^(BOOL granted) { + // handler can be invoked on an arbitrary dispatch queue. + if (granted) { + handler(nil); + } else { + FlutterError *flutterError; + if (forAudio) { + flutterError = [FlutterError + errorWithCode:@"AudioAccessDenied" + message:@"User denied the audio access request." + details:nil]; + } else { + flutterError = [FlutterError + errorWithCode:@"CameraAccessDenied" + message:@"User denied the camera access request." + details:nil]; + } + handler(flutterError); + } + }]; + break; + } + } +} + +@end diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/FLTPermissionServicing.m b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/FLTPermissionServicing.m new file mode 100644 index 000000000000..4596b879b436 --- /dev/null +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/FLTPermissionServicing.m @@ -0,0 +1,16 @@ +// Copyright 2013 The Flutter 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 "./include/camera_avfoundation/FLTPermissionServicing.h" + +@implementation FLTDefaultPermissionService +- (AVAuthorizationStatus)authorizationStatusForMediaType:(AVMediaType)mediaType { + return [AVCaptureDevice authorizationStatusForMediaType:mediaType]; +} + +- (void)requestAccessForMediaType:(AVMediaType)mediaType + completionHandler:(void (^)(BOOL granted))handler { + [AVCaptureDevice requestAccessForMediaType:mediaType completionHandler:handler]; +} +@end diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/CameraPlugin.modulemap b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/CameraPlugin.modulemap index bc864d174927..57d858f894fa 100644 --- a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/CameraPlugin.modulemap +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/CameraPlugin.modulemap @@ -6,12 +6,13 @@ framework module camera_avfoundation { explicit module Test { header "CameraPlugin_Test.h" - header "CameraPermissionUtils.h" header "CameraProperties.h" header "FLTCam.h" header "FLTCam_Test.h" header "FLTSavePhotoDelegate_Test.h" header "FLTThreadSafeEventChannel.h" + header "FLTPermissionServicing.h" + header "FLTCameraPermissionManager.h" header "QueueUtils.h" } } diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/camera_avfoundation/CameraPermissionUtils.h b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/camera_avfoundation/FLTCameraPermissionManager.h similarity index 71% rename from packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/camera_avfoundation/CameraPermissionUtils.h rename to packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/camera_avfoundation/FLTCameraPermissionManager.h index 5cbbab055f34..d431e3f3b3f7 100644 --- a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/camera_avfoundation/CameraPermissionUtils.h +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/camera_avfoundation/FLTCameraPermissionManager.h @@ -5,7 +5,16 @@ @import Foundation; #import -typedef void (^FLTCameraPermissionRequestCompletionHandler)(FlutterError *); +#import "FLTPermissionServicing.h" + +NS_ASSUME_NONNULL_BEGIN + +typedef void (^FLTCameraPermissionRequestCompletionHandler)(FlutterError *_Nullable); + +@interface FLTCameraPermissionManager : NSObject +@property(nonatomic, strong) id permissionService; + +- (instancetype)initWithPermissionService:(id)service; /// Requests camera access permission. /// @@ -16,8 +25,8 @@ typedef void (^FLTCameraPermissionRequestCompletionHandler)(FlutterError *); /// @param handler if access permission is (or was previously) granted, completion handler will be /// called without error; Otherwise completion handler will be called with error. Handler can be /// called on an arbitrary dispatch queue. -extern void FLTRequestCameraPermissionWithCompletionHandler( - FLTCameraPermissionRequestCompletionHandler handler); +- (void)requestCameraPermissionWithCompletionHandler: + (FLTCameraPermissionRequestCompletionHandler)handler; /// Requests audio access permission. /// @@ -28,5 +37,9 @@ extern void FLTRequestCameraPermissionWithCompletionHandler( /// @param handler if access permission is (or was previously) granted, completion handler will be /// called without error; Otherwise completion handler will be called with error. Handler can be /// called on an arbitrary dispatch queue. -extern void FLTRequestAudioPermissionWithCompletionHandler( - FLTCameraPermissionRequestCompletionHandler handler); +- (void)requestAudioPermissionWithCompletionHandler: + (FLTCameraPermissionRequestCompletionHandler)handler; + +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/camera_avfoundation/FLTPermissionServicing.h b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/camera_avfoundation/FLTPermissionServicing.h new file mode 100644 index 000000000000..2e65568baf83 --- /dev/null +++ b/packages/camera/camera_avfoundation/ios/camera_avfoundation/Sources/camera_avfoundation/include/camera_avfoundation/FLTPermissionServicing.h @@ -0,0 +1,19 @@ +// Copyright 2013 The Flutter 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 AVFoundation; +@import Foundation; + +NS_ASSUME_NONNULL_BEGIN + +@protocol FLTPermissionServicing +- (AVAuthorizationStatus)authorizationStatusForMediaType:(AVMediaType)mediaType; +- (void)requestAccessForMediaType:(AVMediaType)mediaType + completionHandler:(void (^)(BOOL granted))handler; +@end + +@interface FLTDefaultPermissionService : NSObject +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/camera/camera_avfoundation/pubspec.yaml b/packages/camera/camera_avfoundation/pubspec.yaml index 8ba11828e97d..507011775bfd 100644 --- a/packages/camera/camera_avfoundation/pubspec.yaml +++ b/packages/camera/camera_avfoundation/pubspec.yaml @@ -2,7 +2,7 @@ name: camera_avfoundation description: iOS implementation of the camera plugin. repository: https://github.com/flutter/packages/tree/main/packages/camera/camera_avfoundation issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 -version: 0.9.17+5 +version: 0.9.17+6 environment: sdk: ^3.4.0