use std::sync::{
Arc, Mutex, OnceLock,
atomic::{AtomicU64, Ordering},
};
use cranpose_core::{State, rememberEventStream};
use crate::registry::ServiceRegistry;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
pub enum ThermalState {
#[default]
Normal,
Light,
Moderate,
Severe,
Critical,
Emergency,
Shutdown,
}
impl ThermalState {
pub fn should_pause_work(self) -> bool {
self >= ThermalState::Severe
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct BatteryStatus {
pub percent: u8,
pub charging: bool,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PowerReading<T> {
Known(T),
Unsupported,
Unknown,
}
impl<T> PowerReading<T> {
pub fn known(self) -> Option<T> {
match self {
PowerReading::Known(value) => Some(value),
_ => None,
}
}
pub fn is_supported(&self) -> bool {
!matches!(self, PowerReading::Unsupported)
}
pub fn unwrap_or(self, fallback: T) -> T {
self.known().unwrap_or(fallback)
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct PowerCapabilities {
pub thermal: bool,
pub battery: bool,
pub background_restriction: bool,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct PowerState {
pub thermal: PowerReading<ThermalState>,
pub battery: PowerReading<BatteryStatus>,
pub unrestricted_background_work: PowerReading<bool>,
}
impl PowerState {
pub const fn unsupported() -> Self {
Self {
thermal: PowerReading::Unsupported,
battery: PowerReading::Unsupported,
unrestricted_background_work: PowerReading::Unsupported,
}
}
pub fn should_pause_work(&self) -> bool {
matches!(self.thermal, PowerReading::Known(level) if level.should_pause_work())
}
}
pub trait PowerMonitor: Send + Sync {
fn capabilities(&self) -> PowerCapabilities {
PowerCapabilities::default()
}
fn thermal_state(&self) -> PowerReading<ThermalState> {
PowerReading::Unsupported
}
fn battery_status(&self) -> PowerReading<BatteryStatus> {
PowerReading::Unsupported
}
fn unrestricted_background_work(&self) -> PowerReading<bool> {
PowerReading::Unsupported
}
fn request_unrestricted_background_work(&self) {}
}
pub type PowerMonitorRef = Arc<dyn PowerMonitor>;
struct DefaultPowerMonitor;
impl PowerMonitor for DefaultPowerMonitor {}
static PLATFORM_POWER_MONITOR: ServiceRegistry<dyn PowerMonitor> = ServiceRegistry::new();
pub fn set_platform_power_monitor(monitor: PowerMonitorRef) {
PLATFORM_POWER_MONITOR.set(monitor);
publish_power_state(power_state());
}
pub fn clear_platform_power_monitor() {
PLATFORM_POWER_MONITOR.clear();
if let Ok(mut observers) = power_observers().lock() {
observers.clear();
}
}
pub fn power_monitor() -> PowerMonitorRef {
PLATFORM_POWER_MONITOR
.get()
.unwrap_or_else(|| Arc::new(DefaultPowerMonitor))
}
pub fn power_state() -> PowerState {
let monitor = power_monitor();
PowerState {
thermal: monitor.thermal_state(),
battery: monitor.battery_status(),
unrestricted_background_work: monitor.unrestricted_background_work(),
}
}
pub fn power_capabilities() -> PowerCapabilities {
power_monitor().capabilities()
}
type PowerObserver = Arc<dyn Fn(PowerState) + Send + Sync>;
fn power_observers() -> &'static Mutex<Vec<(u64, PowerObserver)>> {
static SLOT: OnceLock<Mutex<Vec<(u64, PowerObserver)>>> = OnceLock::new();
SLOT.get_or_init(|| Mutex::new(Vec::new()))
}
static NEXT_OBSERVER: AtomicU64 = AtomicU64::new(1);
pub struct PowerObserverRegistration {
id: u64,
}
impl Drop for PowerObserverRegistration {
fn drop(&mut self) {
if let Ok(mut observers) = power_observers().lock() {
observers.retain(|(id, _)| *id != self.id);
}
}
}
pub fn observe_power_state(
observer: impl Fn(PowerState) + Send + Sync + 'static,
) -> PowerObserverRegistration {
let id = NEXT_OBSERVER.fetch_add(1, Ordering::Relaxed);
if let Ok(mut observers) = power_observers().lock() {
observers.push((id, Arc::new(observer)));
}
PowerObserverRegistration { id }
}
pub fn publish_power_state(state: PowerState) {
let observers = power_observers()
.lock()
.map(|observers| {
observers
.iter()
.map(|(_, observer)| Arc::clone(observer))
.collect::<Vec<_>>()
})
.unwrap_or_default();
for observer in observers {
observer(state);
}
}
#[allow(non_snake_case)]
#[track_caller]
pub fn rememberPowerState() -> State<PowerState> {
let updates = rememberEventStream((), |sender| {
observe_power_state(move |state| sender.send(state))
});
cranpose_core::collectAsState(updates, (), power_state())
}
#[cfg(test)]
mod tests {
use super::*;
struct DesktopMonitor;
impl PowerMonitor for DesktopMonitor {
fn capabilities(&self) -> PowerCapabilities {
PowerCapabilities {
thermal: false,
battery: false,
background_restriction: false,
}
}
}
struct PhoneMonitor;
impl PowerMonitor for PhoneMonitor {
fn capabilities(&self) -> PowerCapabilities {
PowerCapabilities {
thermal: true,
battery: true,
background_restriction: true,
}
}
fn thermal_state(&self) -> PowerReading<ThermalState> {
PowerReading::Known(ThermalState::Severe)
}
fn battery_status(&self) -> PowerReading<BatteryStatus> {
PowerReading::Known(BatteryStatus {
percent: 12,
charging: false,
})
}
fn unrestricted_background_work(&self) -> PowerReading<bool> {
PowerReading::Known(false)
}
}
#[test]
fn a_platform_without_power_apis_says_unsupported_rather_than_full() {
let _guard = crate::registry::test_service_guard();
clear_platform_power_monitor();
let state = power_state();
assert_eq!(state, PowerState::unsupported());
assert!(!state.thermal.is_supported());
assert!(!state.should_pause_work());
assert_eq!(power_capabilities(), PowerCapabilities::default());
}
#[test]
fn a_backend_that_measures_nothing_still_reports_its_capabilities() {
let _guard = crate::registry::test_service_guard();
set_platform_power_monitor(Arc::new(DesktopMonitor));
assert!(!power_capabilities().thermal);
assert_eq!(power_state().battery, PowerReading::Unsupported);
clear_platform_power_monitor();
}
#[test]
fn severe_thermal_pressure_pauses_sustained_work() {
let _guard = crate::registry::test_service_guard();
set_platform_power_monitor(Arc::new(PhoneMonitor));
let state = power_state();
assert!(state.should_pause_work());
assert_eq!(
state.battery.known().map(|battery| battery.percent),
Some(12)
);
assert!(!state.unrestricted_background_work.unwrap_or(true));
clear_platform_power_monitor();
}
#[test]
fn observers_see_published_changes_and_stop_when_dropped() {
let _guard = crate::registry::test_service_guard();
clear_platform_power_monitor();
let seen = Arc::new(Mutex::new(Vec::new()));
let recorder = Arc::clone(&seen);
let registration = observe_power_state(move |state| {
recorder
.lock()
.unwrap_or_else(|error| error.into_inner())
.push(state.thermal)
});
publish_power_state(PowerState {
thermal: PowerReading::Known(ThermalState::Moderate),
..PowerState::unsupported()
});
assert_eq!(
seen.lock().unwrap_or_else(|e| e.into_inner()).as_slice(),
[PowerReading::Known(ThermalState::Moderate)]
);
drop(registration);
publish_power_state(PowerState::unsupported());
assert_eq!(seen.lock().unwrap_or_else(|e| e.into_inner()).len(), 1);
}
}