From f16244b400b29a0b2f582c62a58d2e3b50b34d5a Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Wed, 23 Apr 2025 16:54:18 +0200 Subject: [PATCH] fix(bluetooth): resolve issues with bluetooth settings page --- Cargo.lock | 2 +- cosmic-settings/src/pages/bluetooth/mod.rs | 235 ++++++++++++++++----- i18n/en/cosmic_settings.ftl | 5 + 3 files changed, 194 insertions(+), 48 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f581c1d..4866d59 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1784,7 +1784,7 @@ dependencies = [ [[package]] name = "cosmic-settings-subscriptions" version = "0.1.0" -source = "git+https://github.com/pop-os/cosmic-settings-subscriptions#ebcb941f8bcff9dea9877b8f4e30b5a76c0469b3" +source = "git+https://github.com/pop-os/cosmic-settings-subscriptions#752429e70dc5a06f28922ce159afe52002683088" dependencies = [ "bluez-zbus", "cosmic-dbus-networkmanager", diff --git a/cosmic-settings/src/pages/bluetooth/mod.rs b/cosmic-settings/src/pages/bluetooth/mod.rs index 337d41e..735c197 100644 --- a/cosmic-settings/src/pages/bluetooth/mod.rs +++ b/cosmic-settings/src/pages/bluetooth/mod.rs @@ -13,6 +13,7 @@ use slab::Slab; use slotmap::SlotMap; use std::collections::{HashMap, HashSet}; use std::sync::Arc; +use std::time::Duration; use zbus::zvariant::OwnedObjectPath; enum Dialog { @@ -46,6 +47,8 @@ pub struct Page { devices: HashMap, // Set to true when the org.bluez dbus service is unknown. bluez_service_unknown: bool, + service_is_enabled: bool, + service_is_active: bool, popup_setting: bool, popup_device: Option, subscription: Option>, @@ -75,9 +78,10 @@ impl page::Page for Page { cosmic::task::future(async move { match zbus::Connection::system().await { Ok(connection) => Message::DBusConnect(connection), - Err(why) => Message::DBusError(why.to_string()), + Err(why) => Message::DBusConnectFailed(why), } }) + .chain(cosmic::Task::done(Message::SelectAdapter(None).into())) } fn on_leave(&mut self) -> Task { @@ -147,16 +151,18 @@ pub enum Message { BluetoothEvent(Event), ConnectDevice(OwnedObjectPath), DBusConnect(zbus::Connection), - DBusError(String), + DBusConnectFailed(zbus::Error), DisconnectDevice(OwnedObjectPath), ForgetDevice(OwnedObjectPath), PinCancel, PinConfirm, PopupDevice(Option), PopupSetting(bool), - Nop, SelectAdapter(Option), + ServiceActivate, + ServiceEnable, SetActive(bool), + UpdateStatus, } impl From for crate::app::Message { @@ -197,15 +203,28 @@ impl Page { match message { Message::BluetoothEvent(event) => match event { Event::DBusError(why) => { + tracing::debug!("bluetooth dbus error {why:?}"); tracing::error!( - "dbus connection failed. {}", + "bluetooth service error {}", fl!("bluetooth", "dbus-error", why = why.to_string()) ); } + + Event::NameHasNoOwner => { + self.connection = None; + self.service_is_active = false; + self.service_is_enabled = false; + if let Some(abort_handle) = self.subscription.take() { + _ = abort_handle.send(()); + } + } + 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) { @@ -219,6 +238,7 @@ impl Page { }; } } + Event::SetAdapters(adapters) => { self.adapters = adapters; self.update_status(); @@ -229,6 +249,7 @@ impl Page { )); } } + Event::UpdatedAdapter(path, update) => { if let Some(existing) = self.adapters.get_mut(&path) { tracing::debug!("Adapter {} updated: {update:#?}", existing.address); @@ -250,12 +271,14 @@ impl Page { 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); @@ -263,14 +286,17 @@ impl Page { 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); @@ -278,9 +304,11 @@ impl Page { 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(); @@ -324,6 +352,7 @@ impl Page { } } }, + Message::PinCancel => { if let Some(Dialog::RequestConfirmation { response, .. }) = self.dialog.take() { _ = response.send(false); @@ -344,12 +373,20 @@ impl Page { } else { Active::Disabling }; - self.update_status(); + return cosmic::task::future(change_adapter_status( connection.clone(), path, active, - )); + )) + .then(|event| { + if matches!(event, Event::Ok) { + Task::none() + } else { + Task::done(event.into()) + } + }) + .chain(Task::done(Message::UpdateStatus.into())); } let tasks: Vec> = self .adapters @@ -365,15 +402,29 @@ impl Page { path.clone(), active, )) + .then(|event| { + if matches!(event, Event::Ok) { + Task::none() + } else { + Task::done(event.into()) + } + }) }) .collect(); - self.update_status(); - return cosmic::task::batch(tasks); + + return cosmic::task::batch(tasks) + .chain(Task::done(Message::UpdateStatus.into())); } tracing::warn!("No DBus connection ready"); } + Message::UpdateStatus => { + self.update_status(); + } + Message::DBusConnect(connection) => { + self.service_is_active = systemd::is_bluetooth_active(); + self.service_is_enabled = systemd::is_bluetooth_enabled(); self.connection = Some(connection.clone()); let get_adapters_fut = get_adapters(connection.clone()); @@ -415,38 +466,41 @@ impl Page { return cosmic::task::future(get_adapters_fut); } } + Message::PopupDevice(popup) => { self.popup_device = popup; } + Message::PopupSetting(popup) => { self.popup_setting = popup; } + Message::SelectAdapter(adapter_maybe) => { tracing::debug!("Adapter selected: {adapter_maybe:?}"); self.selected_adapter = adapter_maybe; self.update_status(); - if let Some(connection) = self.connection.as_ref() { - let connection = connection.clone(); - if let Some((path, adapter)) = self.get_selected_adapter_mut() { - let mut fut: Vec> = vec![cosmic::task::future(get_devices( - connection.clone(), - path.clone(), - ))]; - if adapter.enabled == Active::Enabled - && adapter.scanning == Active::Disabled - { - fut.push(cosmic::task::future(start_discovery( - connection, - path.clone(), - ))); - } + let Some(connection) = self.connection.as_ref() else { + tracing::error!("No DBus connection ready"); + return Task::none(); + }; - return cosmic::task::batch(fut); + let connection = connection.clone(); + if let Some((path, adapter)) = self.get_selected_adapter_mut() { + let mut fut: Vec> = vec![cosmic::task::future(get_devices( + connection.clone(), + path.clone(), + ))]; + if adapter.enabled == Active::Enabled && adapter.scanning == Active::Disabled { + fut.push(cosmic::task::future(start_discovery( + connection, + path.clone(), + ))); } - } else { - tracing::warn!("No DBus connection ready"); + + return cosmic::task::batch(fut); } } + Message::ForgetDevice(path) => { tracing::debug!("Forgetting to device {path}"); self.popup_device = None; @@ -463,6 +517,7 @@ impl Page { tracing::warn!("No DBus connection ready"); } } + Message::ConnectDevice(path) => { tracing::debug!("Connecting device {path}"); if self.connection.is_none() { @@ -481,6 +536,7 @@ impl Page { tracing::warn!("No DBus connection ready"); } } + Message::DisconnectDevice(path) => { tracing::debug!("Disconnecting device {path}"); self.popup_device = None; @@ -497,11 +553,42 @@ impl Page { tracing::warn!("No DBus connection ready"); } } - Message::Nop => {} - Message::DBusError(why) => { - tracing::error!("dbus connection failed. {why}"); + + Message::ServiceActivate => { + return cosmic::task::future(async { + systemd::activate_bluetooth().await; + tokio::time::sleep(Duration::from_secs(3)).await; + + match zbus::Connection::system().await { + Ok(connection) => Message::DBusConnect(connection), + Err(why) => Message::DBusConnectFailed(why), + } + }); + } + + Message::ServiceEnable => { + return cosmic::task::future(async { + systemd::enable_bluetooth().await; + tokio::time::sleep(Duration::from_secs(3)).await; + + match zbus::Connection::system().await { + Ok(connection) => Message::DBusConnect(connection), + Err(why) => Message::DBusConnectFailed(why), + } + }); + } + + Message::DBusConnectFailed(why) => { + tracing::error!("dbus connection failed. {why:?}"); + self.connection = None; + self.service_is_active = false; + self.service_is_enabled = false; + if let Some(abort_handle) = self.subscription.take() { + _ = abort_handle.send(()); + } } }; + cosmic::Task::none() } @@ -528,16 +615,14 @@ impl Page { } self.active = if let Some((_, adapter)) = self.get_selected_adapter() { adapter.enabled + } else if self + .adapters + .values() + .any(|adapter| matches!(adapter.enabled, Active::Enabled | Active::Enabling)) + { + Active::Enabled } else { - self.adapters - .values() - .fold(Active::Disabled, |current, adapter| { - if current == Active::Enabled || adapter.enabled == Active::Enabled { - Active::Enabled - } else { - Active::Disabled - } - }) + Active::Disabled } } fn adapter_connected(&self, adapter_path: &OwnedObjectPath) -> bool { @@ -582,15 +667,35 @@ fn status() -> Section { Section::default() .descriptions(descriptions) - .show_while::(|page| !page.adapters.is_empty()) .view::(move |_binder, page, section| { let descriptions = §ion.descriptions; + fn bluetooth_service_issue<'a>( + description: String, + label: String, + message: Message, + ) -> Element<'a, crate::pages::Message> { + widget::settings::item::builder(description) + .control(widget::button::suggested(label).on_press(message.into())) + .apply(|control| Element::from(widget::settings::section().add(control))) + } + if page.bluez_service_unknown { - return widget::text::body( - "The org.bluez DBus service could not be activated. Is bluez installed?", - ) - .apply(Element::from); + let control = widget::text::body(fl!("bluetooth", "unknown")); + + return Element::from(widget::settings::section().add(control)); + } else if !page.service_is_enabled { + return bluetooth_service_issue( + fl!("bluetooth", "disabled"), + fl!("enable"), + Message::ServiceEnable, + ); + } else if !page.service_is_active { + return bluetooth_service_issue( + fl!("bluetooth", "inactive"), + fl!("activate"), + Message::ServiceEnable, + ); } let status = page @@ -603,11 +708,13 @@ fn status() -> Section { } widget::list_column() - .add(bluetooth_toggle.control( - widget::toggler(status == Active::Enabled).on_toggle(Message::SetActive), - )) + .add( + bluetooth_toggle.control( + widget::toggler(matches!(status, Active::Enabling | Active::Enabled)) + .on_toggle(|active| Message::SetActive(active).into()), + ), + ) .apply(Element::from) - .map(crate::pages::Message::Bluetooth) }) } @@ -828,3 +935,37 @@ fn multiple_adapter() -> Section { } impl page::AutoBind for Page {} + +mod systemd { + use futures::FutureExt; + + pub fn activate_bluetooth() -> impl Future + Send { + tokio::process::Command::new("pkexec") + .args(&["systemctl", "start", "bluetooth"]) + .status() + .map(|_| ()) + } + + pub fn enable_bluetooth() -> impl Future + Send { + tokio::process::Command::new("pkexec") + .args(&["systemctl", "enable", "--now", "bluetooth"]) + .status() + .map(|_| ()) + } + + pub fn is_bluetooth_enabled() -> bool { + std::process::Command::new("systemctl") + .args(&["is-enabled", "bluetooth"]) + .status() + .map(|status| status.success()) + .unwrap_or(true) + } + + pub fn is_bluetooth_active() -> bool { + std::process::Command::new("systemctl") + .args(&["is-active", "bluetooth"]) + .status() + .map(|status| status.success()) + .unwrap_or(true) + } +} diff --git a/i18n/en/cosmic_settings.ftl b/i18n/en/cosmic_settings.ftl index b92e27a..f532ed4 100644 --- a/i18n/en/cosmic_settings.ftl +++ b/i18n/en/cosmic_settings.ftl @@ -105,7 +105,9 @@ online-accounts = Online Accounts # Bluetooth +activate = Activate confirm = Confirm +enable = Enable bluetooth = Bluetooth .desc = Manage Bluetooth devices @@ -117,6 +119,9 @@ bluetooth = Bluetooth .disconnect = Disconnect .forget = Forget .dbus-error = An error has occurred while interacting with DBus: { $why } + .disabled = The bluetooth service is disabled + .inactive = The bluetooth service is not active + .unknown = The bluetooth service could not be activated. Is bluez installed? bluetooth-paired = Previously Connected Devices .connect = Connect