[camera] Request access permission for audio (#5766)

diff --git a/packages/camera/camera/CHANGELOG.md b/packages/camera/camera/CHANGELOG.md
index d101f60..9af2301 100644
--- a/packages/camera/camera/CHANGELOG.md
+++ b/packages/camera/camera/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 0.9.6
+
+* Adds audio access permission handling logic on iOS to fix an issue with `prepareForVideoRecording` not awaiting for the audio permission request result.
+
 ## 0.9.5+1
 
 * Suppresses warnings for pre-iOS-11 codepaths.
diff --git a/packages/camera/camera/README.md b/packages/camera/camera/README.md
index 6b2ed7a..ec9d737 100644
--- a/packages/camera/camera/README.md
+++ b/packages/camera/camera/README.md
@@ -88,10 +88,16 @@
 
 - `CameraAccessDenied`: Thrown when user denies the camera access permission.
 
-- `CameraAccessDeniedWithoutPrompt`: iOS only for now. Thrown when user has previously denied the permission. iOS does not allow prompting alert dialog a second time. Users will have to go to Settings > Privacy in order to enable camera access.
+- `CameraAccessDeniedWithoutPrompt`: iOS only for now. Thrown when user has previously denied the permission. iOS does not allow prompting alert dialog a second time. Users will have to go to Settings > Privacy > Camera in order to enable camera access.
 
 - `CameraAccessRestricted`: iOS only for now. Thrown when camera access is restricted and users cannot grant permission (parental control).
 
+- `AudioAccessDenied`: Thrown when user denies the audio access permission.
+
+- `AudioAccessDeniedWithoutPrompt`: iOS only for now. Thrown when user has previously denied the permission. iOS does not allow prompting alert dialog a second time. Users will have to go to Settings > Privacy > Microphone in order to enable audio access.
+
+- `AudioAccessRestricted`: iOS only for now. Thrown when audio access is restricted and users cannot grant permission (parental control).
+
 - `cameraPermission`: Android and Web only. A legacy error code for all kinds of camera permission errors.
 
 ### Example
diff --git a/packages/camera/camera/example/ios/RunnerTests/CameraPermissionTests.m b/packages/camera/camera/example/ios/RunnerTests/CameraPermissionTests.m
index 961b931..541e028 100644
--- a/packages/camera/camera/example/ios/RunnerTests/CameraPermissionTests.m
+++ b/packages/camera/camera/example/ios/RunnerTests/CameraPermissionTests.m
@@ -15,6 +15,8 @@
 
 @implementation CameraPermissionTests
 
+#pragma mark - camera permissions
+
 - (void)testRequestCameraPermission_completeWithoutErrorIfPrevoiuslyAuthorized {
   XCTestExpectation *expectation =
       [self expectationWithDescription:
@@ -120,4 +122,110 @@
   [self waitForExpectationsWithTimeout:1 handler:nil];
 }
 
+#pragma mark - audio permissions
+
+- (void)testRequestAudioPermission_completeWithoutErrorIfPrevoiuslyAuthorized {
+  XCTestExpectation *expectation =
+      [self expectationWithDescription:
+                @"Must copmlete without error if audio access was previously authorized."];
+
+  id mockDevice = OCMClassMock([AVCaptureDevice class]);
+  OCMStub([mockDevice authorizationStatusForMediaType:AVMediaTypeAudio])
+      .andReturn(AVAuthorizationStatusAuthorized);
+
+  FLTRequestAudioPermissionWithCompletionHandler(^(FlutterError *error) {
+    if (error == nil) {
+      [expectation fulfill];
+    }
+  });
+  [self waitForExpectationsWithTimeout:1 handler:nil];
+}
+- (void)testRequestAudioPermission_completeWithErrorIfPreviouslyDenied {
+  XCTestExpectation *expectation =
+      [self expectationWithDescription:
+                @"Must complete with error if audio access was previously denied."];
+  FlutterError *expectedError =
+      [FlutterError errorWithCode:@"AudioAccessDeniedWithoutPrompt"
+                          message:@"User has previously denied the audio access request. Go to "
+                                  @"Settings to enable audio access."
+                          details:nil];
+
+  id mockDevice = OCMClassMock([AVCaptureDevice class]);
+  OCMStub([mockDevice authorizationStatusForMediaType:AVMediaTypeAudio])
+      .andReturn(AVAuthorizationStatusDenied);
+  FLTRequestAudioPermissionWithCompletionHandler(^(FlutterError *error) {
+    if ([error isEqual:expectedError]) {
+      [expectation fulfill];
+    }
+  });
+  [self waitForExpectationsWithTimeout:1 handler:nil];
+}
+
+- (void)testRequestAudioPermission_completeWithErrorIfRestricted {
+  XCTestExpectation *expectation =
+      [self expectationWithDescription:@"Must complete with error if audio access is restricted."];
+  FlutterError *expectedError = [FlutterError errorWithCode:@"AudioAccessRestricted"
+                                                    message:@"Audio access is restricted. "
+                                                    details:nil];
+
+  id mockDevice = OCMClassMock([AVCaptureDevice class]);
+  OCMStub([mockDevice authorizationStatusForMediaType:AVMediaTypeAudio])
+      .andReturn(AVAuthorizationStatusRestricted);
+
+  FLTRequestAudioPermissionWithCompletionHandler(^(FlutterError *error) {
+    if ([error isEqual:expectedError]) {
+      [expectation fulfill];
+    }
+  });
+  [self waitForExpectationsWithTimeout:1 handler:nil];
+}
+
+- (void)testRequestAudioPermission_completeWithoutErrorIfUserGrantAccess {
+  XCTestExpectation *grantedExpectation = [self
+      expectationWithDescription:@"Must complete without error if user choose to grant access"];
+
+  id mockDevice = OCMClassMock([AVCaptureDevice class]);
+  OCMStub([mockDevice authorizationStatusForMediaType:AVMediaTypeAudio])
+      .andReturn(AVAuthorizationStatusNotDetermined);
+  // Mimic user choosing "allow" in permission dialog.
+  OCMStub([mockDevice requestAccessForMediaType:AVMediaTypeAudio
+                              completionHandler:[OCMArg checkWithBlock:^BOOL(void (^block)(BOOL)) {
+                                block(YES);
+                                return YES;
+                              }]]);
+
+  FLTRequestAudioPermissionWithCompletionHandler(^(FlutterError *error) {
+    if (error == nil) {
+      [grantedExpectation fulfill];
+    }
+  });
+  [self waitForExpectationsWithTimeout:1 handler:nil];
+}
+
+- (void)testRequestAudioPermission_completeWithErrorIfUserDenyAccess {
+  XCTestExpectation *expectation =
+      [self expectationWithDescription:@"Must complete with error if user choose to deny access"];
+  FlutterError *expectedError = [FlutterError errorWithCode:@"AudioAccessDenied"
+                                                    message:@"User denied the audio access request."
+                                                    details:nil];
+
+  id mockDevice = OCMClassMock([AVCaptureDevice class]);
+  OCMStub([mockDevice authorizationStatusForMediaType:AVMediaTypeAudio])
+      .andReturn(AVAuthorizationStatusNotDetermined);
+
+  // Mimic user choosing "deny" in permission dialog.
+  OCMStub([mockDevice requestAccessForMediaType:AVMediaTypeAudio
+                              completionHandler:[OCMArg checkWithBlock:^BOOL(void (^block)(BOOL)) {
+                                block(NO);
+                                return YES;
+                              }]]);
+  FLTRequestAudioPermissionWithCompletionHandler(^(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 34942ba..c0181a5 100644
--- a/packages/camera/camera/example/lib/main.dart
+++ b/packages/camera/camera/example/lib/main.dart
@@ -697,6 +697,17 @@
           // iOS only
           showInSnackBar('Camera access is restricted.');
           break;
+        case 'AudioAccessDenied':
+          showInSnackBar('You have denied audio access.');
+          break;
+        case 'AudioAccessDeniedWithoutPrompt':
+          // iOS only
+          showInSnackBar('Please go to Settings app to enable audio access.');
+          break;
+        case 'AudioAccessRestricted':
+          // iOS only
+          showInSnackBar('Audio access is restricted.');
+          break;
         case 'cameraPermission':
           // Android & web only
           showInSnackBar('Unknown permission error.');
diff --git a/packages/camera/camera/ios/Classes/CameraPermissionUtils.h b/packages/camera/camera/ios/Classes/CameraPermissionUtils.h
index 80f55db..5cbbab0 100644
--- a/packages/camera/camera/ios/Classes/CameraPermissionUtils.h
+++ b/packages/camera/camera/ios/Classes/CameraPermissionUtils.h
@@ -18,3 +18,15 @@
 /// called on an arbitrary dispatch queue.
 extern void FLTRequestCameraPermissionWithCompletionHandler(
     FLTCameraPermissionRequestCompletionHandler handler);
+
+/// Requests audio access permission.
+///
+/// If it is the first time requesting audio access, a permission dialog will show up on the
+/// screen. Otherwise AVFoundation simply returns the user's previous choice, and in this case the
+/// user will have to update the choice in Settings app.
+///
+/// @param handler if access permission is (or was previously) granted, completion handler will be
+/// called without error; Otherwise completion handler will be called with error. Handler can be
+/// called on an arbitrary dispatch queue.
+extern void FLTRequestAudioPermissionWithCompletionHandler(
+    FLTCameraPermissionRequestCompletionHandler handler);
diff --git a/packages/camera/camera/ios/Classes/CameraPermissionUtils.m b/packages/camera/camera/ios/Classes/CameraPermissionUtils.m
index 6318338..098265a 100644
--- a/packages/camera/camera/ios/Classes/CameraPermissionUtils.m
+++ b/packages/camera/camera/ios/Classes/CameraPermissionUtils.m
@@ -5,35 +5,83 @@
 @import AVFoundation;
 #import "CameraPermissionUtils.h"
 
-void FLTRequestCameraPermissionWithCompletionHandler(
-    FLTCameraPermissionRequestCompletionHandler handler) {
-  switch ([AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo]) {
+void FLTRequestPermission(BOOL forAudio, FLTCameraPermissionRequestCompletionHandler handler) {
+  AVMediaType mediaType;
+  if (forAudio) {
+    mediaType = AVMediaTypeAudio;
+  } else {
+    mediaType = AVMediaTypeVideo;
+  }
+
+  switch ([AVCaptureDevice authorizationStatusForMediaType:mediaType]) {
     case AVAuthorizationStatusAuthorized:
       handler(nil);
       break;
-    case AVAuthorizationStatusDenied:
-      handler([FlutterError errorWithCode:@"CameraAccessDeniedWithoutPrompt"
-                                  message:@"User has previously denied the camera access request. "
-                                          @"Go to Settings to enable camera access."
-                                  details:nil]);
+    case AVAuthorizationStatusDenied: {
+      FlutterError *flutterError;
+      if (forAudio) {
+        flutterError =
+            [FlutterError errorWithCode:@"AudioAccessDeniedWithoutPrompt"
+                                message:@"User has previously denied the audio access request. "
+                                        @"Go to Settings to enable audio access."
+                                details:nil];
+      } else {
+        flutterError =
+            [FlutterError errorWithCode:@"CameraAccessDeniedWithoutPrompt"
+                                message:@"User has previously denied the camera access request. "
+                                        @"Go to Settings to enable camera access."
+                                details:nil];
+      }
+      handler(flutterError);
       break;
-    case AVAuthorizationStatusRestricted:
-      handler([FlutterError errorWithCode:@"CameraAccessRestricted"
-                                  message:@"Camera access is restricted. "
-                                  details:nil]);
+    }
+    case AVAuthorizationStatusRestricted: {
+      FlutterError *flutterError;
+      if (forAudio) {
+        flutterError = [FlutterError errorWithCode:@"AudioAccessRestricted"
+                                           message:@"Audio access is restricted. "
+                                           details:nil];
+      } else {
+        flutterError = [FlutterError errorWithCode:@"CameraAccessRestricted"
+                                           message:@"Camera access is restricted. "
+                                           details:nil];
+      }
+      handler(flutterError);
       break;
+    }
     case AVAuthorizationStatusNotDetermined: {
-      [AVCaptureDevice
-          requestAccessForMediaType:AVMediaTypeVideo
-                  completionHandler:^(BOOL granted) {
-                    // handler can be invoked on an arbitrary dispatch queue.
-                    handler(granted ? nil
-                                    : [FlutterError
-                                          errorWithCode:@"CameraAccessDenied"
-                                                message:@"User denied the camera access request."
-                                                details:nil]);
-                  }];
+      [AVCaptureDevice requestAccessForMediaType:mediaType
+                               completionHandler:^(BOOL granted) {
+                                 // handler can be invoked on an arbitrary dispatch queue.
+                                 if (granted) {
+                                   handler(nil);
+                                 } else {
+                                   FlutterError *flutterError;
+                                   if (forAudio) {
+                                     flutterError = [FlutterError
+                                         errorWithCode:@"AudioAccessDenied"
+                                               message:@"User denied the audio access request."
+                                               details:nil];
+                                   } else {
+                                     flutterError = [FlutterError
+                                         errorWithCode:@"CameraAccessDenied"
+                                               message:@"User denied the camera access request."
+                                               details:nil];
+                                   }
+                                   handler(flutterError);
+                                 }
+                               }];
       break;
     }
   }
 }
+
+void FLTRequestCameraPermissionWithCompletionHandler(
+    FLTCameraPermissionRequestCompletionHandler handler) {
+  FLTRequestPermission(/*forAudio*/ NO, handler);
+}
+
+void FLTRequestAudioPermissionWithCompletionHandler(
+    FLTCameraPermissionRequestCompletionHandler handler) {
+  FLTRequestPermission(/*forAudio*/ YES, handler);
+}
diff --git a/packages/camera/camera/ios/Classes/CameraPlugin.m b/packages/camera/camera/ios/Classes/CameraPlugin.m
index 43d541e..64952e8 100644
--- a/packages/camera/camera/ios/Classes/CameraPlugin.m
+++ b/packages/camera/camera/ios/Classes/CameraPlugin.m
@@ -132,14 +132,7 @@
       [result sendNotImplemented];
     }
   } else if ([@"create" isEqualToString:call.method]) {
-    FLTRequestCameraPermissionWithCompletionHandler(^(FlutterError *error) {
-      // Create FLTCam only if granted camera access.
-      if (error) {
-        [result sendFlutterError:error];
-      } else {
-        [self createCameraOnSessionQueueWithCreateMethodCall:call result:result];
-      }
-    });
+    [self handleCreateMethodCall:call result:result];
   } else if ([@"startImageStream" isEqualToString:call.method]) {
     [_camera startImageStreamWithMessenger:_messenger];
     [result sendSuccess];
@@ -194,7 +187,7 @@
       [_camera close];
       [result sendSuccess];
     } else if ([@"prepareForVideoRecording" isEqualToString:call.method]) {
-      [_camera setUpCaptureSessionForAudio];
+      [self.camera setUpCaptureSessionForAudio];
       [result sendSuccess];
     } else if ([@"startVideoRecording" isEqualToString:call.method]) {
       [_camera startVideoRecordingWithResult:result];
@@ -258,6 +251,33 @@
   }
 }
 
+- (void)handleCreateMethodCall:(FlutterMethodCall *)call
+                        result:(FLTThreadSafeFlutterResult *)result {
+  // Create FLTCam only if granted camera access (and audio access if audio is enabled)
+  FLTRequestCameraPermissionWithCompletionHandler(^(FlutterError *error) {
+    if (error) {
+      [result sendFlutterError:error];
+    } else {
+      // Request audio permission on `create` call with `enableAudio` argument instead of the
+      // `prepareForVideoRecording` call. This is because `prepareForVideoRecording` call is
+      // optional, and used as a workaround to fix a missing frame issue on iOS.
+      BOOL audioEnabled = [call.arguments[@"enableAudio"] boolValue];
+      if (audioEnabled) {
+        // Setup audio capture session only if granted audio access.
+        FLTRequestAudioPermissionWithCompletionHandler(^(FlutterError *error) {
+          if (error) {
+            [result sendFlutterError:error];
+          } else {
+            [self createCameraOnSessionQueueWithCreateMethodCall:call result:result];
+          }
+        });
+      } else {
+        [self createCameraOnSessionQueueWithCreateMethodCall:call result:result];
+      }
+    }
+  });
+}
+
 - (void)createCameraOnSessionQueueWithCreateMethodCall:(FlutterMethodCall *)createMethodCall
                                                 result:(FLTThreadSafeFlutterResult *)result {
   dispatch_async(self.captureSessionQueue, ^{
diff --git a/packages/camera/camera/pubspec.yaml b/packages/camera/camera/pubspec.yaml
index 14acf32..593e7b5 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.5+1
+version: 0.9.6
 
 environment:
   sdk: ">=2.14.0 <3.0.0"