#![expect(unsafe_code)]
use std::sync::Arc;
use block2::RcBlock;
use cranpose_services::{
MicrophoneAccess, MicrophonePermission, publish_microphone_permission,
set_platform_microphone_access,
};
use dispatch2::DispatchQueue;
use objc2::runtime::Bool;
#[cfg(target_os = "ios")]
use objc2::{MainThreadMarker, rc::Retained, runtime::AnyObject};
use objc2_avf_audio::{
AVAudioSession, AVAudioSessionCategoryOptions, AVAudioSessionCategoryPlayAndRecord,
AVAudioSessionModeDefault, AVAudioSessionRecordPermission,
};
#[cfg(target_os = "ios")]
use objc2_foundation::{NSDictionary, NSString, NSURL};
#[cfg(target_os = "ios")]
use objc2_ui_kit::{UIApplication, UIApplicationOpenSettingsURLString};
pub(crate) fn register() {
set_platform_microphone_access(Some(Arc::new(AppleMicrophone)));
}
struct AppleMicrophone;
impl MicrophoneAccess for AppleMicrophone {
#[expect(deprecated)]
fn permission(&self) -> MicrophonePermission {
let permission = unsafe { AVAudioSession::sharedInstance().recordPermission() };
match permission {
AVAudioSessionRecordPermission::Granted => MicrophonePermission::Granted,
AVAudioSessionRecordPermission::Denied => MicrophonePermission::Blocked,
_ => MicrophonePermission::NotAsked,
}
}
fn request(&self) {
let answered = RcBlock::new(|_granted: Bool| {
DispatchQueue::main().exec_async(publish_microphone_permission);
});
#[expect(deprecated)]
unsafe {
AVAudioSession::sharedInstance().requestRecordPermission(&answered);
}
}
fn open_settings(&self) {
#[cfg(target_os = "ios")]
let open = || {
let Some(mtm) = MainThreadMarker::new() else {
return;
};
let Some(url) = NSURL::URLWithString(unsafe { UIApplicationOpenSettingsURLString })
else {
return;
};
let options: Retained<NSDictionary<NSString, AnyObject>> = NSDictionary::new();
unsafe {
UIApplication::sharedApplication(mtm)
.openURL_options_completionHandler(&url, &options, None);
}
};
#[cfg(target_os = "ios")]
if MainThreadMarker::new().is_some() {
open();
} else {
DispatchQueue::main().exec_async(open);
}
}
fn prepare_recording(&self) -> Result<(), String> {
unsafe {
let session = AVAudioSession::sharedInstance();
let (Some(category), Some(mode)) = (
AVAudioSessionCategoryPlayAndRecord,
AVAudioSessionModeDefault,
) else {
return Err("the audio session names are missing".into());
};
let options = AVAudioSessionCategoryOptions::MixWithOthers;
#[cfg(target_os = "ios")]
let options = options | AVAudioSessionCategoryOptions::AllowBluetoothHFP;
session
.setCategory_mode_options_error(category, mode, options)
.map_err(|error| format!("audio session category: {error}"))?;
session
.setActive_error(true)
.map_err(|error| format!("audio session: {error}"))
}
}
}