use alloc::collections::btree_map::BTreeMap;
use alloc::vec::Vec;
use azul_core::dom::DomNodeId;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(C)]
pub enum Capability {
Camera,
Microphone,
ScreenCapture,
Geolocation,
GeolocationBackground,
Biometric,
Motion,
PhotoLibrary,
PhotoLibraryWrite,
Contacts,
Calendars,
Reminders,
Notifications,
Bluetooth,
BluetoothBackground,
NearbyWifi,
LocalNetwork,
AppTrackingTransparency,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(C)]
pub enum PermissionQuality {
Full,
Reduced,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(C, u8)]
pub enum PermissionState {
NotDetermined,
Requested,
Granted {
quality: PermissionQuality,
},
Denied,
Restricted,
EphemeralGranted {
until_app_close: bool,
},
}
impl PermissionState {
pub fn is_granted(self) -> bool {
matches!(
self,
PermissionState::Granted { .. } | PermissionState::EphemeralGranted { .. }
)
}
pub fn could_re_prompt(self) -> bool {
matches!(self, PermissionState::NotDetermined)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[repr(C, u8)]
pub enum PermissionDiffEvent {
Subscribe {
capability: Capability,
node_id: DomNodeId,
},
Release {
capability: Capability,
},
Reconfigure {
capability: Capability,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CapabilityEntry {
pub state: PermissionState,
pub refcount: u32,
pub last_subscriber: Option<DomNodeId>,
}
impl CapabilityEntry {
fn new() -> Self {
Self {
state: PermissionState::NotDetermined,
refcount: 0,
last_subscriber: None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct PermissionManager {
pub statuses: BTreeMap<Capability, CapabilityEntry>,
pending_events: Vec<PermissionDiffEvent>,
}
impl PermissionManager {
pub fn new() -> Self {
Self::default()
}
pub fn get_status(&self, capability: Capability) -> PermissionState {
self.statuses
.get(&capability)
.map(|e| e.state)
.unwrap_or(PermissionState::NotDetermined)
}
pub fn subscribe(&mut self, capability: Capability, node_id: DomNodeId) {
let entry = self
.statuses
.entry(capability)
.or_insert_with(CapabilityEntry::new);
entry.last_subscriber = Some(node_id);
entry.refcount = entry.refcount.saturating_add(1);
if entry.refcount == 1 {
self.pending_events.push(PermissionDiffEvent::Subscribe {
capability,
node_id,
});
}
}
pub fn release(&mut self, capability: Capability) {
let Some(entry) = self.statuses.get_mut(&capability) else {
return;
};
if entry.refcount == 0 {
return;
}
entry.refcount -= 1;
if entry.refcount == 0 {
entry.last_subscriber = None;
self.pending_events
.push(PermissionDiffEvent::Release { capability });
}
}
pub fn force_release(&mut self, capability: Capability) {
let Some(entry) = self.statuses.get_mut(&capability) else {
return;
};
if entry.refcount == 0 {
return;
}
entry.refcount = 0;
entry.last_subscriber = None;
self.pending_events
.push(PermissionDiffEvent::Release { capability });
}
pub fn set_status(&mut self, capability: Capability, state: PermissionState) -> bool {
let entry = self
.statuses
.entry(capability)
.or_insert_with(CapabilityEntry::new);
if entry.state == state {
return false;
}
entry.state = state;
true
}
pub fn take_pending_events(&mut self) -> Vec<PermissionDiffEvent> {
core::mem::take(&mut self.pending_events)
}
pub fn refcount(&self, capability: Capability) -> u32 {
self.statuses
.get(&capability)
.map(|e| e.refcount)
.unwrap_or(0)
}
pub fn diff_layout<F>(&mut self, mut for_each_bearing_node: F)
where
F: FnMut(&mut dyn FnMut(Capability, DomNodeId)),
{
let mut next: BTreeMap<Capability, (u32, Option<DomNodeId>)> = BTreeMap::new();
for_each_bearing_node(&mut |cap, node| {
let slot = next.entry(cap).or_insert((0, None));
slot.0 = slot.0.saturating_add(1);
if slot.1.is_none() {
slot.1 = Some(node);
}
});
let mut all_caps: Vec<Capability> = self.statuses.keys().copied().collect();
for cap in next.keys() {
if !all_caps.contains(cap) {
all_caps.push(*cap);
}
}
for cap in all_caps {
let (new_count, first_node) = next.get(&cap).copied().unwrap_or((0, None));
let entry = self
.statuses
.entry(cap)
.or_insert_with(CapabilityEntry::new);
let old_count = entry.refcount;
entry.refcount = new_count;
if new_count == 0 && old_count > 0 {
entry.last_subscriber = None;
self.pending_events
.push(PermissionDiffEvent::Release { capability: cap });
} else if new_count > 0 && old_count == 0 {
let node = first_node.unwrap_or(DomNodeId::ROOT);
entry.last_subscriber = first_node;
self.pending_events.push(PermissionDiffEvent::Subscribe {
capability: cap,
node_id: node,
});
}
}
}
}
static ASYNC_RESULTS: std::sync::Mutex<Vec<(Capability, PermissionState)>> =
std::sync::Mutex::new(Vec::new());
pub fn push_async_result(capability: Capability, state: PermissionState) {
let mut q = ASYNC_RESULTS.lock().unwrap_or_else(|e| e.into_inner());
q.push((capability, state));
}
pub fn drain_async_results() -> Vec<(Capability, PermissionState)> {
let mut q = ASYNC_RESULTS.lock().unwrap_or_else(|e| e.into_inner());
core::mem::take(&mut *q)
}
#[cfg(test)]
mod tests {
use super::*;
use azul_core::dom::{DomId, NodeId};
fn node(idx: usize) -> DomNodeId {
DomNodeId {
dom: DomId::ROOT_ID,
node: NodeId::from_usize(idx).into(),
}
}
#[test]
fn subscribe_release_round_trip_emits_paired_events() {
let mut mgr = PermissionManager::new();
assert_eq!(mgr.get_status(Capability::Geolocation), PermissionState::NotDetermined);
assert_eq!(mgr.refcount(Capability::Geolocation), 0);
mgr.subscribe(Capability::Geolocation, node(1));
assert_eq!(mgr.refcount(Capability::Geolocation), 1);
let events = mgr.take_pending_events();
assert_eq!(events.len(), 1);
assert!(matches!(
events[0],
PermissionDiffEvent::Subscribe { capability: Capability::Geolocation, .. }
));
mgr.release(Capability::Geolocation);
assert_eq!(mgr.refcount(Capability::Geolocation), 0);
let events = mgr.take_pending_events();
assert_eq!(events.len(), 1);
assert!(matches!(
events[0],
PermissionDiffEvent::Release { capability: Capability::Geolocation }
));
}
#[test]
fn second_subscriber_does_not_re_emit_subscribe() {
let mut mgr = PermissionManager::new();
mgr.subscribe(Capability::Camera, node(1));
mgr.subscribe(Capability::Camera, node(2));
assert_eq!(mgr.refcount(Capability::Camera), 2);
let events = mgr.take_pending_events();
assert_eq!(events.len(), 1);
}
#[test]
fn release_only_after_last_subscriber_drops() {
let mut mgr = PermissionManager::new();
mgr.subscribe(Capability::Microphone, node(1));
mgr.subscribe(Capability::Microphone, node(2));
let _ = mgr.take_pending_events();
mgr.release(Capability::Microphone);
assert_eq!(mgr.refcount(Capability::Microphone), 1);
assert!(mgr.take_pending_events().is_empty());
mgr.release(Capability::Microphone);
assert_eq!(mgr.refcount(Capability::Microphone), 0);
let events = mgr.take_pending_events();
assert_eq!(events.len(), 1);
assert!(matches!(
events[0],
PermissionDiffEvent::Release { capability: Capability::Microphone }
));
}
#[test]
fn force_release_drops_refcount_and_emits_event() {
let mut mgr = PermissionManager::new();
mgr.subscribe(Capability::Camera, node(1));
mgr.subscribe(Capability::Camera, node(2));
let _ = mgr.take_pending_events();
mgr.force_release(Capability::Camera);
assert_eq!(mgr.refcount(Capability::Camera), 0);
let events = mgr.take_pending_events();
assert_eq!(events.len(), 1);
assert!(matches!(
events[0],
PermissionDiffEvent::Release { capability: Capability::Camera }
));
}
#[test]
fn set_status_returns_change_flag() {
let mut mgr = PermissionManager::new();
assert!(mgr.set_status(Capability::Camera, PermissionState::Requested));
assert!(!mgr.set_status(Capability::Camera, PermissionState::Requested));
assert!(mgr.set_status(
Capability::Camera,
PermissionState::Granted { quality: PermissionQuality::Full }
));
assert!(mgr.get_status(Capability::Camera).is_granted());
}
#[test]
fn diff_layout_picks_up_appearing_node_and_releases_it_next_frame() {
let mut mgr = PermissionManager::new();
mgr.diff_layout(|emit| {
emit(Capability::Geolocation, node(7));
});
assert_eq!(mgr.refcount(Capability::Geolocation), 1);
let events = mgr.take_pending_events();
assert_eq!(events.len(), 1);
assert!(matches!(
events[0],
PermissionDiffEvent::Subscribe { capability: Capability::Geolocation, .. }
));
mgr.diff_layout(|_emit| { });
assert_eq!(mgr.refcount(Capability::Geolocation), 0);
let events = mgr.take_pending_events();
assert_eq!(events.len(), 1);
assert!(matches!(
events[0],
PermissionDiffEvent::Release { capability: Capability::Geolocation }
));
}
#[test]
fn diff_layout_re_emits_subscribe_after_release_cycle() {
let mut mgr = PermissionManager::new();
mgr.diff_layout(|emit| emit(Capability::Camera, node(1)));
let _ = mgr.take_pending_events();
mgr.diff_layout(|_emit| {});
let _ = mgr.take_pending_events();
mgr.diff_layout(|emit| emit(Capability::Camera, node(2)));
let events = mgr.take_pending_events();
assert_eq!(events.len(), 1);
assert!(matches!(
events[0],
PermissionDiffEvent::Subscribe { capability: Capability::Camera, .. }
));
}
#[test]
fn async_results_round_trip_through_manager() {
let _ = drain_async_results();
push_async_result(
Capability::Camera,
PermissionState::Granted {
quality: PermissionQuality::Full,
},
);
push_async_result(Capability::Geolocation, PermissionState::Denied);
let drained = drain_async_results();
assert_eq!(drained.len(), 2, "both parked results drain in order");
assert_eq!(drained[0].0, Capability::Camera);
assert_eq!(drained[1].0, Capability::Geolocation);
let mut mgr = PermissionManager::new();
for (cap, state) in drained {
mgr.set_status(cap, state);
}
assert!(mgr.get_status(Capability::Camera).is_granted());
assert_eq!(mgr.get_status(Capability::Geolocation), PermissionState::Denied);
assert!(drain_async_results().is_empty());
}
}