Skip to main content

cranpose_services/
network_status.rs

1//! Network status (connectivity + whether the connection is metered). Apps use
2//! it to defer or confirm large transfers on cellular/metered links.
3//!
4//! Each part is an observation or [`Unknown`](Connectivity::Unknown): with no
5//! platform monitor installed via [`set_platform_network_monitor`], or one
6//! that stopped, both parts are unknown. Android observes both through
7//! `ConnectivityManager`, iOS and macOS through Network.framework's path
8//! monitor, where Low Data Mode also counts as metered; web observes
9//! connectivity through `navigator.onLine` and cannot tell metering. Linux
10//! and Windows install no monitor. An app that defers large transfers on metered
11//! links starts one on its own only when the status says
12//! [`Unmetered`](Metering::Unmetered).
13
14use std::sync::{Arc, OnceLock};
15
16use crate::registry::{RecoveryGate, ServiceRegistry};
17
18/// Whether the device has a usable network path.
19#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
20pub enum Connectivity {
21    /// A path that can reach the internet.
22    Online,
23    /// No usable path.
24    Offline,
25    /// Nothing observes connectivity: no platform monitor, or one that
26    /// stopped. A request may still succeed.
27    #[default]
28    Unknown,
29}
30
31/// Whether the active network path costs the user by the byte.
32#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
33pub enum Metering {
34    /// Cellular, a hotspot, or a network the user marked as metered.
35    Metered,
36    /// Wi-Fi or Ethernet the user did not mark as metered.
37    Unmetered,
38    /// The platform cannot tell, nothing observes the network, or there is
39    /// no active path to meter.
40    #[default]
41    Unknown,
42}
43
44/// A snapshot of the current network state. The default knows nothing.
45#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
46pub struct NetworkStatus {
47    /// Whether the device has a network path.
48    pub connectivity: Connectivity,
49    /// Whether the network charges for or limits data.
50    pub metering: Metering,
51}
52
53/// Reports the current network status.
54pub trait NetworkMonitor: Send + Sync {
55    /// The current connectivity and metering.
56    fn status(&self) -> NetworkStatus;
57    /// Whether the monitor still observes the network. A stopped monitor knows nothing.
58    fn is_alive(&self) -> bool;
59    /// Restarts a monitor that stopped, so that [`status`](Self::status) reports again.
60    fn reconnect(&self);
61}
62
63/// Shared handle to a [`NetworkMonitor`].
64pub type NetworkMonitorRef = Arc<dyn NetworkMonitor>;
65
66struct DefaultNetworkMonitor;
67
68impl NetworkMonitor for DefaultNetworkMonitor {
69    fn status(&self) -> NetworkStatus {
70        NetworkStatus::default()
71    }
72
73    fn is_alive(&self) -> bool {
74        true
75    }
76
77    fn reconnect(&self) {}
78}
79
80static PLATFORM_NETWORK_MONITOR: ServiceRegistry<dyn NetworkMonitor> = ServiceRegistry::new();
81static DEFAULT_NETWORK_MONITOR: OnceLock<NetworkMonitorRef> = OnceLock::new();
82static NETWORK_MONITOR_HANDLE: OnceLock<NetworkMonitorRef> = OnceLock::new();
83static NETWORK_RECOVERY: RecoveryGate = RecoveryGate::new();
84
85struct PlatformNetworkMonitor;
86
87fn registered_network_monitor() -> NetworkMonitorRef {
88    PLATFORM_NETWORK_MONITOR
89        .get_or_warn("network monitor")
90        .unwrap_or_else(|| {
91            Arc::clone(DEFAULT_NETWORK_MONITOR.get_or_init(|| Arc::new(DefaultNetworkMonitor)))
92        })
93}
94
95fn active_network_monitor() -> NetworkMonitorRef {
96    let monitor = registered_network_monitor();
97    if monitor.is_alive() {
98        NETWORK_RECOVERY.succeeded();
99    } else if NETWORK_RECOVERY.try_start() {
100        monitor.reconnect();
101    }
102    monitor
103}
104
105impl NetworkMonitor for PlatformNetworkMonitor {
106    /// The monitor's observation, or nothing known while it stays stopped.
107    fn status(&self) -> NetworkStatus {
108        let monitor = active_network_monitor();
109        if monitor.is_alive() {
110            monitor.status()
111        } else {
112            NetworkStatus::default()
113        }
114    }
115
116    fn is_alive(&self) -> bool {
117        registered_network_monitor().is_alive()
118    }
119
120    fn reconnect(&self) {
121        registered_network_monitor().reconnect();
122    }
123}
124
125/// Installs a platform network monitor, replacing any previous one.
126pub fn set_platform_network_monitor(monitor: NetworkMonitorRef) {
127    PLATFORM_NETWORK_MONITOR.set(monitor);
128    NETWORK_RECOVERY.succeeded();
129}
130
131/// Removes any registered platform network monitor (tests and teardown).
132pub fn clear_platform_network_monitor() {
133    PLATFORM_NETWORK_MONITOR.clear();
134}
135
136/// The active network monitor: the platform one if installed, else a default
137/// that knows nothing.
138pub fn network_monitor() -> NetworkMonitorRef {
139    Arc::clone(NETWORK_MONITOR_HANDLE.get_or_init(|| Arc::new(PlatformNetworkMonitor)))
140}
141
142/// The current network status. A stopped platform monitor gets a reconnect
143/// attempt first.
144pub fn network_status() -> NetworkStatus {
145    network_monitor().status()
146}
147
148#[cfg(test)]
149#[path = "tests/network_status_tests.rs"]
150mod tests;