Skip to content
This repository has been archived by the owner on Feb 22, 2023. It is now read-only.

Commit

Permalink
[camera]handle access permission
Browse files Browse the repository at this point in the history
  • Loading branch information
hellohuanlin committed Apr 11, 2022
1 parent ad146f1 commit 5675e60
Show file tree
Hide file tree
Showing 15 changed files with 273 additions and 39 deletions.
6 changes: 5 additions & 1 deletion packages/camera/camera/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
## 0.9.5

* Adds camera access permission handling logic on iOS to fix a related crash when first time using the camera.

## 0.9.4+19

* Migrate deprecated Scaffold SnackBar methods to ScaffoldMessenger.
* Migrates deprecated Scaffold SnackBar methods to ScaffoldMessenger.

## 0.9.4+18

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 50;
objectVersion = 46;
objects = {

/* Begin PBXBuildFile section */
Expand All @@ -26,6 +26,7 @@
E04F108627A87CA600573D0C /* FLTSavePhotoDelegateTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E04F108527A87CA600573D0C /* FLTSavePhotoDelegateTests.m */; };
E071CF7227B3061B006EF3BA /* FLTCamPhotoCaptureTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E071CF7127B3061B006EF3BA /* FLTCamPhotoCaptureTests.m */; };
E071CF7427B31DE4006EF3BA /* FLTCamSampleBufferTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E071CF7327B31DE4006EF3BA /* FLTCamSampleBufferTests.m */; };
E0B0D2BB27DFF2AF00E71E4B /* CameraPermissionTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E0B0D2BA27DFF2AF00E71E4B /* CameraPermissionTests.m */; };
E0C6E2002770F01A00EA6AA3 /* ThreadSafeMethodChannelTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E0C6E1FD2770F01A00EA6AA3 /* ThreadSafeMethodChannelTests.m */; };
E0C6E2012770F01A00EA6AA3 /* ThreadSafeTextureRegistryTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E0C6E1FE2770F01A00EA6AA3 /* ThreadSafeTextureRegistryTests.m */; };
E0C6E2022770F01A00EA6AA3 /* ThreadSafeEventChannelTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E0C6E1FF2770F01A00EA6AA3 /* ThreadSafeEventChannelTests.m */; };
Expand Down Expand Up @@ -91,6 +92,7 @@
E04F108527A87CA600573D0C /* FLTSavePhotoDelegateTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FLTSavePhotoDelegateTests.m; sourceTree = "<group>"; };
E071CF7127B3061B006EF3BA /* FLTCamPhotoCaptureTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FLTCamPhotoCaptureTests.m; sourceTree = "<group>"; };
E071CF7327B31DE4006EF3BA /* FLTCamSampleBufferTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLTCamSampleBufferTests.m; sourceTree = "<group>"; };
E0B0D2BA27DFF2AF00E71E4B /* CameraPermissionTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CameraPermissionTests.m; sourceTree = "<group>"; };
E0C6E1FD2770F01A00EA6AA3 /* ThreadSafeMethodChannelTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ThreadSafeMethodChannelTests.m; sourceTree = "<group>"; };
E0C6E1FE2770F01A00EA6AA3 /* ThreadSafeTextureRegistryTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ThreadSafeTextureRegistryTests.m; sourceTree = "<group>"; };
E0C6E1FF2770F01A00EA6AA3 /* ThreadSafeEventChannelTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ThreadSafeEventChannelTests.m; sourceTree = "<group>"; };
Expand Down Expand Up @@ -136,6 +138,7 @@
E04F108527A87CA600573D0C /* FLTSavePhotoDelegateTests.m */,
E071CF7127B3061B006EF3BA /* FLTCamPhotoCaptureTests.m */,
E071CF7327B31DE4006EF3BA /* FLTCamSampleBufferTests.m */,
E0B0D2BA27DFF2AF00E71E4B /* CameraPermissionTests.m */,
E01EE4A72799F3A5008C1950 /* QueueUtilsTests.m */,
E0CDBAC027CD9729002561D9 /* CameraTestUtils.h */,
E0CDBAC127CD9729002561D9 /* CameraTestUtils.m */,
Expand Down Expand Up @@ -422,6 +425,7 @@
788A065A27B0E02900533D74 /* StreamingTest.m in Sources */,
E0C6E2022770F01A00EA6AA3 /* ThreadSafeEventChannelTests.m in Sources */,
E0C6E2012770F01A00EA6AA3 /* ThreadSafeTextureRegistryTests.m in Sources */,
E0B0D2BB27DFF2AF00E71E4B /* CameraPermissionTests.m in Sources */,
E0C6E2002770F01A00EA6AA3 /* ThreadSafeMethodChannelTests.m in Sources */,
E01EE4A82799F3A5008C1950 /* QueueUtilsTests.m in Sources */,
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,11 @@ - (void)testFixForCaptureSessionQueueNullPointerCrashDueToRaceCondition {
result:^(id _Nullable result) {
[disposeExpectation fulfill];
}];
[camera handleMethodCall:createCall
result:^(id _Nullable result) {
[createExpectation fulfill];
}];
[camera createCameraWithCreateMethodCall: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 @@ -37,7 +37,7 @@ - (void)testCreate_ShouldCallResultOnMainThread {
methodCallWithMethodName:@"create"
arguments:@{@"resolutionPreset" : @"medium", @"enableAudio" : @(1)}];

[camera handleMethodCallAsync:call result:resultObject];
[camera createCameraWithCreateMethodCall:call result:resultObject];

// Verify the result
NSDictionary *dictionaryResult = (NSDictionary *)resultObject.receivedResult;
Expand Down
123 changes: 123 additions & 0 deletions packages/camera/camera/example/ios/RunnerTests/CameraPermissionTests.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
// 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 camera;
@import camera.Test;
@import AVFoundation;
@import XCTest;
#import <OCMock/OCMock.h>
#import "CameraTestUtils.h"

@interface CameraPermissionTests : XCTestCase

@end

@implementation CameraPermissionTests

- (void)testRequestCameraPermission_completeWithoutErrorIfPrevoiuslyAuthorized {
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);

FLTRequestCameraPermissionWithCompletionHandler(^(FlutterError *error) {
if (error == nil) {
[expectation fulfill];
}
});
[self waitForExpectationsWithTimeout:1 handler:nil];
}
- (void)testRequestCameraPermission_completeWithErrorIfPreviouslyDenied {
XCTestExpectation *expectation =
[self expectationWithDescription:
@"Must complete with error if camera access was previously denied."];
FlutterError *expectedError =
[FlutterError errorWithCode:@"CameraAccessDeniedWithoutPrompt"
message:@"User has previously denied the camera access request. Go to "
@"Settings to enable camera access."
details:nil];

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

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

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

FLTRequestCameraPermissionWithCompletionHandler(^(FlutterError *error) {
if ([error isEqual:expectedError]) {
[expectation fulfill];
}
});
[self waitForExpectationsWithTimeout:1 handler:nil];
}

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

