Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
[camera]request access permission for audio
  • Loading branch information
hellohuanlin committed May 17, 2022
commit e8e213ed7b3b08c662152ace3630738aa75aca20
4 changes: 4 additions & 0 deletions packages/camera/camera/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 0.9.6

* Adds audio access permission handling logic on iOS to fix an issue with `prepareForVideoRecording` not awaiting for the audio permission request result.

## 0.9.5+1

* Suppresses warnings for pre-iOS-11 codepaths.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,12 @@ - (void)testFixForCaptureSessionQueueNullPointerCrashDueToRaceCondition {
result:^(id _Nullable result) {
[disposeExpectation fulfill];
}];
[camera createCameraOnSessionQueueWithCreateMethodCall:createCall
result:[[FLTThreadSafeFlutterResult alloc]
initWithResult:^(id _Nullable result) {
[createExpectation fulfill];
}]];
[camera createCameraOnCaptureSessionQueueWithCreateMethodCall:createCall
result:[[FLTThreadSafeFlutterResult alloc]
initWithResult:^(
id _Nullable result) {
[createExpectation fulfill];
}]];
[self waitForExpectationsWithTimeout:1 handler:nil];
// `captureSessionQueue` must not be nil after `create` call. Otherwise a nil
// `captureSessionQueue` passed into `AVCaptureVideoDataOutput::setSampleBufferDelegate:queue:`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ - (void)testCreate_ShouldCallResultOnMainThread {
methodCallWithMethodName:@"create"
arguments:@{@"resolutionPreset" : @"medium", @"enableAudio" : @(1)}];

[camera createCameraOnSessionQueueWithCreateMethodCall:call result:resultObject];
[camera createCameraOnCaptureSessionQueueWithCreateMethodCall:call result:resultObject];
[self waitForExpectationsWithTimeout:1 handler:nil];

// Verify the result
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ @interface CameraPermissionTests : XCTestCase

@implementation CameraPermissionTests

#pragma mark - camera permissions

