From 8ee7bd8c6d670be17991dea33280d43751223505 Mon Sep 17 00:00:00 2001 From: hellohuanlin <41930132+hellohuanlin@users.noreply.github.com> Date: Fri, 13 May 2022 18:24:19 -0700 Subject: [PATCH] [camera]handle iOS camera access permission (#5215) --- packages/camera/camera/CHANGELOG.md | 4 + packages/camera/camera/README.md | 25 ++++ .../ios/Runner.xcodeproj/project.pbxproj | 6 +- ...eraCaptureSessionQueueRaceConditionTests.m | 9 +- .../RunnerTests/CameraMethodChannelTests.m | 6 +- .../ios/RunnerTests/CameraPermissionTests.m | 123 ++++++++++++++++++ packages/camera/camera/example/lib/main.dart | 32 ++++- .../example/lib/readme_full_example.dart | 11 ++ .../ios/Classes/CameraPermissionUtils.h | 20 +++ .../ios/Classes/CameraPermissionUtils.m | 39 ++++++ .../camera/camera/ios/Classes/CameraPlugin.m | 63 +++++---- .../camera/ios/Classes/CameraPlugin.modulemap | 1 + .../camera/ios/Classes/CameraPlugin_Test.h | 6 + .../ios/Classes/FLTThreadSafeFlutterResult.h | 20 ++- .../ios/Classes/FLTThreadSafeFlutterResult.m | 4 + packages/camera/camera/pubspec.yaml | 2 +- 16 files changed, 330 insertions(+), 41 deletions(-) create mode 100644 packages/camera/camera/example/ios/RunnerTests/CameraPermissionTests.m create mode 100644 packages/camera/camera/ios/Classes/CameraPermissionUtils.h create mode 100644 packages/camera/camera/ios/Classes/CameraPermissionUtils.m diff --git a/packages/camera/camera/CHANGELOG.md b/packages/camera/camera/CHANGELOG.md index cde2ca284434..8d713c60c276 100644 --- a/packages/camera/camera/CHANGELOG.md +++ b/packages/camera/camera/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.9.5 + +* Adds camera access permission handling logic on iOS to fix a related crash when using the camera for the first time. + ## 0.9.4+24 * Fixes preview orientation when pausing preview with locked orientation. diff --git a/packages/camera/camera/README.md b/packages/camera/camera/README.md index 0bcaeaeb3b7c..6b2ed7a6b687 100644 --- a/packages/camera/camera/README.md +++ b/packages/camera/camera/README.md @@ -80,6 +80,20 @@ void didChangeAppLifecycleState(AppLifecycleState state) { } ``` +### Handling camera access permissions + +Permission errors may be thrown when initializing the camera controller, and you are expected to handle them properly. + +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. + +- `CameraAccessRestricted`: iOS only for now. Thrown when camera 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 Here is a small example flutter app displaying a full screen camera preview. @@ -119,6 +133,17 @@ class _CameraAppState extends State { return; } setState(() {}); + }).catchError((Object e) { + if (e is CameraException) { + switch (e.code) { + case 'CameraAccessDenied': + print('User denied camera access.'); + break; + default: + print('Handle other errors.'); + break; + } + } }); } diff --git a/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj b/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj index 37f56d0ed52e..b5187d5dd1fa 100644 --- a/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 50; + objectVersion = 46; objects = { /* Begin PBXBuildFile section */ @@ -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 */; }; @@ -91,6 +92,7 @@ E04F108527A87CA600573D0C /* FLTSavePhotoDelegateTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FLTSavePhotoDelegateTests.m; sourceTree = ""; }; E071CF7127B3061B006EF3BA /* FLTCamPhotoCaptureTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FLTCamPhotoCaptureTests.m; sourceTree = ""; }; E071CF7327B31DE4006EF3BA /* FLTCamSampleBufferTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLTCamSampleBufferTests.m; sourceTree = ""; }; + E0B0D2BA27DFF2AF00E71E4B /* CameraPermissionTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CameraPermissionTests.m; sourceTree = ""; }; E0C6E1FD2770F01A00EA6AA3 /* ThreadSafeMethodChannelTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ThreadSafeMethodChannelTests.m; sourceTree = ""; }; E0C6E1FE2770F01A00EA6AA3 /* ThreadSafeTextureRegistryTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ThreadSafeTextureRegistryTests.m; sourceTree = ""; }; E0C6E1FF2770F01A00EA6AA3 /* ThreadSafeEventChannelTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ThreadSafeEventChannelTests.m; sourceTree = ""; }; @@ -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 */, @@ -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 */, ); diff --git a/packages/camera/camera/example/ios/RunnerTests/CameraCaptureSessionQueueRaceConditionTests.m b/packages/camera/camera/example/ios/RunnerTests/CameraCaptureSessionQueueRaceConditionTests.m index 667a122d9375..e99ce4e89a94 100644 --- a/packages/camera/camera/example/ios/RunnerTests/CameraCaptureSessionQueueRaceConditionTests.m +++ b/packages/camera/camera/example/ios/RunnerTests/CameraCaptureSessionQueueRaceConditionTests.m @@ -29,10 +29,11 @@ - (void)testFixForCaptureSessionQueueNullPointerCrashDueToRaceCondition { result:^(id _Nullable result) { [disposeExpectation fulfill]; }]; - [camera handleMethodCall:createCall - result:^(id _Nullable result) { - [createExpectation fulfill]; - }]; + [camera createCameraOnSessionQueueWithCreateMethodCall: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:` diff --git a/packages/camera/camera/example/ios/RunnerTests/CameraMethodChannelTests.m b/packages/camera/camera/example/ios/RunnerTests/CameraMethodChannelTests.m index 254a33c7ee4e..62b9cda2ef7b 100644 --- a/packages/camera/camera/example/ios/RunnerTests/CameraMethodChannelTests.m +++ b/packages/camera/camera/example/ios/RunnerTests/CameraMethodChannelTests.m @@ -17,8 +17,7 @@ @implementation CameraMethodChannelTests - (void)testCreate_ShouldCallResultOnMainThread { CameraPlugin *camera = [[CameraPlugin alloc] initWithRegistry:nil messenger:nil]; - XCTestExpectation *expectation = - [[XCTestExpectation alloc] initWithDescription:@"Result finished"]; + XCTestExpectation *expectation = [self expectationWithDescription:@"Result finished"]; // Set up mocks for initWithCameraName method id avCaptureDeviceInputMock = OCMClassMock([AVCaptureDeviceInput class]); @@ -37,7 +36,8 @@ - (void)testCreate_ShouldCallResultOnMainThread { methodCallWithMethodName:@"create" arguments:@{@"resolutionPreset" : @"medium", @"enableAudio" : @(1)}]; - [camera handleMethodCallAsync:call result:resultObject]; + [camera createCameraOnSessionQueueWithCreateMethodCall:call result:resultObject]; + [self waitForExpectationsWithTimeout:1 handler:nil]; // Verify the result NSDictionary *dictionaryResult = (NSDictionary *)resultObject.receivedResult; diff --git a/packages/camera/camera/example/ios/RunnerTests/CameraPermissionTests.m b/packages/camera/camera/example/ios/RunnerTests/CameraPermissionTests.m new file mode 100644 index 000000000000..961b931b7704 --- /dev/null +++ b/packages/camera/camera/example/ios/RunnerTests/CameraPermissionTests.m @@ -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 +#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 diff --git a/packages/camera/camera/example/lib/main.dart b/packages/camera/camera/example/lib/main.dart index 10a8a6f75e16..34942ba5aa77 100644 --- a/packages/camera/camera/example/lib/main.dart +++ b/packages/camera/camera/example/lib/main.dart @@ -633,8 +633,15 @@ class _CameraExampleHomeState extends State } Future onNewCameraSelected(CameraDescription cameraDescription) async { - if (controller != null) { - await controller!.dispose(); + final CameraController? oldController = controller; + if (oldController != null) { + // `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 oldController.dispose(); } final CameraController cameraController = CameraController( @@ -678,7 +685,26 @@ class _CameraExampleHomeState extends State .then((double value) => _minAvailableZoom = value), ]); } on CameraException catch (e) { - _showCameraException(e); + switch (e.code) { + case 'CameraAccessDenied': + showInSnackBar('You have denied camera access.'); + break; + case 'CameraAccessDeniedWithoutPrompt': + // iOS only + showInSnackBar('Please go to Settings app to enable camera access.'); + break; + case 'CameraAccessRestricted': + // iOS only + showInSnackBar('Camera access is restricted.'); + break; + case 'cameraPermission': + // Android & web only + showInSnackBar('Unknown permission error.'); + break; + default: + _showCameraException(e); + break; + } } if (mounted) { diff --git a/packages/camera/camera/example/lib/readme_full_example.dart b/packages/camera/camera/example/lib/readme_full_example.dart index a310fd9daeb0..a3c232ec44f7 100644 --- a/packages/camera/camera/example/lib/readme_full_example.dart +++ b/packages/camera/camera/example/lib/readme_full_example.dart @@ -36,6 +36,17 @@ class _CameraAppState extends State { return; } setState(() {}); + }).catchError((Object e) { + if (e is CameraException) { + switch (e.code) { + case 'CameraAccessDenied': + print('User denied camera access.'); + break; + default: + print('Handle other errors.'); + break; + } + } }); } diff --git a/packages/camera/camera/ios/Classes/CameraPermissionUtils.h b/packages/camera/camera/ios/Classes/CameraPermissionUtils.h new file mode 100644 index 000000000000..80f55db7be32 --- /dev/null +++ b/packages/camera/camera/ios/Classes/CameraPermissionUtils.h @@ -0,0 +1,20 @@ +// 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 + +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); diff --git a/packages/camera/camera/ios/Classes/CameraPermissionUtils.m b/packages/camera/camera/ios/Classes/CameraPermissionUtils.m new file mode 100644 index 000000000000..6318338ea6a2 --- /dev/null +++ b/packages/camera/camera/ios/Classes/CameraPermissionUtils.m @@ -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; + } + } +} diff --git a/packages/camera/camera/ios/Classes/CameraPlugin.m b/packages/camera/camera/ios/Classes/CameraPlugin.m index c0a3833dcd64..43d541e411b4 100644 --- a/packages/camera/camera/ios/Classes/CameraPlugin.m +++ b/packages/camera/camera/ios/Classes/CameraPlugin.m @@ -7,6 +7,7 @@ @import AVFoundation; +#import "CameraPermissionUtils.h" #import "CameraProperties.h" #import "FLTCam.h" #import "FLTThreadSafeEventChannel.h" @@ -131,31 +132,14 @@ - (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 { + [self createCameraOnSessionQueueWithCreateMethodCall: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]; @@ -274,4 +258,35 @@ - (void)handleMethodCallAsync:(FlutterMethodCall *)call } } +- (void)createCameraOnSessionQueueWithCreateMethodCall:(FlutterMethodCall *)createMethodCall + result:(FLTThreadSafeFlutterResult *)result { + dispatch_async(self.captureSessionQueue, ^{ + 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:self.captureSessionQueue + error:&error]; + + if (error) { + [result sendError:error]; + } else { + if (self.camera) { + [self.camera close]; + } + self.camera = cam; + [self.registry registerTexture:cam + completion:^(int64_t textureId) { + [result sendSuccessWithData:@{ + @"cameraId" : @(textureId), + }]; + }]; + } + }); +} + @end diff --git a/packages/camera/camera/ios/Classes/CameraPlugin.modulemap b/packages/camera/camera/ios/Classes/CameraPlugin.modulemap index a23848aaccfc..897302799497 100644 --- a/packages/camera/camera/ios/Classes/CameraPlugin.modulemap +++ b/packages/camera/camera/ios/Classes/CameraPlugin.modulemap @@ -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" diff --git a/packages/camera/camera/ios/Classes/CameraPlugin_Test.h b/packages/camera/camera/ios/Classes/CameraPlugin_Test.h index 826b05043f78..d1903e0829b4 100644 --- a/packages/camera/camera/ios/Classes/CameraPlugin_Test.h +++ b/packages/camera/camera/ios/Classes/CameraPlugin_Test.h @@ -38,4 +38,10 @@ /// that triggered the orientation change. - (void)orientationChanged:(NSNotification *)notification; +/// Creates FLTCam on 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; + @end diff --git a/packages/camera/camera/ios/Classes/FLTThreadSafeFlutterResult.h b/packages/camera/camera/ios/Classes/FLTThreadSafeFlutterResult.h index 70c9f868eda9..6677505671a3 100644 --- a/packages/camera/camera/ios/Classes/FLTThreadSafeFlutterResult.h +++ b/packages/camera/camera/ios/Classes/FLTThreadSafeFlutterResult.h @@ -4,6 +4,8 @@ #import +NS_ASSUME_NONNULL_BEGIN + /** * A thread safe wrapper for FlutterResult that can be called from any thread, by dispatching its * underlying engine calls to the main thread. @@ -13,13 +15,13 @@ /** * Gets the original FlutterResult object wrapped by this FLTThreadSafeFlutterResult instance. */ -@property(readonly, nonatomic, nonnull) FlutterResult flutterResult; +@property(readonly, nonatomic) FlutterResult flutterResult; /** * Initializes with a FlutterResult object. * @param result The FlutterResult object that the result will be given to. */ -- (nonnull instancetype)initWithResult:(nonnull FlutterResult)result; +- (instancetype)initWithResult:(FlutterResult)result; /** * Sends a successful result on the main thread without any data. @@ -30,18 +32,24 @@ * Sends a successful result on the main thread with data. * @param data Result data that is send to the Flutter Dart side. */ -- (void)sendSuccessWithData:(nonnull id)data; +- (void)sendSuccessWithData:(id)data; /** * Sends an NSError as result on the main thread. * @param error Error that will be send as FlutterError. */ -- (void)sendError:(nonnull NSError *)error; +- (void)sendError:(NSError *)error; + +/** + * Sends a FlutterError as result on the main thread. + * @param flutterError FlutterError that will be sent to the Flutter Dart side. + */ +- (void)sendFlutterError:(FlutterError *)flutterError; /** * Sends a FlutterError as result on the main thread. */ -- (void)sendErrorWithCode:(nonnull NSString *)code +- (void)sendErrorWithCode:(NSString *)code message:(nullable NSString *)message details:(nullable id)details; @@ -50,3 +58,5 @@ */ - (void)sendNotImplemented; @end + +NS_ASSUME_NONNULL_END diff --git a/packages/camera/camera/ios/Classes/FLTThreadSafeFlutterResult.m b/packages/camera/camera/ios/Classes/FLTThreadSafeFlutterResult.m index 58c2e788cdc0..ad125f7f32ed 100644 --- a/packages/camera/camera/ios/Classes/FLTThreadSafeFlutterResult.m +++ b/packages/camera/camera/ios/Classes/FLTThreadSafeFlutterResult.m @@ -39,6 +39,10 @@ - (void)sendErrorWithCode:(NSString *)code [self send:flutterError]; } +- (void)sendFlutterError:(FlutterError *)flutterError { + [self send:flutterError]; +} + - (void)sendNotImplemented { [self send:FlutterMethodNotImplemented]; } diff --git a/packages/camera/camera/pubspec.yaml b/packages/camera/camera/pubspec.yaml index d763843d0572..59cde43dd66a 100644 --- a/packages/camera/camera/pubspec.yaml +++ b/packages/camera/camera/pubspec.yaml @@ -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.4+24 +version: 0.9.5 environment: sdk: ">=2.14.0 <3.0.0"