fix(bluetooth): resolve issues with bluetooth settings page

This commit is contained in:
Michael Aaron Murphy 2025-04-23 16:54:18 +02:00 • committed by Michael Murphy
parent db54245aa7
commit f16244b400
3 changed files with 194 additions and 48 deletions

2
Cargo.lock generated
View file

@ -1784,7 +1784,7 @@ dependencies = [
[[package]] [[package]]
name = "cosmic-settings-subscriptions" name = "cosmic-settings-subscriptions"
version = "0.1.0" 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 = [ dependencies = [
"bluez-zbus", "bluez-zbus",
"cosmic-dbus-networkmanager", "cosmic-dbus-networkmanager",

View file

@ -13,6 +13,7 @@ use slab::Slab;
use slotmap::SlotMap; use slotmap::SlotMap;
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration;
use zbus::zvariant::OwnedObjectPath; use zbus::zvariant::OwnedObjectPath;
enum Dialog { enum Dialog {
@ -46,6 +47,8 @@ pub struct Page {
devices: HashMap<OwnedObjectPath, Device>, devices: HashMap<OwnedObjectPath, Device>,
// Set to true when the org.bluez dbus service is unknown. // Set to true when the org.bluez dbus service is unknown.
bluez_service_unknown: bool, bluez_service_unknown: bool,
service_is_enabled: bool,
service_is_active: bool,
popup_setting: bool, popup_setting: bool,
popup_device: Option<OwnedObjectPath>, popup_device: Option<OwnedObjectPath>,
subscription: Option<tokio::sync::oneshot::Sender<()>>, subscription: Option<tokio::sync::oneshot::Sender<()>>,
@ -75,9 +78,10 @@ impl page::Page<crate::pages::Message> for Page {
cosmic::task::future(async move { cosmic::task::future(async move {
match zbus::Connection::system().await { match zbus::Connection::system().await {
Ok(connection) => Message::DBusConnect(connection), 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<crate::pages::Message> { fn on_leave(&mut self) -> Task<crate::pages::Message> {
@ -147,16 +151,18 @@ pub enum Message {
BluetoothEvent(Event), BluetoothEvent(Event),
ConnectDevice(OwnedObjectPath), ConnectDevice(OwnedObjectPath),
DBusConnect(zbus::Connection), DBusConnect(zbus::Connection),
DBusError(String), DBusConnectFailed(zbus::Error),
DisconnectDevice(OwnedObjectPath), DisconnectDevice(OwnedObjectPath),
ForgetDevice(OwnedObjectPath), ForgetDevice(OwnedObjectPath),
PinCancel, PinCancel,
PinConfirm, PinConfirm,
PopupDevice(Option<OwnedObjectPath>), PopupDevice(Option<OwnedObjectPath>),
PopupSetting(bool), PopupSetting(bool),
Nop,
SelectAdapter(Option<OwnedObjectPath>), SelectAdapter(Option<OwnedObjectPath>),
ServiceActivate,
ServiceEnable,
SetActive(bool), SetActive(bool),
UpdateStatus,
} }
impl From<Message> for crate::app::Message { impl From<Message> for crate::app::Message {
@ -197,15 +203,28 @@ impl Page {
match message { match message {
Message::BluetoothEvent(event) => match event { Message::BluetoothEvent(event) => match event {
Event::DBusError(why) => { Event::DBusError(why) => {
tracing::debug!("bluetooth dbus error {why:?}");
tracing::error!( tracing::error!(
"dbus connection failed. {}", "bluetooth service error {}",
fl!("bluetooth", "dbus-error", why = why.to_string()) 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::Ok => {}
Event::SetDevices(devices) => { Event::SetDevices(devices) => {
self.devices = devices; self.devices = devices;
} }
Event::DeviceFailed(path) => { Event::DeviceFailed(path) => {
tracing::warn!("Failed operation on device {path}"); tracing::warn!("Failed operation on device {path}");
if let Some(device) = self.devices.get_mut(&path) { if let Some(device) = self.devices.get_mut(&path) {
@ -219,6 +238,7 @@ impl Page {
}; };
} }
} }
Event::SetAdapters(adapters) => { Event::SetAdapters(adapters) => {
self.adapters = adapters; self.adapters = adapters;
self.update_status(); self.update_status();
@ -229,6 +249,7 @@ impl Page {
)); ));
} }
} }
Event::UpdatedAdapter(path, update) => { Event::UpdatedAdapter(path, update) => {
if let Some(existing) = self.adapters.get_mut(&path) { if let Some(existing) = self.adapters.get_mut(&path) {
tracing::debug!("Adapter {} updated: {update:#?}", existing.address); tracing::debug!("Adapter {} updated: {update:#?}", existing.address);
@ -250,12 +271,14 @@ impl Page {
tracing::warn!("No DBus connection ready"); tracing::warn!("No DBus connection ready");
} }
} }
Event::UpdatedDevice(path, update) => { Event::UpdatedDevice(path, update) => {
if let Some(existing) = self.devices.get_mut(&path) { if let Some(existing) = self.devices.get_mut(&path) {
tracing::debug!("Device {} updated", existing.address); tracing::debug!("Device {} updated", existing.address);
existing.update(update); existing.update(update);
} }
} }
Event::RemovedAdapter(path) => { Event::RemovedAdapter(path) => {
tracing::debug!("Device {path} removed"); tracing::debug!("Device {path} removed");
self.adapters.remove(&path); self.adapters.remove(&path);
@ -263,14 +286,17 @@ impl Page {
self.selected_adapter = None; self.selected_adapter = None;
} }
} }
Event::RemovedDevice(path) => { Event::RemovedDevice(path) => {
tracing::debug!("Device {path} removed"); tracing::debug!("Device {path} removed");
self.devices.remove(&path); self.devices.remove(&path);
} }
Event::AddedDevice(path, device) => { Event::AddedDevice(path, device) => {
tracing::debug!("Device {} added", device.address); tracing::debug!("Device {} added", device.address);
self.devices.insert(path, device); self.devices.insert(path, device);
} }
Event::AddedAdapter(path, adapter) => { Event::AddedAdapter(path, adapter) => {
tracing::debug!("Adapter {} added", adapter.address); tracing::debug!("Adapter {} added", adapter.address);
self.adapters.insert(path.clone(), adapter); self.adapters.insert(path.clone(), adapter);
@ -278,9 +304,11 @@ impl Page {
return cosmic::task::message(Message::SelectAdapter(Some(path))); return cosmic::task::message(Message::SelectAdapter(Some(path)));
} }
} }
Event::DBusServiceUnknown => { Event::DBusServiceUnknown => {
self.bluez_service_unknown = true; self.bluez_service_unknown = true;
} }
Event::Agent(message) => { Event::Agent(message) => {
let Some(message) = Arc::into_inner(message) else { let Some(message) = Arc::into_inner(message) else {
return Task::none(); return Task::none();
@ -324,6 +352,7 @@ impl Page {
} }
} }
}, },
Message::PinCancel => { Message::PinCancel => {
if let Some(Dialog::RequestConfirmation { response, .. }) = self.dialog.take() { if let Some(Dialog::RequestConfirmation { response, .. }) = self.dialog.take() {
_ = response.send(false); _ = response.send(false);
@ -344,12 +373,20 @@ impl Page {
} else { } else {
Active::Disabling Active::Disabling
}; };
self.update_status();
return cosmic::task::future(change_adapter_status( return cosmic::task::future(change_adapter_status(
connection.clone(), connection.clone(),
path, path,
active, active,
)); ))
.then(|event| {
if matches!(event, Event::Ok) {
Task::none()
} else {
Task::done(event.into())
}
})
.chain(Task::done(Message::UpdateStatus.into()));
} }
let tasks: Vec<Task<Message>> = self let tasks: Vec<Task<Message>> = self
.adapters .adapters
@ -365,15 +402,29 @@ impl Page {
path.clone(), path.clone(),
active, active,
)) ))
.then(|event| {
if matches!(event, Event::Ok) {
Task::none()
} else {
Task::done(event.into())
}
})
}) })
.collect(); .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"); tracing::warn!("No DBus connection ready");
} }
Message::UpdateStatus => {
self.update_status();
}
Message::DBusConnect(connection) => { Message::DBusConnect(connection) => {
self.service_is_active = systemd::is_bluetooth_active();
self.service_is_enabled = systemd::is_bluetooth_enabled();
self.connection = Some(connection.clone()); self.connection = Some(connection.clone());
let get_adapters_fut = get_adapters(connection.clone()); let get_adapters_fut = get_adapters(connection.clone());
@ -415,38 +466,41 @@ impl Page {
return cosmic::task::future(get_adapters_fut); return cosmic::task::future(get_adapters_fut);
} }
} }
Message::PopupDevice(popup) => { Message::PopupDevice(popup) => {
self.popup_device = popup; self.popup_device = popup;
} }
Message::PopupSetting(popup) => { Message::PopupSetting(popup) => {
self.popup_setting = popup; self.popup_setting = popup;
} }
Message::SelectAdapter(adapter_maybe) => { Message::SelectAdapter(adapter_maybe) => {
tracing::debug!("Adapter selected: {adapter_maybe:?}"); tracing::debug!("Adapter selected: {adapter_maybe:?}");
self.selected_adapter = adapter_maybe; self.selected_adapter = adapter_maybe;
self.update_status(); self.update_status();
if let Some(connection) = self.connection.as_ref() { let Some(connection) = self.connection.as_ref() else {
let connection = connection.clone(); tracing::error!("No DBus connection ready");
if let Some((path, adapter)) = self.get_selected_adapter_mut() { return Task::none();
let mut fut: Vec<Task<Message>> = 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(),
)));
}
return cosmic::task::batch(fut); let connection = connection.clone();
if let Some((path, adapter)) = self.get_selected_adapter_mut() {
let mut fut: Vec<Task<Message>> = 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) => { Message::ForgetDevice(path) => {
tracing::debug!("Forgetting to device {path}"); tracing::debug!("Forgetting to device {path}");
self.popup_device = None; self.popup_device = None;
@ -463,6 +517,7 @@ impl Page {
tracing::warn!("No DBus connection ready"); tracing::warn!("No DBus connection ready");
} }
} }
Message::ConnectDevice(path) => { Message::ConnectDevice(path) => {
tracing::debug!("Connecting device {path}"); tracing::debug!("Connecting device {path}");
if self.connection.is_none() { if self.connection.is_none() {
@ -481,6 +536,7 @@ impl Page {
tracing::warn!("No DBus connection ready"); tracing::warn!("No DBus connection ready");
} }
} }
Message::DisconnectDevice(path) => { Message::DisconnectDevice(path) => {
tracing::debug!("Disconnecting device {path}"); tracing::debug!("Disconnecting device {path}");
self.popup_device = None; self.popup_device = None;
@ -497,11 +553,42 @@ impl Page {
tracing::warn!("No DBus connection ready"); tracing::warn!("No DBus connection ready");
} }
} }
Message::Nop => {}
Message::DBusError(why) => { Message::ServiceActivate => {
tracing::error!("dbus connection failed. {why}"); 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() cosmic::Task::none()
} }
@ -528,16 +615,14 @@ impl Page {
} }
self.active = if let Some((_, adapter)) = self.get_selected_adapter() { self.active = if let Some((_, adapter)) = self.get_selected_adapter() {
adapter.enabled adapter.enabled
} else if self
.adapters
.values()
.any(|adapter| matches!(adapter.enabled, Active::Enabled | Active::Enabling))
{
Active::Enabled
} else { } else {
self.adapters Active::Disabled
.values()
.fold(Active::Disabled, |current, adapter| {
if current == Active::Enabled || adapter.enabled == Active::Enabled {
Active::Enabled
} else {
Active::Disabled
}
})
} }
} }
fn adapter_connected(&self, adapter_path: &OwnedObjectPath) -> bool { fn adapter_connected(&self, adapter_path: &OwnedObjectPath) -> bool {
@ -582,15 +667,35 @@ fn status() -> Section<crate::pages::Message> {
Section::default() Section::default()
.descriptions(descriptions) .descriptions(descriptions)
.show_while::<Page>(|page| !page.adapters.is_empty())
.view::<Page>(move |_binder, page, section| { .view::<Page>(move |_binder, page, section| {
let descriptions = &section.descriptions; let descriptions = &section.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 { if page.bluez_service_unknown {
return widget::text::body( let control = widget::text::body(fl!("bluetooth", "unknown"));
"The org.bluez DBus service could not be activated. Is bluez installed?",
) return Element::from(widget::settings::section().add(control));
.apply(Element::from); } 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 let status = page
@ -603,11 +708,13 @@ fn status() -> Section<crate::pages::Message> {
} }
widget::list_column() widget::list_column()
.add(bluetooth_toggle.control( .add(
widget::toggler(status == Active::Enabled).on_toggle(Message::SetActive), bluetooth_toggle.control(
)) widget::toggler(matches!(status, Active::Enabling | Active::Enabled))
.on_toggle(|active| Message::SetActive(active).into()),
),
)
.apply(Element::from) .apply(Element::from)
.map(crate::pages::Message::Bluetooth)
}) })
} }
@ -828,3 +935,37 @@ fn multiple_adapter() -> Section<crate::pages::Message> {
} }
impl page::AutoBind<crate::pages::Message> for Page {} impl page::AutoBind<crate::pages::Message> for Page {}
mod systemd {
use futures::FutureExt;
pub fn activate_bluetooth() -> impl Future<Output = ()> + Send {
tokio::process::Command::new("pkexec")
.args(&["systemctl", "start", "bluetooth"])
.status()
.map(|_| ())
}
pub fn enable_bluetooth() -> impl Future<Output = ()> + 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)
}
}

View file

@ -105,7 +105,9 @@ online-accounts = Online Accounts
# Bluetooth # Bluetooth
activate = Activate
confirm = Confirm confirm = Confirm
enable = Enable
bluetooth = Bluetooth bluetooth = Bluetooth
.desc = Manage Bluetooth devices .desc = Manage Bluetooth devices
@ -117,6 +119,9 @@ bluetooth = Bluetooth
.disconnect = Disconnect .disconnect = Disconnect
.forget = Forget .forget = Forget
.dbus-error = An error has occurred while interacting with DBus: { $why } .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 bluetooth-paired = Previously Connected Devices
.connect = Connect .connect = Connect