crabcamera 0.9.2

Advanced cross-platform camera integration for Tauri applications
Documentation
#[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;

/// Request camera permission (platform-specific)
///
/// # Errors
/// Returns an `Err` if the current platform is not supported, or, on macOS,
/// if `AVFoundation` is unavailable or the permission request times out.
#[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);
    }

    // Platform-specific permission request
    #[cfg(target_os = "macos")]
    {
        request_permission_macos().await
    }

    #[cfg(target_os = "windows")]
    {
        // Windows doesn't have programmatic permission request
        // User must enable in Settings > Privacy > Camera
        Ok(PermissionInfo {
            status: PermissionStatus::NotDetermined,
            message: "Please enable camera access in Windows Settings > Privacy > Camera"
                .to_string(),
            can_request: false,
        })
    }

    #[cfg(target_os = "linux")]
    {
        // Linux permissions are group-based
        // User must add themselves to video group
        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")?;

        // Build an NSString for the video media type (AVMediaTypeVideo == @"vide").
        // AVCaptureDevice has no `mediaTypeForString:` selector; sending it raises
        // an unrecognized-selector NSException which aborts the process.
        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();

        // Create a proper Objective-C block using the block crate
        // This replaces the invalid inline ^(granted: bool) {} syntax
        let tx_clone = tx.clone();
        let handler = ConcreteBlock::new(move |granted: bool| {
            let _ = tx_clone.send(granted);
        });
        // Copy the block to the heap so it survives the async callback
        let handler = handler.copy();

        // Request access (this will show system dialog)
        let _: () = msg_send![av_capture_device_class, requestAccessForMediaType:media_type completionHandler:&*handler]; // Wait for user response (with timeout)
        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())
            }
        }
    }
}

/// Check camera permission status
///
/// # Errors
/// This function always succeeds and never returns an `Err`.
#[command]
pub async fn check_camera_permission_status() -> Result<PermissionInfo, String> {
    log::debug!("Checking camera permission status");
    Ok(check_permission_detailed())
}

/// Get human-readable permission status string (legacy compatibility)
#[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}");
    }
}