use std::collections::HashSet;
use std::time::Duration;
use futures::{FutureExt, StreamExt, future};
use futures_timer::Delay;
use log::warn;
use std::pin::pin;
use zbus::Connection;
use crate::api::models::{AirplaneModeState, RadioState};
use crate::core::rfkill::read_rfkill;
use crate::dbus::{BluezAdapterProxy, NMDeviceProxy, NMProxy};
use crate::types::constants::device_type;
use crate::{ConnectionError, Result};
const BLUEZ_POWER_SETTLE_TIMEOUT: Duration = Duration::from_secs(2);
pub(crate) async fn wifi_state(
conn: &Connection,
present_device_types: Option<&HashSet<u32>>,
) -> Result<RadioState> {
let nm = NMProxy::new(conn).await?;
let enabled = nm.wireless_enabled().await?;
let nm_hw = nm.wireless_hardware_enabled().await?;
let rfkill = read_rfkill();
let hardware_enabled = reconcile_hardware(nm_hw, rfkill.wlan_hard_block, "wifi");
let present = match present_device_types {
Some(types) => types.contains(&device_type::WIFI),
None => true, };
Ok(RadioState::with_presence(
enabled,
hardware_enabled,
present,
))
}
pub(crate) async fn wwan_state(
conn: &Connection,
present_device_types: Option<&HashSet<u32>>,
) -> Result<RadioState> {
let nm = NMProxy::new(conn).await?;
let enabled = nm.wwan_enabled().await?;
let nm_hw = nm.wwan_hardware_enabled().await?;
let rfkill = read_rfkill();
let hardware_enabled = reconcile_hardware(nm_hw, rfkill.wwan_hard_block, "wwan");
let present = match present_device_types {
Some(types) => types.contains(&device_type::MODEM),
None => true, };
Ok(RadioState::with_presence(
enabled,
hardware_enabled,
present,
))
}
pub(crate) async fn bluetooth_radio_state(conn: &Connection) -> Result<RadioState> {
let adapter_paths = match enumerate_bluetooth_adapters(conn).await {
Ok(paths) if !paths.is_empty() => paths,
Ok(_) | Err(_) => {
return Ok(RadioState::with_presence(false, false, false));
}
};
let mut any_powered = false;
for path in &adapter_paths {
match BluezAdapterProxy::builder(conn)
.path(path.as_str())?
.build()
.await
{
Ok(proxy) => {
if proxy.powered().await.unwrap_or(false) {
any_powered = true;
break;
}
}
Err(e) => {
warn!("failed to query BlueZ adapter {}: {}", path, e);
}
}
}
let rfkill = read_rfkill();
let hardware_enabled = !rfkill.bluetooth_hard_block;
Ok(RadioState::with_presence(
any_powered,
hardware_enabled,
true,
))
}
pub(crate) async fn airplane_mode_state(conn: &Connection) -> Result<AirplaneModeState> {
let present_types = fetch_present_device_types(conn).await;
let (wifi, wwan, bt) = futures::future::join3(
wifi_state(conn, present_types.as_ref()),
wwan_state(conn, present_types.as_ref()),
bluetooth_radio_state(conn),
)
.await;
Ok(AirplaneModeState::new(wifi?, wwan?, bt?))
}
pub(crate) async fn set_wireless_enabled(conn: &Connection, enabled: bool) -> Result<()> {
let nm = NMProxy::new(conn).await?;
Ok(nm.set_wireless_enabled(enabled).await?)
}
pub(crate) async fn set_wwan_enabled(conn: &Connection, enabled: bool) -> Result<()> {
let nm = NMProxy::new(conn).await?;
Ok(nm.set_wwan_enabled(enabled).await?)
}
pub(crate) async fn set_bluetooth_radio_enabled(conn: &Connection, enabled: bool) -> Result<()> {
let adapter_paths = enumerate_bluetooth_adapters(conn).await.map_err(|e| {
ConnectionError::BluezUnavailable(format!("failed to enumerate adapters: {e}"))
})?;
if adapter_paths.is_empty() {
return Err(ConnectionError::BluezUnavailable(
"no Bluetooth adapters found".to_string(),
));
}
let n_adapters = adapter_paths.len();
let toggle_futures = adapter_paths.iter().map(|path| async move {
let proxy = match BluezAdapterProxy::builder(conn).path(path.as_str()) {
Ok(builder) => match builder.build().await {
Ok(proxy) => proxy,
Err(e) => {
warn!("failed to build proxy for adapter {}: {}", path, e);
return None;
}
},
Err(e) => {
warn!("invalid adapter path {}: {}", path, e);
return None;
}
};
if let Err(e) = proxy.set_powered(enabled).await {
warn!("failed to set Powered on {}: {}", path, e);
return None;
}
Some(proxy)
});
let results: Vec<_> = futures::future::join_all(toggle_futures).await;
let n_ok = results.iter().filter(|r| r.is_some()).count();
if n_ok != n_adapters {
return Err(ConnectionError::BluetoothToggleFailed(format!(
"failed to toggle {} of {} Bluetooth adapter(s)",
n_adapters.saturating_sub(n_ok),
n_adapters
)));
}
let successful_proxies: Vec<_> = results.into_iter().flatten().collect();
let wait_futures = successful_proxies
.iter()
.map(|proxy| wait_for_powered_no_timeout(proxy, enabled));
let all_waits = futures::future::join_all(wait_futures);
let timer = Delay::new(BLUEZ_POWER_SETTLE_TIMEOUT);
let all_waits = pin!(all_waits.fuse());
let timer = pin!(timer.fuse());
let _ = future::select(all_waits, timer).await;
for proxy in &successful_proxies {
match proxy.powered().await {
Ok(v) if v == enabled => {}
Ok(_) => {
return Err(ConnectionError::BluetoothToggleFailed(
"Bluetooth adapter Powered did not reach requested state in time".to_string(),
));
}
Err(e) => {
return Err(ConnectionError::BluetoothToggleFailed(format!(
"could not read Powered after toggle: {e}"
)));
}
}
}
Ok(())
}
pub(crate) async fn set_airplane_mode(conn: &Connection, enabled: bool) -> Result<()> {
let radio_on = !enabled;
let (wifi_res, wwan_res, bt_res) = futures::future::join3(
set_wireless_enabled(conn, radio_on),
set_wwan_enabled(conn, radio_on),
set_bluetooth_radio_enabled(conn, radio_on),
)
.await;
wifi_res?;
wwan_res?;
match bt_res {
Ok(()) => {}
Err(ConnectionError::BluezUnavailable(message)) => {
warn!(
"Ignoring Bluetooth airplane-mode toggle because BlueZ is unavailable: {}",
message
);
}
Err(e) => return Err(e),
}
Ok(())
}
async fn enumerate_bluetooth_adapters(conn: &Connection) -> Result<Vec<String>> {
let manager = zbus::fdo::ObjectManagerProxy::builder(conn)
.destination("org.bluez")?
.path("/")?
.build()
.await
.map_err(|e| {
ConnectionError::BluezUnavailable(format!("failed to connect to BlueZ: {e}"))
})?;
let objects = manager.get_managed_objects().await.map_err(|e| {
ConnectionError::BluezUnavailable(format!("failed to enumerate BlueZ objects: {e}"))
})?;
let adapters: Vec<String> = objects
.into_iter()
.filter(|(_, ifaces)| ifaces.contains_key("org.bluez.Adapter1"))
.map(|(path, _)| path.to_string())
.collect();
Ok(adapters)
}
fn reconcile_hardware(nm_hardware_enabled: bool, rfkill_hard_block: bool, radio: &str) -> bool {
if nm_hardware_enabled && rfkill_hard_block {
warn!(
"{radio}: NM reports hardware enabled but rfkill reports hard block — trusting rfkill"
);
return false;
}
nm_hardware_enabled && !rfkill_hard_block
}
pub(crate) async fn fetch_present_device_types(conn: &Connection) -> Option<HashSet<u32>> {
let nm = NMProxy::new(conn).await.ok()?;
let paths = nm.get_devices().await.ok()?;
let mut types = HashSet::new();
for p in paths {
let builder = NMDeviceProxy::builder(conn).path(p).ok()?;
let dev = builder.build().await.ok()?;
let t = dev.device_type().await.ok()?;
types.insert(t);
}
Some(types)
}
async fn wait_for_powered_no_timeout(proxy: &BluezAdapterProxy<'_>, target: bool) {
let mut stream = proxy.receive_powered_changed().await;
if let Ok(value) = proxy.powered().await
&& value == target
{
return;
}
while let Some(change) = stream.next().await {
if let Ok(value) = change.get().await
&& value == target
{
return;
}
}
}