Skip to main content

cranpose_services/
microphone_permission.rs

1//! Permission to record from the microphone.
2//!
3//! Stores refuse an application that records without asking, or that asks
4//! without saying why, so the framework asks only when the application calls
5//! [`request_microphone_permission`], and the person reads the reason the
6//! build script gives `cranpose_capabilities::Use::microphone`. Reading the
7//! permission never asks.
8//!
9//! A person who refused twice, or chose not to be asked again, can allow the
10//! microphone only in the system settings of the application:
11//! [`MicrophonePermission::Blocked`] says so and [`open_microphone_settings`]
12//! goes there. [`rememberMicrophonePermission`] reads the permission again
13//! each time the application comes back to the front, so an answer given in
14//! the settings shows at once.
15//!
16//! Where no backend is installed (desktop systems, which ask by themselves
17//! the first time an application records), the permission reads
18//! [`MicrophonePermission::Granted`].
19
20use std::sync::{Arc, Mutex, OnceLock, atomic::Ordering};
21
22use cranpose_core::{State, rememberEventStream};
23use portable_atomic::AtomicU64;
24
25use crate::registry::ServiceRegistry;
26
27/// Whether the application may record.
28#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
29pub enum MicrophonePermission {
30    /// The person has not been asked.
31    NotAsked,
32    /// The person said yes.
33    Granted,
34    /// The person said no; asking again shows the prompt again.
35    Denied,
36    /// The person said no for good; only the system settings can allow it.
37    Blocked,
38}
39
40/// A platform's microphone permission, installed by the platform backend.
41pub trait MicrophoneAccess: Send + Sync {
42    /// The permission as the platform reports it now.
43    fn permission(&self) -> MicrophonePermission;
44    /// Shows the system prompt. The backend calls
45    /// [`publish_microphone_permission`] when the person answers.
46    fn request(&self);
47    /// Opens the system settings page of this application.
48    fn open_settings(&self);
49    /// Readies the platform's audio for recording, where opening an input
50    /// stream is not enough: iOS and watchOS record only in a session whose
51    /// category takes input.
52    fn prepare_recording(&self) -> Result<(), String> {
53        Ok(())
54    }
55}
56
57static PLATFORM: ServiceRegistry<dyn MicrophoneAccess> = ServiceRegistry::new();
58
59/// Installs the platform's permission, or removes it with `None`. Platform
60/// backends call it.
61pub fn set_platform_microphone_access(access: Option<Arc<dyn MicrophoneAccess>>) {
62    match access {
63        Some(access) => PLATFORM.set(access),
64        None => PLATFORM.clear(),
65    }
66    publish_microphone_permission();
67}
68
69/// Whether the application may record now.
70pub fn microphone_permission() -> MicrophonePermission {
71    PLATFORM
72        .get()
73        .map_or(MicrophonePermission::Granted, |access| access.permission())
74}
75
76/// Asks the person, when the platform can still show its prompt: a no-op
77/// once the microphone is allowed or blocked.
78pub fn request_microphone_permission() {
79    if let Some(access) = PLATFORM.get()
80        && matches!(
81            access.permission(),
82            MicrophonePermission::NotAsked | MicrophonePermission::Denied
83        )
84    {
85        access.request();
86    }
87}
88
89/// Opens the system settings page of this application, where a blocked
90/// microphone can be allowed.
91pub fn open_microphone_settings() {
92    if let Some(access) = PLATFORM.get() {
93        access.open_settings();
94    }
95}
96
97/// Readies the platform's audio for recording; call it before opening a
98/// microphone stream. On iOS and watchOS it sets the audio session to record
99/// and play and activates it; elsewhere it does nothing.
100pub fn prepare_microphone() -> Result<(), String> {
101    PLATFORM
102        .get()
103        .map_or(Ok(()), |access| access.prepare_recording())
104}
105
106type Observer = Arc<dyn Fn(MicrophonePermission) + Send + Sync>;
107
108static NEXT_OBSERVER: AtomicU64 = AtomicU64::new(1);
109
110fn observers() -> &'static Mutex<Vec<(u64, Observer)>> {
111    static OBSERVERS: OnceLock<Mutex<Vec<(u64, Observer)>>> = OnceLock::new();
112    OBSERVERS.get_or_init(|| Mutex::new(Vec::new()))
113}
114
115/// Tells the observers the permission as it is now. Backends call this when
116/// the person answers the prompt.
117pub fn publish_microphone_permission() {
118    let permission = microphone_permission();
119    let listeners: Vec<Observer> = observers()
120        .lock()
121        .map(|observers| observers.iter().map(|(_, o)| Arc::clone(o)).collect())
122        .unwrap_or_default();
123    for observer in listeners {
124        observer(permission);
125    }
126}
127
128/// A registration that keeps an observer.
129pub struct MicrophonePermissionObserver {
130    id: u64,
131}
132
133impl Drop for MicrophonePermissionObserver {
134    fn drop(&mut self) {
135        if let Ok(mut observers) = observers().lock() {
136            observers.retain(|(id, _)| *id != self.id);
137        }
138    }
139}
140
141/// Registers `observer` for answers, delivering the permission as it is now
142/// at once.
143pub fn observe_microphone_permission(
144    observer: impl Fn(MicrophonePermission) + Send + Sync + 'static,
145) -> MicrophonePermissionObserver {
146    let id = NEXT_OBSERVER.fetch_add(1, Ordering::Relaxed);
147    let observer: Observer = Arc::new(observer);
148    if let Ok(mut observers) = observers().lock() {
149        observers.push((id, Arc::clone(&observer)));
150    }
151    observer(microphone_permission());
152    MicrophonePermissionObserver { id }
153}
154
155/// The permission as observable state, read again each time the
156/// application's lifecycle changes, such as on its return from the system
157/// settings. This never asks.
158#[expect(non_snake_case)]
159#[track_caller]
160pub fn rememberMicrophonePermission() -> State<MicrophonePermission> {
161    let lifecycle = crate::rememberLifecycleState().get();
162    let updates = rememberEventStream(lifecycle, move |sender| {
163        observe_microphone_permission(move |permission| sender.send(permission))
164    });
165    cranpose_core::collectAsState(updates, lifecycle, microphone_permission())
166}
167
168#[cfg(test)]
169#[path = "tests/microphone_permission_tests.rs"]
170mod tests;