jellytau_lib/download/
network.rs1use serde::{Deserialize, Serialize};
14use std::sync::Arc;
15use tokio::sync::RwLock;
16
17#[derive(specta::Type, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
24#[serde(rename_all = "lowercase")]
25pub enum NetworkType {
26 None,
28 Wifi,
30 Ethernet,
32 Cellular,
34 Other,
36 Unknown,
38}
39
40#[derive(specta::Type, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
44#[serde(rename_all = "camelCase")]
45pub struct NetworkState {
46 pub network_type: NetworkType,
47 pub unmetered: bool,
49}
50
51impl Default for NetworkState {
52 fn default() -> Self {
53 Self {
56 network_type: NetworkType::Ethernet,
57 unmetered: true,
58 }
59 }
60}
61
62impl NetworkState {
63 pub fn allows_download(&self, wifi_only: bool) -> bool {
72 if !wifi_only {
73 return true;
74 }
75 match self.network_type {
76 NetworkType::Cellular | NetworkType::None | NetworkType::Unknown => false,
77 NetworkType::Wifi | NetworkType::Ethernet | NetworkType::Other => self.unmetered,
80 }
81 }
82}
83
84#[derive(Clone, Default)]
89pub struct NetworkStateHandle {
90 state: Arc<RwLock<NetworkState>>,
91}
92
93impl NetworkStateHandle {
94 pub fn new() -> Self {
95 Self {
96 state: Arc::new(RwLock::new(NetworkState::default())),
97 }
98 }
99
100 pub async fn get(&self) -> NetworkState {
101 *self.state.read().await
102 }
103
104 pub async fn set(&self, new_state: NetworkState) {
105 *self.state.write().await = new_state;
106 }
107
108 pub async fn allows_download(&self, wifi_only: bool) -> bool {
110 self.state.read().await.allows_download(wifi_only)
111 }
112}
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117
118 fn state(network_type: NetworkType, unmetered: bool) -> NetworkState {
119 NetworkState {
120 network_type,
121 unmetered,
122 }
123 }
124
125 #[test]
126 fn wifi_only_off_allows_every_transport() {
127 for t in [
128 NetworkType::None,
129 NetworkType::Wifi,
130 NetworkType::Ethernet,
131 NetworkType::Cellular,
132 NetworkType::Other,
133 NetworkType::Unknown,
134 ] {
135 assert!(
136 state(t, false).allows_download(false),
137 "{t:?} should be allowed when wifi_only is off"
138 );
139 }
140 }
141
142 #[test]
143 fn cellular_is_blocked_when_wifi_only() {
144 assert!(!state(NetworkType::Cellular, true).allows_download(true));
146 assert!(!state(NetworkType::Cellular, false).allows_download(true));
147 }
148
149 #[test]
150 fn unmetered_wifi_and_ethernet_are_allowed() {
151 assert!(state(NetworkType::Wifi, true).allows_download(true));
152 assert!(state(NetworkType::Ethernet, true).allows_download(true));
153 }
154
155 #[test]
156 fn metered_wifi_is_blocked() {
157 assert!(!state(NetworkType::Wifi, false).allows_download(true));
160 }
161
162 #[test]
163 fn unknown_and_none_fail_closed() {
164 assert!(!state(NetworkType::Unknown, true).allows_download(true));
165 assert!(!state(NetworkType::None, true).allows_download(true));
166 }
167
168 #[test]
169 fn desktop_default_is_never_gated() {
170 assert!(NetworkState::default().allows_download(true));
171 }
172
173 #[tokio::test]
174 async fn handle_roundtrips_state() {
175 let handle = NetworkStateHandle::new();
176 assert!(handle.allows_download(true).await);
177
178 handle.set(state(NetworkType::Cellular, false)).await;
179 assert!(!handle.allows_download(true).await);
180 assert!(handle.allows_download(false).await);
181 assert_eq!(handle.get().await.network_type, NetworkType::Cellular);
182 }
183
184 #[test]
185 fn network_type_serializes_lowercase() {
186 assert_eq!(
188 serde_json::to_string(&NetworkType::Wifi).unwrap(),
189 "\"wifi\""
190 );
191 assert_eq!(
192 serde_json::to_string(&NetworkType::Cellular).unwrap(),
193 "\"cellular\""
194 );
195 }
196}