feat(bluetooth): PIN confirmation support

This commit is contained in:
Michael Aaron Murphy 2024-10-02 10:17:02 +02:00
parent a742d3829c
commit 894cf9fc3f
No known key found for this signature in database
GPG key ID: B2732D4240C9212C
6 changed files with 503 additions and 271 deletions

View file

@ -0,0 +1,64 @@
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<super::Message>,
) -> 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(())
}
pub enum Message {}

View file

@ -60,7 +60,10 @@ impl DeviceUpdate {
))))
}
// Battery
_ => None,
(message, value) => {
tracing::debug!(message, ?value, "device update");
None
}
}
})
.collect()
@ -108,7 +111,10 @@ impl AdapterUpdate {
Some(Self::Address(value.into()))
}
// Battery
_ => None,
(message, value) => {
tracing::error!(message, ?value, "adapter update");
None
}
}
})
.collect()
@ -269,18 +275,26 @@ impl Adapter {
pub async fn from_device(
proxy: &bluez_zbus::adapter1::Adapter1Proxy<'_>,
) -> zbus::Result<Self> {
let address = proxy.address().await?;
let alias = proxy.alias().await?;
let scanning = if proxy.discoverable().await? && proxy.discovering().await? {
Active::Enabled
} else {
Active::Disabled
};
let enabled = if proxy.powered().await? {
Active::Enabled
} else {
Active::Disabled
};
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,
@ -320,38 +334,42 @@ pub async fn start_discovery(
adapter_path: OwnedObjectPath,
) -> Message {
let result: zbus::Result<()> = Ok(());
match bluez_zbus::get_adapter(&connection, adapter_path).await {
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) => {
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;
}
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 {
return Message::DBusError(why.to_string());
}
Message::Nop
return if let Err(why) = result {
Message::DBusError(why.to_string())
} else {
Message::Nop
};
}
pub async fn stop_discovery(
@ -359,36 +377,36 @@ pub async fn stop_discovery(
adapter_path: OwnedObjectPath,
) -> Message {
let result: zbus::Result<()> = Ok(());
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) => {
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;
}
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());
}
@ -399,103 +417,115 @@ pub async fn disconnect_device(
connection: zbus::Connection,
device_path: OwnedObjectPath,
) -> Message {
match bluez_zbus::get_device(&connection, device_path.clone()).await {
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) => {
for attempt in 1..5 {
let result = async {
if !proxy.device.connected().await? {
return Ok(());
}
Ok(proxy) => proxy,
};
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;
}
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 {
match bluez_zbus::get_device(&connection, device_path.clone()).await {
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) => {
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;
}
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(());
match bluez_zbus::get_device(&connection, device_path.clone()).await {
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) => match proxy.device.adapter().await {
Err(why) => {
tracing::error!("Unable to get the adapter: {why}");
return Message::DeviceFailed(device_path);
}
Ok(adapter) => match bluez_zbus::get_adapter(&connection, adapter).await {
Err(why) => {
tracing::error!("Unable to get the adapter: {why}");
return Message::DeviceFailed(device_path);
}
Ok(adapter) => {
for attempt in 1..5 {
result = async {
if proxy.device.connected().await? {
proxy.device.disconnect().await?;
}
Ok(proxy) => proxy,
};
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;
}
}
}
},
},
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() {
return Message::DeviceFailed(device_path);
}
Message::Nop
return if result.is_err() {
Message::DeviceFailed(device_path)
} else {
Message::Nop
};
}
pub async fn change_adapter_status(

View file

@ -8,19 +8,43 @@ use cosmic::widget::{self, settings, text};
use cosmic::Command;
use cosmic::{Apply, Element};
use cosmic_settings_page::{self as page, section, Section};
use futures::channel::oneshot;
use slab::Slab;
use slotmap::SlotMap;
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,
// response: oneshot::Sender<bool>,
// },
RequestConfirmation {
device: String,
passkey: u32,
response: oneshot::Sender<bool>,
},
// RequestPasskey {
// device: OwnedObjectPath,
// response: oneshot::Sender<Option<u32>>,
// },
// RequestPinCode {
// device: OwnedObjectPath,
// response: oneshot::Sender<Option<String>>,
// },
}
#[derive(Default)]
pub struct Page {
active: Active,
connection: Option<zbus::Connection>,
dialog: Option<Dialog>,
adapters: HashMap<OwnedObjectPath, Adapter>,
selected_adapter: Option<OwnedObjectPath>,
heading: String,
@ -69,7 +93,12 @@ impl page::Page<crate::pages::Message> for Page {
_ = cancel.send(());
}
self.connection = None;
if let Some(connection) = self.connection.take() {
tokio::spawn(async move {
_ = agent::unregister(connection).await;
});
}
self.adapters.clear();
self.selected_adapter = None;
self.devices.clear();
@ -79,12 +108,55 @@ impl page::Page<crate::pages::Message> for Page {
Command::none()
}
fn dialog(&self) -> Option<Element<'_, crate::pages::Message>> {
match self.dialog.as_ref()? {
Dialog::RequestConfirmation {
device, passkey, ..
} => {
let description = widget::text::body(fl!(
"bluetooth-confirm-pin",
"description",
device = device
))
.wrap(Wrap::Word);
let pin = widget::text::title1(itoa::Buffer::new().format(*passkey).to_owned())
.width(Length::Fill)
.horizontal_alignment(alignment::Horizontal::Center)
.wrap(Wrap::None);
let control = widget::column::with_capacity(2)
.push(description)
.push(pin)
.spacing(cosmic::theme::active().cosmic().spacing.space_xxs);
let confirm_button =
widget::button::suggested(fl!("confirm")).on_press(Message::PinConfirm);
let cancel_button =
widget::button::standard(fl!("cancel")).on_press(Message::PinCancel);
let dialog = widget::dialog(fl!("bluetooth-confirm-pin"))
.control(control)
.primary_action(confirm_button)
.secondary_action(cancel_button)
.apply(Element::from)
.map(Into::into);
Some(dialog)
}
_ => None,
}
}
}
#[derive(Clone, Debug)]
pub enum Message {
AddedAdapter(OwnedObjectPath, Adapter),
AddedDevice(OwnedObjectPath, Device),
Agent(Arc<bluez_zbus::agent1::Message>),
ConnectDevice(OwnedObjectPath),
DBusConnect(
zbus::Connection,
@ -94,6 +166,8 @@ pub enum Message {
DeviceFailed(OwnedObjectPath),
DisconnectDevice(OwnedObjectPath),
ForgetDevice(OwnedObjectPath),
PinCancel,
PinConfirm,
PopupDevice(Option<OwnedObjectPath>),
PopupSetting(bool),
Nop,
@ -126,6 +200,61 @@ impl Page {
let _span = span.enter();
match message {
Message::Agent(message) => {
let Some(message) = Arc::into_inner(message) else {
return Command::none();
};
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(),
);
self.dialog = Some(Dialog::RequestConfirmation {
device,
passkey,
response,
});
}
bluez_zbus::agent1::Message::RequestPasskey { response, .. } => {
_ = response.send(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);
}
}
Message::PinConfirm => {
if let Some(Dialog::RequestConfirmation { response, .. }) = self.dialog.take() {
_ = response.send(true);
}
}
Message::SetActive(active) => {
if let Some(connection) = self.connection.clone() {
if let Some((path, adapter)) = self.get_selected_adapter_mut() {
@ -162,6 +291,7 @@ impl Page {
}
tracing::warn!("No DBus connection ready");
}
Message::DBusConnect(connection, sender) => {
self.connection = Some(connection.clone());
@ -170,7 +300,12 @@ impl Page {
self.subscription = Some(crate::utils::forward_event_loop(
sender,
crate::pages::Message::Bluetooth,
move |tx| async move { subscription::watch(connection, tx).await },
move |tx| async move {
_ = futures::join!(
subscription::watch(connection.clone(), tx.clone()),
agent::watch(connection, tx),
);
},
));
}

View file

@ -76,7 +76,7 @@ pub async fn watch(
connection: zbus::Connection,
mut tx: futures::channel::mpsc::Sender<bluetooth::Message>,
) {
let span = tracing::span!(tracing::Level::INFO, "bluetooth::watch");
let span = tracing::span!(tracing::Level::INFO, "bluetooth::subscription::watch");
let _span = span.enter();
loop {