linux-rust: add listening mode picker for airpods

This commit is contained in:
Kavish Devar
2025-11-10 13:32:47 +05:30
parent bf6630dbd1
commit 3853e8ec9a
6 changed files with 378 additions and 76 deletions
+202 -35
View File
@@ -1,15 +1,17 @@
use std::collections::HashMap;
use std::sync::Arc;
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::Alignment::End;
use iced::border::Radius;
use iced::overlay::menu;
use iced::widget::button::Style;
use iced::widget::rule::FillMode;
use log::error;
use tokio::runtime::Runtime;
use crate::bluetooth::aacp::{AACPManager, ControlCommandIdentifiers};
// use crate::bluetooth::att::ATTManager;
use crate::devices::enums::{AirPodsState, DeviceData, DeviceInformation, DeviceState};
use crate::ui::window::Message;
@@ -17,11 +19,11 @@ pub fn airpods_view<'a>(
mac: &'a str,
devices_list: &HashMap<String, DeviceData>,
state: &'a AirPodsState,
aacp_manager: Arc<AACPManager>
aacp_manager: Arc<AACPManager>,
// att_manager: Arc<ATTManager>
) -> 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
let aacp_manager_for_rename = aacp_manager.clone();
let rename_input = container(
row![
@@ -57,19 +59,23 @@ pub fn airpods_view<'a>(
}
)
.align_x(End)
.on_input( move |new_name| {
let aacp_manager = aacp_manager_for_rename.clone();
run_async_in_thread(
{
let new_name = new_name.clone();
async move {
aacp_manager.send_rename_packet(&new_name).await.expect("Failed to send rename packet");
.on_input({
let mac = mac.clone();
let state = state.clone();
move|new_name| {
let aacp_manager = aacp_manager_for_rename.clone();
run_async_in_thread(
{
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();
state.device_name = new_name.clone();
Message::StateChanged(mac.to_string(), DeviceState::AirPods(state))
);
let mut state = state.clone();
state.device_name = new_name.clone();
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![
container(
text("Audio Settings").size(18).style(
@@ -126,20 +230,27 @@ pub fn airpods_view<'a>(
],
Space::with_width(Length::Fill),
toggler(state.personalized_volume_enabled)
.on_toggle(move |is_enabled| {
let aacp_manager = aacp_manager_pv.clone();
run_async_in_thread(
async move {
aacp_manager.send_control_command(
ControlCommandIdentifiers::AdaptiveVolumeConfig,
if is_enabled { &[0x01] } else { &[0x02] }
).await.expect("Failed to send Personalized Volume command");
}
);
let mut state = state.clone();
state.personalized_volume_enabled = is_enabled;
Message::StateChanged(mac.to_string(), DeviceState::AirPods(state))
})
.on_toggle(
{
let mac = mac_audio.clone();
let state = state.clone();
move |is_enabled| {
let aacp_manager = aacp_manager_pv.clone();
let mac = mac.clone();
run_async_in_thread(
async move {
aacp_manager.send_control_command(
ControlCommandIdentifiers::AdaptiveVolumeConfig,
if is_enabled { &[0x01] } else { &[0x02] }
).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)
.size(20)
]
@@ -182,7 +293,7 @@ pub fn airpods_view<'a>(
);
let mut state = state.clone();
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)
.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 mac = mac.to_string();
if let Some(device) = devices_list.get(mac.as_str()) {
if let Some(device) = devices_list.get(mac_information.as_str()) {
if let Some(DeviceInformation::AirPods(ref airpods_info)) = device.information {
let info_rows = column![
row![
@@ -378,7 +541,7 @@ pub fn airpods_view<'a>(
)
];
} 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![
rename_input,
Space::with_height(Length::from(20)),
listening_mode,
Space::with_height(Length::from(20)),
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
]
)
+1 -1
View File
@@ -1,7 +1,7 @@
use std::collections::HashMap;
use std::sync::Arc;
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::devices::enums::{DeviceData, DeviceInformation, NothingState};
use crate::ui::window::Message;
+116 -24
View File
@@ -12,7 +12,7 @@ use tokio::sync::mpsc::UnboundedReceiver;
use tokio::sync::{Mutex, RwLock};
use crate::bluetooth::aacp::{AACPEvent, ControlCommandIdentifiers};
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::utils::{get_devices_path, get_app_settings_path, MyTheme};
use crate::ui::airpods::airpods_view;
@@ -301,6 +301,29 @@ impl App {
};
self.device_states.insert(mac.clone(), DeviceState::AirPods(AirPodsState {
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| {
status.identifier == ControlCommandIdentifiers::ConversationDetectConfig &&
matches!(status.value.as_slice(), [0x01])
@@ -309,6 +332,10 @@ impl App {
status.identifier == ControlCommandIdentifiers::AdaptiveVolumeConfig &&
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) => {
@@ -346,6 +373,12 @@ impl App {
match event {
AACPEvent::ControlCommand(status) => {
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 => {
let is_enabled = match status.value.as_slice() {
[0x01] => true,
@@ -372,6 +405,32 @@ impl App {
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);
}
@@ -449,7 +508,39 @@ impl App {
Task::none()
}
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()
}
}
@@ -622,33 +713,34 @@ impl App {
debug!("Rendering device view for {}: type={:?}, state={:?}", id, device_type, device_state);
match device_type {
Some(DeviceType::AirPods) => {
if let Some(DeviceState::AirPods(state)) = device_state {
if let Some(device_managers) = device_managers.get(id) {
if let Some(aacp_manager) = device_managers.get_aacp() {
airpods_view(id, &devices_list, state, aacp_manager.clone())
} else {
error!("No AACP manager found for AirPods device {}", id);
container(
text("No valid AACP manager found for this AirPods device").size(16)
)
.center_x(Length::Fill)
.center_y(Length::Fill)
let view = device_state.as_ref().and_then(|state| {
match state {
DeviceState::AirPods(state) => {
device_managers.get(id).and_then(|managers| {
managers.get_aacp().and_then(|aacp_manager| {
// managers.get_att().map(|att_manager| {
Some(airpods_view(
id,
&devices_list,
state,
aacp_manager.clone()
),
// att_manager.clone(),
)
// })
})
})
}
} else {
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)
_ => None,
}
} else {
}).unwrap_or_else(|| {
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_y(Length::Fill)
}
});
view
}
Some(DeviceType::Nothing) => {
if let Some(DeviceState::Nothing(state)) = device_state {
@@ -725,7 +817,7 @@ impl App {
border: Border {
width: 1.0,
color: theme.palette().text,
radius: Radius::from(8.0)
radius: Radius::from(4.0)
},
text_color: theme.palette().text,
selected_text_color: theme.palette().text,