[camera]handle iOS camera access permission (#5215)
diff --git a/packages/camera/camera/CHANGELOG.md b/packages/camera/camera/CHANGELOG.md
index cde2ca2..8d713c6 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 0bcaeae..6b2ed7a 100644
--- a/packages/camera/camera/README.md
+++ b/packages/camera/camera/README.md
@@ -80,6 +80,20 @@
}
```
+### 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 @@
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 37f56d0..b5187d5 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 = "<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>"; };
@@ -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 667a122..e99ce4e 100644
--- a/packages/camera/camera/example/ios/RunnerTests/CameraCaptureSessionQueueRaceConditionTests.m
+++ b/packages/camera/camera/example/ios/RunnerTests/CameraCaptureSessionQueueRaceConditionTests.m
@@ -29,10 +29,11 @@
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 254a33c..62b9cda 100644
--- a/packages/camera/camera/example/ios/RunnerTests/CameraMethodChannelTests.m
+++ b/packages/camera/camera/example/ios/RunnerTests/CameraMethodChannelTests.m
@@ -17,8 +17,7 @@
- (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 @@
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 0000000..961b931
--- /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 <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
diff --git a/packages/camera/camera/example/lib/main.dart b/packages/camera/camera/example/lib/main.dart
index 10a8a6f..34942ba 100644
--- a/packages/camera/camera/example/lib/main.dart
+++ b/packages/camera/camera/example/lib/main.dart
@@ -633,8 +633,15 @@
}
Future<void> 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 @@
.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 a310fd9..a3c232e 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 @@
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 0000000..80f55db
--- /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 <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);
diff --git a/packages/camera/camera/ios/Classes/CameraPermissionUtils.m b/packages/camera/camera/ios/Classes/CameraPermissionUtils.m
new file mode 100644
index 0000000..6318338
--- /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 c0a3833..43d541e 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 @@
[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)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 a23848a..8973027 100644
--- a/packages/camera/camera/ios/Classes/CameraPlugin.modulemap
+++ b/packages/camera/camera/ios/Classes/CameraPlugin.modulemap
@@ -6,6 +6,7 @@
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 826b050..d1903e0 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 70c9f86..6677505 100644
--- a/packages/camera/camera/ios/Classes/FLTThreadSafeFlutterResult.h
+++ b/packages/camera/camera/ios/Classes/FLTThreadSafeFlutterResult.h
@@ -4,6 +4,8 @@
#import <Flutter/Flutter.h>
+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 58c2e78..ad125f7 100644
--- a/packages/camera/camera/ios/Classes/FLTThreadSafeFlutterResult.m
+++ b/packages/camera/camera/ios/Classes/FLTThreadSafeFlutterResult.m
@@ -39,6 +39,10 @@
[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 d763843..59cde43 100644
--- a/packages/camera/camera/pubspec.yaml
+++ b/packages/camera/camera/pubspec.yaml
@@ -4,7 +4,7 @@
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"