Krolyxon
Commit eed1fb9

Branches

Refs →

budctlcratesbudctl-androidsrcphone.rs

257 lines · 8.5 KB · Rust

1//! The app's side of the worker, as the worker sees it: the paired devices, the RFCOMM link, and
2//! where events and log lines go. `kotlin.rs` provides it over JNI; the tests fake it.
3
4use std::fmt::Display;
5use std::io;
6use std::sync::Arc;
7use std::time::{Duration, Instant};
8
9use budctl_core::devices;
10use budctl_core::transport::Transport;
11use budctl_core::worker::{Connect, Event, Report};
12use budctl_core::{Error, Found, Result};
13
14use crate::json;
15
16/// What the worker needs from the app. Every method is called on the worker's thread.
17pub trait Phone: Send + Sync {
18 /// The paired devices, as JSON (read by [`json::paired_devices`]).
19 fn paired_devices(&self) -> Result<String>;
20
21 /// Opens the RFCOMM link to `service` on the device at `address`, in place of any link open
22 /// before.
23 fn connect(&self, address: &str, service: &str) -> Result<()>;
24
25 fn send(&self, bytes: &[u8]) -> Result<()>;
26
27 /// The next bytes from the buds, `None` once `timeout` passes with nothing, or no bytes at all
28 /// once the link is closed.
29 fn recv(&self, timeout: Duration) -> Result<Option<Vec<u8>>>;
30
31 fn close(&self);
32
33 fn event(&self, json: &str);
34
35 fn log(&self, line: &str);
36}
37
38/// An error in the app's side, rather than with the buds: a bug.
39pub fn app_error(context: &'static str, detail: impl Display) -> Error {
40 Error::Os {
41 context,
42 source: io::Error::other(detail.to_string()),
43 }
44}
45
46/// Finds the buds among the paired devices and opens a session, as `budctl` does through BlueZ.
47pub fn connect(phone: Arc<dyn Phone>) -> Connect {
48 Box::new(move |found| {
49 let paired = json::paired_devices(&phone.paired_devices()?)?;
50 let found_list = paired
51 .into_iter()
52 .map(|device| Found {
53 model: devices::identify(&device),
54 device,
55 })
56 .collect();
57 let (device, model) = budctl_core::select(found_list, None)?;
58 found(device.alias.clone());
59 budctl_core::ensure_connected(&device)?;
60
61 // The first of the model's services the buds list, as SDP finds it on Linux. Android
62 // caches the list; when it has none yet, the first service is tried.
63 let address = device.address.to_string();
64 let listed = |service: &String| device.uuids.is_empty() || device.uuids.contains(service);
65 let service = model
66 .rfcomm_uuids()
67 .iter()
68 .map(ToString::to_string)
69 .find(listed)
70 .ok_or_else(|| Error::NoService(address.clone()))?;
71 phone.connect(&address, &service)?;
72 budctl_core::open(model, Box::new(Link(phone.clone())))
73 })
74}
75
76/// The RFCOMM link the app opened, as a [`Transport`]. Dropping it closes the link.
77struct Link(Arc<dyn Phone>);
78
79impl Transport for Link {
80 fn send(&mut self, bytes: &[u8]) -> Result<()> {
81 self.0.send(bytes)
82 }
83
84 fn recv(&mut self, deadline: Instant) -> Result<Option<Vec<u8>>> {
85 match self.0.recv(deadline.saturating_duration_since(Instant::now()))? {
86 Some(bytes) if bytes.is_empty() => Err(Error::NotConnected(
87 "the buds closed the control link (case lid closed?)".to_owned(),
88 )),
89 received => Ok(received),
90 }
91 }
92}
93
94impl Drop for Link {
95 fn drop(&mut self) {
96 self.0.close();
97 }
98}
99
100/// The worker's reports, passed on to the app as JSON.
101pub struct Reporter(pub Arc<dyn Phone>);
102
103impl Report for Reporter {
104 fn event(&self, event: Event) {
105 match json::event(&event) {
106 Ok(json) => self.0.event(&json),
107 Err(e) => self.0.log(&format!("could not write an event as JSON: {e}")),
108 }
109 }
110
111 fn log(&self, line: &str) {
112 self.0.log(line);
113 }
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119 use budctl_core::Kind;
120 use std::collections::VecDeque;
121 use std::sync::Mutex;
122
123 const T310: &str = "realme Buds T310";
124 const FIRST: &str = "0000079a-d102-11e1-9b23-00025b00a5a5";
125 const SECOND: &str = "00001107-d102-11e1-9b23-00025b00a5a5";
126
127 /// A phone with one paired device, whose connect answers as told.
128 #[derive(Default)]
129 struct Fake {
130 devices: String,
131 refuse: Option<String>,
132 /// What `recv` returns, in order; then nothing.
133 incoming: Mutex<VecDeque<Option<Vec<u8>>>>,
134 calls: Mutex<Vec<String>>,
135 }
136
137 impl Fake {
138 fn with(name: &str, connected: bool, uuids: &[&str]) -> Self {
139 let device = serde_json::json!([{
140 "address": "00:11:22:33:44:55", "name": name, "alias": "My buds",
141 "connected": connected, "uuids": uuids,
142 }]);
143 Fake {
144 devices: device.to_string(),
145 ..Fake::default()
146 }
147 }
148
149 fn calls(&self) -> Vec<String> {
150 self.calls.lock().unwrap().clone()
151 }
152 }
153
154 impl Phone for Fake {
155 fn paired_devices(&self) -> Result<String> {
156 Ok(self.devices.clone())
157 }
158
159 fn connect(&self, address: &str, service: &str) -> Result<()> {
160 self.calls.lock().unwrap().push(format!("connect {address} {service}"));
161 match &self.refuse {
162 Some(json) => Err(json::connect_failure(json)),
163 None => Ok(()),
164 }
165 }
166
167 fn send(&self, _bytes: &[u8]) -> Result<()> {
168 Ok(())
169 }
170
171 fn recv(&self, _timeout: Duration) -> Result<Option<Vec<u8>>> {
172 Ok(self.incoming.lock().unwrap().pop_front().flatten())
173 }
174
175 fn close(&self) {
176 self.calls.lock().unwrap().push("close".to_owned());
177 }
178
179 fn event(&self, json: &str) {
180 self.calls.lock().unwrap().push(json.to_owned());
181 }
182
183 fn log(&self, _line: &str) {}
184 }
185
186 /// Runs the Android connect against `phone`, and the names it reported.
187 fn open(phone: &Arc<Fake>) -> (Result<()>, Vec<String>) {
188 let found = Mutex::new(Vec::new());
189 let result = connect(phone.clone())(&|name| found.lock().unwrap().push(name)).map(drop);
190 (result, found.into_inner().unwrap())
191 }
192
193 #[test]
194 fn no_supported_buds_is_not_found() {
195 let phone = Arc::new(Fake::with("Some Speaker", true, &[]));
196 let (result, found) = open(&phone);
197 assert!(matches!(result, Err(Error::NoDevice(_))));
198 assert!(found.is_empty());
199 }
200
201 #[test]
202 fn buds_that_are_not_connected_are_named_but_not_dialled() {
203 let phone = Arc::new(Fake::with(T310, false, &[FIRST]));
204 let (result, found) = open(&phone);
205 assert!(matches!(result, Err(Error::NotConnected(message)) if message.contains("My buds")));
206 assert_eq!(found, ["My buds"]);
207 assert!(phone.calls().is_empty());
208 }
209
210 #[test]
211 fn a_refused_connect_keeps_its_kind() {
212 let phone = Arc::new(Fake {
213 refuse: Some(r#"{"kind": "busy", "message": "refused"}"#.to_owned()),
214 ..Fake::with(T310, true, &[FIRST])
215 });
216 let (result, _) = open(&phone);
217 assert_eq!(result.err().map(|e| e.kind()), Some(Kind::Busy));
218 assert_eq!(phone.calls(), [format!("connect 00:11:22:33:44:55 {FIRST}")]);
219 }
220
221 #[test]
222 fn the_first_service_the_buds_list_is_used() {
223 let phone = Arc::new(Fake {
224 refuse: Some(r#"{"kind": "busy", "message": "refused"}"#.to_owned()),
225 ..Fake::with(T310, true, &[SECOND])
226 });
227 let _ = open(&phone);
228 assert_eq!(phone.calls(), [format!("connect 00:11:22:33:44:55 {SECOND}")]);
229
230 let phone = Arc::new(Fake::with(T310, true, &["0000110b-0000-1000-8000-00805f9b34fb"]));
231 assert!(matches!(open(&phone).0, Err(Error::NoService(_))));
232 }
233
234 #[test]
235 fn the_link_reports_silence_and_a_closed_link() {
236 let phone = Arc::new(Fake::default());
237 phone
238 .incoming
239 .lock()
240 .unwrap()
241 .extend([Some(vec![0xAA, 0x07]), None, Some(Vec::new())]);
242 let mut link = Link(phone.clone());
243 let deadline = Instant::now();
244 assert_eq!(link.recv(deadline).unwrap(), Some(vec![0xAA, 0x07]));
245 assert_eq!(link.recv(deadline).unwrap(), None);
246 assert!(matches!(link.recv(deadline), Err(Error::NotConnected(_))));
247 drop(link);
248 assert_eq!(phone.calls(), ["close"]);
249 }
250
251 #[test]
252 fn reports_reach_the_app_as_json() {
253 let phone = Arc::new(Fake::default());
254 Reporter(phone.clone()).event(Event::Lost("gone".to_owned()));
255 assert_eq!(phone.calls(), [r#"{"event":"lost","message":"gone"}"#]);
256 }
257}