Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/camera/camera_avfoundation/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 0.9.22+8

* Migrates `FLTPermissionServicing` and `FLTCameraPermissionManager` classes to Swift.

## 0.9.22+7

* Migrates `FLTCaptureConnection`, `FLTCaptureDeviceFormat` and `FLTAssetWriter` classes to Swift.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ final class AvailableCamerasTest: XCTestCase {
messenger: MockFlutterBinaryMessenger(),
globalAPI: MockGlobalEventApi(),
deviceDiscoverer: deviceDiscoverer,
permissionManager: MockFLTCameraPermissionManager(),
permissionManager: MockCameraPermissionManager(),
deviceFactory: { _ in MockCaptureDevice() },
captureSessionFactory: { MockCaptureSession() },
captureDeviceInputFactory: MockCaptureDeviceInputFactory(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ final class CameraInitRaceConditionsTests: XCTestCase {
messenger: MockFlutterBinaryMessenger(),
globalAPI: MockGlobalEventApi(),
deviceDiscoverer: MockCameraDeviceDiscoverer(),
permissionManager: MockFLTCameraPermissionManager(),
permissionManager: MockCameraPermissionManager(),
deviceFactory: { _ in MockCaptureDevice() },
captureSessionFactory: { MockCaptureSession() },
captureDeviceInputFactory: MockCaptureDeviceInputFactory(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ final class CameraMethodChannelTests: XCTestCase {
messenger: MockFlutterBinaryMessenger(),
globalAPI: MockGlobalEventApi(),
deviceDiscoverer: MockCameraDeviceDiscoverer(),
permissionManager: MockFLTCameraPermissionManager(),
permissionManager: MockCameraPermissionManager(),
deviceFactory: { _ in MockCaptureDevice() },
captureSessionFactory: { session },
captureDeviceInputFactory: MockCaptureDeviceInputFactory(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ final class CameraOrientationTests: XCTestCase {
messenger: MockFlutterBinaryMessenger(),
globalAPI: mockEventAPI,
deviceDiscoverer: mockDeviceDiscoverer,
permissionManager: MockFLTCameraPermissionManager(),
permissionManager: MockCameraPermissionManager(),
deviceFactory: { _ in mockDevice },
captureSessionFactory: { MockCaptureSession() },
captureDeviceInputFactory: MockCaptureDeviceInputFactory(),
Expand Down Expand Up @@ -130,7 +130,7 @@ final class CameraOrientationTests: XCTestCase {
messenger: MockFlutterBinaryMessenger(),
globalAPI: mockEventAPI,
deviceDiscoverer: mockDeviceDiscoverer,
permissionManager: MockFLTCameraPermissionManager(),
permissionManager: MockCameraPermissionManager(),
deviceFactory: { _ in weakDevice! },
captureSessionFactory: { MockCaptureSession() },
captureDeviceInputFactory: MockCaptureDeviceInputFactory(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,23 @@ import XCTest
import camera_avfoundation_objc
#endif

private final class MockPermissionService: NSObject, FLTPermissionServicing {
private final class MockPermissionService: NSObject, PermissionServicing {
var authorizationStatusStub: ((AVMediaType) -> AVAuthorizationStatus)?
var requestAccessStub: ((AVMediaType, @escaping (Bool) -> Void) -> Void)?
var requestAccessStub: ((AVMediaType, @escaping @Sendable (Bool) -> Void) -> Void)?

func authorizationStatus(for mediaType: AVMediaType) -> AVAuthorizationStatus {
return authorizationStatusStub?(mediaType) ?? .notDetermined
}

func requestAccess(for mediaType: AVMediaType, completion: @escaping (Bool) -> Void) {
func requestAccess(for mediaType: AVMediaType, completion: @escaping @Sendable (Bool) -> Void) {
requestAccessStub?(mediaType, completion)
}
}

final class CameraPermissionManagerTests: XCTestCase {
private func createSutAndMocks() -> (FLTCameraPermissionManager, MockPermissionService) {
private func createSutAndMocks() -> (CameraPermissionManager, MockPermissionService) {
let mockPermissionService = MockPermissionService()
let permissionManager = FLTCameraPermissionManager(permissionService: mockPermissionService)
let permissionManager = CameraPermissionManager(permissionService: mockPermissionService)

return (permissionManager, mockPermissionService)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ import XCTest

final class CameraPluginCreateCameraTests: XCTestCase {
private func createCameraPlugin() -> (
CameraPlugin, MockFLTCameraPermissionManager, MockCaptureSession
CameraPlugin, MockCameraPermissionManager, MockCaptureSession
) {
let mockPermissionManager = MockFLTCameraPermissionManager()
let mockPermissionManager = MockCameraPermissionManager()
let mockCaptureSession = MockCaptureSession()

let cameraPlugin = CameraPlugin(
Expand All @@ -41,13 +41,13 @@ final class CameraPluginCreateCameraTests: XCTestCase {
mockPermissionManager.requestCameraPermissionStub = { completion in
requestCameraPermissionCalled = true
// Permission is granted
completion?(nil)
completion(nil)
}
var requestAudioPermissionCalled = false
mockPermissionManager.requestAudioPermissionStub = { completion in
requestAudioPermissionCalled = true
// Permission is granted
completion?(nil)
completion(nil)
}

cameraPlugin.createCamera(
Expand Down Expand Up @@ -76,13 +76,13 @@ final class CameraPluginCreateCameraTests: XCTestCase {
mockPermissionManager.requestCameraPermissionStub = { completion in
requestCameraPermissionCalled = true
// Permission is granted
completion?(nil)
completion(nil)
}
var requestAudioPermissionCalled = false
mockPermissionManager.requestAudioPermissionStub = { completion in
requestAudioPermissionCalled = true
// Permission is granted
completion?(nil)
completion(nil)
}

cameraPlugin.createCamera(
Expand All @@ -109,11 +109,11 @@ final class CameraPluginCreateCameraTests: XCTestCase {

mockPermissionManager.requestCameraPermissionStub = { completion in
// Permission is granted
completion?(nil)
completion(nil)
}
mockPermissionManager.requestAudioPermissionStub = { completion in
// Permission is granted
completion?(nil)
completion(nil)
}
mockCaptureSession.canSetSessionPresetStub = { _ in true }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ final class CameraPluginDelegatingMethodTests: XCTestCase {
messenger: MockFlutterBinaryMessenger(),
globalAPI: MockGlobalEventApi(),
deviceDiscoverer: MockCameraDeviceDiscoverer(),
permissionManager: MockFLTCameraPermissionManager(),
permissionManager: MockCameraPermissionManager(),
deviceFactory: { _ in MockCaptureDevice() },
captureSessionFactory: { MockCaptureSession() },
captureDeviceInputFactory: MockCaptureDeviceInputFactory(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ final class CameraPluginInitializeCameraTests: XCTestCase {
messenger: MockFlutterBinaryMessenger(),
globalAPI: mockGlobalEventApi,
deviceDiscoverer: MockCameraDeviceDiscoverer(),
permissionManager: MockFLTCameraPermissionManager(),
permissionManager: MockCameraPermissionManager(),
deviceFactory: { _ in MockCaptureDevice() },
captureSessionFactory: { MockCaptureSession() },
captureDeviceInputFactory: MockCaptureDeviceInputFactory(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ final class CameraSettingsTests: XCTestCase {
messenger: MockFlutterBinaryMessenger(),
globalAPI: MockGlobalEventApi(),
deviceDiscoverer: MockCameraDeviceDiscoverer(),
permissionManager: MockFLTCameraPermissionManager(),
permissionManager: MockCameraPermissionManager(),
deviceFactory: { _ in mockDevice },
captureSessionFactory: { mockSession },
captureDeviceInputFactory: MockCaptureDeviceInputFactory(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,34 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import camera_avfoundation
@testable import camera_avfoundation

// Import Objective-C part of the implementation when SwiftPM is used.
#if canImport(camera_avfoundation_objc)
import camera_avfoundation_objc
#endif

final class MockFLTCameraPermissionManager: FLTCameraPermissionManager {
var requestCameraPermissionStub: ((((FlutterError?) -> Void)?) -> Void)?
var requestAudioPermissionStub: ((((FlutterError?) -> Void)?) -> Void)?
final class MockCameraPermissionManager: CameraPermissionManager {
var requestCameraPermissionStub: ((@escaping (FlutterError?) -> Void) -> Void)?
var requestAudioPermissionStub: ((@escaping (FlutterError?) -> Void) -> Void)?

override func requestCameraPermission(completionHandler: ((FlutterError?) -> Void)?) {
requestCameraPermissionStub?(completionHandler)
init() {
super.init(permissionService: DefaultPermissionService())
}

override func requestAudioPermission(completionHandler: ((FlutterError?) -> Void)?) {
requestAudioPermissionStub?(completionHandler)
override func requestCameraPermission(completionHandler: @escaping (FlutterError?) -> Void) {
if let stub = requestCameraPermissionStub {
stub(completionHandler)
} else {
super.requestCameraPermission(completionHandler: completionHandler)
}
}

override func requestAudioPermission(completionHandler: @escaping (FlutterError?) -> Void) {
if let stub = requestAudioPermissionStub {
stub(completionHandler)
} else {
super.requestAudioPermission(completionHandler: completionHandler)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
// Copyright 2013 The Flutter Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import AVFoundation
import Flutter

/// Completion handler for camera permission requests.
typealias CameraPermissionRequestCompletionHandler = (FlutterError?) -> Void

/// Manages camera and audio permission requests.
class CameraPermissionManager: NSObject {
let permissionService: PermissionServicing

init(permissionService: PermissionServicing) {
self.permissionService = permissionService
super.init()
}

/// 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.
func requestCameraPermission(
completionHandler handler: @escaping CameraPermissionRequestCompletionHandler
) {
requestPermission(forAudio: false, handler: 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.
func requestAudioPermission(
completionHandler handler: @escaping CameraPermissionRequestCompletionHandler
) {
requestPermission(forAudio: true, handler: handler)
}

private func requestPermission(
forAudio: Bool,
handler: @escaping CameraPermissionRequestCompletionHandler
) {
let mediaType: AVMediaType = forAudio ? .audio : .video

switch permissionService.authorizationStatus(for: mediaType) {
case .authorized:
handler(nil)

case .denied:
let flutterError: FlutterError
if forAudio {
flutterError = FlutterError(
code: "AudioAccessDeniedWithoutPrompt",
message:
"User has previously denied the audio access request. Go to Settings to enable audio access.",
details: nil
)
} else {
flutterError = FlutterError(
code: "CameraAccessDeniedWithoutPrompt",
message:
"User has previously denied the camera access request. Go to Settings to enable camera access.",
details: nil
)
}
handler(flutterError)

case .restricted:
let flutterError: FlutterError
if forAudio {
flutterError = FlutterError(
code: "AudioAccessRestricted",
message: "Audio access is restricted.",
details: nil
)
} else {
flutterError = FlutterError(
code: "CameraAccessRestricted",
message: "Camera access is restricted.",
details: nil
)
}
handler(flutterError)

case .notDetermined:
permissionService.requestAccess(for: mediaType) { granted in
// handler can be invoked on an arbitrary dispatch queue.
if granted {
handler(nil)
} else {
let flutterError: FlutterError
if forAudio {
flutterError = FlutterError(
code: "AudioAccessDenied",
message: "User denied the audio access request.",
details: nil
)
} else {
flutterError = FlutterError(
code: "CameraAccessDenied",
message: "User denied the camera access request.",
details: nil
)
}
handler(flutterError)
}
}

@unknown default:
assertionFailure("Unknown authorization status")
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public final class CameraPlugin: NSObject, FlutterPlugin {
private let messenger: FlutterBinaryMessenger
private let globalEventAPI: FCPCameraGlobalEventApi
private let deviceDiscoverer: CameraDeviceDiscoverer
private let permissionManager: FLTCameraPermissionManager
private let permissionManager: CameraPermissionManager
private let captureDeviceFactory: VideoCaptureDeviceFactory
private let captureSessionFactory: CaptureSessionFactory
private let captureDeviceInputFactory: CaptureDeviceInputFactory
Expand All @@ -32,8 +32,8 @@ public final class CameraPlugin: NSObject, FlutterPlugin {
messenger: registrar.messenger(),
globalAPI: FCPCameraGlobalEventApi(binaryMessenger: registrar.messenger()),
deviceDiscoverer: DefaultCameraDeviceDiscoverer(),
permissionManager: FLTCameraPermissionManager(
permissionService: FLTDefaultPermissionService()),
permissionManager: CameraPermissionManager(
permissionService: DefaultPermissionService()),
deviceFactory: { name in
// TODO(RobertOdrowaz) Implement better error handling and remove non-null assertion
AVCaptureDevice(uniqueID: name)!
Expand All @@ -51,7 +51,7 @@ public final class CameraPlugin: NSObject, FlutterPlugin {
messenger: FlutterBinaryMessenger,
globalAPI: FCPCameraGlobalEventApi,
deviceDiscoverer: CameraDeviceDiscoverer,
permissionManager: FLTCameraPermissionManager,
permissionManager: CameraPermissionManager,
deviceFactory: @escaping VideoCaptureDeviceFactory,
captureSessionFactory: @escaping CaptureSessionFactory,
captureDeviceInputFactory: CaptureDeviceInputFactory,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Copyright 2013 The Flutter Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import AVFoundation

/// A protocol for permission-related operations on AVCaptureDevice.
/// It exists to allow mocking permission checks in tests.
protocol PermissionServicing: NSObjectProtocol {
func authorizationStatus(for mediaType: AVMediaType) -> AVAuthorizationStatus
func requestAccess(
for mediaType: AVMediaType,
completion handler: @escaping @Sendable (Bool) -> Void
)
}

/// Default implementation of PermissionServicing that forwards calls to AVCaptureDevice.
class DefaultPermissionService: NSObject, PermissionServicing {
func authorizationStatus(for mediaType: AVMediaType) -> AVAuthorizationStatus {
return AVCaptureDevice.authorizationStatus(for: mediaType)
}

func requestAccess(
for mediaType: AVMediaType,
completion handler: @escaping @Sendable (Bool) -> Void
) {
AVCaptureDevice.requestAccess(for: mediaType, completionHandler: handler)
}
}
Loading