#[cfg(target_os = "macos")]
use crate::constants::AV_MEDIA_TYPE_VIDEO;
#[cfg(target_os = "macos")]
use crate::constants::PERMISSION_REQUEST_TIMEOUT_SECS;
use crate::permissions::{check_permission_detailed, PermissionInfo, PermissionStatus};
use tauri::command;
#[command]
pub async fn request_camera_permission() -> Result<PermissionInfo, String> {
log::info!("Requesting camera permission");
let current_status = check_permission_detailed();
if current_status.status == PermissionStatus::Granted {
log::info!("Permission already granted");
return Ok(current_status);
}
if !current_status.can_request {
log::warn!("Cannot request permission: {}", current_status.message);
return Ok(current_status);
}
#[cfg(target_os = "macos")]
{
request_permission_macos().await
}
#[cfg(target_os = "windows")]
{
Ok(PermissionInfo {
status: PermissionStatus::NotDetermined,
message: "Please enable camera access in Windows Settings > Privacy > Camera"
.to_string(),
can_request: false,
})
}
#[cfg(target_os = "linux")]
{
Ok(PermissionInfo {
status: PermissionStatus::NotDetermined,
message: "Run: sudo usermod -a -G video $USER && newgrp video".to_string(),
can_request: false,
})
}
#[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))]
{
Err("Platform not supported".to_string())
}
}
#[cfg(target_os = "macos")]
#[allow(clippy::unused_async)]
async fn request_permission_macos() -> Result<PermissionInfo, String> {
use block::ConcreteBlock;
use objc::runtime::{Class, Object};
use objc::{msg_send, sel, sel_impl};
use std::ffi::CString;
use std::sync::mpsc;
use std::time::Duration;
log::info!("Requesting macOS camera permission");
unsafe {
let av_capture_device_class =
Class::get("AVCaptureDevice").ok_or("AVFoundation not available")?;
let ns_string_class = Class::get("NSString").ok_or("Foundation not available")?;
let av_media_type_video =
CString::new(AV_MEDIA_TYPE_VIDEO).map_err(|_| "Invalid media type string")?;
let media_type: *mut Object =
msg_send![ns_string_class, stringWithUTF8String: av_media_type_video.as_ptr()];
let (tx, rx) = mpsc::channel();
let tx_clone = tx.clone();
let handler = ConcreteBlock::new(move |granted: bool| {
let _ = tx_clone.send(granted);
});
let handler = handler.copy();
let _: () = msg_send![av_capture_device_class, requestAccessForMediaType:media_type completionHandler:&*handler]; match rx.recv_timeout(Duration::from_secs(PERMISSION_REQUEST_TIMEOUT_SECS)) {
Ok(granted) if granted => {
log::info!("Camera permission granted");
Ok(PermissionInfo {
status: PermissionStatus::Granted,
message: "Camera access authorized".to_string(),
can_request: false,
})
}
Ok(_) => {
log::warn!("Camera permission denied");
Ok(PermissionInfo {
status: PermissionStatus::Denied,
message: "Camera access denied by user".to_string(),
can_request: false,
})
}
Err(_) => {
log::error!("Permission request timed out");
Err("Permission request timed out".to_string())
}
}
}
}
#[command]
pub async fn check_camera_permission_status() -> Result<PermissionInfo, String> {
log::debug!("Checking camera permission status");
Ok(check_permission_detailed())
}
#[command]
pub fn get_permission_status_string() -> String {
let info = check_permission_detailed();
format!("{:?}", info.status)
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_check_permission_status_shape() {
let result = check_camera_permission_status().await;
assert!(result.is_ok());
let info = result.expect("permission status should return info");
assert!(!info.message.is_empty());
match info.status {
PermissionStatus::Granted
| PermissionStatus::Denied
| PermissionStatus::NotDetermined
| PermissionStatus::Restricted => {}
}
}
#[test]
fn test_permission_status_string_is_known_debug_variant() {
let status = get_permission_status_string();
assert!(
matches!(
status.as_str(),
"Granted" | "Denied" | "NotDetermined" | "Restricted"
),
"unexpected permission status string: {status}"
);
}
#[tokio::test]
#[cfg(any(target_os = "windows", target_os = "linux"))]
async fn test_request_permission_platform_message_non_empty() {
let result = request_camera_permission().await;
assert!(result.is_ok());
let info = result.expect("request should return guidance info");
assert!(!info.message.is_empty());
}
#[tokio::test]
#[ignore = "Requires camera hardware and OS permissions - run manually"]
async fn test_check_permission_status() {
let result = check_camera_permission_status().await;
assert!(result.is_ok());
let info = result.expect("permission status expected");
println!("Permission status: {:?}", info.status);
println!("Message: {}", info.message);
}
#[test]
#[ignore = "Requires camera hardware and OS permissions - run manually"]
fn test_permission_status_string() {
let status = get_permission_status_string();
assert!(!status.is_empty());
println!("Status string: {status}");
}
}