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