FLTRequestCameraPermissionWithCompletionHandler(^(FlutterError *error) {
if (error == nil) {
[grantedExpectation fulfill];
}
});
[self waitForExpectationsWithTimeout:1 handler:nil];
}

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

id mockDevice = OCMClassMock([AVCaptureDevice class]);
OCMStub([mockDevice authorizationStatusForMediaType:AVMediaTypeVideo])
.andReturn(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) {
if ([error isEqual:expectedError]) {
[expectation fulfill];
}
});

[self waitForExpectationsWithTimeout:1 handler:nil];
}

@end
9 changes: 8 additions & 1 deletion packages/camera/camera/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -631,7 +631,14 @@ class _CameraExampleHomeState extends State<CameraExampleHome>

Future<void> onNewCameraSelected(CameraDescription cameraDescription) async {
if (controller != null) {
await controller!.dispose();
final cameraController = controller!;
// `controller` needs to be set to null before getting disposed,
// to avoid a race condition when we use the controller that is being
// disposed. This happens when camera permission dialog shows up,
// which triggers `didChangeAppLifecycleState`, which disposes and
// re-creates the controller.
controller = null;
await cameraController.dispose();
}

final CameraController cameraController = CameraController(
Expand Down
18 changes: 18 additions & 0 deletions packages/camera/camera/ios/Classes/CameraPermissionUtils.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// 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 Foundation;
#import <Flutter/Flutter.h>

typedef void (^FLTCameraPermissionRequestCompletionHandler)(FlutterError *);

/// Requests camera access permission.
/// If it is the first time requesting camera 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 FLTRequestCameraPermissionWithCompletionHandler(
FLTCameraPermissionRequestCompletionHandler handler);
39 changes: 39 additions & 0 deletions packages/camera/camera/ios/Classes/CameraPermissionUtils.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// 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 "CameraPermissionUtils.h"

void FLTRequestCameraPermissionWithCompletionHandler(
FLTCameraPermissionRequestCompletionHandler handler) {
switch ([AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo]) {
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]);
break;
case AVAuthorizationStatusRestricted:
handler([FlutterError errorWithCode:@"CameraAccessRestricted"
message:@"Camera access is restricted. "
details:nil]);
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]);
}];
break;
}
}
}
65 changes: 41 additions & 24 deletions packages/camera/camera/ios/Classes/CameraPlugin.m
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

