mirror of
https://github.com/kavishdevar/librepods.git
synced 2026-02-03 00:29:16 +00:00
linux-rust: fix conv-detect and add le auto-connect
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
use crate::bluetooth::aacp::{AACPManager, ProximityKeyType, AACPEvent};
|
||||
use crate::bluetooth::aacp::ControlCommandIdentifiers;
|
||||
// use crate::bluetooth::att::ATTManager;
|
||||
use crate::bluetooth::att::ATTManager;
|
||||
use crate::media_controller::MediaController;
|
||||
use bluer::Address;
|
||||
use log::{debug, info, error};
|
||||
@@ -13,7 +13,7 @@ use crate::ui::tray::MyTray;
|
||||
pub struct AirPodsDevice {
|
||||
pub mac_address: Address,
|
||||
pub aacp_manager: AACPManager,
|
||||
// pub att_manager: ATTManager,
|
||||
pub att_manager: ATTManager,
|
||||
pub media_controller: Arc<Mutex<MediaController>>,
|
||||
}
|
||||
|
||||
@@ -23,8 +23,8 @@ impl AirPodsDevice {
|
||||
let mut aacp_manager = AACPManager::new();
|
||||
aacp_manager.connect(mac_address).await;
|
||||
|
||||
// let mut att_manager = ATTManager::new();
|
||||
// att_manager.connect(mac_address).await.expect("Failed to connect ATT");
|
||||
let mut att_manager = ATTManager::new();
|
||||
att_manager.connect(mac_address).await.expect("Failed to connect ATT");
|
||||
|
||||
if let Some(handle) = &tray_handle {
|
||||
handle.update(|tray: &mut MyTray| tray.connected = true).await;
|
||||
@@ -226,7 +226,7 @@ impl AirPodsDevice {
|
||||
AirPodsDevice {
|
||||
mac_address,
|
||||
aacp_manager,
|
||||
// att_manager,
|
||||
att_manager,
|
||||
media_controller,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -242,20 +242,21 @@ pub enum AACPEvent {
|
||||
OwnershipToFalseRequest,
|
||||
}
|
||||
|
||||
struct AACPManagerState {
|
||||
sender: Option<mpsc::Sender<Vec<u8>>>,
|
||||
control_command_status_list: Vec<ControlCommandStatus>,
|
||||
control_command_subscribers: HashMap<ControlCommandIdentifiers, Vec<mpsc::UnboundedSender<Vec<u8>>>>,
|
||||
owns: bool,
|
||||
old_connected_devices: Vec<ConnectedDevice>,
|
||||
connected_devices: Vec<ConnectedDevice>,
|
||||
audio_source: Option<AudioSource>,
|
||||
battery_info: Vec<BatteryInfo>,
|
||||
pub struct AACPManagerState {
|
||||
pub sender: Option<mpsc::Sender<Vec<u8>>>,
|
||||
pub control_command_status_list: Vec<ControlCommandStatus>,
|
||||
pub control_command_subscribers: HashMap<ControlCommandIdentifiers, Vec<mpsc::UnboundedSender<Vec<u8>>>>,
|
||||
pub owns: bool,
|
||||
pub old_connected_devices: Vec<ConnectedDevice>,
|
||||
pub connected_devices: Vec<ConnectedDevice>,
|
||||
pub audio_source: Option<AudioSource>,
|
||||
pub battery_info: Vec<BatteryInfo>,
|
||||
pub conversational_awareness_status: u8,
|
||||
old_ear_detection_status: Vec<EarDetectionStatus>,
|
||||
ear_detection_status: Vec<EarDetectionStatus>,
|
||||
pub old_ear_detection_status: Vec<EarDetectionStatus>,
|
||||
pub ear_detection_status: Vec<EarDetectionStatus>,
|
||||
event_tx: Option<mpsc::UnboundedSender<AACPEvent>>,
|
||||
proximity_keys: HashMap<ProximityKeyType, Vec<u8>>,
|
||||
pub airpods_mac: Option<Address>,
|
||||
}
|
||||
|
||||
impl AACPManagerState {
|
||||
@@ -278,6 +279,7 @@ impl AACPManagerState {
|
||||
ear_detection_status: Vec::new(),
|
||||
event_tx: None,
|
||||
proximity_keys,
|
||||
airpods_mac: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -300,6 +302,11 @@ impl AACPManager {
|
||||
info!("AACPManager connecting to {} on PSM {:#06X}...", addr, PSM);
|
||||
let target_sa = SocketAddr::new(addr, AddressType::BrEdr, PSM);
|
||||
|
||||
{
|
||||
let mut state = self.state.lock().await;
|
||||
state.airpods_mac = Some(addr);
|
||||
}
|
||||
|
||||
let socket = match Socket::new_seq_packet() {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
@@ -384,11 +391,7 @@ impl AACPManager {
|
||||
let mut state = self.state.lock().await;
|
||||
state.event_tx = Some(tx);
|
||||
}
|
||||
|
||||
pub async fn get_connected_devices(&self) -> Vec<ConnectedDevice> {
|
||||
self.state.lock().await.connected_devices.clone()
|
||||
}
|
||||
|
||||
|
||||
pub async fn subscribe_to_control_command(&self, identifier: ControlCommandIdentifiers, tx: mpsc::UnboundedSender<Vec<u8>>) {
|
||||
let mut state = self.state.lock().await;
|
||||
state.control_command_subscribers.entry(identifier).or_default().push(tx);
|
||||
@@ -573,17 +576,28 @@ impl AACPManager {
|
||||
}
|
||||
}
|
||||
|
||||
let json = serde_json::to_string(&state.proximity_keys).unwrap();
|
||||
let path = get_proximity_keys_path();
|
||||
if let Some(parent) = path.parent() {
|
||||
if let Err(e) = tokio::fs::create_dir_all(&parent).await {
|
||||
error!("Failed to create directory for proximity keys: {}", e);
|
||||
return;
|
||||
if let Some(mac) = state.airpods_mac {
|
||||
let path = get_proximity_keys_path();
|
||||
let mut all_keys: HashMap<String, HashMap<ProximityKeyType, Vec<u8>>> =
|
||||
std::fs::read_to_string(&path)
|
||||
.ok()
|
||||
.and_then(|s| serde_json::from_str(&s).ok())
|
||||
.unwrap_or_default();
|
||||
|
||||
all_keys.insert(mac.to_string(), state.proximity_keys.clone());
|
||||
|
||||
let json = serde_json::to_string(&all_keys).unwrap();
|
||||
if let Some(parent) = path.parent() {
|
||||
if let Err(e) = tokio::fs::create_dir_all(&parent).await {
|
||||
error!("Failed to create directory for proximity keys: {}", e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if let Err(e) = tokio::fs::write(&path, json).await {
|
||||
error!("Failed to save proximity keys: {}", e);
|
||||
}
|
||||
}
|
||||
if let Err(e) = tokio::fs::write(&path, json).await {
|
||||
error!("Failed to save proximity keys: {}", e);
|
||||
}
|
||||
|
||||
if let Some(ref tx) = state.event_tx {
|
||||
let _ = tx.send(AACPEvent::ProximityKeys(keys));
|
||||
}
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
use bluer::monitor::{Monitor, MonitorEvent, Pattern, RssiSamplingPeriod};
|
||||
use bluer::monitor::{Monitor, MonitorEvent, Pattern};
|
||||
use bluer::{Address, Session};
|
||||
use aes::Aes128;
|
||||
use aes::cipher::{BlockEncrypt, KeyInit, BlockDecrypt};
|
||||
use aes::cipher::generic_array::GenericArray;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use log::{info, error, debug};
|
||||
use log::{info, debug};
|
||||
use serde_json;
|
||||
use crate::bluetooth::aacp::ProximityKeyType;
|
||||
use futures::StreamExt;
|
||||
use hex;
|
||||
use std::time::Duration;
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
use crate::bluetooth::aacp::BatteryStatus;
|
||||
use crate::ui::tray::MyTray;
|
||||
|
||||
@@ -20,6 +22,12 @@ fn get_proximity_keys_path() -> PathBuf {
|
||||
PathBuf::from(data_dir).join("librepods").join("proximity_keys.json")
|
||||
}
|
||||
|
||||
fn get_preferences_path() -> PathBuf {
|
||||
let config_dir = std::env::var("XDG_CONFIG_HOME")
|
||||
.unwrap_or_else(|_| format!("{}/.config", std::env::var("HOME").unwrap_or_default()));
|
||||
PathBuf::from(config_dir).join("librepods").join("preferences.json")
|
||||
}
|
||||
|
||||
fn e(key: &[u8; 16], data: &[u8; 16]) -> [u8; 16] {
|
||||
let mut swapped_key = *key;
|
||||
swapped_key.reverse();
|
||||
@@ -73,15 +81,15 @@ pub async fn start_le_monitor(tray_handle: Option<ksni::Handle<MyTray>>) -> blue
|
||||
let adapter = session.default_adapter().await?;
|
||||
adapter.set_powered(true).await?;
|
||||
|
||||
let proximity_keys: HashMap<ProximityKeyType, Vec<u8>> = std::fs::read_to_string(get_proximity_keys_path())
|
||||
.ok()
|
||||
.and_then(|s| serde_json::from_str(&s).ok())
|
||||
.unwrap_or_default();
|
||||
let irk = proximity_keys.get(&ProximityKeyType::Irk)
|
||||
.and_then(|v| if v.len() == 16 { Some(<[u8; 16]>::try_from(v.as_slice()).unwrap()) } else { None });
|
||||
let enc_key = proximity_keys.get(&ProximityKeyType::EncKey)
|
||||
.and_then(|v| if v.len() == 16 { Some(<[u8; 16]>::try_from(v.as_slice()).unwrap()) } else { None });
|
||||
let mut verified_macs: HashSet<Address> = HashSet::new();
|
||||
let all_proximity_keys: HashMap<String, HashMap<ProximityKeyType, Vec<u8>>> =
|
||||
std::fs::read_to_string(get_proximity_keys_path())
|
||||
.ok()
|
||||
.and_then(|s| serde_json::from_str(&s).ok())
|
||||
.unwrap_or_default();
|
||||
|
||||
let mut verified_macs: HashMap<Address, String> = HashMap::new();
|
||||
let mut failed_macs: HashSet<Address> = HashSet::new();
|
||||
let connecting_macs = Arc::new(Mutex::new(HashSet::<Address>::new()));
|
||||
|
||||
let pattern = Pattern {
|
||||
data_type: 0xFF, // Manufacturer specific data
|
||||
@@ -107,41 +115,128 @@ pub async fn start_le_monitor(tray_handle: Option<ksni::Handle<MyTray>>) -> blue
|
||||
|
||||
while let Some(mevt) = monitor_handle.next().await {
|
||||
if let MonitorEvent::DeviceFound(devid) = mevt {
|
||||
let dev = adapter.device(devid.device)?;
|
||||
let adapter_monitor_clone = adapter.clone();
|
||||
let dev = adapter_monitor_clone.device(devid.device)?;
|
||||
let addr = dev.address();
|
||||
let addr_str = addr.to_string();
|
||||
|
||||
debug!("Found device: {}", addr_str);
|
||||
let matched_airpods_mac: Option<String>;
|
||||
let mut matched_enc_key: Option<[u8; 16]> = None;
|
||||
|
||||
if !verified_macs.contains(&addr) {
|
||||
if let Some(airpods_mac) = verified_macs.get(&addr) {
|
||||
matched_airpods_mac = Some(airpods_mac.clone());
|
||||
} else if failed_macs.contains(&addr) {
|
||||
continue;
|
||||
} else {
|
||||
debug!("Checking RPA for device: {}", addr_str);
|
||||
if let Some(irk) = &irk {
|
||||
if verify_rpa(&addr_str, irk) {
|
||||
verified_macs.insert(addr);
|
||||
info!("Matched our device ({}) with the irk", addr);
|
||||
} else {
|
||||
debug!("Device {} did not match our irk", addr);
|
||||
let mut found_mac = None;
|
||||
for (airpods_mac, keys) in &all_proximity_keys {
|
||||
if let Some(irk_vec) = keys.get(&ProximityKeyType::Irk) {
|
||||
if irk_vec.len() == 16 {
|
||||
let irk: [u8; 16] = irk_vec.as_slice().try_into().unwrap();
|
||||
debug!("Verifying RPA {} for airpods MAC {} with IRK {}", addr_str, airpods_mac, hex::encode(irk));
|
||||
if verify_rpa(&addr_str, &irk) {
|
||||
info!("Matched our device ({}) with the irk for {}", addr, airpods_mac);
|
||||
verified_macs.insert(addr, airpods_mac.clone());
|
||||
found_mac = Some(airpods_mac.clone());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(mac) = found_mac {
|
||||
matched_airpods_mac = Some(mac);
|
||||
} else {
|
||||
failed_macs.insert(addr);
|
||||
debug!("Device {} did not match any of our irks", addr);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ref mac) = matched_airpods_mac {
|
||||
if let Some(keys) = all_proximity_keys.get(mac) {
|
||||
if let Some(enc_key_vec) = keys.get(&ProximityKeyType::EncKey) {
|
||||
if enc_key_vec.len() == 16 {
|
||||
matched_enc_key = Some(enc_key_vec.as_slice().try_into().unwrap());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if verified_macs.contains(&addr) {
|
||||
if matched_airpods_mac.is_some() {
|
||||
let mut events = dev.events().await?;
|
||||
let tray_handle_clone = tray_handle.clone();
|
||||
let connecting_macs_clone = Arc::clone(&connecting_macs);
|
||||
tokio::spawn(async move {
|
||||
while let Some(ev) = events.next().await {
|
||||
match ev {
|
||||
bluer::DeviceEvent::PropertyChanged(prop) => {
|
||||
match prop {
|
||||
bluer::DeviceProperty::ManufacturerData(data) => {
|
||||
debug!("Manufacturer data from {}: {:?}", addr_str, data.iter().map(|(k, v)| (k, hex::encode(v))).collect::<HashMap<_, _>>());
|
||||
if let Some(enc_key) = &enc_key {
|
||||
if let Some(enc_key) = &matched_enc_key {
|
||||
if let Some(apple_data) = data.get(&76) {
|
||||
if apple_data.len() > 20 {
|
||||
let last_16: [u8; 16] = apple_data[apple_data.len() - 16..].try_into().unwrap();
|
||||
let decrypted = decrypt(enc_key, &last_16);
|
||||
debug!("Decrypted data from {}: {}", addr_str, hex::encode(decrypted));
|
||||
|
||||
debug!("Decrypted data from airpods_mac {}: {}",
|
||||
matched_airpods_mac.as_ref().unwrap_or(&"unknown".to_string()),
|
||||
hex::encode(&decrypted));
|
||||
|
||||
let connection_state = apple_data[10] as usize;
|
||||
debug!("Connection state: {}", connection_state);
|
||||
if connection_state == 0x00 {
|
||||
let pref_path = get_preferences_path();
|
||||
let preferences: HashMap<String, HashMap<String, bool>> =
|
||||
std::fs::read_to_string(&pref_path)
|
||||
.ok()
|
||||
.and_then(|s| serde_json::from_str(&s).ok())
|
||||
.unwrap_or_default();
|
||||
let auto_connect = preferences.get(matched_airpods_mac.as_ref().unwrap())
|
||||
.and_then(|prefs| prefs.get("autoConnect"))
|
||||
.copied()
|
||||
.unwrap_or(true);
|
||||
debug!("Auto-connect preference for {}: {}", matched_airpods_mac.as_ref().unwrap(), auto_connect);
|
||||
if auto_connect {
|
||||
let real_address = Address::from_str(&addr_str).unwrap();
|
||||
let mut cm = connecting_macs_clone.lock().await;
|
||||
if cm.contains(&real_address) {
|
||||
info!("Already connecting to {}, skipping duplicate attempt.", matched_airpods_mac.as_ref().unwrap());
|
||||
return;
|
||||
}
|
||||
cm.insert(real_address);
|
||||
// let adapter_clone = adapter_monitor_clone.clone();
|
||||
// let real_device = adapter_clone.device(real_address).unwrap();
|
||||
info!("AirPods are disconnected, attempting to connect to {}", matched_airpods_mac.as_ref().unwrap());
|
||||
// if let Err(e) = real_device.connect().await {
|
||||
// info!("Failed to connect to AirPods {}: {}", matched_airpods_mac.as_ref().unwrap(), e);
|
||||
// } else {
|
||||
// info!("Successfully connected to AirPods {}", matched_airpods_mac.as_ref().unwrap());
|
||||
// }
|
||||
// call bluetoothctl connect <mac> for now, I don't know why bluer connect isn't working
|
||||
let output = tokio::process::Command::new("bluetoothctl")
|
||||
.arg("connect")
|
||||
.arg(matched_airpods_mac.as_ref().unwrap())
|
||||
.output()
|
||||
.await;
|
||||
match output {
|
||||
Ok(output) => {
|
||||
if output.status.success() {
|
||||
info!("Successfully connected to AirPods {}", matched_airpods_mac.as_ref().unwrap());
|
||||
cm.remove(&real_address);
|
||||
} else {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
info!("Failed to connect to AirPods {}: {}", matched_airpods_mac.as_ref().unwrap(), stderr);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
info!("Failed to execute bluetoothctl to connect to AirPods {}: {}", matched_airpods_mac.as_ref().unwrap(), e);
|
||||
}
|
||||
}
|
||||
info!("Auto-connect is disabled for {}, not attempting to connect.", matched_airpods_mac.as_ref().unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
let status = apple_data[5] as usize;
|
||||
let primary_left = (status >> 5) & 0x01 == 1;
|
||||
let this_in_case = (status >> 6) & 0x01 == 1;
|
||||
@@ -184,7 +279,7 @@ pub async fn start_le_monitor(tray_handle: Option<ksni::Handle<MyTray>>) -> blue
|
||||
}).await;
|
||||
}
|
||||
|
||||
info!("Battery status: Left: {}, Right: {}, Case: {}, InEar: L:{} R:{}",
|
||||
debug!("Battery status: Left: {}, Right: {}, Case: {}, InEar: L:{} R:{}",
|
||||
if left_byte == 0xff { "disconnected".to_string() } else { format!("{}% (charging: {})", left_battery, left_charging) },
|
||||
if right_byte == 0xff { "disconnected".to_string() } else { format!("{}% (charging: {})", right_battery, right_charging) },
|
||||
if case_byte == 0xff { "disconnected".to_string() } else { format!("{}% (charging: {})", case_battery, case_charging) },
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
pub(crate) mod discovery;
|
||||
pub mod aacp;
|
||||
// pub mod att;
|
||||
pub mod att;
|
||||
pub mod le;
|
||||
@@ -4,7 +4,7 @@ mod media_controller;
|
||||
mod ui;
|
||||
|
||||
use std::env;
|
||||
use log::{debug, info};
|
||||
use log::info;
|
||||
use dbus::blocking::Connection;
|
||||
use dbus::blocking::stdintf::org_freedesktop_dbus::Properties;
|
||||
use dbus::message::MatchRule;
|
||||
|
||||
@@ -124,13 +124,18 @@ impl MediaController {
|
||||
drop(state);
|
||||
|
||||
if !was_playing && is_playing {
|
||||
let aacp_state = aacp_manager.state.lock().await;
|
||||
if !aacp_state.ear_detection_status.contains(&EarDetectionStatus::InEar) {
|
||||
info!("Media playback started but buds not in ear, skipping takeover");
|
||||
continue;
|
||||
}
|
||||
info!("Media playback started, taking ownership and activating a2dp");
|
||||
let _ = control_tx.send((crate::bluetooth::aacp::ControlCommandIdentifiers::OwnsConnection, vec![0x01]));
|
||||
self.activate_a2dp_profile().await;
|
||||
|
||||
info!("already connected locally, hijacking connection by asking AirPods");
|
||||
|
||||
let connected_devices = aacp_manager.get_connected_devices().await;
|
||||
let connected_devices = aacp_state.connected_devices.clone();
|
||||
for device in connected_devices {
|
||||
if device.mac != local_mac {
|
||||
if let Err(e) = aacp_manager.send_media_information(&local_mac, &device.mac, true).await {
|
||||
@@ -144,6 +149,8 @@ impl MediaController {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
debug!("completed playback takeover process");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -718,6 +725,7 @@ impl MediaController {
|
||||
|
||||
if let Some(list) = card_info_list.borrow().as_ref() {
|
||||
for card in list {
|
||||
debug!("Checking card index {} for MAC match", card.index);
|
||||
let props = &card.proplist;
|
||||
if let Some(device_string) = props.get_str("device.string") {
|
||||
if device_string.contains(&mac_clone) {
|
||||
@@ -905,6 +913,29 @@ impl MediaController {
|
||||
debug!("No stored original volume to restore to on status 6");
|
||||
}
|
||||
}
|
||||
9 => {
|
||||
let mut maybe_original = None;
|
||||
{
|
||||
let mut state = self.state.lock().await;
|
||||
if state.conv_conversation_started {
|
||||
maybe_original = state.conv_original_volume;
|
||||
state.conv_original_volume = None;
|
||||
state.conv_conversation_started = false;
|
||||
} else {
|
||||
debug!("Received status 9 but conversation was not started; ignoring restore");
|
||||
return;
|
||||
}
|
||||
}
|
||||
if let Some(orig) = maybe_original {
|
||||
let sink_clone = sink.clone();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
transition_sink_volume(&sink_clone, orig)
|
||||
}).await.unwrap_or(false);
|
||||
info!("Conversation end (9): restored volume to original {}", orig);
|
||||
} else {
|
||||
debug!("No stored original volume to restore to on status 9");
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
debug!("Unknown conversational awareness status: {}", status);
|
||||
}
|
||||
@@ -913,30 +944,48 @@ impl MediaController {
|
||||
}
|
||||
|
||||
fn get_sink_volume_percent_by_name_sync(sink_name: &str) -> Option<u32> {
|
||||
match Command::new("pactl").args(&["get-sink-volume", sink_name]).output() {
|
||||
Ok(output) if output.status.success() => {
|
||||
if let Ok(s) = String::from_utf8(output.stdout) {
|
||||
if let Some(pct_idx) = s.find('%') {
|
||||
let mut start = pct_idx;
|
||||
let bytes = s.as_bytes();
|
||||
while start > 0 {
|
||||
let b = bytes[start - 1];
|
||||
if b.is_ascii_digit() {
|
||||
start -= 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if start < pct_idx {
|
||||
if let Ok(num) = s[start..pct_idx].trim().parse::<u32>() {
|
||||
return Some(num);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
let mut mainloop = Mainloop::new().unwrap();
|
||||
let mut context = Context::new(&mut mainloop, "LibrePods-get_sink_volume").unwrap();
|
||||
context.connect(None, ContextFlagSet::NOAUTOSPAWN, None).unwrap();
|
||||
loop {
|
||||
match mainloop.iterate(false) {
|
||||
_ if context.get_state() == libpulse_binding::context::State::Ready => break,
|
||||
_ if context.get_state() == libpulse_binding::context::State::Failed || context.get_state() == libpulse_binding::context::State::Terminated => return None,
|
||||
_ => {},
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
|
||||
let introspector = context.introspect();
|
||||
let sink_info_option = Rc::new(RefCell::new(None));
|
||||
let op = introspector.get_sink_info_by_name(sink_name, {
|
||||
let sink_info_option = sink_info_option.clone();
|
||||
move |result: ListResult<&SinkInfo>| {
|
||||
if let ListResult::Item(item) = result {
|
||||
let owned_item = OwnedSinkInfo {
|
||||
name: item.name.as_ref().map(|s| s.to_string()),
|
||||
proplist: item.proplist.clone(),
|
||||
volume: item.volume,
|
||||
};
|
||||
*sink_info_option.borrow_mut() = Some(owned_item);
|
||||
}
|
||||
}
|
||||
});
|
||||
while op.get_state() == OperationState::Running {
|
||||
mainloop.iterate(false);
|
||||
}
|
||||
mainloop.quit(Retval(0));
|
||||
|
||||
if let Some(sink_info) = sink_info_option.borrow().as_ref() {
|
||||
let channels = sink_info.volume.len();
|
||||
if channels == 0 {
|
||||
return None;
|
||||
}
|
||||
let total: f64 = sink_info.volume.get().iter().map(|v| v.0 as f64).sum();
|
||||
let average_raw = total / channels as f64;
|
||||
let percent = ((average_raw / Volume::NORMAL.0 as f64) * 100.0).round() as u32;
|
||||
Some(percent)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -90,13 +90,13 @@ impl ksni::Tray for MyTray {
|
||||
let options = if allow_off {
|
||||
vec![
|
||||
("Off", 0x01),
|
||||
("ANC", 0x02),
|
||||
("Noise Cancellation", 0x02),
|
||||
("Transparency", 0x03),
|
||||
("Adaptive", 0x04),
|
||||
]
|
||||
} else {
|
||||
vec![
|
||||
("ANC", 0x02),
|
||||
("Noise Cancellation", 0x02),
|
||||
("Transparency", 0x03),
|
||||
("Adaptive", 0x04),
|
||||
]
|
||||
@@ -121,6 +121,7 @@ impl ksni::Tray for MyTray {
|
||||
..Default::default()
|
||||
}
|
||||
.into(),
|
||||
MenuItem::Separator,
|
||||
CheckmarkItem {
|
||||
label: "Conversation Detection".into(),
|
||||
checked: self.conversation_detect_enabled.unwrap_or(false),
|
||||
|
||||
Reference in New Issue
Block a user