mirror of
https://github.com/kavishdevar/librepods.git
synced 2026-02-10 19:52:24 +00:00
linux-rust: add listening mode picker for airpods
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
use crate::bluetooth::aacp::{AACPManager, ProximityKeyType, AACPEvent, AirPodsLEKeys};
|
use crate::bluetooth::aacp::{AACPManager, ProximityKeyType, AACPEvent, AirPodsLEKeys};
|
||||||
use crate::bluetooth::aacp::ControlCommandIdentifiers;
|
use crate::bluetooth::aacp::ControlCommandIdentifiers;
|
||||||
// use crate::bluetooth::att::ATTManager;
|
use crate::bluetooth::att::ATTManager;
|
||||||
use crate::media_controller::MediaController;
|
use crate::media_controller::MediaController;
|
||||||
use bluer::Address;
|
use bluer::Address;
|
||||||
use log::{debug, info, error};
|
use log::{debug, info, error};
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
|
use iced::widget::{combo_box, ComboBox};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use crate::devices::airpods::AirPodsInformation;
|
use crate::devices::airpods::AirPodsInformation;
|
||||||
use crate::devices::nothing::NothingInformation;
|
use crate::devices::nothing::NothingInformation;
|
||||||
@@ -53,8 +54,50 @@ impl Display for DeviceState {
|
|||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct AirPodsState {
|
pub struct AirPodsState {
|
||||||
pub device_name: String,
|
pub device_name: String,
|
||||||
|
pub noise_control_mode: AirPodsNoiseControlMode,
|
||||||
|
pub noise_control_state: combo_box::State<AirPodsNoiseControlMode>,
|
||||||
pub conversation_awareness_enabled: bool,
|
pub conversation_awareness_enabled: bool,
|
||||||
pub personalized_volume_enabled: bool,
|
pub personalized_volume_enabled: bool,
|
||||||
|
pub allow_off_mode: bool
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub enum AirPodsNoiseControlMode {
|
||||||
|
Off,
|
||||||
|
NoiseCancellation,
|
||||||
|
Transparency,
|
||||||
|
Adaptive
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for AirPodsNoiseControlMode {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
AirPodsNoiseControlMode::Off => write!(f, "Off"),
|
||||||
|
AirPodsNoiseControlMode::NoiseCancellation => write!(f, "Noise Cancellation"),
|
||||||
|
AirPodsNoiseControlMode::Transparency => write!(f, "Transparency"),
|
||||||
|
AirPodsNoiseControlMode::Adaptive => write!(f, "Adaptive"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AirPodsNoiseControlMode {
|
||||||
|
pub fn from_byte(value: &u8) -> Self {
|
||||||
|
match value {
|
||||||
|
0x01 => AirPodsNoiseControlMode::Off,
|
||||||
|
0x02 => AirPodsNoiseControlMode::NoiseCancellation,
|
||||||
|
0x03 => AirPodsNoiseControlMode::Transparency,
|
||||||
|
0x04 => AirPodsNoiseControlMode::Adaptive,
|
||||||
|
_ => AirPodsNoiseControlMode::Off,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn to_byte(&self) -> u8 {
|
||||||
|
match self {
|
||||||
|
AirPodsNoiseControlMode::Off => 0x01,
|
||||||
|
AirPodsNoiseControlMode::NoiseCancellation => 0x02,
|
||||||
|
AirPodsNoiseControlMode::Transparency => 0x03,
|
||||||
|
AirPodsNoiseControlMode::Adaptive => 0x04,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
|
|||||||
@@ -123,18 +123,17 @@ async fn async_main(
|
|||||||
Ok(device) => {
|
Ok(device) => {
|
||||||
let name = device.name().await?.unwrap_or_else(|| "Unknown".to_string());
|
let name = device.name().await?.unwrap_or_else(|| "Unknown".to_string());
|
||||||
info!("Found connected AirPods: {}, initializing.", name);
|
info!("Found connected AirPods: {}, initializing.", name);
|
||||||
let ui_tx_clone = ui_tx.clone();
|
let airpods_device = AirPodsDevice::new(device.address(), tray_handle.clone(), ui_tx.clone()).await;
|
||||||
ui_tx_clone.send(BluetoothUIMessage::DeviceConnected(device.address().to_string())).unwrap();
|
|
||||||
let airpods_device = AirPodsDevice::new(device.address(), tray_handle.clone(), ui_tx_clone).await;
|
|
||||||
|
|
||||||
let mut managers = device_managers.write().await;
|
let mut managers = device_managers.write().await;
|
||||||
|
// let dev_managers = DeviceManagers::with_both(airpods_device.aacp_manager.clone(), airpods_device.att_manager.clone());
|
||||||
let dev_managers = DeviceManagers::with_aacp(airpods_device.aacp_manager.clone());
|
let dev_managers = DeviceManagers::with_aacp(airpods_device.aacp_manager.clone());
|
||||||
managers
|
managers
|
||||||
.entry(device.address().to_string())
|
.entry(device.address().to_string())
|
||||||
.or_insert(dev_managers)
|
.or_insert(dev_managers)
|
||||||
.set_aacp(airpods_device.aacp_manager)
|
.set_aacp(airpods_device.aacp_manager);
|
||||||
;
|
drop(managers);
|
||||||
drop(managers)
|
ui_tx.send(BluetoothUIMessage::DeviceConnected(device.address().to_string())).unwrap();
|
||||||
}
|
}
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
info!("No connected AirPods found.");
|
info!("No connected AirPods found.");
|
||||||
@@ -150,16 +149,16 @@ async fn async_main(
|
|||||||
let ui_tx_clone = ui_tx.clone();
|
let ui_tx_clone = ui_tx.clone();
|
||||||
let device_managers = device_managers.clone();
|
let device_managers = device_managers.clone();
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
ui_tx_clone.send(BluetoothUIMessage::DeviceConnected(addr_str.clone())).unwrap();
|
|
||||||
let mut managers = device_managers.write().await;
|
let mut managers = device_managers.write().await;
|
||||||
match type_ {
|
match type_ {
|
||||||
devices::enums::DeviceType::Nothing => {
|
devices::enums::DeviceType::Nothing => {
|
||||||
let dev = devices::nothing::NothingDevice::new(device.address(), ui_tx_clone).await;
|
let dev = devices::nothing::NothingDevice::new(device.address(), ui_tx_clone.clone()).await;
|
||||||
let dev_managers = DeviceManagers::with_att(dev.att_manager.clone());
|
let dev_managers = DeviceManagers::with_att(dev.att_manager.clone());
|
||||||
managers
|
managers
|
||||||
.entry(addr_str)
|
.entry(addr_str.clone())
|
||||||
.or_insert(dev_managers)
|
.or_insert(dev_managers)
|
||||||
.set_att(dev.att_manager);
|
.set_att(dev.att_manager);
|
||||||
|
ui_tx_clone.send(BluetoothUIMessage::DeviceConnected(addr_str)).unwrap();
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
@@ -212,14 +211,14 @@ async fn async_main(
|
|||||||
let device_managers = device_managers.clone();
|
let device_managers = device_managers.clone();
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let mut managers = device_managers.write().await;
|
let mut managers = device_managers.write().await;
|
||||||
ui_tx_clone.send(BluetoothUIMessage::DeviceConnected(addr_str.clone())).unwrap();
|
let dev = devices::nothing::NothingDevice::new(addr, ui_tx_clone.clone()).await;
|
||||||
let dev = devices::nothing::NothingDevice::new(addr, ui_tx_clone).await;
|
|
||||||
let dev_managers = DeviceManagers::with_att(dev.att_manager.clone());
|
let dev_managers = DeviceManagers::with_att(dev.att_manager.clone());
|
||||||
managers
|
managers
|
||||||
.entry(addr_str)
|
.entry(addr_str.clone())
|
||||||
.or_insert(dev_managers)
|
.or_insert(dev_managers)
|
||||||
.set_att(dev.att_manager);
|
.set_att(dev.att_manager);
|
||||||
drop(managers);
|
drop(managers);
|
||||||
|
ui_tx_clone.send(BluetoothUIMessage::DeviceConnected(addr_str.clone())).unwrap();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
@@ -236,15 +235,16 @@ async fn async_main(
|
|||||||
let ui_tx_clone = ui_tx.clone();
|
let ui_tx_clone = ui_tx.clone();
|
||||||
let device_managers = device_managers.clone();
|
let device_managers = device_managers.clone();
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
ui_tx_clone.send(BluetoothUIMessage::DeviceConnected(addr_str.clone())).unwrap();
|
let airpods_device = AirPodsDevice::new(addr, handle_clone, ui_tx_clone.clone()).await;
|
||||||
let airpods_device = AirPodsDevice::new(addr, handle_clone, ui_tx_clone).await;
|
|
||||||
let mut managers = device_managers.write().await;
|
let mut managers = device_managers.write().await;
|
||||||
|
// let dev_managers = DeviceManagers::with_both(airpods_device.aacp_manager.clone(), airpods_device.att_manager.clone());
|
||||||
let dev_managers = DeviceManagers::with_aacp(airpods_device.aacp_manager.clone());
|
let dev_managers = DeviceManagers::with_aacp(airpods_device.aacp_manager.clone());
|
||||||
managers
|
managers
|
||||||
.entry(addr_str)
|
.entry(addr_str.clone())
|
||||||
.or_insert(dev_managers)
|
.or_insert(dev_managers)
|
||||||
.set_aacp(airpods_device.aacp_manager);
|
.set_aacp(airpods_device.aacp_manager);
|
||||||
drop(managers);
|
drop(managers);
|
||||||
|
ui_tx_clone.send(BluetoothUIMessage::DeviceConnected(addr_str.clone())).unwrap();
|
||||||
});
|
});
|
||||||
true
|
true
|
||||||
})?;
|
})?;
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::thread;
|
use std::thread;
|
||||||
use iced::widget::{button, column, container, row, rule, text, text_input, toggler, Rule, Space};
|
use iced::widget::{button, column, combo_box, container, row, rule, text, text_input, toggler, Rule, Space};
|
||||||
use iced::{Background, Border, Center, Color, Length, Padding, Theme};
|
use iced::{Background, Border, Center, Color, Length, Padding, Theme};
|
||||||
use iced::Alignment::End;
|
use iced::Alignment::End;
|
||||||
use iced::border::Radius;
|
use iced::border::Radius;
|
||||||
|
use iced::overlay::menu;
|
||||||
use iced::widget::button::Style;
|
use iced::widget::button::Style;
|
||||||
use iced::widget::rule::FillMode;
|
use iced::widget::rule::FillMode;
|
||||||
use log::error;
|
use log::error;
|
||||||
use tokio::runtime::Runtime;
|
use tokio::runtime::Runtime;
|
||||||
use crate::bluetooth::aacp::{AACPManager, ControlCommandIdentifiers};
|
use crate::bluetooth::aacp::{AACPManager, ControlCommandIdentifiers};
|
||||||
|
// use crate::bluetooth::att::ATTManager;
|
||||||
use crate::devices::enums::{AirPodsState, DeviceData, DeviceInformation, DeviceState};
|
use crate::devices::enums::{AirPodsState, DeviceData, DeviceInformation, DeviceState};
|
||||||
use crate::ui::window::Message;
|
use crate::ui::window::Message;
|
||||||
|
|
||||||
@@ -17,11 +19,11 @@ pub fn airpods_view<'a>(
|
|||||||
mac: &'a str,
|
mac: &'a str,
|
||||||
devices_list: &HashMap<String, DeviceData>,
|
devices_list: &HashMap<String, DeviceData>,
|
||||||
state: &'a AirPodsState,
|
state: &'a AirPodsState,
|
||||||
aacp_manager: Arc<AACPManager>
|
aacp_manager: Arc<AACPManager>,
|
||||||
|
// att_manager: Arc<ATTManager>
|
||||||
) -> iced::widget::Container<'a, Message> {
|
) -> iced::widget::Container<'a, Message> {
|
||||||
|
let mac = mac.to_string();
|
||||||
// order: name, noise control, press and hold config, call controls (not sure if why it might be needed, adding it just in case), audio (personalized volume, conversational awareness, adaptive audio slider), connection settings, microphone, head gestures (not adding this), off listening mode, device information
|
// order: name, noise control, press and hold config, call controls (not sure if why it might be needed, adding it just in case), audio (personalized volume, conversational awareness, adaptive audio slider), connection settings, microphone, head gestures (not adding this), off listening mode, device information
|
||||||
|
|
||||||
let aacp_manager_for_rename = aacp_manager.clone();
|
let aacp_manager_for_rename = aacp_manager.clone();
|
||||||
let rename_input = container(
|
let rename_input = container(
|
||||||
row![
|
row![
|
||||||
@@ -57,19 +59,23 @@ pub fn airpods_view<'a>(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
.align_x(End)
|
.align_x(End)
|
||||||
.on_input( move |new_name| {
|
.on_input({
|
||||||
let aacp_manager = aacp_manager_for_rename.clone();
|
let mac = mac.clone();
|
||||||
run_async_in_thread(
|
let state = state.clone();
|
||||||
{
|
move|new_name| {
|
||||||
let new_name = new_name.clone();
|
let aacp_manager = aacp_manager_for_rename.clone();
|
||||||
async move {
|
run_async_in_thread(
|
||||||
aacp_manager.send_rename_packet(&new_name).await.expect("Failed to send rename packet");
|
{
|
||||||
|
let new_name = new_name.clone();
|
||||||
|
async move {
|
||||||
|
aacp_manager.send_rename_packet(&new_name).await.expect("Failed to send rename packet");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
);
|
||||||
);
|
let mut state = state.clone();
|
||||||
let mut state = state.clone();
|
state.device_name = new_name.clone();
|
||||||
state.device_name = new_name.clone();
|
Message::StateChanged(mac.to_string(), DeviceState::AirPods(state))
|
||||||
Message::StateChanged(mac.to_string(), DeviceState::AirPods(state))
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
@@ -92,6 +98,104 @@ pub fn airpods_view<'a>(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let listening_mode = container(row![
|
||||||
|
text("Listening Mode").size(16).style(
|
||||||
|
|theme: &Theme| {
|
||||||
|
let mut style = text::Style::default();
|
||||||
|
style.color = Some(theme.palette().text);
|
||||||
|
style
|
||||||
|
}
|
||||||
|
),
|
||||||
|
Space::with_width(Length::Fill),
|
||||||
|
{
|
||||||
|
let state_clone = state.clone();
|
||||||
|
let mac = mac.clone();
|
||||||
|
// this combo_box doesn't go really well with the design, but I am not writing my own dropdown menu for this
|
||||||
|
combo_box(
|
||||||
|
&state.noise_control_state,
|
||||||
|
"Select Listening Mode",
|
||||||
|
Some(&state.noise_control_mode.clone()),
|
||||||
|
{
|
||||||
|
let aacp_manager = aacp_manager.clone();
|
||||||
|
move |selected_mode| {
|
||||||
|
let aacp_manager = aacp_manager.clone();
|
||||||
|
let selected_mode_c = selected_mode.clone();
|
||||||
|
run_async_in_thread(
|
||||||
|
async move {
|
||||||
|
aacp_manager.send_control_command(
|
||||||
|
ControlCommandIdentifiers::ListeningMode,
|
||||||
|
&[selected_mode_c.to_byte()]
|
||||||
|
).await.expect("Failed to send Noise Control Mode command");
|
||||||
|
}
|
||||||
|
);
|
||||||
|
let mut state = state_clone.clone();
|
||||||
|
state.noise_control_mode = selected_mode.clone();
|
||||||
|
Message::StateChanged(mac.to_string(), DeviceState::AirPods(state))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.width(Length::from(200))
|
||||||
|
.input_style(
|
||||||
|
|theme: &Theme, _status| {
|
||||||
|
text_input::Style {
|
||||||
|
background: Background::Color(theme.palette().primary.scale_alpha(0.2)),
|
||||||
|
border: Border {
|
||||||
|
width: 1.0,
|
||||||
|
color: theme.palette().text.scale_alpha(0.3),
|
||||||
|
radius: Radius::from(4.0)
|
||||||
|
},
|
||||||
|
icon: Default::default(),
|
||||||
|
placeholder: theme.palette().text,
|
||||||
|
value: theme.palette().text,
|
||||||
|
selection: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.padding(Padding{
|
||||||
|
top: 5.0,
|
||||||
|
bottom: 5.0,
|
||||||
|
left: 10.0,
|
||||||
|
right: 10.0,
|
||||||
|
})
|
||||||
|
.menu_style(
|
||||||
|
|theme: &Theme| {
|
||||||
|
menu::Style {
|
||||||
|
background: Background::Color(theme.palette().background),
|
||||||
|
border: Border {
|
||||||
|
width: 1.0,
|
||||||
|
color: theme.palette().text,
|
||||||
|
radius: Radius::from(4.0)
|
||||||
|
},
|
||||||
|
text_color: theme.palette().text,
|
||||||
|
selected_text_color: theme.palette().text,
|
||||||
|
selected_background: Background::Color(theme.palette().primary.scale_alpha(0.3)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
.align_y(Center)
|
||||||
|
)
|
||||||
|
.padding(Padding{
|
||||||
|
top: 5.0,
|
||||||
|
bottom: 5.0,
|
||||||
|
left: 18.0,
|
||||||
|
right: 18.0,
|
||||||
|
})
|
||||||
|
.style(
|
||||||
|
|theme: &Theme| {
|
||||||
|
let mut style = container::Style::default();
|
||||||
|
style.background = Some(Background::Color(theme.palette().primary.scale_alpha(0.1)));
|
||||||
|
let mut border = Border::default();
|
||||||
|
border.color = theme.palette().primary.scale_alpha(0.5);
|
||||||
|
style.border = border.rounded(16);
|
||||||
|
style
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
let mac_audio = mac.clone();
|
||||||
|
let mac_information = mac.clone();
|
||||||
|
|
||||||
let audio_settings_col = column![
|
let audio_settings_col = column![
|
||||||
container(
|
container(
|
||||||
text("Audio Settings").size(18).style(
|
text("Audio Settings").size(18).style(
|
||||||
@@ -126,20 +230,27 @@ pub fn airpods_view<'a>(
|
|||||||
],
|
],
|
||||||
Space::with_width(Length::Fill),
|
Space::with_width(Length::Fill),
|
||||||
toggler(state.personalized_volume_enabled)
|
toggler(state.personalized_volume_enabled)
|
||||||
.on_toggle(move |is_enabled| {
|
.on_toggle(
|
||||||
let aacp_manager = aacp_manager_pv.clone();
|
{
|
||||||
run_async_in_thread(
|
let mac = mac_audio.clone();
|
||||||
async move {
|
let state = state.clone();
|
||||||
aacp_manager.send_control_command(
|
move |is_enabled| {
|
||||||
ControlCommandIdentifiers::AdaptiveVolumeConfig,
|
let aacp_manager = aacp_manager_pv.clone();
|
||||||
if is_enabled { &[0x01] } else { &[0x02] }
|
let mac = mac.clone();
|
||||||
).await.expect("Failed to send Personalized Volume command");
|
run_async_in_thread(
|
||||||
}
|
async move {
|
||||||
);
|
aacp_manager.send_control_command(
|
||||||
let mut state = state.clone();
|
ControlCommandIdentifiers::AdaptiveVolumeConfig,
|
||||||
state.personalized_volume_enabled = is_enabled;
|
if is_enabled { &[0x01] } else { &[0x02] }
|
||||||
Message::StateChanged(mac.to_string(), DeviceState::AirPods(state))
|
).await.expect("Failed to send Personalized Volume command");
|
||||||
})
|
}
|
||||||
|
);
|
||||||
|
let mut state = state.clone();
|
||||||
|
state.personalized_volume_enabled = is_enabled;
|
||||||
|
Message::StateChanged(mac, DeviceState::AirPods(state))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
.spacing(0)
|
.spacing(0)
|
||||||
.size(20)
|
.size(20)
|
||||||
]
|
]
|
||||||
@@ -182,7 +293,7 @@ pub fn airpods_view<'a>(
|
|||||||
);
|
);
|
||||||
let mut state = state.clone();
|
let mut state = state.clone();
|
||||||
state.conversation_awareness_enabled = is_enabled;
|
state.conversation_awareness_enabled = is_enabled;
|
||||||
Message::StateChanged(mac.to_string(), DeviceState::AirPods(state))
|
Message::StateChanged(mac_audio.to_string(), DeviceState::AirPods(state))
|
||||||
})
|
})
|
||||||
.spacing(0)
|
.spacing(0)
|
||||||
.size(20)
|
.size(20)
|
||||||
@@ -211,9 +322,61 @@ pub fn airpods_view<'a>(
|
|||||||
)
|
)
|
||||||
];
|
];
|
||||||
|
|
||||||
|
let off_listening_mode_toggle = {
|
||||||
|
let aacp_manager_olm = aacp_manager.clone();
|
||||||
|
let mac = mac.clone();
|
||||||
|
container(row![
|
||||||
|
column![
|
||||||
|
text("Off Listening Mode").size(16),
|
||||||
|
text("When this is on, AIrPods listening modes will include an Off option. Loud sound levels are not reduced when listening mode is set to Off.").size(12).style(
|
||||||
|
|theme: &Theme| {
|
||||||
|
let mut style = text::Style::default();
|
||||||
|
style.color = Some(theme.palette().text.scale_alpha(0.7));
|
||||||
|
style
|
||||||
|
}
|
||||||
|
)
|
||||||
|
],
|
||||||
|
Space::with_width(Length::Fill),
|
||||||
|
toggler(state.allow_off_mode)
|
||||||
|
.on_toggle(move |is_enabled| {
|
||||||
|
let aacp_manager = aacp_manager_olm.clone();
|
||||||
|
run_async_in_thread(
|
||||||
|
async move {
|
||||||
|
aacp_manager.send_control_command(
|
||||||
|
ControlCommandIdentifiers::AllowOffOption,
|
||||||
|
if is_enabled { &[0x01] } else { &[0x02] }
|
||||||
|
).await.expect("Failed to send Off Listening Mode command");
|
||||||
|
}
|
||||||
|
);
|
||||||
|
let mut state = state.clone();
|
||||||
|
state.allow_off_mode = is_enabled;
|
||||||
|
Message::StateChanged(mac.to_string(), DeviceState::AirPods(state))
|
||||||
|
})
|
||||||
|
.spacing(0)
|
||||||
|
.size(20)
|
||||||
|
]
|
||||||
|
.align_y(Center)
|
||||||
|
)
|
||||||
|
.padding(Padding{
|
||||||
|
top: 5.0,
|
||||||
|
bottom: 5.0,
|
||||||
|
left: 18.0,
|
||||||
|
right: 18.0,
|
||||||
|
})
|
||||||
|
.style(
|
||||||
|
|theme: &Theme| {
|
||||||
|
let mut style = container::Style::default();
|
||||||
|
style.background = Some(Background::Color(theme.palette().primary.scale_alpha(0.1)));
|
||||||
|
let mut border = Border::default();
|
||||||
|
border.color = theme.palette().primary.scale_alpha(0.5);
|
||||||
|
style.border = border.rounded(16);
|
||||||
|
style
|
||||||
|
}
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
let mut information_col = column![];
|
let mut information_col = column![];
|
||||||
let mac = mac.to_string();
|
if let Some(device) = devices_list.get(mac_information.as_str()) {
|
||||||
if let Some(device) = devices_list.get(mac.as_str()) {
|
|
||||||
if let Some(DeviceInformation::AirPods(ref airpods_info)) = device.information {
|
if let Some(DeviceInformation::AirPods(ref airpods_info)) = device.information {
|
||||||
let info_rows = column![
|
let info_rows = column![
|
||||||
row![
|
row![
|
||||||
@@ -378,7 +541,7 @@ pub fn airpods_view<'a>(
|
|||||||
)
|
)
|
||||||
];
|
];
|
||||||
} else {
|
} else {
|
||||||
error!("Expected AirPodsInformation for device {}, got something else", mac);
|
error!("Expected AirPodsInformation for device {}, got something else", mac.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -386,8 +549,12 @@ pub fn airpods_view<'a>(
|
|||||||
column![
|
column![
|
||||||
rename_input,
|
rename_input,
|
||||||
Space::with_height(Length::from(20)),
|
Space::with_height(Length::from(20)),
|
||||||
|
listening_mode,
|
||||||
|
Space::with_height(Length::from(20)),
|
||||||
audio_settings_col,
|
audio_settings_col,
|
||||||
Space::with_height(Length::from(10)),
|
Space::with_height(Length::from(20)),
|
||||||
|
off_listening_mode_toggle,
|
||||||
|
Space::with_height(Length::from(20)),
|
||||||
information_col
|
information_col
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use iced::{Background, Border, Length, Theme};
|
use iced::{Background, Border, Length, Theme};
|
||||||
use iced::widget::{container, text, column, row, Space, combo_box};
|
use iced::widget::{container, text, column, row, Space};
|
||||||
use crate::bluetooth::att::ATTManager;
|
use crate::bluetooth::att::ATTManager;
|
||||||
use crate::devices::enums::{DeviceData, DeviceInformation, NothingState};
|
use crate::devices::enums::{DeviceData, DeviceInformation, NothingState};
|
||||||
use crate::ui::window::Message;
|
use crate::ui::window::Message;
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ use tokio::sync::mpsc::UnboundedReceiver;
|
|||||||
use tokio::sync::{Mutex, RwLock};
|
use tokio::sync::{Mutex, RwLock};
|
||||||
use crate::bluetooth::aacp::{AACPEvent, ControlCommandIdentifiers};
|
use crate::bluetooth::aacp::{AACPEvent, ControlCommandIdentifiers};
|
||||||
use crate::bluetooth::managers::DeviceManagers;
|
use crate::bluetooth::managers::DeviceManagers;
|
||||||
use crate::devices::enums::{AirPodsState, DeviceData, DeviceState, DeviceType, NothingAncMode, NothingState};
|
use crate::devices::enums::{AirPodsNoiseControlMode, AirPodsState, DeviceData, DeviceState, DeviceType, NothingAncMode, NothingState};
|
||||||
use crate::ui::messages::BluetoothUIMessage;
|
use crate::ui::messages::BluetoothUIMessage;
|
||||||
use crate::utils::{get_devices_path, get_app_settings_path, MyTheme};
|
use crate::utils::{get_devices_path, get_app_settings_path, MyTheme};
|
||||||
use crate::ui::airpods::airpods_view;
|
use crate::ui::airpods::airpods_view;
|
||||||
@@ -301,6 +301,29 @@ impl App {
|
|||||||
};
|
};
|
||||||
self.device_states.insert(mac.clone(), DeviceState::AirPods(AirPodsState {
|
self.device_states.insert(mac.clone(), DeviceState::AirPods(AirPodsState {
|
||||||
device_name,
|
device_name,
|
||||||
|
noise_control_mode: state.control_command_status_list.iter().find_map(|status| {
|
||||||
|
if status.identifier == ControlCommandIdentifiers::ListeningMode {
|
||||||
|
status.value.get(0).map(|b| AirPodsNoiseControlMode::from_byte(b))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}).unwrap_or(AirPodsNoiseControlMode::Transparency),
|
||||||
|
noise_control_state: combo_box::State::new(
|
||||||
|
{
|
||||||
|
let mut modes = vec![
|
||||||
|
AirPodsNoiseControlMode::Transparency,
|
||||||
|
AirPodsNoiseControlMode::NoiseCancellation,
|
||||||
|
AirPodsNoiseControlMode::Adaptive
|
||||||
|
];
|
||||||
|
if state.control_command_status_list.iter().any(|status| {
|
||||||
|
status.identifier == ControlCommandIdentifiers::AllowOffOption &&
|
||||||
|
matches!(status.value.as_slice(), [0x01])
|
||||||
|
}) {
|
||||||
|
modes.insert(0, AirPodsNoiseControlMode::Off);
|
||||||
|
}
|
||||||
|
modes
|
||||||
|
}
|
||||||
|
),
|
||||||
conversation_awareness_enabled: state.control_command_status_list.iter().any(|status| {
|
conversation_awareness_enabled: state.control_command_status_list.iter().any(|status| {
|
||||||
status.identifier == ControlCommandIdentifiers::ConversationDetectConfig &&
|
status.identifier == ControlCommandIdentifiers::ConversationDetectConfig &&
|
||||||
matches!(status.value.as_slice(), [0x01])
|
matches!(status.value.as_slice(), [0x01])
|
||||||
@@ -309,6 +332,10 @@ impl App {
|
|||||||
status.identifier == ControlCommandIdentifiers::AdaptiveVolumeConfig &&
|
status.identifier == ControlCommandIdentifiers::AdaptiveVolumeConfig &&
|
||||||
matches!(status.value.as_slice(), [0x01])
|
matches!(status.value.as_slice(), [0x01])
|
||||||
}),
|
}),
|
||||||
|
allow_off_mode: state.control_command_status_list.iter().any(|status| {
|
||||||
|
status.identifier == ControlCommandIdentifiers::AllowOffOption &&
|
||||||
|
matches!(status.value.as_slice(), [0x01])
|
||||||
|
}),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
Some(DeviceType::Nothing) => {
|
Some(DeviceType::Nothing) => {
|
||||||
@@ -346,6 +373,12 @@ impl App {
|
|||||||
match event {
|
match event {
|
||||||
AACPEvent::ControlCommand(status) => {
|
AACPEvent::ControlCommand(status) => {
|
||||||
match status.identifier {
|
match status.identifier {
|
||||||
|
ControlCommandIdentifiers::ListeningMode => {
|
||||||
|
let mode = status.value.get(0).map(|b| AirPodsNoiseControlMode::from_byte(b)).unwrap_or(AirPodsNoiseControlMode::Transparency);
|
||||||
|
if let Some(DeviceState::AirPods(state)) = self.device_states.get_mut(&mac) {
|
||||||
|
state.noise_control_mode = mode;
|
||||||
|
}
|
||||||
|
}
|
||||||
ControlCommandIdentifiers::ConversationDetectConfig => {
|
ControlCommandIdentifiers::ConversationDetectConfig => {
|
||||||
let is_enabled = match status.value.as_slice() {
|
let is_enabled = match status.value.as_slice() {
|
||||||
[0x01] => true,
|
[0x01] => true,
|
||||||
@@ -372,6 +405,32 @@ impl App {
|
|||||||
state.personalized_volume_enabled = is_enabled;
|
state.personalized_volume_enabled = is_enabled;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
ControlCommandIdentifiers::AllowOffOption => {
|
||||||
|
let is_enabled = match status.value.as_slice() {
|
||||||
|
[0x01] => true,
|
||||||
|
[0x02] => false,
|
||||||
|
_ => {
|
||||||
|
error!("Unknown Allow Off Option value: {:?}", status.value);
|
||||||
|
false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if let Some(DeviceState::AirPods(state)) = self.device_states.get_mut(&mac) {
|
||||||
|
state.allow_off_mode = is_enabled;
|
||||||
|
state.noise_control_state = combo_box::State::new(
|
||||||
|
{
|
||||||
|
let mut modes = vec![
|
||||||
|
AirPodsNoiseControlMode::Transparency,
|
||||||
|
AirPodsNoiseControlMode::NoiseCancellation,
|
||||||
|
AirPodsNoiseControlMode::Adaptive
|
||||||
|
];
|
||||||
|
if is_enabled {
|
||||||
|
modes.insert(0, AirPodsNoiseControlMode::Off);
|
||||||
|
}
|
||||||
|
modes
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
_ => {
|
_ => {
|
||||||
debug!("Unhandled Control Command Status: {:?}", status);
|
debug!("Unhandled Control Command Status: {:?}", status);
|
||||||
}
|
}
|
||||||
@@ -449,7 +508,39 @@ impl App {
|
|||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
Message::StateChanged(mac, state) => {
|
Message::StateChanged(mac, state) => {
|
||||||
self.device_states.insert(mac, state);
|
self.device_states.insert(mac.clone(), state);
|
||||||
|
// if airpods, update the noise control state combo box based on allow off mode
|
||||||
|
let type_ = {
|
||||||
|
let devices_json = std::fs::read_to_string(get_devices_path()).unwrap_or_else(|e| {
|
||||||
|
error!("Failed to read devices file: {}", e);
|
||||||
|
"{}".to_string()
|
||||||
|
});
|
||||||
|
let devices_list: HashMap<String, DeviceData> = serde_json::from_str(&devices_json).unwrap_or_else(|e| {
|
||||||
|
error!("Deserialization failed: {}", e);
|
||||||
|
HashMap::new()
|
||||||
|
});
|
||||||
|
devices_list.get(&mac).map(|d| d.type_.clone())
|
||||||
|
};
|
||||||
|
match type_ {
|
||||||
|
Some(DeviceType::AirPods) => {
|
||||||
|
if let Some(DeviceState::AirPods(state)) = self.device_states.get_mut(&mac) {
|
||||||
|
state.noise_control_state = combo_box::State::new(
|
||||||
|
{
|
||||||
|
let mut modes = vec![
|
||||||
|
AirPodsNoiseControlMode::Transparency,
|
||||||
|
AirPodsNoiseControlMode::NoiseCancellation,
|
||||||
|
AirPodsNoiseControlMode::Adaptive
|
||||||
|
];
|
||||||
|
if state.allow_off_mode {
|
||||||
|
modes.insert(0, AirPodsNoiseControlMode::Off);
|
||||||
|
}
|
||||||
|
modes
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -622,33 +713,34 @@ impl App {
|
|||||||
debug!("Rendering device view for {}: type={:?}, state={:?}", id, device_type, device_state);
|
debug!("Rendering device view for {}: type={:?}, state={:?}", id, device_type, device_state);
|
||||||
match device_type {
|
match device_type {
|
||||||
Some(DeviceType::AirPods) => {
|
Some(DeviceType::AirPods) => {
|
||||||
if let Some(DeviceState::AirPods(state)) = device_state {
|
let view = device_state.as_ref().and_then(|state| {
|
||||||
if let Some(device_managers) = device_managers.get(id) {
|
match state {
|
||||||
if let Some(aacp_manager) = device_managers.get_aacp() {
|
DeviceState::AirPods(state) => {
|
||||||
airpods_view(id, &devices_list, state, aacp_manager.clone())
|
device_managers.get(id).and_then(|managers| {
|
||||||
} else {
|
managers.get_aacp().and_then(|aacp_manager| {
|
||||||
error!("No AACP manager found for AirPods device {}", id);
|
// managers.get_att().map(|att_manager| {
|
||||||
container(
|
Some(airpods_view(
|
||||||
text("No valid AACP manager found for this AirPods device").size(16)
|
id,
|
||||||
)
|
&devices_list,
|
||||||
.center_x(Length::Fill)
|
state,
|
||||||
.center_y(Length::Fill)
|
aacp_manager.clone()
|
||||||
|
),
|
||||||
|
// att_manager.clone(),
|
||||||
|
)
|
||||||
|
// })
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
} else {
|
_ => None,
|
||||||
error!("No manager found for AirPods device {}", id);
|
|
||||||
container(
|
|
||||||
text("No manager found for this AirPods device").size(16)
|
|
||||||
)
|
|
||||||
.center_x(Length::Fill)
|
|
||||||
.center_y(Length::Fill)
|
|
||||||
}
|
}
|
||||||
} else {
|
}).unwrap_or_else(|| {
|
||||||
container(
|
container(
|
||||||
text("No state available for this AirPods device").size(16)
|
text("Required managers or state not available for this AirPods device").size(16)
|
||||||
)
|
)
|
||||||
.center_x(Length::Fill)
|
.center_x(Length::Fill)
|
||||||
.center_y(Length::Fill)
|
.center_y(Length::Fill)
|
||||||
}
|
});
|
||||||
|
view
|
||||||
}
|
}
|
||||||
Some(DeviceType::Nothing) => {
|
Some(DeviceType::Nothing) => {
|
||||||
if let Some(DeviceState::Nothing(state)) = device_state {
|
if let Some(DeviceState::Nothing(state)) = device_state {
|
||||||
@@ -725,7 +817,7 @@ impl App {
|
|||||||
border: Border {
|
border: Border {
|
||||||
width: 1.0,
|
width: 1.0,
|
||||||
color: theme.palette().text,
|
color: theme.palette().text,
|
||||||
radius: Radius::from(8.0)
|
radius: Radius::from(4.0)
|
||||||
},
|
},
|
||||||
text_color: theme.palette().text,
|
text_color: theme.palette().text,
|
||||||
selected_text_color: theme.palette().text,
|
selected_text_color: theme.palette().text,
|
||||||
|
|||||||
Reference in New Issue
Block a user