1#[cfg(test)]
4mod tests;
5
6use std::cell::RefCell;
7use std::collections::{BTreeMap, HashMap};
8use std::rc::Rc;
9use std::sync::Once;
10
11use crabshell_reactive::{ReadSignal, Signal};
12use crabshell_ui::Cx;
13use zbus::{Connection, Message, ObjectPath, OwnedObjectPath, Variant};
14
15use crate::Shared;
16use crate::dbus::{self, CALL_TIMEOUT, Daemon, OBJECT_MANAGER, PROPERTIES, Props, set_changed};
17
18const BLUEZ: &str = "org.bluez";
19const ADAPTER: &str = "org.bluez.Adapter1";
20const DEVICE: &str = "org.bluez.Device1";
21const BATTERY: &str = "org.bluez.Battery1";
22
23const MAX_ADAPTERS: usize = 16;
25const MAX_DEVICES: usize = 256;
27const MAX_OBJECTS: usize = 4096;
29const SLOW_CALL: std::time::Duration = std::time::Duration::from_secs(90);
32
33fn read(interface: &str, name: &str) -> bool {
37 match interface {
38 ADAPTER => matches!(
39 name,
40 "Alias" | "Address" | "Powered" | "PowerState" | "Discovering"
41 ),
42 DEVICE => matches!(
43 name,
44 "Address" | "Alias" | "Name" | "Icon" | "Connected" | "Paired" | "Trusted" | "Adapter"
45 ),
46 BATTERY => name == "Percentage",
47 _ => false,
48 }
49}
50
51#[derive(Clone, Copy, Debug, PartialEq, Eq)]
53pub enum BluetoothAdapterState {
54 Enabled,
56 Disabled,
58 Enabling,
60 Disabling,
62 Blocked,
64}
65
66#[derive(Clone, Copy, Debug, PartialEq, Eq)]
69pub enum BluetoothDeviceState {
70 Disconnected,
72 Connecting,
74 Connected,
76 Disconnecting,
78}
79
80#[derive(Clone, Debug, PartialEq)]
82pub struct BluetoothAdapter {
83 pub path: String,
85 pub name: String,
87 pub address: String,
89 pub powered: bool,
91 pub state: BluetoothAdapterState,
95 pub discovering: bool,
97}
98
99impl BluetoothAdapter {
100 fn from_props(path: String, props: &Props) -> BluetoothAdapter {
101 let powered = dbus::get(props, "Powered").unwrap_or(false);
102 let state = match dbus::get::<&str>(props, "PowerState") {
103 Some("on") => BluetoothAdapterState::Enabled,
104 Some("off") => BluetoothAdapterState::Disabled,
105 Some("off-enabling") => BluetoothAdapterState::Enabling,
106 Some("on-disabling") => BluetoothAdapterState::Disabling,
107 Some("off-blocked") => BluetoothAdapterState::Blocked,
108 unknown => {
109 if let Some(unknown) = unknown {
110 tracing::debug!(
111 path,
112 state = unknown,
113 "an unknown power state; read from Powered"
114 );
115 }
116 if powered {
117 BluetoothAdapterState::Enabled
118 } else {
119 BluetoothAdapterState::Disabled
120 }
121 }
122 };
123 BluetoothAdapter {
124 name: dbus::text(props, "Alias"),
125 address: dbus::text(props, "Address"),
126 powered,
127 state,
128 discovering: dbus::get(props, "Discovering").unwrap_or(false),
129 path,
130 }
131 }
132}
133
134#[derive(Clone, Debug, PartialEq)]
136pub struct BluetoothDevice {
137 pub path: String,
139 pub adapter: String,
141 pub address: String,
143 pub name: String,
145 pub icon: String,
147 pub connected: bool,
149 pub state: BluetoothDeviceState,
152 pub paired: bool,
154 pub trusted: bool,
156 pub battery: Option<u8>,
158}
159
160impl BluetoothDevice {
161 fn from_props(
162 path: String,
163 props: &Props,
164 battery: Option<&Props>,
165 call: Option<BluetoothDeviceState>,
166 ) -> BluetoothDevice {
167 let address = dbus::text(props, "Address");
168 let connected = dbus::get(props, "Connected").unwrap_or(false);
169 let state = match (call, connected) {
170 (Some(call), _) => call,
171 (None, true) => BluetoothDeviceState::Connected,
172 (None, false) => BluetoothDeviceState::Disconnected,
173 };
174 let name = [dbus::text(props, "Alias"), dbus::text(props, "Name")]
175 .into_iter()
176 .find(|name| !name.is_empty())
177 .unwrap_or_else(|| address.clone());
178 BluetoothDevice {
179 path,
180 adapter: dbus::get::<&ObjectPath>(props, "Adapter")
181 .map_or(String::new(), |at| at.to_string()),
182 address,
183 name,
184 icon: dbus::text(props, "Icon"),
185 connected,
186 state,
187 paired: dbus::get(props, "Paired").unwrap_or(false),
188 trusted: dbus::get(props, "Trusted").unwrap_or(false),
189 battery: battery
190 .and_then(|props| dbus::get::<u8>(props, "Percentage"))
191 .map(|percent| percent.min(100)),
192 }
193 }
194}
195
196#[derive(Clone)]
198pub struct Bluetooth {
199 service: Option<Rc<BluetoothService>>,
200 adapters: ReadSignal<Vec<BluetoothAdapter>>,
201 devices: ReadSignal<Vec<BluetoothDevice>>,
202}
203
204impl Bluetooth {
205 pub fn adapters(&self) -> ReadSignal<Vec<BluetoothAdapter>> {
207 self.adapters
208 }
209
210 pub fn devices(&self) -> ReadSignal<Vec<BluetoothDevice>> {
213 self.devices
214 }
215
216 pub fn set_powered(&self, adapter: &str, powered: bool) {
218 let body = (ADAPTER, "Powered", Variant::from(powered));
219 self.call(adapter, PROPERTIES, "Set", body);
220 }
221
222 pub fn start_discovery(&self, adapter: &str) {
228 self.call(adapter, ADAPTER, "StartDiscovery", ());
229 }
230
231 pub fn stop_discovery(&self, adapter: &str) {
233 self.call(adapter, ADAPTER, "StopDiscovery", ());
234 }
235
236 pub fn connect(&self, device: &str) {
240 self.call(device, DEVICE, "Connect", ());
241 }
242
243 pub fn disconnect(&self, device: &str) {
247 self.call(device, DEVICE, "Disconnect", ());
248 }
249
250 pub fn pair(&self, device: &str) {
255 self.call(device, DEVICE, "Pair", ());
256 }
257
258 pub fn set_trusted(&self, device: &str, trusted: bool) {
260 let body = (DEVICE, "Trusted", Variant::from(trusted));
261 self.call(device, PROPERTIES, "Set", body);
262 }
263
264 pub fn forget(&self, device: &str) {
266 let adapter = self.service.as_ref().and_then(|service| {
267 let state = service.state.borrow();
268 let object = state.as_ref()?.objects.get(device)?;
269 dbus::get::<&ObjectPath>(object.device.as_ref()?, "Adapter").map(|at| at.to_string())
270 });
271 let (Some(adapter), Ok(path)) = (adapter, ObjectPath::try_from(device.to_owned())) else {
272 return tracing::info!(device, "no such Bluetooth device; nothing forgotten");
273 };
274 self.call(&adapter, ADAPTER, "RemoveDevice", path);
275 }
276
277 fn call<B>(&self, path: &str, interface: &'static str, method: &'static str, body: B)
279 where
280 B: serde::Serialize + zbus::DynamicType + 'static,
281 {
282 let Some(service) = &self.service else {
283 return;
284 };
285 let Some(connection) = service
286 .state
287 .borrow()
288 .as_ref()
289 .and_then(|state| state.connection.clone())
290 else {
291 return tracing::info!(method, "BlueZ isn't running; nothing sent");
292 };
293 let marked = match method {
294 "Connect" => Some(service.mark(path, BluetoothDeviceState::Connecting)),
295 "Disconnect" => Some(service.mark(path, BluetoothDeviceState::Disconnecting)),
296 _ => None,
297 };
298 let (shared, service, path) = (&service.shared, Rc::clone(service), path.to_owned());
299 shared.spawn(async move {
300 let call = connection.call_method(Some(BLUEZ), path.as_str(), Some(interface), method, &body);
301 let limit = if matches!(method, "Pair" | "Connect") {
302 SLOW_CALL
303 } else {
304 CALL_TIMEOUT
305 };
306 let answer = service.shared.timeout(limit, call).await;
307 if let Some(number) = marked {
308 service.unmark(&path, number);
309 }
310 if let Err(error) = answer {
311 tracing::warn!(%error, path, method, "BlueZ refused it");
312 }
313 });
314 }
315}
316
317pub(crate) fn bluetooth(cx: &Cx) -> Bluetooth {
319 let Some(service) = cx.service::<BluetoothService>() else {
320 static LOGGED: Once = Once::new();
321 LOGGED.call_once(|| {
322 tracing::warn!("cx.bluetooth() needs the services the shell provides; it stays empty");
323 });
324 return Bluetooth {
325 service: None,
326 adapters: cx.signal(Vec::new()).read_only(),
327 devices: cx.signal(Vec::new()).read_only(),
328 };
329 };
330 service.start()
331}
332
333pub(crate) struct BluetoothService {
335 shared: Rc<Shared>,
336 state: RefCell<Option<BluetoothState>>,
337}
338
339struct BluetoothState {
340 adapters: Signal<Vec<BluetoothAdapter>>,
341 devices: Signal<Vec<BluetoothDevice>>,
342 objects: BTreeMap<String, Object>,
344 connection: Option<Connection>,
346 generation: u64,
347 calls: HashMap<String, (BluetoothDeviceState, u64)>,
351 next_call: u64,
353}
354
355#[derive(Default)]
358struct Object {
359 adapter: Option<Props>,
360 device: Option<Props>,
361 battery: Option<Props>,
362}
363
364impl Object {
365 fn slot(&mut self, interface: &str) -> Option<&mut Option<Props>> {
368 match interface {
369 ADAPTER => Some(&mut self.adapter),
370 DEVICE => Some(&mut self.device),
371 BATTERY => Some(&mut self.battery),
372 _ => None,
373 }
374 }
375}
376
377fn add(objects: &mut BTreeMap<String, Object>, path: String, interfaces: HashMap<String, Props>) {
380 if objects.len() >= MAX_OBJECTS && !objects.contains_key(&path) {
381 return tracing::debug!(path, "too many Bluetooth objects; leaving one out");
382 }
383 let mut object = objects.remove(&path).unwrap_or_default();
384 for (interface, props) in interfaces {
385 if let Some(slot) = object.slot(&interface) {
386 *slot = Some(props);
387 }
388 }
389 if object.adapter.is_some() || object.device.is_some() {
390 objects.insert(path, object);
391 }
392}
393
394impl BluetoothService {
395 pub(crate) fn new(shared: Rc<Shared>) -> BluetoothService {
396 BluetoothService {
397 shared,
398 state: RefCell::new(None),
399 }
400 }
401
402 fn start(self: &Rc<Self>) -> Bluetooth {
403 if let Some(state) = &*self.state.borrow() {
404 return Bluetooth {
405 service: Some(Rc::clone(self)),
406 adapters: state.adapters.read_only(),
407 devices: state.devices.read_only(),
408 };
409 }
410 let root = &self.shared.root;
411 let (adapters, devices) = (root.signal(Vec::new()), root.signal(Vec::new()));
412 *self.state.borrow_mut() = Some(BluetoothState {
413 adapters,
414 devices,
415 objects: BTreeMap::new(),
416 connection: None,
417 generation: 0,
418 calls: HashMap::new(),
419 next_call: 0,
420 });
421 match self.shared.config.system_bus.clone() {
422 Some(address) => {
423 let follow = dbus::follow(Rc::clone(&self.shared), Rc::clone(self), address);
424 self.shared.spawn(follow);
425 }
426 None => tracing::info!("no system bus, so there is no Bluetooth"),
427 }
428 Bluetooth {
429 service: Some(Rc::clone(self)),
430 adapters: adapters.read_only(),
431 devices: devices.read_only(),
432 }
433 }
434
435 fn publish(&self) {
437 let shown = self.state.borrow().as_ref().map(|state| {
439 let objects = || {
440 state
441 .objects
442 .iter()
443 .map(|(path, object)| (path.clone(), object))
444 };
445 let adapters = objects()
446 .filter_map(|(path, object)| {
447 Some(BluetoothAdapter::from_props(path, object.adapter.as_ref()?))
448 })
449 .take(MAX_ADAPTERS)
450 .collect::<Vec<_>>();
451 let mut devices = objects()
452 .filter_map(|(path, object)| {
453 let device = object.device.as_ref()?;
454 let call = state.calls.get(&path).map(|(call, _)| *call);
455 Some(BluetoothDevice::from_props(
456 path,
457 device,
458 object.battery.as_ref(),
459 call,
460 ))
461 })
462 .collect::<Vec<_>>();
463 if devices.len() > MAX_DEVICES {
466 devices.sort_by_key(|device| (!device.connected, !device.paired));
467 devices.truncate(MAX_DEVICES);
468 devices.sort_by(|a, b| a.path.cmp(&b.path));
469 }
470 (state.adapters, adapters, state.devices, devices)
471 });
472 if let Some((adapters_signal, adapters, devices_signal, devices)) = shown {
473 set_changed(adapters_signal, adapters);
474 set_changed(devices_signal, devices);
475 }
476 }
477
478 fn mark(&self, device: &str, state: BluetoothDeviceState) -> u64 {
481 let number = {
482 let mut held = self.state.borrow_mut();
483 let Some(held) = held.as_mut() else {
484 return 0;
485 };
486 held.next_call += 1;
487 held
488 .calls
489 .insert(device.to_owned(), (state, held.next_call));
490 held.next_call
491 };
492 self.publish();
493 number
494 }
495
496 fn unmark(&self, device: &str, number: u64) {
499 let ended = {
500 let mut state = self.state.borrow_mut();
501 let Some(calls) = state.as_mut().map(|state| &mut state.calls) else {
502 return;
503 };
504 let current = calls.get(device).is_some_and(|(_, at)| *at == number);
505 current && calls.remove(device).is_some()
506 };
507 if ended {
508 self.publish();
509 }
510 }
511}
512
513impl Daemon for BluetoothService {
514 const NAME: &'static str = BLUEZ;
515
516 fn rules() -> Vec<String> {
520 let mut rules = vec![format!(
521 "type='signal',sender='{BLUEZ}',interface='{OBJECT_MANAGER}'"
522 )];
523 rules.extend([ADAPTER, DEVICE, BATTERY].map(|interface| {
524 format!(
525 "type='signal',sender='{BLUEZ}',interface='{PROPERTIES}',member='PropertiesChanged',\
526 arg0='{interface}'"
527 )
528 }));
529 rules
530 }
531
532 fn alive(&self) -> bool {
533 let state = self.state.borrow();
534 state
535 .as_ref()
536 .is_some_and(|state| state.adapters.try_with(|_| ()).is_some())
537 }
538
539 fn clear(&self) -> u64 {
540 let generation = {
541 let mut state = self.state.borrow_mut();
542 let Some(state) = state.as_mut() else {
543 return 0;
544 };
545 state.objects.clear();
546 state.calls.clear();
547 state.connection = None;
548 state.generation += 1;
549 state.generation
550 };
551 self.publish();
552 generation
553 }
554
555 async fn read(self: Rc<Self>, connection: Connection, generation: u64) {
556 let call = connection.call_method(
557 Some(BLUEZ),
558 "/",
559 Some(OBJECT_MANAGER),
560 "GetManagedObjects",
561 &(),
562 );
563 let read = async {
564 let reply = self.shared.timeout(CALL_TIMEOUT, call).await?;
565 reply
566 .body()
567 .deserialize::<HashMap<OwnedObjectPath, HashMap<String, Props>>>()
568 };
569 let managed = match read.await {
570 Ok(managed) => managed,
571 Err(error) => return tracing::warn!(%error, "couldn't read BlueZ"),
572 };
573 let managed = managed
575 .into_iter()
576 .map(|(path, interfaces)| (path.as_str().to_owned(), interfaces))
577 .collect::<BTreeMap<_, _>>();
578 {
579 let mut state = self.state.borrow_mut();
580 let Some(state) = state
581 .as_mut()
582 .filter(|state| state.generation == generation)
583 else {
584 return;
585 };
586 state.objects.clear();
587 for (path, interfaces) in managed {
588 add(&mut state.objects, path, interfaces);
589 }
590 state.connection = Some(connection);
591 }
592 self.publish();
593 }
594
595 fn signal(self: &Rc<Self>, _: &Connection, message: &Message) {
596 if let Some((path, interface, changed, dropped)) = dbus::properties_changed(message) {
597 {
598 let mut state = self.state.borrow_mut();
599 let Some(state) = state.as_mut() else {
600 return;
601 };
602 let reached = match dbus::get::<bool>(&changed, "Connected") {
605 Some(true) => Some(BluetoothDeviceState::Connecting),
606 Some(false) => Some(BluetoothDeviceState::Disconnecting),
607 None => None,
608 };
609 let headed = state.calls.get(&path).map(|(call, _)| *call);
610 if interface == DEVICE && reached.is_some() && headed == reached {
611 state.calls.remove(&path);
612 }
613 let props = state
614 .objects
615 .get_mut(&path)
616 .and_then(|object| object.slot(&interface)?.as_mut());
617 let Some(props) = props else {
618 return;
619 };
620 let shown = changed
621 .keys()
622 .chain(&dropped)
623 .any(|name| read(&interface, name));
624 dbus::apply(props, changed, dropped);
625 if !shown {
626 return;
627 }
628 }
629 return self.publish();
630 }
631 let header = message.header();
632 if header.interface().map(|interface| interface.as_str()) != Some(OBJECT_MANAGER) {
633 return;
634 }
635 let body = message.body();
636 {
637 let mut state = self.state.borrow_mut();
638 let Some(state) = state.as_mut() else {
639 return;
640 };
641 match header.member().map(|member| member.as_str()) {
642 Some("InterfacesAdded") => {
643 let Ok((path, interfaces)) =
644 body.deserialize::<(OwnedObjectPath, HashMap<String, Props>)>()
645 else {
646 return;
647 };
648 add(&mut state.objects, path.as_str().to_owned(), interfaces);
649 }
650 Some("InterfacesRemoved") => {
651 let Ok((path, interfaces)) = body.deserialize::<(OwnedObjectPath, Vec<String>)>() else {
652 return;
653 };
654 let Some(object) = state.objects.get_mut(path.as_str()) else {
655 return;
656 };
657 for interface in interfaces {
658 if let Some(slot) = object.slot(&interface) {
659 *slot = None;
660 }
661 }
662 if object.adapter.is_none() && object.device.is_none() {
663 state.objects.remove(path.as_str());
664 }
665 }
666 _ => return,
667 }
668 }
669 self.publish();
670 }
671}