diff --git a/Cargo.lock b/Cargo.lock index 13ce4a2..ecf90d7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "ab_glyph" @@ -1766,8 +1766,9 @@ dependencies = [ [[package]] name = "cosmic-settings-subscriptions" version = "0.1.0" -source = "git+https://github.com/pop-os/cosmic-settings-subscriptions#ff9883a029b44fb8eafb7c7a06d08b36a563e481" +source = "git+https://github.com/pop-os/cosmic-settings-subscriptions#f666c7cfcad79547e55a368640784bca51081bd2" dependencies = [ + "bluez-zbus", "cosmic-dbus-networkmanager", "futures", "iced_futures", diff --git a/cosmic-settings/Cargo.toml b/cosmic-settings/Cargo.toml index d375c46..4ecd6ec 100644 --- a/cosmic-settings/Cargo.toml +++ b/cosmic-settings/Cargo.toml @@ -87,7 +87,7 @@ async-fn-stream = "0.2.2" [dependencies.cosmic-settings-subscriptions] git = "https://github.com/pop-os/cosmic-settings-subscriptions" #TODO: only select features as needed -features = ["network_manager", "pipewire", "pulse"] +features = ["network_manager", "pipewire", "pulse", "bluetooth"] optional = true [dependencies.icu] @@ -128,7 +128,7 @@ linux = [ # Pages page-about = ["dep:cosmic-settings-system", "dep:hostname1-zbus", "dep:zbus"] -page-bluetooth = ["dep:bluez-zbus", "dep:zbus"] +page-bluetooth = ["dep:bluez-zbus", "dep:zbus", "dep:cosmic-settings-subscriptions"] page-date = ["dep:timedate-zbus", "dep:zbus"] page-default-apps = ["dep:mime-apps"] page-input = [ diff --git a/cosmic-settings/src/pages/bluetooth/agent.rs b/cosmic-settings/src/pages/bluetooth/agent.rs deleted file mode 100644 index f120938..0000000 --- a/cosmic-settings/src/pages/bluetooth/agent.rs +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright 2024 System76 -// SPDX-License-Identifier: GPL-3.0-only - -use std::sync::Arc; - -use futures::{SinkExt, StreamExt}; -use zbus::zvariant::ObjectPath; - -const AGENT_PATH: &str = "/org/bluez/agent/cosmic_settings"; - -pub async fn unregister(connection: zbus::Connection) -> zbus::Result<()> { - let agent_path = ObjectPath::from_static_str_unchecked(AGENT_PATH); - let bluez = bluez_zbus::agent_manager1::AgentManager1Proxy::new(&connection).await?; - bluez.unregister_agent(&agent_path).await -} - -pub async fn watch( - connection: zbus::Connection, - mut tx: futures::channel::mpsc::Sender, -) -> zbus::Result<()> { - let span = tracing::span!(tracing::Level::INFO, "bluetooth::agent::watch"); - let _span = span.enter(); - - let (agent, mut receiver) = bluez_zbus::agent1::create(); - - let agent_path = ObjectPath::from_static_str_unchecked(AGENT_PATH); - - tracing::debug!("connecting agent"); - - connection.object_server().at(&agent_path, agent).await?; - - tracing::debug!("connecting to bluez agent manager"); - - let bluez = bluez_zbus::agent_manager1::AgentManager1Proxy::new(&connection).await?; - - tracing::debug!("registering agent"); - - bluez - .register_agent( - &agent_path, - <&'static str>::from(bluez_zbus::agent1::Capability::DisplayYesNo), - ) - .await?; - - if let Err(why) = bluez.request_default_agent(&agent_path).await { - _ = bluez.unregister_agent(&agent_path).await; - Err(why)?; - } - - tracing::debug!("registered"); - - while let Some(msg) = receiver.next().await { - tracing::debug!(?msg, "agent message received"); - - if tx.send(super::Message::Agent(Arc::new(msg))).await.is_err() { - break; - } - } - - _ = bluez.unregister_agent(&agent_path).await; - - tracing::debug!("exiting"); - - Ok(()) -} diff --git a/cosmic-settings/src/pages/bluetooth/backend.rs b/cosmic-settings/src/pages/bluetooth/backend.rs deleted file mode 100644 index 845654f..0000000 --- a/cosmic-settings/src/pages/bluetooth/backend.rs +++ /dev/null @@ -1,671 +0,0 @@ -// Copyright 2024 System76 -// SPDX-License-Identifier: MPL-2.0 - -use futures::join; -use std::{ - collections::HashMap, - hash::{Hash, Hasher}, - time::Duration, -}; -use zbus::zvariant::OwnedObjectPath; - -use super::Message; - -#[derive(Default, Debug, Clone)] -pub struct Device { - alias: Option, - pub address: String, - pub adapter: OwnedObjectPath, - pub enabled: Active, - pub paired: bool, - pub icon: &'static str, - pub battery: Option, -} -#[derive(Debug, Clone)] -pub enum DeviceUpdate { - Alias(Option), - Enabled(Active), - Paired(bool), - Icon(&'static str), - Battery(Option), -} - -impl DeviceUpdate { - pub fn from_update(update: HashMap<&'_ str, zbus::zvariant::Value<'_>>) -> Vec { - update - .into_iter() - .filter_map(|(key, value)| { - match (key, value) { - ("Alias", zbus::zvariant::Value::Str(value)) => { - Some(DeviceUpdate::Alias(Some(value.into()))) - } - ("Connected", zbus::zvariant::Value::Bool(value)) => { - Some(DeviceUpdate::Enabled(if value { - Active::Enabled - } else { - Active::Disabled - })) - } - ("Paired", zbus::zvariant::Value::Bool(value)) => { - Some(DeviceUpdate::Paired(value)) - } - ("Icon", zbus::zvariant::Value::Str(value)) => { - Some(DeviceUpdate::Icon(device_type_to_icon(&value))) - } - ("Percentage", zbus::zvariant::Value::U8(percentage)) => { - Some(DeviceUpdate::Battery(Some(fl!( - "bluetooth-paired", - "battery", - percentage = percentage.to_string() - )))) - } - // Battery - (message, value) => { - tracing::debug!(message, ?value, "device update"); - None - } - } - }) - .collect() - } -} - -#[derive(Default, Debug, Clone)] -pub struct Adapter { - pub alias: String, - pub address: String, - pub scanning: Active, - pub enabled: Active, -} -#[derive(Debug, Clone)] -pub enum AdapterUpdate { - Alias(String), - Address(String), - Scanning(Active), - Enabled(Active), -} - -impl AdapterUpdate { - #[must_use] - pub fn from_update(update: HashMap<&'_ str, zbus::zvariant::Value<'_>>) -> Vec { - update - .into_iter() - .filter_map(|(key, value)| { - match (key, value) { - ("Alias", zbus::zvariant::Value::Str(value)) => Some(Self::Alias(value.into())), - ("Discovering" | "Discoverable", zbus::zvariant::Value::Bool(value)) => { - Some(Self::Scanning(if value { - Active::Enabled - } else { - Active::Disabled - })) - } - ("Powered", zbus::zvariant::Value::Bool(value)) => { - Some(Self::Enabled(if value { - Active::Enabled - } else { - Active::Disabled - })) - } - ("Address", zbus::zvariant::Value::Str(value)) => { - Some(Self::Address(value.into())) - } - // Battery - (message, value) => { - tracing::error!(message, ?value, "adapter update"); - None - } - } - }) - .collect() - } -} - -#[derive(Default, Debug, Clone, Copy, Eq, PartialEq)] -pub enum Active { - #[default] - Disabled, - Disabling, - Enabling, - Enabled, -} - -impl Hash for Device { - fn hash(&self, state: &mut H) { - self.address.hash(state); - } -} - -impl PartialEq for Device { - fn eq(&self, other: &Self) -> bool { - self.address == other.address - } -} - -impl Eq for Device {} - -impl Hash for Adapter { - fn hash(&self, state: &mut H) { - self.address.hash(state); - } -} - -impl PartialEq for Adapter { - fn eq(&self, other: &Self) -> bool { - self.address == other.address - } -} - -impl Eq for Adapter {} - -const DEFAULT_DEVICE_ICON: &str = "bluetooth-symbolic"; - -fn device_type_to_icon(device_type: &str) -> &'static str { - match device_type { - "computer" => "laptop-symbolic", - "phone" => "smartphone-symbolic", - "network-wireless" => "network-wireless-symbolic", - "audio-headset" => "audio-headset-symbolic", - "audio-headphones" => "audio-headphones-symbolic", - "camera-video" => "camera-video-symbolic", - "audio-card" => "audio-card-symbolic", - "input-gaming" => "input-gaming-symbolic", - "input-keyboard" => "input-keyboard-symbolic", - "input-tablet" => "input-tablet-symbolic", - "input-mouse" => "input-mouse-symbolic", - "printer" => "printer-network-symbolic", - "camera-photo" => "camera-photo-symbolic", - _ => DEFAULT_DEVICE_ICON, - } -} - -impl Device { - pub async fn from_device(proxy: &bluez_zbus::BluetoothDevice<'_>) -> zbus::Result { - let (address, adapter, alias) = join!( - proxy.device.address(), - proxy.device.adapter(), - proxy.device.name() - ); - let address = address?; - if address.is_empty() { - return Err(zbus::Error::Failure("Device has no MAC address".to_owned())); - } - let adapter = adapter?; - if adapter.is_empty() { - return Err(zbus::Error::Failure("Device has no adapter".to_owned())); - } - let alias = alias.ok(); - let device_type: String = proxy.icon().await; - let paired = proxy.device.paired().await.unwrap_or(false); - let enabled = if proxy.device.connected().await.unwrap_or(false) && paired { - Active::Enabled - } else { - Active::Disabled - }; - let battery = match &proxy.battery { - Some(battery) => match battery.percentage().await { - Ok(percentage) => Some(fl!( - "bluetooth-paired", - "battery", - percentage = percentage.to_string() - )), - Err(why) => { - eprintln!("couldn't fetch battery percentage: {why}"); - None - } - }, - None => None, - }; - - // Copied from https://github.com/bluez/bluez/blob/39467578207889fd015775cbe81a3db9dd26abea/src/dbus-common.c#L53 - let icon = device_type_to_icon(device_type.as_str()); - - Ok(Self { - alias, - address, - adapter, - enabled, - paired, - icon, - battery, - }) - } - #[must_use] - pub fn is_connected(&self) -> bool { - self.enabled == Active::Enabled - } - /// Update the state of the device without overriding intermediary states. - /// - /// # Panics - /// - /// Panics if the device used for update doesn't have the same MAC address - pub fn update(&mut self, updates: Vec) { - for udpate in updates { - match udpate { - DeviceUpdate::Alias(alias) => self.alias = alias, - DeviceUpdate::Enabled(enabled) => { - self.enabled = match (self.enabled, enabled) { - (Active::Enabling, Active::Enabled) => Active::Enabled, - (Active::Disabling, Active::Disabled) => Active::Disabled, - (Active::Enabled | Active::Disabled, status) => status, - (status, _) => status, - } - } - DeviceUpdate::Paired(paired) => { - self.enabled = Active::Disabling; - self.paired = paired; - } - DeviceUpdate::Icon(icon) => self.icon = icon, - DeviceUpdate::Battery(battery) => self.battery = battery, - } - } - if self.enabled == Active::Disabled { - self.battery = None; - } - } - #[must_use] - pub fn has_alias(&self) -> bool { - self.alias.is_some() - } - #[must_use] - pub fn is_known_device_type(&self) -> bool { - self.icon != DEFAULT_DEVICE_ICON - } - #[must_use] - pub fn alias_or_addr(&self) -> &str { - self.alias.as_ref().unwrap_or(&self.address) - } -} - -impl Adapter { - pub async fn from_device( - proxy: &bluez_zbus::adapter1::Adapter1Proxy<'_>, - ) -> zbus::Result { - let (address, alias, scanning, enabled) = futures::try_join!( - proxy.address(), - proxy.alias(), - async { - Ok( - if proxy.discoverable().await? && proxy.discovering().await? { - Active::Enabled - } else { - Active::Disabled - }, - ) - }, - async { - Ok(if proxy.powered().await? { - Active::Enabled - } else { - Active::Disabled - }) - } - )?; - - Ok(Self { - alias, - address, - scanning, - enabled, - }) - } - pub fn update(&mut self, updates: Vec) { - for update in updates { - match update { - AdapterUpdate::Alias(alias) => self.alias = alias, - AdapterUpdate::Address(address) => self.address = address, - AdapterUpdate::Enabled(enabled) => { - self.enabled = match (self.enabled, enabled) { - (Active::Enabling, Active::Enabled) => Active::Enabled, - (Active::Disabling, Active::Disabled) => Active::Disabled, - (Active::Enabled | Active::Disabled, status) => status, - (status, _) => status, - } - } - AdapterUpdate::Scanning(scanning) => { - self.scanning = match (self.scanning, scanning) { - (Active::Enabling, Active::Enabled) => Active::Enabled, - (Active::Disabling, Active::Disabled) => Active::Disabled, - (Active::Enabled | Active::Disabled, status) => status, - (status, _) => status, - } - } - } - } - } -} - -pub async fn start_discovery( - connection: zbus::Connection, - adapter_path: OwnedObjectPath, -) -> Message { - let result: zbus::Result<()> = Ok(()); - - let adapter = match bluez_zbus::get_adapter(&connection, adapter_path).await { - Err(why) => { - tracing::error!("Unable to get the adapter: {why}"); - return Message::DBusError(why.to_string()); - } - Ok(adapter) => adapter, - }; - - for attempt in 1..5 { - let result = async { - tracing::debug!("Starting discovery"); - // We don't seem to be able to use join here as it seem to lead to some kind of race condition and not start scanning occasionally - adapter.set_pairable(true).await?; - adapter.set_discoverable(true).await?; - if adapter.discovering().await? { - return Ok(()); - } - adapter.start_discovery().await - } - .await; - - if let Err(why) = result { - tracing::warn!("Unable to start bluetooth scanning: {why}"); - tokio::time::sleep(Duration::from_millis(1000 * attempt)).await; - } else { - tracing::debug!("Discovery started"); - return Message::Nop; - } - } - - if let Err(why) = result { - Message::DBusError(why.to_string()) - } else { - Message::Nop - } -} - -pub async fn stop_discovery( - connection: zbus::Connection, - adapter_path: OwnedObjectPath, -) -> Message { - let result: zbus::Result<()> = Ok(()); - - let adapter = match bluez_zbus::get_adapter(&connection, adapter_path).await { - Err(why) => return Message::DBusError(format!("Unable to get the adapter: {why}")), - Ok(adapter) => adapter, - }; - - for attempt in 1..5 { - let result = async { - tracing::debug!("Stopping discovery"); - - // We don't seem to be able to use join here as it seem to lead to some kind of race condition and not stop scanning occasionally - adapter.set_pairable(false).await?; - adapter.set_discoverable(false).await?; - if adapter.discovering().await? { - adapter.stop_discovery().await - } else { - Ok(()) - } - } - .await; - - if let Err(why) = result { - tracing::warn!("Unable to stop bluetooth scanning: {why}"); - tokio::time::sleep(Duration::from_millis(1000 * attempt)).await; - } else { - tracing::debug!("Discovery stopped"); - return Message::Nop; - } - } - - if let Err(why) = result { - return Message::DBusError(why.to_string()); - } - Message::Nop -} - -pub async fn disconnect_device( - connection: zbus::Connection, - device_path: OwnedObjectPath, -) -> Message { - let proxy = match bluez_zbus::get_device(&connection, device_path.clone()).await { - Err(why) => { - tracing::error!("Unable to get the device: {why}"); - return Message::DeviceFailed(device_path); - } - Ok(proxy) => proxy, - }; - - for attempt in 1..5 { - let result = async { - if !proxy.device.connected().await? { - return Ok(()); - } - - proxy.device.disconnect().await - } - .await; - - if let Err(why) = result { - tracing::warn!("Unable to disconnect to device: {why}"); - tokio::time::sleep(Duration::from_millis(1000 * attempt)).await; - } else { - return Message::Nop; - } - } - - Message::DeviceFailed(device_path) -} - -pub async fn connect_device(connection: zbus::Connection, device_path: OwnedObjectPath) -> Message { - let proxy = match bluez_zbus::get_device(&connection, device_path.clone()).await { - Err(why) => { - tracing::error!("Unable to get the device: {why}"); - return Message::DeviceFailed(device_path); - } - Ok(proxy) => proxy, - }; - - for attempt in 1..5 { - let result = async { - if proxy.device.connected().await? { - Ok(()) - } else { - proxy.device.connect().await - } - } - .await; - - if let Err(why) = result { - tracing::warn!("Unable to connect to device: {why}"); - tokio::time::sleep(Duration::from_millis(1000 * attempt)).await; - } else { - return Message::Nop; - } - } - - Message::DeviceFailed(device_path) -} - -pub async fn forget_device(connection: zbus::Connection, device_path: OwnedObjectPath) -> Message { - let mut result: zbus::Result<()> = Ok(()); - - let proxy = match bluez_zbus::get_device(&connection, device_path.clone()).await { - Err(why) => { - tracing::error!("Unable to get the device: {why}"); - return Message::DeviceFailed(device_path); - } - Ok(proxy) => proxy, - }; - - let adapter_path = match proxy.device.adapter().await { - Err(why) => { - tracing::error!("Unable to get the adapter: {why}"); - return Message::DeviceFailed(device_path); - } - Ok(adapter_path) => adapter_path, - }; - - let adapter = match bluez_zbus::get_adapter(&connection, adapter_path).await { - Err(why) => { - tracing::error!("Unable to get the adapter: {why}"); - return Message::DeviceFailed(device_path); - } - Ok(adapter) => adapter, - }; - - for attempt in 1..5 { - result = async { - if proxy.device.connected().await? { - proxy.device.disconnect().await?; - } - - adapter.remove_device(&proxy.path()).await - } - .await; - - if let Err(why) = &result { - tracing::warn!("Unable to connect to device: {why}"); - tokio::time::sleep(Duration::from_millis(1000 * attempt)).await; - } else { - return Message::Nop; - } - } - - if result.is_err() { - Message::DeviceFailed(device_path) - } else { - Message::Nop - } -} - -pub async fn change_adapter_status( - connection: zbus::Connection, - adapter_path: OwnedObjectPath, - active: bool, -) -> Message { - let mut result: zbus::Result<()> = Ok(()); - for attempt in 1..5 { - result = async { - let adapter = bluez_zbus::get_adapter(&connection, adapter_path.clone()).await?; - if active { - adapter.set_powered(true).await?; - adapter.set_discoverable(true).await - } else { - if let Err(why) = adapter.set_discoverable(false).await { - tracing::warn!("Unable to change discoverability: {why}"); - } - adapter.set_powered(false).await - } - } - .await; - if let Err(why) = &result { - tracing::warn!("Unable to change the adapter state: {why}"); - tokio::time::sleep(Duration::from_millis(1000 * attempt)).await; - } else { - return Message::Nop; - } - } - - if let Err(why) = result { - tracing::error!("Failed to change the adapter state!"); - return Message::DBusError(why.to_string()); - } - - Message::Nop -} - -pub async fn get_devices(connection: zbus::Connection, adapter_path: OwnedObjectPath) -> Message { - // TODO error handling - let result: zbus::Result> = async { - futures::future::join_all( - bluez_zbus::get_devices(&connection, Some(&adapter_path)) - .await? - .into_iter() - .map( - |(path, device)| async move { Ok((path, Device::from_device(&device).await?)) }, - ), - ) - .await - .into_iter() - .collect::, _>>() - } - .await; - match result { - Ok(devices) => Message::SetDevices(devices), - Err(why) => { - tracing::error!("zbus connection failed. {why}"); - Message::DBusError(fl!("bluetooth", "dbus-error", why = why.to_string())) - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_update_device_with_intermediary_state() { - let mut device = Device { - alias: None, - adapter: OwnedObjectPath::try_from("/dev/bluez/hci0").unwrap(), - address: "AA:BB:CC:DD:EE:FF".to_owned(), - enabled: Active::Disabled, - paired: false, - icon: "bluetooth-symbolic", - battery: None, - }; - device.update(vec![ - DeviceUpdate::Enabled(Active::Enabled), - DeviceUpdate::Alias(Some("Foo".to_owned())), - ]); - assert_eq!(device.enabled, Active::Enabled); - assert_eq!(device.alias, Some("Foo".to_owned())); - - device.enabled = Active::Disabling; - device.update(vec![ - DeviceUpdate::Enabled(Active::Enabled), - DeviceUpdate::Alias(Some("Foo".to_owned())), - ]); - assert_eq!(device.enabled, Active::Disabling); - - device.enabled = Active::Enabling; - device.update(vec![ - DeviceUpdate::Enabled(Active::Enabled), - DeviceUpdate::Alias(Some("Foo".to_owned())), - ]); - assert_eq!(device.enabled, Active::Enabled); - } - - #[test] - fn test_adapter_device_with_intermediary_state() { - let mut adapter = Adapter { - alias: "foo".to_owned(), - address: "AA:BB:CC:DD:EE:FF".to_owned(), - scanning: Active::Disabled, - enabled: Active::Disabled, - }; - adapter.update(vec![ - AdapterUpdate::Enabled(Active::Enabled), - AdapterUpdate::Alias("xxx".to_owned()), - ]); - assert_eq!(adapter.enabled, Active::Enabled); - assert_eq!(&adapter.alias, "xxx"); - - adapter.enabled = Active::Disabling; - adapter.update(vec![ - AdapterUpdate::Enabled(Active::Enabled), - AdapterUpdate::Alias("xxx".to_owned()), - ]); - assert_eq!(adapter.enabled, Active::Disabling); - - adapter.scanning = Active::Enabling; - adapter.update(vec![ - AdapterUpdate::Scanning(Active::Disabled), - AdapterUpdate::Alias("xxx".to_owned()), - ]); - assert_eq!(adapter.scanning, Active::Enabling); - - adapter.update(vec![ - AdapterUpdate::Scanning(Active::Enabled), - AdapterUpdate::Alias("xxx".to_owned()), - ]); - assert_eq!(adapter.scanning, Active::Enabled); - assert_eq!(&adapter.alias, "xxx"); - } -} diff --git a/cosmic-settings/src/pages/bluetooth/mod.rs b/cosmic-settings/src/pages/bluetooth/mod.rs index 68c14e8..4051f9e 100644 --- a/cosmic-settings/src/pages/bluetooth/mod.rs +++ b/cosmic-settings/src/pages/bluetooth/mod.rs @@ -6,6 +6,7 @@ use cosmic::iced_core::text::Wrapping; use cosmic::widget::{self, settings, text}; use cosmic::{theme, Apply, Element, Task}; use cosmic_settings_page::{self as page, section, Section}; +use cosmic_settings_subscriptions::bluetooth::*; use futures::channel::oneshot; use slab::Slab; use slotmap::SlotMap; @@ -13,11 +14,6 @@ use std::collections::{HashMap, HashSet}; use std::sync::Arc; use zbus::zvariant::OwnedObjectPath; -mod agent; -mod backend; -pub use backend::*; -mod subscription; - enum Dialog { // RequestAuthorization { // device: OwnedObjectPath, @@ -150,17 +146,13 @@ impl page::Page for Page { #[derive(Clone, Debug)] pub enum Message { - AddedAdapter(OwnedObjectPath, Adapter), - AddedDevice(OwnedObjectPath, Device), - Agent(Arc), + BluetoothEvent(Event), ConnectDevice(OwnedObjectPath), DBusConnect( zbus::Connection, tokio::sync::mpsc::Sender, ), DBusError(String), - DBusServiceUnknown, - DeviceFailed(OwnedObjectPath), DisconnectDevice(OwnedObjectPath), ForgetDevice(OwnedObjectPath), PinCancel, @@ -168,14 +160,8 @@ pub enum Message { PopupDevice(Option), PopupSetting(bool), Nop, - RemovedAdapter(OwnedObjectPath), - RemovedDevice(OwnedObjectPath), SelectAdapter(Option), SetActive(bool), - SetAdapters(HashMap), - SetDevices(HashMap), - UpdatedAdapter(OwnedObjectPath, Vec), - UpdatedDevice(OwnedObjectPath, Vec), } impl From for crate::app::Message { @@ -190,55 +176,159 @@ impl From for crate::pages::Message { } } +impl From for crate::app::Message { + fn from(event: Event) -> Self { + crate::pages::Message::Bluetooth(Message::BluetoothEvent(event)).into() + } +} + +impl From for crate::pages::Message { + fn from(event: Event) -> Self { + crate::pages::Message::Bluetooth(Message::BluetoothEvent(event)) + } +} + +impl From for Message { + fn from(event: Event) -> Self { + Message::BluetoothEvent(event) + } +} + impl Page { pub fn update(&mut self, message: Message) -> cosmic::Task { let span = tracing::span!(tracing::Level::INFO, "bluetooth::update"); let _span = span.enter(); match message { - Message::Agent(message) => { - let Some(message) = Arc::into_inner(message) else { - return Task::none(); - }; - - match message { - bluez_zbus::agent1::Message::RequestAuthorization { response, .. } => { - _ = response.send(true); + Message::BluetoothEvent(event) => match event { + Event::DBusError(why) => { + tracing::error!( + "dbus connection failed. {}", + fl!("bluetooth", "dbus-error", why = why.to_string()) + ); + } + Event::Ok => {} + Event::SetDevices(devices) => { + self.devices = devices; + } + Event::DeviceFailed(path) => { + tracing::warn!("Failed operation on device {path}"); + if let Some(device) = self.devices.get_mut(&path) { + if matches!(device.enabled, Active::Disabled | Active::Disabling) { + return cosmic::Task::none(); + } + device.enabled = match device.enabled { + Active::Disabling => Active::Enabled, + Active::Enabling => Active::Disabled, + e => e, + }; } + } + Event::SetAdapters(adapters) => { + self.adapters = adapters; + self.update_status(); - bluez_zbus::agent1::Message::RequestConfirmation { - device, - passkey, - response, - } => { - let device = self.devices.get(&device).map_or_else( - || device.to_string(), - |device| device.alias_or_addr().to_owned(), - ); + if self.selected_adapter.is_none() && self.adapters.len() == 1 { + return cosmic::task::message(Message::SelectAdapter( + self.adapters.keys().next().cloned(), + )); + } + } + Event::UpdatedAdapter(path, update) => { + if let Some(existing) = self.adapters.get_mut(&path) { + tracing::debug!("Adapter {} updated: {update:#?}", existing.address); + existing.update(update); + } + self.update_status(); + if let Some(connection) = self.connection.clone() { + match self.get_selected_adapter_mut() { + Some((path, existing)) + if existing.enabled == Active::Enabled + && existing.scanning == Active::Disabled => + { + existing.scanning = Active::Enabling; + return cosmic::task::future(start_discovery(connection, path)); + } + _ => {} + } + } else { + tracing::warn!("No DBus connection ready"); + } + } + Event::UpdatedDevice(path, update) => { + if let Some(existing) = self.devices.get_mut(&path) { + tracing::debug!("Device {} updated", existing.address); + existing.update(update); + } + } + Event::RemovedAdapter(path) => { + tracing::debug!("Device {path} removed"); + self.adapters.remove(&path); + if self.selected_adapter == Some(path) { + self.selected_adapter = None; + } + } + Event::RemovedDevice(path) => { + tracing::debug!("Device {path} removed"); + self.devices.remove(&path); + } + Event::AddedDevice(path, device) => { + tracing::debug!("Device {} added", device.address); + self.devices.insert(path, device); + } + Event::AddedAdapter(path, adapter) => { + tracing::debug!("Adapter {} added", adapter.address); + self.adapters.insert(path.clone(), adapter); + if self.selected_adapter.is_none() { + return cosmic::task::message(Message::SelectAdapter(Some(path))); + } + } + Event::DBusServiceUnknown => { + self.bluez_service_unknown = true; + } + Event::Agent(message) => { + let Some(message) = Arc::into_inner(message) else { + return Task::none(); + }; - self.dialog = Some(Dialog::RequestConfirmation { + match message { + bluez_zbus::agent1::Message::RequestAuthorization { response, .. } => { + _ = response.send(true); + } + + bluez_zbus::agent1::Message::RequestConfirmation { device, passkey, response, - }); - } + } => { + let device = self.devices.get(&device).map_or_else( + || device.to_string(), + |device| device.alias_or_addr().to_owned(), + ); - bluez_zbus::agent1::Message::RequestPasskey { response, .. } => { - _ = response.send(None); - } + self.dialog = Some(Dialog::RequestConfirmation { + device, + passkey, + response, + }); + } - bluez_zbus::agent1::Message::RequestPinCode { response, .. } => { - _ = response.send(None); - } + bluez_zbus::agent1::Message::RequestPasskey { response, .. } => { + _ = response.send(None); + } - bluez_zbus::agent1::Message::Cancel => { - self.dialog = None; - } + bluez_zbus::agent1::Message::RequestPinCode { response, .. } => { + _ = response.send(None); + } - _ => (), + bluez_zbus::agent1::Message::Cancel => { + self.dialog = None; + } + + _ => (), + } } - } - + }, Message::PinCancel => { if let Some(Dialog::RequestConfirmation { response, .. }) = self.dialog.take() { _ = response.send(false); @@ -295,7 +385,9 @@ impl Page { let connection = connection.clone(); self.subscription = Some(crate::utils::forward_event_loop( sender, - crate::pages::Message::Bluetooth, + |response| { + crate::pages::Message::Bluetooth(Message::BluetoothEvent(response)) + }, move |tx| async move { _ = futures::join!( subscription::watch(connection.clone(), tx.clone()), @@ -305,95 +397,7 @@ impl Page { )); } - return cosmic::task::future(async move { - let result: zbus::Result> = async { - futures::future::join_all( - bluez_zbus::get_adapters(&connection) - .await? - .into_iter() - .map(|(path, proxy)| async move { - Ok((path.to_owned(), Adapter::from_device(&proxy).await?)) - }), - ) - .await - .into_iter() - .collect::>>() - } - .await; - match result { - Ok(adapters) => Message::SetAdapters(adapters), - Err(why) => { - tracing::error!("dbus connection failed. {why}"); - Message::DBusError(fl!( - "bluetooth", - "dbus-error", - why = why.to_string() - )) - } - } - }); - } - Message::SetDevices(devices) => { - self.devices = devices; - } - Message::SetAdapters(adapters) => { - self.adapters = adapters; - self.update_status(); - - if self.selected_adapter.is_none() && self.adapters.len() == 1 { - return cosmic::task::message(Message::SelectAdapter( - self.adapters.keys().next().cloned(), - )); - } - } - Message::AddedDevice(path, device) => { - tracing::debug!("Device {} added", device.address); - self.devices.insert(path, device); - } - Message::UpdatedDevice(path, update) => { - if let Some(existing) = self.devices.get_mut(&path) { - tracing::debug!("Device {} updated", existing.address); - existing.update(update); - } - } - Message::RemovedDevice(path) => { - tracing::debug!("Device {path} removed"); - self.devices.remove(&path); - } - Message::AddedAdapter(path, adapter) => { - tracing::debug!("Adapter {} added", adapter.address); - self.adapters.insert(path.clone(), adapter); - if self.selected_adapter.is_none() { - return cosmic::task::message(Message::SelectAdapter(Some(path))); - } - } - Message::UpdatedAdapter(path, update) => { - if let Some(existing) = self.adapters.get_mut(&path) { - tracing::debug!("Adapter {} updated: {update:#?}", existing.address); - existing.update(update); - } - self.update_status(); - if let Some(connection) = self.connection.clone() { - match self.get_selected_adapter_mut() { - Some((path, existing)) - if existing.enabled == Active::Enabled - && existing.scanning == Active::Disabled => - { - existing.scanning = Active::Enabling; - return cosmic::task::future(start_discovery(connection, path)); - } - _ => {} - } - } else { - tracing::warn!("No DBus connection ready"); - } - } - Message::RemovedAdapter(path) => { - tracing::debug!("Device {path} removed"); - self.adapters.remove(&path); - if self.selected_adapter == Some(path) { - self.selected_adapter = None; - } + return cosmic::task::future(get_adapters(connection.clone())); } Message::PopupDevice(popup) => { self.popup_device = popup; @@ -477,26 +481,10 @@ impl Page { tracing::warn!("No DBus connection ready"); } } - Message::DeviceFailed(path) => { - tracing::warn!("Failed operation on device {path}"); - if let Some(device) = self.devices.get_mut(&path) { - if matches!(device.enabled, Active::Disabled | Active::Disabling) { - return cosmic::Task::none(); - } - device.enabled = match device.enabled { - Active::Disabling => Active::Enabled, - Active::Enabling => Active::Disabled, - e => e, - }; - } - } Message::Nop => {} Message::DBusError(why) => { tracing::error!("dbus connection failed. {why}"); } - Message::DBusServiceUnknown => { - self.bluez_service_unknown = true; - } }; cosmic::Task::none() } @@ -691,7 +679,11 @@ fn connected_devices() -> Section { if let Some(battery) = &device.battery { widget::column::with_capacity(2) .push(text::body(device.alias_or_addr())) - .push(text::caption(battery)) + .push(text::caption(fl!( + "bluetooth-paired", + "battery", + percentage = battery + ))) .into() } else { widget::text(device.alias_or_addr()) diff --git a/cosmic-settings/src/pages/bluetooth/subscription.rs b/cosmic-settings/src/pages/bluetooth/subscription.rs deleted file mode 100644 index 0baaa29..0000000 --- a/cosmic-settings/src/pages/bluetooth/subscription.rs +++ /dev/null @@ -1,226 +0,0 @@ -// Copyright 2024 System76 -// SPDX-License-Identifier: MPL-2.0 - -use crate::pages::bluetooth; -use std::pin::Pin; - -use bluez_zbus::BluetoothDevice; -use cosmic::iced::futures::{SinkExt, StreamExt}; -use futures::{channel::mpsc, stream::FusedStream}; -use zbus::{fdo, zvariant::OwnedObjectPath}; - -enum DevicePropertyWatcherTask { - Add(OwnedObjectPath), - Removed(OwnedObjectPath), -} - -struct DevicePropertyWatcher<'a> { - stream: futures::stream::SelectAll>, - rx: mpsc::Receiver, -} - -struct SignalWatcher<'a> { - stream: zbus::fdo::PropertiesChangedStream<'a>, - path: OwnedObjectPath, -} - -impl<'a> futures::Stream for SignalWatcher<'a> { - type Item = zbus::fdo::PropertiesChanged; - - fn poll_next( - mut self: std::pin::Pin<&mut Self>, - cx: &mut std::task::Context<'_>, - ) -> std::task::Poll> { - futures::Stream::poll_next(Pin::new(&mut self.stream), cx) - } - fn size_hint(&self) -> (usize, Option) { - self.stream.size_hint() - } -} - -impl<'a> DevicePropertyWatcher<'a> { - fn new() -> (Self, mpsc::Sender) { - let stream = futures::stream::select_all(vec![]); - let (tx, rx) = mpsc::channel(10); - - (Self { stream, rx }, tx) - } - async fn insert( - &mut self, - connection: &zbus::Connection, - path: OwnedObjectPath, - ) -> zbus::Result<()> { - if let Some(signal) = self.stream.iter_mut().find(|s| s.path.eq(&path)) { - if signal.stream.is_terminated() { - let property_proxy = - zbus::fdo::PropertiesProxy::new(connection, "org.bluez", path.clone()).await?; - signal.stream = property_proxy.receive_properties_changed().await?; - } - return Ok(()); - } - let property_proxy = - zbus::fdo::PropertiesProxy::new(connection, "org.bluez", path.clone()).await?; - let stream = property_proxy.receive_properties_changed().await?; - self.stream.push(SignalWatcher { stream, path }); - Ok(()) - } - fn remove(mut self, path: &OwnedObjectPath) -> Self { - self.stream = - futures::stream::select_all(self.stream.into_iter().filter(|p| !p.path.eq(path))); - self - } -} - -/// Watching new/removed devices, connected state changed -pub async fn watch( - connection: zbus::Connection, - mut tx: futures::channel::mpsc::Sender, -) { - let span = tracing::span!(tracing::Level::INFO, "bluetooth::subscription::watch"); - let _span = span.enter(); - - loop { - let result = async { - let managed_object_proxy = - zbus::fdo::ObjectManagerProxy::new(&connection, "org.bluez", "/") - .await?; - - let mut receive_interfaces_added = managed_object_proxy - .receive_interfaces_added() - .await?; - let mut receive_interfaces_removed = managed_object_proxy - .receive_interfaces_removed() - .await?; - - let (mut property_watcher, mut property_watcher_task) = DevicePropertyWatcher::new(); - - for (path, interfaces) in managed_object_proxy.get_managed_objects().await? { - if interfaces.contains_key("org.bluez.Device1") - || interfaces.contains_key("org.bluez.Adapter1") - || interfaces.contains_key("org.bluez.Battery1") - { - property_watcher.insert(&connection, path).await?; - } - } - - while !property_watcher.rx.is_terminated() { - futures::select! { - task = property_watcher.rx.next() => match task { - Some(DevicePropertyWatcherTask::Add(path)) => { - property_watcher.insert(&connection, path).await?; - } - Some(DevicePropertyWatcherTask::Removed(path)) => { - property_watcher = property_watcher.remove(&path); - } - None => { - tracing::error!("Bluetooth property watcher has shutdown unexpectedly"); - } - }, - signal = property_watcher.stream.next() => match signal { - Some(signal) => { - let args = signal.args()?; - let header = signal.message().header(); - match header.path() { - Some(path) if path.contains("/dev_") => - tx - .send(bluetooth::Message::UpdatedDevice(path.to_owned().into(), bluetooth::DeviceUpdate::from_update(args.changed_properties))) - .await - .map_err(|e| zbus::Error::Failure(e.to_string()))?, - Some(path) => tx - .send(bluetooth::Message::UpdatedAdapter(path.to_owned().into(), bluetooth::AdapterUpdate::from_update(args.changed_properties))) - .await - .map_err(|e| zbus::Error::Failure(e.to_string()))?, - None => continue - } - } - None => { - tracing::error!("Bluetooth object watcher has shutdown unexpectedly"); - } - }, - signal = receive_interfaces_added.next() => match signal { - Some(signal) => { - let args = signal.args()?; - match BluetoothDevice::new(&connection, args.object_path.clone()).await { - Ok(device) => { - match bluetooth::Device::from_device(&device).await { - Ok(device) => { - property_watcher_task - .send(DevicePropertyWatcherTask::Add(args.object_path.to_owned().into())).await.map_err(|e| zbus::Error::Failure(e.to_string()))?; - - tx - .send(bluetooth::Message::AddedDevice(args.object_path.to_owned().into(), device)) - .await - .map_err(|e| zbus::Error::Failure(e.to_string()))?; - - } - Err(why) => { - tracing::warn!("Cannot deserialise device: {why}"); - } - } - } - Err(zbus::Error::InterfaceNotFound) => continue, - Err(e) => return Err(e), - } - } - None => { - tracing::error!("Bluetooth object watcher has shutdown unexpectedly"); - } - }, - signal = receive_interfaces_removed.next() => match signal { - Some(signal) => { - let args = signal.args()?; - if args.interfaces.contains(&"org.bluez.Device1") { - property_watcher_task.send(DevicePropertyWatcherTask::Removed( - args.object_path.to_owned().into(), - )).await.map_err(|e| zbus::Error::Failure(e.to_string()))?; - tx - .send(bluetooth::Message::RemovedDevice(args.object_path.to_owned().into())) - .await - .map_err(|e| zbus::Error::Failure(e.to_string()))?; - - } else if args.interfaces.contains(&"org.bluez.Battery1") { - tx - .send(bluetooth::Message::UpdatedDevice(args.object_path.to_owned().into(), vec![bluetooth::DeviceUpdate::Battery(None)])) - .await - .map_err(|e| zbus::Error::Failure(e.to_string()))?; - } else if args.interfaces.contains(&"org.bluez.Adapter1") { - tx - .send(bluetooth::Message::RemovedAdapter(args.object_path.to_owned().into())) - .await - .map_err(|e| zbus::Error::Failure(e.to_string()))?; - } - }, - None => { - tracing::error!("Bluetooth object watcher has shutdown unexpectedly"); - } - }, - } - } - tracing::warn!("bluetooth event loop gracefully terminated"); - Ok(()) - }.await; - - if let Err(why) = result { - _ = tx - .send(bluetooth::Message::DBusError(why.to_string())) - .await; - - tracing::error!("failed to watch bluetooth event: {why}."); - - // Exit if the dbus service is not found. - if let zbus::Error::FDO(fdo_error) = why { - match *fdo_error { - fdo::Error::ServiceUnknown(_) => { - tracing::error!( - "The org.bluez dbus service is unknown. Is the bluez service installed and activatable?" - ); - _ = tx.send(bluetooth::Message::DBusServiceUnknown).await; - return; - } - - _ => (), - } - } - } - } -}