Skip to main content

kobo_core/
capabilities.rs

1//! Capability detection (plan S5): drives the read-aloud vs plain-reader state
2//! machine. `MockCapabilities` is for the desktop simulator; the real impl
3//! (`KoboCapabilities`) lives in the `kobo` backend crate (checks network reach
4//! + A2DP sink presence).
5
6/// Device-state query surface. Implemented concretely by `KoboCapabilities`
7/// (reads sysfs/dbus) and by `MockCapabilities` (scripted for desktop). Routing
8/// device queries through this trait lets the reader's status-refresh and
9/// play-enabled decisions be unit-tested without hardware.
10pub trait Capabilities {
11    fn network_available(&self) -> bool;
12    fn audio_sink_available(&self) -> bool;
13
14    fn read_aloud_available(&self) -> bool {
15        self.network_available() && self.audio_sink_available()
16    }
17
18    fn battery_pct(&self) -> i32 {
19        0
20    }
21    fn wifi_name(&self) -> Option<String> {
22        None
23    }
24    fn bt_name(&self) -> Option<String> {
25        None
26    }
27    fn current_clock(&self) -> String {
28        "--:--".to_string()
29    }
30}
31
32#[derive(Debug, Clone, Default)]
33pub struct MockCapabilities {
34    pub network: bool,
35    pub audio_sink: bool,
36    pub battery: i32,
37    pub wifi_ssid: Option<String>,
38    pub bt_device: Option<String>,
39    pub clock: String,
40}
41
42impl Capabilities for MockCapabilities {
43    fn network_available(&self) -> bool {
44        self.network
45    }
46    fn audio_sink_available(&self) -> bool {
47        self.audio_sink
48    }
49    fn battery_pct(&self) -> i32 {
50        self.battery
51    }
52    fn wifi_name(&self) -> Option<String> {
53        self.wifi_ssid.clone()
54    }
55    fn bt_name(&self) -> Option<String> {
56        self.bt_device.clone()
57    }
58    fn current_clock(&self) -> String {
59        if self.clock.is_empty() {
60            "--:--".to_string()
61        } else {
62            self.clock.clone()
63        }
64    }
65}
66
67#[cfg(test)]
68mod tests {
69    use super::*;
70
71    #[test]
72    fn state_machine() {
73        assert!(MockCapabilities {
74            network: true,
75            audio_sink: true,
76            ..Default::default()
77        }
78        .read_aloud_available());
79        assert!(!MockCapabilities {
80            network: false,
81            audio_sink: true,
82            ..Default::default()
83        }
84        .read_aloud_available());
85        assert!(!MockCapabilities {
86            network: true,
87            audio_sink: false,
88            ..Default::default()
89        }
90        .read_aloud_available());
91        assert!(!MockCapabilities {
92            network: false,
93            audio_sink: false,
94            ..Default::default()
95        }
96        .read_aloud_available());
97    }
98
99    #[test]
100    fn mock_battery_and_names_round_trip() {
101        let mut m = MockCapabilities::default();
102        m.battery = 73;
103        m.wifi_ssid = Some("Net".into());
104        m.bt_device = Some("Speaker".into());
105        m.clock = "9:41 PM".into();
106        assert_eq!(m.battery_pct(), 73);
107        assert_eq!(m.wifi_name().as_deref(), Some("Net"));
108        assert_eq!(m.bt_name().as_deref(), Some("Speaker"));
109        assert_eq!(m.current_clock(), "9:41 PM");
110    }
111
112    #[test]
113    fn mock_defaults_are_benign() {
114        let m = MockCapabilities::default();
115        assert_eq!(m.battery_pct(), 0);
116        assert!(m.wifi_name().is_none());
117        assert!(m.bt_name().is_none());
118        assert_eq!(m.current_clock(), "--:--");
119    }
120}