[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"