use alloc::vec::Vec;
use azul_core::dom::DomNodeId;
use azul_core::events::{
EventData, EventProvider, EventSource as CoreEventSource, EventType, SyntheticEvent,
};
use azul_core::task::Instant;
pub use azul_core::biometric::{BiometricKind, BiometricPrompt, BiometricResult};
#[derive(Copy, Debug, Clone, PartialEq, Eq)]
pub struct BiometricManager {
pub last_result: Option<BiometricResult>,
pub availability: BiometricKind,
pub in_flight: u32,
pub pending_event: bool,
}
impl Default for BiometricManager {
fn default() -> Self {
Self {
last_result: None,
availability: BiometricKind::NotAvailable,
in_flight: 0,
pending_event: false,
}
}
}
impl BiometricManager {
#[must_use] pub fn new() -> Self {
Self::default()
}
#[must_use] pub const fn last_result(&self) -> Option<BiometricResult> {
self.last_result
}
#[must_use] pub const fn availability(&self) -> BiometricKind {
self.availability
}
#[must_use] pub const fn is_available(&self) -> bool {
self.availability.is_available()
}
pub fn set_availability(&mut self, kind: BiometricKind) -> bool {
let changed = self.availability != kind;
self.availability = kind;
changed
}
pub fn set_last_result(&mut self, result: BiometricResult) -> bool {
let changed = self.last_result != Some(result);
self.last_result = Some(result);
self.pending_event = true;
self.in_flight = self.in_flight.saturating_sub(1);
changed
}
pub const fn mark_requests_dispatched(&mut self, n: u32) {
self.in_flight = self.in_flight.saturating_add(n);
}
pub const fn clear_pending_event(&mut self) {
self.pending_event = false;
}
#[must_use] pub const fn has_pending_async(&self) -> bool {
self.in_flight > 0
}
#[must_use] pub const fn last_was_success(&self) -> bool {
matches!(self.last_result, Some(r) if r.is_success())
}
}
impl EventProvider for BiometricManager {
fn get_pending_events(&self, timestamp: Instant) -> Vec<SyntheticEvent> {
if self.pending_event {
alloc::vec![SyntheticEvent::new(
EventType::BiometricResult,
CoreEventSource::User,
DomNodeId::ROOT,
timestamp,
EventData::None,
)]
} else {
Vec::new()
}
}
}
static PENDING_RESULTS: std::sync::Mutex<Vec<BiometricResult>> =
std::sync::Mutex::new(Vec::new());
pub fn push_biometric_result(result: BiometricResult) {
let mut q = PENDING_RESULTS.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
q.push(result);
}
pub fn drain_biometric_results() -> Vec<BiometricResult> {
let mut q = PENDING_RESULTS.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
core::mem::take(&mut *q)
}
static PENDING_REQUESTS: std::sync::Mutex<Vec<BiometricPrompt>> =
std::sync::Mutex::new(Vec::new());
pub fn push_biometric_request(prompt: BiometricPrompt) {
let mut q = PENDING_REQUESTS.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
q.push(prompt);
}
pub fn drain_biometric_requests() -> Vec<BiometricPrompt> {
let mut q = PENDING_REQUESTS.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
core::mem::take(&mut *q)
}
pub fn has_queued_requests() -> bool {
PENDING_REQUESTS
.lock()
.map_or_else(|e| !e.into_inner().is_empty(), |q| !q.is_empty())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn manager_defaults_to_unavailable_and_no_result() {
let mgr = BiometricManager::new();
assert_eq!(mgr.availability(), BiometricKind::NotAvailable);
assert!(!mgr.is_available());
assert_eq!(mgr.last_result(), None);
assert!(!mgr.last_was_success());
}
#[test]
fn set_availability_returns_change_flag() {
let mut mgr = BiometricManager::new();
assert!(mgr.set_availability(BiometricKind::Face));
assert!(mgr.is_available());
assert_eq!(mgr.availability(), BiometricKind::Face);
assert!(!mgr.set_availability(BiometricKind::Face));
assert!(mgr.set_availability(BiometricKind::Fingerprint));
}
#[test]
fn set_last_result_returns_change_flag() {
let mut mgr = BiometricManager::new();
assert!(mgr.set_last_result(BiometricResult::Failed));
assert_eq!(mgr.last_result(), Some(BiometricResult::Failed));
assert!(!mgr.last_was_success());
assert!(!mgr.set_last_result(BiometricResult::Failed));
assert!(mgr.set_last_result(BiometricResult::Authenticated));
assert!(mgr.last_was_success());
}
#[test]
fn passcode_fallback_counts_as_success() {
let mut mgr = BiometricManager::new();
mgr.set_last_result(BiometricResult::FellBackToPasscode);
assert!(mgr.last_was_success());
assert!(BiometricResult::FellBackToPasscode.is_success());
for r in [
BiometricResult::Cancelled,
BiometricResult::Failed,
BiometricResult::Unavailable,
BiometricResult::Error,
] {
assert!(!r.is_success(), "{r:?} must not be a success");
}
}
#[test]
fn async_results_round_trip_through_manager() {
drop(drain_biometric_results());
push_biometric_result(BiometricResult::Failed);
push_biometric_result(BiometricResult::Authenticated); let drained = drain_biometric_results();
assert_eq!(drained.len(), 2, "both parked results drain in order");
assert_eq!(drained[0], BiometricResult::Failed);
assert_eq!(drained[1], BiometricResult::Authenticated);
let mut mgr = BiometricManager::new();
for r in &drained {
mgr.set_last_result(*r);
}
assert_eq!(
mgr.last_result(),
Some(BiometricResult::Authenticated),
"the last applied result wins"
);
assert!(mgr.last_was_success());
assert!(drain_biometric_results().is_empty());
}
#[test]
fn requests_round_trip_through_channel() {
drop(drain_biometric_requests());
push_biometric_request(BiometricPrompt::new("Unlock A".into()));
push_biometric_request(BiometricPrompt::new("Unlock B".into()));
let drained = drain_biometric_requests();
assert_eq!(drained.len(), 2, "both queued requests drain in order");
assert_eq!(drained[0].reason.as_str(), "Unlock A");
assert_eq!(drained[1].reason.as_str(), "Unlock B");
assert!(drain_biometric_requests().is_empty());
}
#[test]
fn biometric_prompt_defaults_and_constructor() {
let d = BiometricPrompt::default();
assert!(!d.allow_device_credential);
assert_eq!(d.reason.as_str(), "");
let p = BiometricPrompt::new("Unlock your vault".into());
assert_eq!(p.reason.as_str(), "Unlock your vault");
assert_eq!(p.cancel_label.as_str(), "");
assert!(!p.allow_device_credential);
}
}
#[cfg(test)]
mod pump_provider_tests {
use super::*;
use azul_core::task::{Instant, SystemTick};
fn ts() -> Instant {
Instant::Tick(SystemTick::new(0))
}
#[test]
fn in_flight_tracks_dispatch_and_completion() {
let mut mgr = BiometricManager::new();
assert!(!mgr.has_pending_async());
mgr.mark_requests_dispatched(2);
assert!(mgr.has_pending_async());
mgr.set_last_result(BiometricResult::Cancelled);
assert!(mgr.has_pending_async(), "one of two still outstanding");
mgr.set_last_result(BiometricResult::Authenticated);
assert!(!mgr.has_pending_async());
mgr.set_last_result(BiometricResult::Failed);
assert!(!mgr.has_pending_async());
}
#[test]
fn every_completion_fires_an_event_even_identical_repeats() {
let mut mgr = BiometricManager::new();
assert!(mgr.get_pending_events(ts()).is_empty());
mgr.set_last_result(BiometricResult::Cancelled);
assert_eq!(mgr.get_pending_events(ts()).len(), 1);
mgr.clear_pending_event();
assert!(mgr.get_pending_events(ts()).is_empty());
mgr.set_last_result(BiometricResult::Cancelled);
assert_eq!(mgr.get_pending_events(ts()).len(), 1);
assert_eq!(
mgr.get_pending_events(ts())[0].event_type,
EventType::BiometricResult
);
}
}