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            DEFAULT_NETWORK_MONITOR
92                .get_or_init(|| Arc::new(DefaultNetworkMonitor))
93                .clone()
94        })
95}
96
97fn active_network_monitor() -> NetworkMonitorRef {
98    let monitor = registered_network_monitor();
99    if monitor.is_alive() {
100        NETWORK_RECOVERY.succeeded();
101    } else if NETWORK_RECOVERY.try_start() {
102        monitor.reconnect();
103    }
104    monitor
105}
106
107impl NetworkMonitor for PlatformNetworkMonitor {
108    /// The monitor's observation, or nothing known while it stays stopped.
109    fn status(&self) -> NetworkStatus {
110        let monitor = active_network_monitor();
111        if monitor.is_alive() {
112            monitor.status()
113        } else {
114            NetworkStatus::default()
115        }
116    }
117
118    fn is_alive(&self) -> bool {
119        registered_network_monitor().is_alive()
120    }
121
122    fn reconnect(&self) {
123        registered_network_monitor().reconnect();
124    }
125}
126
127/// Installs a platform network monitor, replacing any previous one.
128pub fn set_platform_network_monitor(monitor: NetworkMonitorRef) {
129    PLATFORM_NETWORK_MONITOR.set(monitor);
130    NETWORK_RECOVERY.succeeded();
131}
132
133/// Removes any registered platform network monitor (tests and teardown).
134pub fn clear_platform_network_monitor() {
135    PLATFORM_NETWORK_MONITOR.clear();
136}
137
138/// The active network monitor: the platform one if installed, else a default
139/// that knows nothing.
140pub fn network_monitor() -> NetworkMonitorRef {
141    NETWORK_MONITOR_HANDLE
142        .get_or_init(|| Arc::new(PlatformNetworkMonitor))
143        .clone()
144}
145
146/// The current network status. A stopped platform monitor gets a reconnect
147/// attempt first.
148pub fn network_status() -> NetworkStatus {
149    network_monitor().status()
150}
151
152#[cfg(test)]
153#[path = "tests/network_status_tests.rs"]
154mod tests;