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 3 commits
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
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
8 changes: 7 additions & 1 deletion packages/camera/camera/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,10 +88,16 @@ Here is a list of all permission error codes that can be thrown:

- `CameraAccessDenied`: Thrown when user denies the camera access permission.

- `CameraAccessDeniedWithoutPrompt`: iOS only for now. Thrown when user has previously denied the permission. iOS does not allow prompting alert dialog a second time. Users will have to go to Settings > Privacy in order to enable camera access.
- `CameraAccessDeniedWithoutPrompt`: iOS only for now. Thrown when user has previously denied the permission. iOS does not allow prompting alert dialog a second time. Users will have to go to Settings > Privacy > Camera in order to enable camera access.

- `CameraAccessRestricted`: iOS only for now. Thrown when camera access is restricted and users cannot grant permission (parental control).

- `AudioAccessDenied`: Thrown when user denies the audio access permission.
Copy link
Member

Choose a reason for hiding this comment

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

So per your design doc edits this will also be implemented for Android?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yep, android also handles camera and audio permissions separately.


- `AudioAccessDeniedWithoutPrompt`: iOS only for now. Thrown when user has previously denied the permission. iOS does not allow prompting alert dialog a second time. Users will have to go to Settings > Privacy > Microphone in order to enable audio access.

- `AudioAccessRestricted`: iOS only for now. Thrown when audio access is restricted and users cannot grant permission (parental control).

- `cameraPermission`: Android and Web only. A legacy error code for all kinds of camera permission errors.

### Example
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(^(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(^(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(^(FlutterError *error) {
if ([error isEqual:expectedError]) {
[expectation fulfill];
}
Expand All @@ -85,7 +87,7 @@ - (void)testRequestCameraPermission_completeWithoutErrorIfUserGrantAccess {
return YES;
}]]);

FLTRequestCameraPermissionWithCompletionHandler(^(FlutterError *error) {
FLTRequestCameraPermission(^(FlutterError *error) {
if (error == nil) {
[grantedExpectation fulfill];
}
Expand All @@ -111,7 +113,113 @@ - (void)testRequestCameraPermission_completeWithErrorIfUserDenyAccess {
block(NO);
return YES;
}]]);
FLTRequestCameraPermissionWithCompletionHandler(^(FlutterError *error) {
FLTRequestCameraPermission(^(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);

FLTRequestAudioPermission(^(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);
FLTRequestAudioPermission(^(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);

FLTRequestAudioPermission(^(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;
}]]);

FLTRequestAudioPermission(^(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;
}]]);
FLTRequestAudioPermission(^(FlutterError *error) {
if ([error isEqual:expectedError]) {
[expectation fulfill];
}
Expand Down
11 changes: 11 additions & 0 deletions packages/camera/camera/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -697,6 +697,17 @@ class _CameraExampleHomeState extends State<CameraExampleHome>
// iOS only
showInSnackBar('Camera access is restricted.');
break;
case 'AudioAccessDenied':
showInSnackBar('You have denied audio access.');
break;
case 'AudioAccessDeniedWithoutPrompt':
// iOS only
showInSnackBar('Please go to Settings app to enable audio access.');
break;
case 'AudioAccessRestricted':
// iOS only
showInSnackBar('Audio access is restricted.');
break;
case 'cameraPermission':
// Android & web only
showInSnackBar('Unknown permission error.');
Expand Down
14 changes: 12 additions & 2 deletions packages/camera/camera/ios/Classes/CameraPermissionUtils.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,15 @@ 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);
extern void FLTRequestCameraPermission(FLTCameraPermissionRequestCompletionHandler handler);

/// Requests audio access permission.
///
/// If it is the first time requesting audio access, a permission dialog will show up on the
/// 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 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 FLTRequestAudioPermission(FLTCameraPermissionRequestCompletionHandler handler);
90 changes: 68 additions & 22 deletions packages/camera/camera/ios/Classes/CameraPermissionUtils.m
Original file line number Diff line number Diff line change
Expand Up @@ -5,35 +5,81 @@
@import AVFoundation;
#import "CameraPermissionUtils.h"

void FLTRequestCameraPermissionWithCompletionHandler(
FLTCameraPermissionRequestCompletionHandler handler) {
switch ([AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo]) {
void RequestPermission(BOOL forAudio, FLTCameraPermissionRequestCompletionHandler handler) {
Copy link
Member

Choose a reason for hiding this comment

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

This isn't exposed on the header, but best not to collide with some other system function with the same name.

Suggested change
void RequestPermission(BOOL forAudio, FLTCameraPermissionRequestCompletionHandler handler) {
void FLTRequestPermission(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;
}
}
}

void FLTRequestCameraPermission(FLTCameraPermissionRequestCompletionHandler handler) {
RequestPermission(/*forAudio*/ NO, handler);
}

void FLTRequestAudioPermission(FLTCameraPermissionRequestCompletionHandler handler) {
RequestPermission(/*forAudio*/ YES, handler);
}
17 changes: 14 additions & 3 deletions packages/camera/camera/ios/Classes/CameraPlugin.m
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ - (void)handleMethodCallAsync:(FlutterMethodCall *)call
[result sendNotImplemented];
}
} else if ([@"create" isEqualToString:call.method]) {
FLTRequestCameraPermissionWithCompletionHandler(^(FlutterError *error) {
FLTRequestCameraPermission(^(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.

I think the old name WithCompletionHandler was good.

// Create FLTCam only if granted camera access.
if (error) {
[result sendFlutterError:error];
Expand Down Expand Up @@ -194,8 +194,19 @@ - (void)handleMethodCallAsync:(FlutterMethodCall *)call
[_camera close];
[result sendSuccess];
} else if ([@"prepareForVideoRecording" isEqualToString:call.method]) {
[_camera setUpCaptureSessionForAudio];
[result sendSuccess];
// Setup audio capture session only if granted audio access.
FLTRequestAudioPermission(^(FlutterError *error) {
if (error) {
[result sendFlutterError:error];
} else {
// Permission completion handler may be called on arbitrary queue.
// Dispatch to `captureSessionQueue` to setup audio capture session.
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
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