- (void)testRequestCameraPermission_completeWithoutErrorIfPrevoiuslyAuthorized {
XCTestExpectation *expectation =
[self expectationWithDescription:
Expand All @@ -24,7 +26,7 @@ - (void)testRequestCameraPermission_completeWithoutErrorIfPrevoiuslyAuthorized {
OCMStub([mockDevice authorizationStatusForMediaType:AVMediaTypeVideo])
.andReturn(AVAuthorizationStatusAuthorized);

FLTRequestCameraPermissionWithCompletionHandler(^(FlutterError *error) {
FLTRequestCameraPermission(/*forAudio*/ false, ^(FlutterError *error) {
if (error == nil) {
[expectation fulfill];
}
Expand All @@ -44,7 +46,7 @@ - (void)testRequestCameraPermission_completeWithErrorIfPreviouslyDenied {
id mockDevice = OCMClassMock([AVCaptureDevice class]);
OCMStub([mockDevice authorizationStatusForMediaType:AVMediaTypeVideo])
.andReturn(AVAuthorizationStatusDenied);
FLTRequestCameraPermissionWithCompletionHandler(^(FlutterError *error) {
FLTRequestCameraPermission(/*forAudio*/ false, ^(FlutterError *error) {
if ([error isEqual:expectedError]) {
[expectation fulfill];
}
Expand All @@ -63,7 +65,7 @@ - (void)testRequestCameraPermission_completeWithErrorIfRestricted {
OCMStub([mockDevice authorizationStatusForMediaType:AVMediaTypeVideo])
.andReturn(AVAuthorizationStatusRestricted);

FLTRequestCameraPermissionWithCompletionHandler(^(FlutterError *error) {
FLTRequestCameraPermission(/*forAudio*/ false, ^(FlutterError *error) {
if ([error isEqual:expectedError]) {
[expectation fulfill];
}
Expand All @@ -85,7 +87,7 @@ - (void)testRequestCameraPermission_completeWithoutErrorIfUserGrantAccess {
return YES;
}]]);

FLTRequestCameraPermissionWithCompletionHandler(^(FlutterError *error) {
FLTRequestCameraPermission(/*forAudio*/ false, ^(FlutterError *error) {
if (error == nil) {
[grantedExpectation fulfill];
}
Expand All @@ -111,7 +113,113 @@ - (void)testRequestCameraPermission_completeWithErrorIfUserDenyAccess {
block(NO);
return YES;
}]]);
FLTRequestCameraPermissionWithCompletionHandler(^(FlutterError *error) {
FLTRequestCameraPermission(/*forAudio*/ false, ^(FlutterError *error) {
if ([error isEqual:expectedError]) {
[expectation fulfill];
}
});

[self waitForExpectationsWithTimeout:1 handler:nil];
}

#pragma mark - audio permissions

- (void)testRequestAudioPermission_completeWithoutErrorIfPrevoiuslyAuthorized {
XCTestExpectation *expectation =
[self expectationWithDescription:
@"Must copmlete without error if audio access was previously authorized."];

id mockDevice = OCMClassMock([AVCaptureDevice class]);
OCMStub([mockDevice authorizationStatusForMediaType:AVMediaTypeAudio])
.andReturn(AVAuthorizationStatusAuthorized);

FLTRequestCameraPermission(/*forAudio*/ true, ^(FlutterError *error) {
if (error == nil) {
[expectation fulfill];
}
});
[self waitForExpectationsWithTimeout:1 handler:nil];
}
- (void)testRequestAudioPermission_completeWithErrorIfPreviouslyDenied {
XCTestExpectation *expectation =
[self expectationWithDescription:
@"Must complete with error if audio access was previously denied."];
FlutterError *expectedError =
[FlutterError errorWithCode:@"AudioAccessDeniedWithoutPrompt"
message:@"User has previously denied the audio access request. Go to "
@"Settings to enable audio access."
details:nil];

id mockDevice = OCMClassMock([AVCaptureDevice class]);
OCMStub([mockDevice authorizationStatusForMediaType:AVMediaTypeAudio])
.andReturn(AVAuthorizationStatusDenied);
FLTRequestCameraPermission(/*forAudio*/ true, ^(FlutterError *error) {
if ([error isEqual:expectedError]) {
[expectation fulfill];
}
});
[self waitForExpectationsWithTimeout:1 handler:nil];
}

- (void)testRequestAudioPermission_completeWithErrorIfRestricted {
XCTestExpectation *expectation =
[self expectationWithDescription:@"Must complete with error if audio access is restricted."];
FlutterError *expectedError = [FlutterError errorWithCode:@"AudioAccessRestricted"
message:@"Audio access is restricted. "
details:nil];

id mockDevice = OCMClassMock([AVCaptureDevice class]);
OCMStub([mockDevice authorizationStatusForMediaType:AVMediaTypeAudio])
.andReturn(AVAuthorizationStatusRestricted);

FLTRequestCameraPermission(/*forAudio*/ true, ^(FlutterError *error) {
if ([error isEqual:expectedError]) {
[expectation fulfill];
}
});
[self waitForExpectationsWithTimeout:1 handler:nil];
}

- (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);
// Mimic user choosing "allow" in permission dialog.
OCMStub([mockDevice requestAccessForMediaType:AVMediaTypeAudio
completionHandler:[OCMArg checkWithBlock:^BOOL(void (^block)(BOOL)) {
block(YES);
return YES;
}]]);

FLTRequestCameraPermission(/*forAudio*/ true, ^(FlutterError *error) {
if (error == nil) {
[grantedExpectation fulfill];
}
});
[self waitForExpectationsWithTimeout:1 handler:nil];
}

- (void)testRequestAudioPermission_completeWithErrorIfUserDenyAccess {
XCTestExpectation *expectation =
[self expectationWithDescription:@"Must complete with error if user choose to deny access"];
FlutterError *expectedError = [FlutterError errorWithCode:@"AudioAccessDenied"
message:@"User denied the audio access request."
details:nil];

id mockDevice = OCMClassMock([AVCaptureDevice class]);
OCMStub([mockDevice authorizationStatusForMediaType:AVMediaTypeAudio])
.andReturn(AVAuthorizationStatusNotDetermined);

// Mimic user choosing "deny" in permission dialog.
OCMStub([mockDevice requestAccessForMediaType:AVMediaTypeAudio
completionHandler:[OCMArg checkWithBlock:^BOOL(void (^block)(BOOL)) {
block(NO);
return YES;
}]]);
FLTRequestCameraPermission(/*forAudio*/ true, ^(FlutterError *error) {
if ([error isEqual:expectedError]) {
[expectation fulfill];
}
Expand Down
7 changes: 5 additions & 2 deletions packages/camera/camera/ios/Classes/CameraPermissionUtils.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,11 @@ typedef void (^FLTCameraPermissionRequestCompletionHandler)(FlutterError *);
/// screen. Otherwise AVFoundation simply returns the user's previous choice, and in this case the
/// user will have to update the choice in Settings app.
///
///
/// @param forAudio Requests for `AVMediaTypeAudio` permission if `forAudio` is true, and
/// `AVMediaTypeVideo` permission otherwise.
/// @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);
extern void FLTRequestCameraPermission(BOOL forAudio,
FLTCameraPermissionRequestCompletionHandler handler);
83 changes: 61 additions & 22 deletions packages/camera/camera/ios/Classes/CameraPermissionUtils.m
Original file line number Diff line number Diff line change
Expand Up @@ -5,34 +5,73 @@
@import AVFoundation;
#import "CameraPermissionUtils.h"

void FLTRequestCameraPermissionWithCompletionHandler(
FLTCameraPermissionRequestCompletionHandler handler) {
switch ([AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo]) {
void FLTRequestCameraPermission(BOOL forAudio,
FLTCameraPermissionRequestCompletionHandler handler) {
AVMediaType mediaType;
if (forAudio) {
mediaType = AVMediaTypeAudio;
} else {
mediaType = AVMediaTypeVideo;
}

switch ([AVCaptureDevice authorizationStatusForMediaType:mediaType]) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know authorizationStatusForMediaType:mediaType was already used before, but should this be +requestAccessForMediaType:completionHandler: instead to actually request it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh we should always check the status first, and only request the permission if it's in .notDetermined status.

case AVAuthorizationStatusAuthorized:
handler(nil);
break;
case AVAuthorizationStatusDenied:
handler([FlutterError errorWithCode:@"CameraAccessDeniedWithoutPrompt"
message:@"User has previously denied the camera access request. "
@"Go to Settings to enable camera access."
details:nil]);
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:
handler([FlutterError errorWithCode:@"CameraAccessRestricted"
message:@"Camera access is restricted. "
details:nil]);
}
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:AVMediaTypeVideo
completionHandler:^(BOOL granted) {
// handler can be invoked on an arbitrary dispatch queue.
handler(granted ? nil
: [FlutterError
errorWithCode:@"CameraAccessDenied"
message:@"User denied the camera access request."
details:nil]);
}];
[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;
}
}
Expand Down
21 changes: 15 additions & 6 deletions packages/camera/camera/ios/Classes/CameraPlugin.m
Original file line number Diff line number Diff line change
Expand Up @@ -132,12 +132,12 @@ - (void)handleMethodCallAsync:(FlutterMethodCall *)call
[result sendNotImplemented];
}
} else if ([@"create" isEqualToString:call.method]) {
FLTRequestCameraPermissionWithCompletionHandler(^(FlutterError *error) {
FLTRequestCameraPermission(/*forAudio*/ false, ^(FlutterError *error) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of this BOOL (which should be NO not false btw), how about hiding the details of forAudio and creating FLTRequestCameraPermission and FLTRequestAudioPermission functions that can share unexposed logic?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can do

// Create FLTCam only if granted camera access.
if (error) {
[result sendFlutterError:error];
} else {
[self createCameraOnSessionQueueWithCreateMethodCall:call result:result];
[self createCameraOnCaptureSessionQueueWithCreateMethodCall:call result:result];
}
});
} else if ([@"startImageStream" isEqualToString:call.method]) {
Expand Down Expand Up @@ -194,8 +194,17 @@ - (void)handleMethodCallAsync:(FlutterMethodCall *)call
[_camera close];
[result sendSuccess];
} else if ([@"prepareForVideoRecording" isEqualToString:call.method]) {
[_camera setUpCaptureSessionForAudio];
[result sendSuccess];
FLTRequestCameraPermission(/*forAudio*/ true, ^(FlutterError *error) {
// Setup audio capture session only if granted audio access
if (error) {
[result sendFlutterError:error];
} else {
dispatch_async(self.captureSessionQueue, ^{
[self.camera setUpCaptureSessionForAudio];
[result sendSuccess];
});
}
});
} else if ([@"startVideoRecording" isEqualToString:call.method]) {
[_camera startVideoRecordingWithResult:result];
} else if ([@"stopVideoRecording" isEqualToString:call.method]) {
Expand Down Expand Up @@ -258,8 +267,8 @@ - (void)handleMethodCallAsync:(FlutterMethodCall *)call
}
}

- (void)createCameraOnSessionQueueWithCreateMethodCall:(FlutterMethodCall *)createMethodCall
result:(FLTThreadSafeFlutterResult *)result {
- (void)createCameraOnCaptureSessionQueueWithCreateMethodCall:(FlutterMethodCall *)createMethodCall
result:(FLTThreadSafeFlutterResult *)result {
dispatch_async(self.captureSessionQueue, ^{
NSString *cameraName = createMethodCall.arguments[@"cameraName"];
NSString *resolutionPreset = createMethodCall.arguments[@"resolutionPreset"];
Expand Down
6 changes: 3 additions & 3 deletions packages/camera/camera/ios/Classes/CameraPlugin_Test.h
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,10 @@
/// that triggered the orientation change.
- (void)orientationChanged:(NSNotification *)notification;

/// Creates FLTCam on session queue and reports the creation result.
/// Creates FLTCam on capture session queue and reports the creation result.
/// @param createMethodCall the create method call
/// @param result a thread safe flutter result wrapper object to report creation result.
- (void)createCameraOnSessionQueueWithCreateMethodCall:(FlutterMethodCall *)createMethodCall
result:(FLTThreadSafeFlutterResult *)result;
- (void)createCameraOnCaptureSessionQueueWithCreateMethodCall:(FlutterMethodCall *)createMethodCall
result:(FLTThreadSafeFlutterResult *)result;

@end
2 changes: 1 addition & 1 deletion packages/camera/camera/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ description: A Flutter plugin for controlling the camera. Supports previewing
Dart.
repository: https://github.com/flutter/plugins/tree/main/packages/camera/camera
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22
version: 0.9.5+1
version: 0.9.6

environment:
sdk: ">=2.14.0 <3.0.0"
Expand Down