chore(bluetooth): refactor to make it easier to share logic

This commit is contained in:
Michael Aaron Murphy 2025-05-01 13:38:08 +02:00
parent 6701a634b7
commit 5f6561c4ab
No known key found for this signature in database
GPG key ID: B2732D4240C9212C
2 changed files with 189 additions and 128 deletions

View file

@ -43,7 +43,7 @@ debug = true
[profile.release] [profile.release]
opt-level = "s" opt-level = "s"
lto = "thin" lto = "off"
# [patch.'https://github.com/smithay/client-toolkit/'] # [patch.'https://github.com/smithay/client-toolkit/']
# smithay-client-toolkit = { git = "https://github.com/smithay/client-toolkit//", rev = "c583de8" } # smithay-client-toolkit = { git = "https://github.com/smithay/client-toolkit//", rev = "c583de8" }

View file

@ -38,20 +38,128 @@ enum Dialog {
} }
#[derive(Default)] #[derive(Default)]
pub struct Page { pub struct Model {
active: Active, active: Active,
connection: Option<zbus::Connection>,
dialog: Option<Dialog>,
adapters: HashMap<OwnedObjectPath, Adapter>, adapters: HashMap<OwnedObjectPath, Adapter>,
selected_adapter: Option<OwnedObjectPath>, selected_adapter: Option<OwnedObjectPath>,
heading: String,
devices: HashMap<OwnedObjectPath, Device>, devices: HashMap<OwnedObjectPath, Device>,
popup_device: Option<OwnedObjectPath>,
popup_setting: bool,
}
impl Model {
pub fn clear(&mut self) {
self.adapters.clear();
self.selected_adapter = None;
self.devices.clear();
self.popup_device = None;
self.popup_setting = false;
}
/// Updates known adapters, and selects an adapter if no adapter was selected.
pub fn set_adapters(
&mut self,
adapters: HashMap<OwnedObjectPath, Adapter>,
) -> Option<OwnedObjectPath> {
self.adapters = adapters;
self.update_status();
if self.selected_adapter.is_none() && self.adapters.len() == 1 {
return self.adapters.keys().next().cloned();
}
None
}
fn update_status(&mut self) {
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 {
Active::Disabled
}
}
// Updates an adapter, and initiates discovery if an existing adapter is enabled.
fn updated_adapter(
&mut self,
path: OwnedObjectPath,
update: Vec<AdapterUpdate>,
connection: zbus::Connection,
) -> Option<impl Future<Output = Event> + Send + 'static> {
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((path, existing)) = self.get_selected_adapter_mut() {
if existing.enabled == Active::Enabled && existing.scanning == Active::Disabled {
existing.scanning = Active::Enabling;
return Some(start_discovery(connection, path));
}
}
None
}
fn adapter_connected(&self, adapter_path: &OwnedObjectPath) -> bool {
self.devices
.iter()
.any(|(path, device)| path.starts_with(adapter_path.as_str()) && device.is_connected())
}
fn get_selected_adapter(&self) -> Option<(&'_ OwnedObjectPath, &'_ Adapter)> {
if let Some(iface) = &self.selected_adapter {
self.adapters.get_key_value(iface)
} else {
None
}
}
fn devices_for_adapter<'a>(
&'a self,
adapter_path: &'a OwnedObjectPath,
) -> impl Iterator<Item = (&'a OwnedObjectPath, &'a Device)> {
self.devices.iter().filter_map(|(path, device)| {
if device.adapter.eq(adapter_path) {
Some((path, device))
} else {
None
}
})
}
fn get_selected_adapter_mut(&mut self) -> Option<(OwnedObjectPath, &'_ mut Adapter)> {
if let Some(path) = &self.selected_adapter {
self.adapters
.get_mut(path)
.map(|adapter| (path.to_owned(), adapter))
} else {
None
}
}
}
#[derive(Default)]
pub struct Page {
model: Model,
connection: Option<zbus::Connection>,
dialog: Option<Dialog>,
heading: String,
// 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_enabled: bool,
service_is_active: bool, service_is_active: bool,
popup_setting: bool,
popup_device: Option<OwnedObjectPath>,
subscription: Option<tokio::sync::oneshot::Sender<()>>, subscription: Option<tokio::sync::oneshot::Sender<()>>,
} }
@ -103,11 +211,7 @@ impl page::Page<crate::pages::Message> for Page {
}); });
} }
self.adapters.clear(); self.model.clear();
self.selected_adapter = None;
self.devices.clear();
self.popup_device = None;
self.popup_setting = false;
Task::none() Task::none()
} }
@ -230,12 +334,12 @@ impl Page {
Event::Ok => {} Event::Ok => {}
Event::SetDevices(devices) => { Event::SetDevices(devices) => {
self.devices = devices; self.model.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.model.devices.get_mut(&path) {
if matches!(device.enabled, Active::Disabled | Active::Disabling) { if matches!(device.enabled, Active::Disabled | Active::Disabling) {
return cosmic::Task::none(); return cosmic::Task::none();
} }
@ -248,13 +352,32 @@ impl Page {
} }
Event::SetAdapters(adapters) => { Event::SetAdapters(adapters) => {
self.adapters = adapters; let select_adapter = self.model.set_adapters(adapters);
self.update_status();
if self.selected_adapter.is_none() && self.adapters.len() == 1 { if let Some((_, adapter)) = self.model.get_selected_adapter() {
return cosmic::task::message(Message::SelectAdapter( self.heading = fl!(
self.adapters.keys().next().cloned(), "bluetooth",
)); "status",
aliases = format!("“{}”", adapter.alias)
);
} else {
self.heading = fl!(
"bluetooth",
"status",
aliases = self
.model
.adapters
.values()
.map(|adapter| format!("“{}”", adapter.alias))
.collect::<HashSet<String>>()
.into_iter()
.collect::<Vec<String>>()
.join(", ")
);
}
if let Some(adapter) = select_adapter {
return cosmic::task::message(Message::SelectAdapter(Some(adapter)));
} }
} }
@ -266,15 +389,10 @@ impl Page {
self.update_status(); self.update_status();
if let Some(connection) = self.connection.clone() { if let Some(connection) = self.connection.clone() {
match self.get_selected_adapter_mut() { if let Some(discovery_future) =
Some((path, existing)) self.model.updated_adapter(path, update, connection)
if existing.enabled == Active::Enabled {
&& existing.scanning == Active::Disabled => return cosmic::task::future(discovery_future);
{
existing.scanning = Active::Enabling;
return cosmic::task::future(start_discovery(connection, path));
}
_ => {}
} }
} else { } else {
tracing::warn!("No DBus connection ready"); tracing::warn!("No DBus connection ready");
@ -282,7 +400,7 @@ impl Page {
} }
Event::UpdatedDevice(path, update) => { Event::UpdatedDevice(path, update) => {
if let Some(existing) = self.devices.get_mut(&path) { if let Some(existing) = self.model.devices.get_mut(&path) {
tracing::debug!("Device {} updated", existing.address); tracing::debug!("Device {} updated", existing.address);
existing.update(update); existing.update(update);
} }
@ -290,26 +408,26 @@ impl Page {
Event::RemovedAdapter(path) => { Event::RemovedAdapter(path) => {
tracing::debug!("Device {path} removed"); tracing::debug!("Device {path} removed");
self.adapters.remove(&path); self.model.adapters.remove(&path);
if self.selected_adapter == Some(path) { if self.model.selected_adapter == Some(path) {
self.selected_adapter = None; self.model.selected_adapter = None;
} }
} }
Event::RemovedDevice(path) => { Event::RemovedDevice(path) => {
tracing::debug!("Device {path} removed"); tracing::debug!("Device {path} removed");
self.devices.remove(&path); self.model.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.model.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.model.adapters.insert(path.clone(), adapter);
if self.selected_adapter.is_none() { if self.model.selected_adapter.is_none() {
return cosmic::task::message(Message::SelectAdapter(Some(path))); return cosmic::task::message(Message::SelectAdapter(Some(path)));
} }
} }
@ -333,7 +451,7 @@ impl Page {
passkey, passkey,
response, response,
} => { } => {
let device = self.devices.get(&device).map_or_else( let device = self.model.devices.get(&device).map_or_else(
|| device.to_string(), || device.to_string(),
|device| device.alias_or_addr().to_owned(), |device| device.alias_or_addr().to_owned(),
); );
@ -376,7 +494,7 @@ impl Page {
Message::SetActive(active) => { Message::SetActive(active) => {
if let Some(connection) = self.connection.clone() { if let Some(connection) = self.connection.clone() {
if let Some((path, adapter)) = self.get_selected_adapter_mut() { if let Some((path, adapter)) = self.model.get_selected_adapter_mut() {
adapter.enabled = if active { adapter.enabled = if active {
Active::Enabling Active::Enabling
} else { } else {
@ -398,6 +516,7 @@ impl Page {
.chain(Task::done(Message::UpdateStatus.into())); .chain(Task::done(Message::UpdateStatus.into()));
} }
let tasks: Vec<Task<Message>> = self let tasks: Vec<Task<Message>> = self
.model
.adapters .adapters
.iter_mut() .iter_mut()
.map(|(path, adapter)| { .map(|(path, adapter)| {
@ -428,7 +547,7 @@ impl Page {
} }
Message::UpdateStatus => { Message::UpdateStatus => {
self.update_status(); self.model.update_status();
} }
Message::DBusConnect(connection) => { Message::DBusConnect(connection) => {
@ -477,24 +596,24 @@ impl Page {
} }
Message::PopupDevice(popup) => { Message::PopupDevice(popup) => {
self.popup_device = popup; self.model.popup_device = popup;
} }
Message::PopupSetting(popup) => { Message::PopupSetting(popup) => {
self.popup_setting = popup; self.model.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.model.selected_adapter = adapter_maybe;
self.update_status(); self.model.update_status();
let Some(connection) = self.connection.as_ref() else { let Some(connection) = self.connection.as_ref() else {
tracing::error!("No DBus connection ready"); tracing::error!("No DBus connection ready");
return Task::none(); return Task::none();
}; };
let connection = connection.clone(); let connection = connection.clone();
if let Some((path, adapter)) = self.get_selected_adapter_mut() { if let Some((path, adapter)) = self.model.get_selected_adapter_mut() {
let mut fut: Vec<Task<Message>> = vec![cosmic::task::future(get_devices( let mut fut: Vec<Task<Message>> = vec![cosmic::task::future(get_devices(
connection.clone(), connection.clone(),
path.clone(), path.clone(),
@ -512,13 +631,13 @@ impl Page {
Message::ForgetDevice(path) => { Message::ForgetDevice(path) => {
tracing::debug!("Forgetting to device {path}"); tracing::debug!("Forgetting to device {path}");
self.popup_device = None; self.model.popup_device = None;
if self.connection.is_none() { if self.connection.is_none() {
return cosmic::Task::none(); return cosmic::Task::none();
} }
if let Some(connection) = self.connection.as_ref() { if let Some(connection) = self.connection.as_ref() {
let connection = connection.clone(); let connection = connection.clone();
if let Some(device) = self.devices.get_mut(&path) { if let Some(device) = self.model.devices.get_mut(&path) {
device.enabled = Active::Disabling; device.enabled = Active::Disabling;
return cosmic::task::future(forget_device(connection, path.clone())); return cosmic::task::future(forget_device(connection, path.clone()));
} }
@ -534,7 +653,7 @@ impl Page {
} }
if let Some(connection) = self.connection.as_ref() { if let Some(connection) = self.connection.as_ref() {
let connection = connection.clone(); let connection = connection.clone();
if let Some(device) = self.devices.get_mut(&path) { if let Some(device) = self.model.devices.get_mut(&path) {
if matches!(device.enabled, Active::Enabled | Active::Enabling) { if matches!(device.enabled, Active::Enabled | Active::Enabling) {
return cosmic::Task::none(); return cosmic::Task::none();
} }
@ -548,10 +667,10 @@ impl Page {
Message::DisconnectDevice(path) => { Message::DisconnectDevice(path) => {
tracing::debug!("Disconnecting device {path}"); tracing::debug!("Disconnecting device {path}");
self.popup_device = None; self.model.popup_device = None;
if let Some(connection) = self.connection.as_ref() { if let Some(connection) = self.connection.as_ref() {
let connection = connection.clone(); let connection = connection.clone();
if let Some(device) = self.devices.get_mut(&path) { if let Some(device) = self.model.devices.get_mut(&path) {
if matches!(device.enabled, Active::Disabled | Active::Disabling) { if matches!(device.enabled, Active::Disabled | Active::Disabling) {
return cosmic::Task::none(); return cosmic::Task::none();
} }
@ -600,73 +719,6 @@ impl Page {
cosmic::Task::none() cosmic::Task::none()
} }
fn update_status(&mut self) {
if let Some((_, adapter)) = self.get_selected_adapter() {
self.heading = fl!(
"bluetooth",
"status",
aliases = format!("“{}”", adapter.alias)
);
} else {
self.heading = fl!(
"bluetooth",
"status",
aliases = self
.adapters
.values()
.map(|adapter| format!("“{}”", adapter.alias))
.collect::<HashSet<String>>()
.into_iter()
.collect::<Vec<String>>()
.join(", ")
);
}
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 {
Active::Disabled
}
}
fn adapter_connected(&self, adapter_path: &OwnedObjectPath) -> bool {
self.devices
.iter()
.any(|(path, device)| path.starts_with(adapter_path.as_str()) && device.is_connected())
}
fn get_selected_adapter(&self) -> Option<(&'_ OwnedObjectPath, &'_ Adapter)> {
if let Some(iface) = &self.selected_adapter {
self.adapters.get_key_value(iface)
} else {
None
}
}
fn devices_for_adapter<'a>(
&'a self,
adapter_path: &'a OwnedObjectPath,
) -> impl Iterator<Item = (&'a OwnedObjectPath, &'a Device)> {
self.devices.iter().filter_map(|(path, device)| {
if device.adapter.eq(adapter_path) {
Some((path, device))
} else {
None
}
})
}
fn get_selected_adapter_mut(&mut self) -> Option<(OwnedObjectPath, &'_ mut Adapter)> {
if let Some(path) = &self.selected_adapter {
self.adapters
.get_mut(path)
.map(|adapter| (path.to_owned(), adapter))
} else {
None
}
}
} }
fn status() -> Section<crate::pages::Message> { fn status() -> Section<crate::pages::Message> {
@ -708,8 +760,9 @@ fn status() -> Section<crate::pages::Message> {
} }
let status = page let status = page
.model
.get_selected_adapter() .get_selected_adapter()
.map_or(page.active, |(_, adapter)| adapter.enabled); .map_or(page.model.active, |(_, adapter)| adapter.enabled);
let mut bluetooth_toggle = settings::item::builder(&descriptions[bluetooth]); let mut bluetooth_toggle = settings::item::builder(&descriptions[bluetooth]);
if matches!(status, Active::Enabling | Active::Enabled) { if matches!(status, Active::Enabling | Active::Enabled) {
@ -754,23 +807,26 @@ fn connected_devices() -> Section<crate::pages::Message> {
.title(fl!("bluetooth-paired")) .title(fl!("bluetooth-paired"))
.descriptions(descriptions) .descriptions(descriptions)
.show_while::<Page>(|page| { .show_while::<Page>(|page| {
page.selected_adapter.as_ref().map(|adapter| { page.model.selected_adapter.as_ref().map(|adapter| {
page.devices_for_adapter(adapter) page.model
.devices_for_adapter(adapter)
.any(|(_, device)| device.paired) .any(|(_, device)| device.paired)
}) == Some(true) }) == Some(true)
&& page.active != Active::Disabled && page.model.active != Active::Disabled
}) })
.view::<Page>(move |_binder, page, section| { .view::<Page>(move |_binder, page, section| {
let descriptions = &section.descriptions; let descriptions = &section.descriptions;
let section = settings::section().title(&section.title); let section = settings::section().title(&section.title);
page.devices_for_adapter(page.selected_adapter.as_ref().unwrap()) page.model
.devices_for_adapter(page.model.selected_adapter.as_ref().unwrap())
.filter_map(|(path, device)| { .filter_map(|(path, device)| {
if !device.paired { if !device.paired {
return None; return None;
} }
let device_menu: Element<_> = if page let device_menu: Element<_> = if page
.model
.popup_device .popup_device
.as_deref() .as_deref()
.map_or(false, |p| path.as_str() == p.as_str()) .map_or(false, |p| path.as_str() == p.as_str())
@ -854,17 +910,19 @@ fn available_devices() -> Section<crate::pages::Message> {
.title(fl!("bluetooth-available")) .title(fl!("bluetooth-available"))
.descriptions(descriptions) .descriptions(descriptions)
.show_while::<Page>(|page| { .show_while::<Page>(|page| {
page.selected_adapter.as_ref().map(|adapter| { page.model.selected_adapter.as_ref().map(|adapter| {
page.devices_for_adapter(adapter) page.model
.devices_for_adapter(adapter)
.any(|(_, device)| !device.paired) .any(|(_, device)| !device.paired)
}) == Some(true) }) == Some(true)
&& page.active != Active::Disabled && page.model.active != Active::Disabled
}) })
.view::<Page>(move |_binder, page, section| { .view::<Page>(move |_binder, page, section| {
let descriptions = &section.descriptions; let descriptions = &section.descriptions;
let section = settings::section().title(&section.title); let section = settings::section().title(&section.title);
page.devices_for_adapter(page.selected_adapter.as_ref().unwrap()) page.model
.devices_for_adapter(page.model.selected_adapter.as_ref().unwrap())
.filter_map(|(path, device)| { .filter_map(|(path, device)| {
if device.paired { if device.paired {
return None::<Element<'_, Message>>; return None::<Element<'_, Message>>;
@ -907,12 +965,15 @@ fn multiple_adapter() -> Section<crate::pages::Message> {
Section::default() Section::default()
.title(fl!("bluetooth-adapters")) .title(fl!("bluetooth-adapters"))
.descriptions(descriptions) .descriptions(descriptions)
.show_while::<Page>(|page| page.adapters.len() > 1 && page.selected_adapter.is_none()) .show_while::<Page>(|page| {
page.model.adapters.len() > 1 && page.model.selected_adapter.is_none()
})
.view::<Page>(move |_binder, page, section| { .view::<Page>(move |_binder, page, section| {
let descriptions = &section.descriptions; let descriptions = &section.descriptions;
let section = settings::section().title(&section.title); let section = settings::section().title(&section.title);
page.adapters page.model
.adapters
.iter() .iter()
.map(|(path, adapter)| { .map(|(path, adapter)| {
let mut items = vec![ let mut items = vec![