@import AVFoundation;

#import "CameraPermissionUtils.h"
#import "CameraProperties.h"
#import "FLTCam.h"
#import "FLTThreadSafeEventChannel.h"
Expand Down Expand Up @@ -131,31 +132,18 @@ - (void)handleMethodCallAsync:(FlutterMethodCall *)call
[result sendNotImplemented];
}
} else if ([@"create" isEqualToString:call.method]) {
NSString *cameraName = call.arguments[@"cameraName"];
NSString *resolutionPreset = call.arguments[@"resolutionPreset"];
NSNumber *enableAudio = call.arguments[@"enableAudio"];
NSError *error;
FLTCam *cam = [[FLTCam alloc] initWithCameraName:cameraName
resolutionPreset:resolutionPreset
enableAudio:[enableAudio boolValue]
orientation:[[UIDevice currentDevice] orientation]
captureSessionQueue:_captureSessionQueue
error:&error];

if (error) {
[result sendError:error];
} else {
if (_camera) {
[_camera close];
FLTRequestCameraPermissionWithCompletionHandler(^(FlutterError *error) {
// Create FLTCam only if granted camera access.
if (error) {
[result sendFlutterError:error];
} else {
// completionHandle may be called on an arbitrary dispatch queue.
// Dispatch back to the capture session queue to create the camera.
dispatch_async(self.captureSessionQueue, ^{
[self createCameraWithCreateMethodCall:call result:result];
});
}
_camera = cam;
[self.registry registerTexture:cam
completion:^(int64_t textureId) {
[result sendSuccessWithData:@{
@"cameraId" : @(textureId),
}];
}];
}
});
} else if ([@"startImageStream" isEqualToString:call.method]) {
[_camera startImageStreamWithMessenger:_messenger];
[result sendSuccess];
Expand Down Expand Up @@ -274,4 +262,33 @@ - (void)handleMethodCallAsync:(FlutterMethodCall *)call
}
}

- (void)createCameraWithCreateMethodCall:(FlutterMethodCall *)createMethodCall
result:(FLTThreadSafeFlutterResult *)result {
NSString *cameraName = createMethodCall.arguments[@"cameraName"];
NSString *resolutionPreset = createMethodCall.arguments[@"resolutionPreset"];
NSNumber *enableAudio = createMethodCall.arguments[@"enableAudio"];
NSError *error;
FLTCam *cam = [[FLTCam alloc] initWithCameraName:cameraName
resolutionPreset:resolutionPreset
enableAudio:[enableAudio boolValue]
orientation:[[UIDevice currentDevice] orientation]
captureSessionQueue:_captureSessionQueue
error:&error];

if (error) {
[result sendError:error];
} else {
if (_camera) {
[_camera close];
}
_camera = cam;
[self.registry registerTexture:cam
completion:^(int64_t textureId) {
[result sendSuccessWithData:@{
@"cameraId" : @(textureId),
}];
}];
}
}

@end
1 change: 1 addition & 0 deletions packages/camera/camera/ios/Classes/CameraPlugin.modulemap
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ framework module camera {

explicit module Test {
header "CameraPlugin_Test.h"
header "CameraPermissionUtils.h"
header "CameraProperties.h"
header "FLTCam.h"
header "FLTCam_Test.h"
Expand Down
6 changes: 6 additions & 0 deletions packages/camera/camera/ios/Classes/CameraPlugin_Test.h
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,10 @@
/// that triggered the orientation change.
- (void)orientationChanged:(NSNotification *)notification;

/// Creates FLTCam 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)createCameraWithCreateMethodCall:(FlutterMethodCall *)createMethodCall
result:(FLTThreadSafeFlutterResult *)result;

@end
Loading

0 comments on commit 5675e60

Please sign in to comment.