diff --git a/.envrc b/.envrc new file mode 100644 index 000000000..3550a30f2 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake diff --git a/.gitignore b/.gitignore index c18fa3330..853ec03d0 100644 --- a/.gitignore +++ b/.gitignore @@ -660,5 +660,9 @@ obj/ # End of https://www.toptal.com/developers/gitignore/api/qt,c++,clion,kotlin,python,android,pycharm,androidstudio,visualstudiocode,linux linux/.qmlls.ini -# Nix build symlink +# Nix result +result-* + +# direnv +.direnv diff --git a/default.nix b/default.nix new file mode 100644 index 000000000..c48b1fb8c --- /dev/null +++ b/default.nix @@ -0,0 +1,12 @@ +(import ( + let + lock = builtins.fromJSON (builtins.readFile ./flake.lock); + nodeName = lock.nodes.root.inputs.flake-compat; + in + fetchTarball { + url = + lock.nodes.${nodeName}.locked.url + or "https://github.com/edolstra/flake-compat/archive/${lock.nodes.${nodeName}.locked.rev}.tar.gz"; + sha256 = lock.nodes.${nodeName}.locked.narHash; + } +) { src = ./.; }).defaultNix diff --git a/flake.lock b/flake.lock index c630f979b..f6f27ad4d 100644 --- a/flake.lock +++ b/flake.lock @@ -1,24 +1,140 @@ { "nodes": { + "crane": { + "locked": { + "lastModified": 1765145449, + "narHash": "sha256-aBVHGWWRzSpfL++LubA0CwOOQ64WNLegrYHwsVuVN7A=", + "owner": "ipetkov", + "repo": "crane", + "rev": "69f538cdce5955fcd47abfed4395dc6d5194c1c5", + "type": "github" + }, + "original": { + "owner": "ipetkov", + "repo": "crane", + "type": "github" + } + }, + "flake-compat": { + "locked": { + "lastModified": 1733328505, + "narHash": "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU=", + "rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec", + "revCount": 69, + "type": "tarball", + "url": "https://api.flakehub.com/f/pinned/edolstra/flake-compat/1.1.0/01948eb7-9cba-704f-bbf3-3fa956735b52/source.tar.gz" + }, + "original": { + "type": "tarball", + "url": "https://flakehub.com/f/edolstra/flake-compat/1.tar.gz" + } + }, + "flake-parts": { + "inputs": { + "nixpkgs-lib": "nixpkgs-lib" + }, + "locked": { + "lastModified": 1765495779, + "narHash": "sha256-MhA7wmo/7uogLxiewwRRmIax70g6q1U/YemqTGoFHlM=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "5635c32d666a59ec9a55cab87e898889869f7b71", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, "nixpkgs": { "locked": { - "lastModified": 1764517877, - "narHash": "sha256-pp3uT4hHijIC8JUK5MEqeAWmParJrgBVzHLNfJDZxg4=", + "lastModified": 1765425892, + "narHash": "sha256-jlQpSkg2sK6IJVzTQBDyRxQZgKADC2HKMRfGCSgNMHo=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "5d6bdbddb4695a62f0d00a3620b37a15275a5093", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-lib": { + "locked": { + "lastModified": 1761765539, + "narHash": "sha256-b0yj6kfvO8ApcSE+QmA6mUfu8IYG6/uU28OFn4PaC8M=", + "owner": "nix-community", + "repo": "nixpkgs.lib", + "rev": "719359f4562934ae99f5443f20aa06c2ffff91fc", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "nixpkgs.lib", + "type": "github" + } + }, + "nixpkgs_2": { + "locked": { + "lastModified": 1761236834, + "narHash": "sha256-+pthv6hrL5VLW2UqPdISGuLiUZ6SnAXdd2DdUE+fV2Q=", "owner": "nixos", "repo": "nixpkgs", - "rev": "2d293cbfa5a793b4c50d17c05ef9e385b90edf6c", + "rev": "d5faa84122bc0a1fd5d378492efce4e289f8eac1", "type": "github" }, "original": { "owner": "nixos", - "ref": "nixos-unstable", + "ref": "nixpkgs-unstable", "repo": "nixpkgs", "type": "github" } }, "root": { "inputs": { - "nixpkgs": "nixpkgs" + "crane": "crane", + "flake-compat": "flake-compat", + "flake-parts": "flake-parts", + "nixpkgs": "nixpkgs", + "systems": "systems", + "treefmt-nix": "treefmt-nix" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "treefmt-nix": { + "inputs": { + "nixpkgs": "nixpkgs_2" + }, + "locked": { + "lastModified": 1762938485, + "narHash": "sha256-AlEObg0syDl+Spi4LsZIBrjw+snSVU4T8MOeuZJUJjM=", + "owner": "numtide", + "repo": "treefmt-nix", + "rev": "5b4ee75aeefd1e2d5a1cc43cf6ba65eba75e83e4", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "treefmt-nix", + "type": "github" } } }, diff --git a/flake.nix b/flake.nix index 2caf7a53b..5994a9803 100644 --- a/flake.nix +++ b/flake.nix @@ -1,67 +1,141 @@ - { - description = "A Nix-flake-based Rust development environment"; + description = "AirPods liberated from Apple's ecosystem"; inputs = { - nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable"; + nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; + crane.url = "github:ipetkov/crane"; + flake-parts.url = "github:hercules-ci/flake-parts"; + flake-compat.url = "https://flakehub.com/f/edolstra/flake-compat/1.tar.gz"; + systems.url = "github:nix-systems/default"; + treefmt-nix.url = "github:numtide/treefmt-nix"; }; outputs = - { self, nixpkgs, ... }@inputs: - - let - supportedSystems = [ - "x86_64-linux" + inputs@{ + self, + crane, + flake-parts, + systems, + ... + }: + flake-parts.lib.mkFlake { inherit inputs; } { + systems = import systems; + imports = [ + inputs.treefmt-nix.flakeModule ]; - forAllSystems = nixpkgs.lib.genAttrs supportedSystems; - pkgsFor = system: import nixpkgs { - inherit system; - }; - in - { - packages = forAllSystems (system: let - pkgs = pkgsFor system; - in { - default = pkgs.rustPlatform.buildRustPackage rec { - name = "librepods"; - version = "0.1.0"; - - doCheck = false; - - nativeBuildInputs = with pkgs; [ - pkg-config - libpulseaudio - autoPatchelfHook - makeWrapper - ]; - - buildInputs = with pkgs; [ - dbus - libpulseaudio - wayland - - # From https://github.com/max-privatevoid/iced/blob/master/DEPENDENCIES.md - expat - fontconfig - freetype - freetype.dev - libGL - pkg-config - xorg.libX11 - xorg.libXcursor - xorg.libXi - xorg.libXrandr - wayland - libxkbcommon - ]; - - src = ./linux-rust; - cargoHash = "sha256-Ebqx+UU2tdygvqvDGjBSxbkmPnkR47/yL3sCVWo54CU="; - - postFixup = '' - wrapProgram $out/bin/librepods --suffix LD_LIBRARY_PATH : ${pkgs.lib.makeLibraryPath buildInputs} - ''; - }; - }); - }; + + perSystem = + { + self', + pkgs, + lib, + ... + }: + let + buildInputs = + with pkgs; + [ + dbus + libpulseaudio + alsa-lib + bluez + + # https://github.com/max-privatevoid/iced/blob/master/DEPENDENCIES.md + expat + fontconfig + freetype + freetype.dev + libGL + pkg-config + xorg.libX11 + xorg.libXcursor + xorg.libXi + xorg.libXrandr + wayland + libxkbcommon + vulkan-loader + ] + ++ pkgs.lib.optionals pkgs.stdenv.isDarwin [ + pkgs.libiconv + ]; + + nativeBuildInputs = with pkgs; [ + pkg-config + makeWrapper + ]; + + craneLib = crane.mkLib pkgs; + unfilteredRoot = ./linux-rust/.; + src = lib.fileset.toSource { + root = unfilteredRoot; + fileset = lib.fileset.unions [ + # Default files from crane (Rust and cargo files) + (craneLib.fileset.commonCargoSources unfilteredRoot) + (lib.fileset.maybeMissing ./linux-rust/assets/font) + ]; + }; + + commonArgs = { + inherit buildInputs nativeBuildInputs src; + strictDeps = true; + + # RUST_BACKTRACE = "1"; + }; + + librepods = craneLib.buildPackage ( + commonArgs + // { + cargoArtifacts = craneLib.buildDepsOnly commonArgs; + + doCheck = false; + + # Wrap the binary after build to set runtime library path + postInstall = '' + wrapProgram $out/bin/librepods \ + --prefix LD_LIBRARY_PATH : ${lib.makeLibraryPath buildInputs} + ''; + + meta = { + description = "AirPods liberated from Apple's ecosystem"; + homepage = "https://github.com/kavishdevar/librepods"; + license = pkgs.lib.licenses.gpl3Only; + maintainers = [ "kavishdevar" ]; + platforms = pkgs.lib.platforms.unix; + mainProgram = "librepods"; + }; + } + ); + in + { + checks = { + inherit librepods; + }; + + packages.default = librepods; + apps.default = { + type = "app"; + program = lib.getExe librepods; + }; + + devShells.default = craneLib.devShell { + name = "librepods-dev"; + checks = self'.checks; + + # NOTE: cargo and rustc are provided by default. + buildInputs = + with pkgs; + [ + rust-analyzer + ] + ++ buildInputs; + + LD_LIBRARY_PATH = lib.makeLibraryPath buildInputs; + }; + + treefmt = { + programs.nixfmt.enable = pkgs.lib.meta.availableOn pkgs.stdenv.buildPlatform pkgs.nixfmt-rfc-style.compiler; + programs.nixfmt.package = pkgs.nixfmt-rfc-style; + }; + }; + }; } diff --git a/linux-rust/src/bluetooth/aacp.rs b/linux-rust/src/bluetooth/aacp.rs index 78b79d39c..a2b6d491f 100644 --- a/linux-rust/src/bluetooth/aacp.rs +++ b/linux-rust/src/bluetooth/aacp.rs @@ -1,16 +1,19 @@ -use bluer::{l2cap::{SocketAddr, Socket, SeqPacket}, Address, AddressType, Result, Error}; -use std::time::Duration; -use log::{info, error, debug}; -use std::sync::Arc; -use tokio::sync::{Mutex, mpsc}; -use tokio::task::JoinSet; -use tokio::time::{sleep, Instant}; -use std::collections::HashMap; -use serde::{Deserialize, Serialize}; -use serde_json; use crate::devices::airpods::AirPodsInformation; use crate::devices::enums::{DeviceData, DeviceInformation, DeviceType}; use crate::utils::get_devices_path; +use bluer::{ + Address, AddressType, Error, Result, + l2cap::{SeqPacket, Socket, SocketAddr}, +}; +use log::{debug, error, info}; +use serde::{Deserialize, Serialize}; +use serde_json; +use std::collections::HashMap; +use std::sync::Arc; +use std::time::Duration; +use tokio::sync::{Mutex, mpsc}; +use tokio::task::JoinSet; +use tokio::time::{Instant, sleep}; const PSM: u16 = 0x1001; const CONNECT_TIMEOUT: Duration = Duration::from_secs(10); @@ -218,7 +221,7 @@ pub enum BatteryComponent { Headphone = 1, Left = 4, Right = 2, - Case = 8 + Case = 8, } #[repr(u8)] @@ -226,7 +229,7 @@ pub enum BatteryComponent { pub enum BatteryStatus { Charging = 1, NotCharging = 2, - Disconnected = 4 + Disconnected = 4, } #[repr(u8)] @@ -235,7 +238,7 @@ pub enum EarDetectionStatus { InEar = 0x00, OutOfEar = 0x01, InCase = 0x02, - Disconnected = 0x03 + Disconnected = 0x03, } impl AudioSourceType { @@ -291,7 +294,8 @@ pub struct AirPodsLEKeys { pub struct AACPManagerState { pub sender: Option>>, pub control_command_status_list: Vec, - pub control_command_subscribers: HashMap>>>, + pub control_command_subscribers: + HashMap>>>, pub owns: bool, pub old_connected_devices: Vec, pub connected_devices: Vec, @@ -307,11 +311,10 @@ pub struct AACPManagerState { impl AACPManagerState { fn new() -> Self { - let devices: HashMap = - std::fs::read_to_string(get_devices_path()) - .ok() - .and_then(|s| serde_json::from_str(&s).ok()) - .unwrap_or_default(); + let devices: HashMap = std::fs::read_to_string(get_devices_path()) + .ok() + .and_then(|s| serde_json::from_str(&s).ok()) + .unwrap_or_default(); AACPManagerState { sender: None, control_command_status_list: Vec::new(), @@ -362,17 +365,18 @@ impl AACPManager { } }; - let seq_packet = match tokio::time::timeout(CONNECT_TIMEOUT, socket.connect(target_sa)).await { - Ok(Ok(s)) => Arc::new(s), - Ok(Err(e)) => { - error!("L2CAP connect failed: {}", e); - return; - } - Err(_) => { - error!("L2CAP connect timed out"); - return; - } - }; + let seq_packet = + match tokio::time::timeout(CONNECT_TIMEOUT, socket.connect(target_sa)).await { + Ok(Ok(s)) => Arc::new(s), + Ok(Err(e)) => { + error!("L2CAP connect failed: {}", e); + return; + } + Err(_) => { + error!("L2CAP connect timed out"); + return; + } + }; // Wait for connection to be fully established let start = Instant::now(); @@ -381,7 +385,8 @@ impl AACPManager { Ok(peer) if peer.cid != 0 => break, Ok(_) => { /* still waiting */ } Err(e) => { - if e.raw_os_error() == Some(107) { // ENOTCONN + if e.raw_os_error() == Some(107) { + // ENOTCONN error!("Peer has disconnected during connection setup."); return; } @@ -438,19 +443,40 @@ impl AACPManager { let mut state = self.state.lock().await; state.event_tx = Some(tx); } - - pub async fn subscribe_to_control_command(&self, identifier: ControlCommandIdentifiers, tx: mpsc::UnboundedSender>) { + + pub async fn subscribe_to_control_command( + &self, + identifier: ControlCommandIdentifiers, + tx: mpsc::UnboundedSender>, + ) { let mut state = self.state.lock().await; - state.control_command_subscribers.entry(identifier).or_default().push(tx); + state + .control_command_subscribers + .entry(identifier) + .or_default() + .push(tx); // send initial value if available - if let Some(status) = state.control_command_status_list.iter().find(|s| s.identifier == identifier) { - let _ = state.control_command_subscribers.get(&identifier).unwrap().last().unwrap().send(status.value.clone()); + if let Some(status) = state + .control_command_status_list + .iter() + .find(|s| s.identifier == identifier) + { + let _ = state + .control_command_subscribers + .get(&identifier) + .unwrap() + .last() + .unwrap() + .send(status.value.clone()); } } pub async fn receive_packet(&self, packet: &[u8]) { if !packet.starts_with(&HEADER_BYTES) { - debug!("Received packet does not start with expected header: {}", hex::encode(packet)); + debug!( + "Received packet does not start with expected header: {}", + hex::encode(packet) + ); return; } if packet.len() < 5 { @@ -469,7 +495,10 @@ impl AACPManager { } let count = payload[2] as usize; if payload.len() < 3 + count * 5 { - error!("Battery Info packet length mismatch: {}", hex::encode(payload)); + error!( + "Battery Info packet length mismatch: {}", + hex::encode(payload) + ); return; } let mut batteries = Vec::with_capacity(count); @@ -495,7 +524,7 @@ impl AACPManager { error!("Unknown battery status: {:#04x}", payload[base_index + 3]); continue; } - } + }, }); } let mut state = self.state.lock().await; @@ -520,9 +549,16 @@ impl AACPManager { }; if let Some(identifier) = ControlCommandIdentifiers::from_u8(identifier_byte) { - let status = ControlCommandStatus { identifier, value: value.clone() }; + let status = ControlCommandStatus { + identifier, + value: value.clone(), + }; let mut state = self.state.lock().await; - if let Some(existing) = state.control_command_status_list.iter_mut().find(|s| s.identifier == identifier) { + if let Some(existing) = state + .control_command_status_list + .iter_mut() + .find(|s| s.identifier == identifier) + { existing.value = value.clone(); } else { state.control_command_status_list.push(status.clone()); @@ -538,9 +574,16 @@ impl AACPManager { if let Some(ref tx) = state.event_tx { let _ = tx.send(AACPEvent::ControlCommand(status)); } - info!("Received Control Command: {:?}, value: {}", identifier, hex::encode(&value)); + info!( + "Received Control Command: {:?}, value: {}", + identifier, + hex::encode(&value) + ); } else { - error!("Unknown Control Command identifier: {:#04x}", identifier_byte); + error!( + "Unknown Control Command identifier: {:#04x}", + identifier_byte + ); } } opcodes::EAR_DETECTION => { @@ -570,12 +613,21 @@ impl AACPManager { let mut state = self.state.lock().await; state.old_ear_detection_status = state.ear_detection_status.clone(); state.ear_detection_status = statuses.clone(); - + if let Some(ref tx) = state.event_tx { - debug!("Sending Ear Detection event: old: {:?}, new: {:?}", state.old_ear_detection_status, statuses); - let _ = tx.send(AACPEvent::EarDetection(state.old_ear_detection_status.clone(), statuses)); + debug!( + "Sending Ear Detection event: old: {:?}, new: {:?}", + state.old_ear_detection_status, statuses + ); + let _ = tx.send(AACPEvent::EarDetection( + state.old_ear_detection_status.clone(), + statuses, + )); } - info!("Received Ear Detection Status: {:?}", state.ear_detection_status); + info!( + "Received Ear Detection Status: {:?}", + state.ear_detection_status + ); } opcodes::CONVERSATION_AWARENESS => { if packet.len() == 10 { @@ -587,7 +639,10 @@ impl AACPManager { } info!("Received Conversation Awareness: {}", status); } else { - info!("Received Conversation Awareness packet with unexpected length: {}", packet.len()); + info!( + "Received Conversation Awareness packet with unexpected length: {}", + packet.len() + ); } } opcodes::INFORMATION => { @@ -619,7 +674,7 @@ impl AACPManager { } strings.remove(0); let info = AirPodsInformation { - name: strings.get(0).cloned().unwrap_or_default(), + name: strings.first().cloned().unwrap_or_default(), model_number: strings.get(1).cloned().unwrap_or_default(), manufacturer: strings.get(2).cloned().unwrap_or_default(), serial_number: strings.get(3).cloned().unwrap_or_default(), @@ -636,28 +691,31 @@ impl AACPManager { }, }; let mut state = self.state.lock().await; - if let Some(mac) = state.airpods_mac { - if let Some(device_data) = state.devices.get_mut(&mac.to_string()) { - device_data.name = info.name.clone(); - device_data.information = Some(DeviceInformation::AirPods(info.clone())); - } + if let Some(mac) = state.airpods_mac + && let Some(device_data) = state.devices.get_mut(&mac.to_string()) + { + device_data.name = info.name.clone(); + device_data.information = Some(DeviceInformation::AirPods(info.clone())); } let json = serde_json::to_string(&state.devices).unwrap(); - if let Some(parent) = get_devices_path().parent() { - if let Err(e) = tokio::fs::create_dir_all(&parent).await { - error!("Failed to create directory for devices: {}", e); - return; - } + if let Some(parent) = get_devices_path().parent() + && let Err(e) = tokio::fs::create_dir_all(&parent).await + { + error!("Failed to create directory for devices: {}", e); + return; } if let Err(e) = tokio::fs::write(&get_devices_path(), json).await { error!("Failed to save devices: {}", e); } info!("Received Information: {:?}", info); - }, + } opcodes::PROXIMITY_KEYS_RSP => { if payload.len() < 4 { - error!("Proximity Keys Response packet too short: {}", hex::encode(payload)); + error!( + "Proximity Keys Response packet too short: {}", + hex::encode(payload) + ); return; } let key_count = payload[2] as usize; @@ -666,67 +724,77 @@ impl AACPManager { let mut keys = Vec::new(); for _ in 0..key_count { if offset + 3 >= payload.len() { - error!("Proximity Keys Response packet too short while parsing keys: {}", hex::encode(payload)); + error!( + "Proximity Keys Response packet too short while parsing keys: {}", + hex::encode(payload) + ); return; } let key_type = payload[offset]; let key_length = payload[offset + 2] as usize; offset += 4; if offset + key_length > payload.len() { - error!("Proximity Keys Response packet too short for key data: {}", hex::encode(payload)); + error!( + "Proximity Keys Response packet too short for key data: {}", + hex::encode(payload) + ); return; } let key_data = payload[offset..offset + key_length].to_vec(); keys.push((key_type, key_data)); offset += key_length; } - info!("Received Proximity Keys Response: {:?}", keys.iter().map(|(kt, kd)| (kt, hex::encode(kd))).collect::>()); + info!( + "Received Proximity Keys Response: {:?}", + keys.iter() + .map(|(kt, kd)| (kt, hex::encode(kd))) + .collect::>() + ); let mut state = self.state.lock().await; for (key_type, key_data) in &keys { - if let Some(kt) = ProximityKeyType::from_u8(*key_type) { - if let Some(mac) = state.airpods_mac { - let mac_str = mac.to_string(); - let device_data = state.devices.entry(mac_str.clone()).or_insert(DeviceData { + if let Some(kt) = ProximityKeyType::from_u8(*key_type) + && let Some(mac) = state.airpods_mac + { + let mac_str = mac.to_string(); + let device_data = + state.devices.entry(mac_str.clone()).or_insert(DeviceData { name: mac_str.clone(), type_: DeviceType::AirPods, information: None, }); - match kt { - ProximityKeyType::Irk => { - match device_data.information.as_mut() { - Some(DeviceInformation::AirPods(info)) => { - info.le_keys.irk = hex::encode(key_data); - } - _ => { - error!("Device information is not AirPods for adding LE IRK."); - } - } + match kt { + ProximityKeyType::Irk => match device_data.information.as_mut() { + Some(DeviceInformation::AirPods(info)) => { + info.le_keys.irk = hex::encode(key_data); } - ProximityKeyType::EncKey => { - match device_data.information.as_mut() { - Some(DeviceInformation::AirPods(info)) => { - info.le_keys.enc_key = hex::encode(key_data); - } - _ => { - error!("Device information is not AirPods for adding LE encryption key."); - } - } + _ => { + error!("Device information is not AirPods for adding LE IRK."); } - } + }, + ProximityKeyType::EncKey => match device_data.information.as_mut() { + Some(DeviceInformation::AirPods(info)) => { + info.le_keys.enc_key = hex::encode(key_data); + } + _ => { + error!( + "Device information is not AirPods for adding LE encryption key." + ); + } + }, } } } let json = serde_json::to_string(&state.devices).unwrap(); - if let Some(parent) = get_devices_path().parent() { - if let Err(e) = tokio::fs::create_dir_all(&parent).await { - error!("Failed to create directory for devices: {}", e); - return; - } + if let Some(parent) = get_devices_path().parent() + && let Err(e) = tokio::fs::create_dir_all(&parent).await + { + error!("Failed to create directory for devices: {}", e); + return; } if let Err(e) = tokio::fs::write(&get_devices_path(), json).await { error!("Failed to save devices: {}", e); } - }, + } opcodes::STEM_PRESS => info!("Received Stem Press packet."), opcodes::AUDIO_SOURCE => { if payload.len() < 9 { @@ -748,12 +816,18 @@ impl AACPManager { } opcodes::CONNECTED_DEVICES => { if payload.len() < 3 { - error!("Connected Devices packet too short: {}", hex::encode(payload)); + error!( + "Connected Devices packet too short: {}", + hex::encode(payload) + ); return; } let count = payload[2] as usize; if payload.len() < 3 + count * 8 { - error!("Connected Devices packet length mismatch: {}", hex::encode(payload)); + error!( + "Connected Devices packet length mismatch: {}", + hex::encode(payload) + ); return; } let mut devices = Vec::with_capacity(count); @@ -761,17 +835,30 @@ impl AACPManager { let base = 5 + i * 8; let mac = format!( "{:02X}:{:02X}:{:02X}:{:02X}:{:02X}:{:02X}", - payload[base], payload[base + 1], payload[base + 2], payload[base + 3], payload[base + 4], payload[base + 5] + payload[base], + payload[base + 1], + payload[base + 2], + payload[base + 3], + payload[base + 4], + payload[base + 5] ); let info1 = payload[base + 6]; let info2 = payload[base + 7]; - devices.push(ConnectedDevice { mac, info1, info2, r#type: None }); + devices.push(ConnectedDevice { + mac, + info1, + info2, + r#type: None, + }); } let mut state = self.state.lock().await; state.old_connected_devices = state.connected_devices.clone(); state.connected_devices = devices.clone(); if let Some(ref tx) = state.event_tx { - let _ = tx.send(AACPEvent::ConnectedDevices(state.old_connected_devices.clone(), devices)); + let _ = tx.send(AACPEvent::ConnectedDevices( + state.old_connected_devices.clone(), + devices, + )); } info!("Received Connected Devices: {:?}", state.connected_devices); } @@ -786,7 +873,7 @@ impl AACPManager { } } opcodes::EQ_DATA => { - debug!("Received EQ Data"); + debug!("Received EQ Data"); } _ => debug!("Received unknown packet with opcode {:#04x}", opcode), } @@ -809,17 +896,18 @@ impl AACPManager { pub async fn send_handshake(&self) -> Result<()> { let packet = [ - 0x00, 0x00, 0x04, 0x00, - 0x01, 0x00, 0x02, 0x00, - 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00 + 0x00, 0x00, 0x04, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, ]; self.send_packet(&packet).await } - pub async fn send_proximity_keys_request(&self, key_types: Vec) -> Result<()> { + pub async fn send_proximity_keys_request( + &self, + key_types: Vec, + ) -> Result<()> { let opcode = [opcodes::PROXIMITY_KEYS_REQ, 0x00]; - let mut data = Vec::with_capacity( 2); + let mut data = Vec::with_capacity(2); data.push(key_types.iter().fold(0u8, |acc, kt| acc | (*kt as u8))); data.push(0x00); let packet = [opcode.as_slice(), data.as_slice()].concat(); @@ -837,8 +925,12 @@ impl AACPManager { packet.extend_from_slice(name_bytes); self.send_data_packet(&packet).await } - - pub async fn send_control_command(&self, identifier: ControlCommandIdentifiers, value: &[u8]) -> Result<()> { + + pub async fn send_control_command( + &self, + identifier: ControlCommandIdentifiers, + value: &[u8], + ) -> Result<()> { let opcode = [opcodes::CONTROL_COMMAND, 0x00]; let mut data = vec![identifier as u8]; for i in 0..4 { @@ -848,10 +940,17 @@ impl AACPManager { self.send_data_packet(&packet).await } - pub async fn send_media_information_new_device(&self, self_mac_address: &str, target_mac_address: &str) -> Result<()> { + pub async fn send_media_information_new_device( + &self, + self_mac_address: &str, + target_mac_address: &str, + ) -> Result<()> { let opcode = [opcodes::SMART_ROUTING, 0x00]; let mut buffer = Vec::with_capacity(112); - let target_mac_bytes: Vec = target_mac_address.split(':').map(|s| u8::from_str_radix(s, 16).unwrap()).collect(); + let target_mac_bytes: Vec = target_mac_address + .split(':') + .map(|s| u8::from_str_radix(s, 16).unwrap()) + .collect(); buffer.extend_from_slice(&target_mac_bytes.iter().rev().cloned().collect::>()); buffer.extend_from_slice(&[0x68, 0x00]); @@ -883,7 +982,10 @@ impl AACPManager { pub async fn send_hijack_request(&self, target_mac_address: &str) -> Result<()> { let opcode = [opcodes::SMART_ROUTING, 0x00]; let mut buffer = Vec::with_capacity(106); - let target_mac_bytes: Vec = target_mac_address.split(':').map(|s| u8::from_str_radix(s, 16).unwrap()).collect(); + let target_mac_bytes: Vec = target_mac_address + .split(':') + .map(|s| u8::from_str_radix(s, 16).unwrap()) + .collect(); buffer.extend_from_slice(&target_mac_bytes.iter().rev().cloned().collect::>()); buffer.extend_from_slice(&[0x62, 0x00]); buffer.extend_from_slice(&[0x01, 0xE5]); @@ -911,10 +1013,18 @@ impl AACPManager { self.send_data_packet(&packet).await } - pub async fn send_media_information(&self, self_mac_address: &str, target_mac_address: &str, streaming_state: bool) -> Result<()> { + pub async fn send_media_information( + &self, + self_mac_address: &str, + target_mac_address: &str, + streaming_state: bool, + ) -> Result<()> { let opcode = [opcodes::SMART_ROUTING, 0x00]; let mut buffer = Vec::with_capacity(138); - let target_mac_bytes: Vec = target_mac_address.split(':').map(|s| u8::from_str_radix(s, 16).unwrap()).collect(); + let target_mac_bytes: Vec = target_mac_address + .split(':') + .map(|s| u8::from_str_radix(s, 16).unwrap()) + .collect(); buffer.extend_from_slice(&target_mac_bytes.iter().rev().cloned().collect::>()); buffer.extend_from_slice(&[0x82, 0x00]); buffer.extend_from_slice(&[0x01, 0xE5, 0x4A]); @@ -947,7 +1057,10 @@ impl AACPManager { pub async fn send_smart_routing_show_ui(&self, target_mac_address: &str) -> Result<()> { let opcode = [opcodes::SMART_ROUTING, 0x00]; let mut buffer = Vec::with_capacity(134); - let target_mac_bytes: Vec = target_mac_address.split(':').map(|s| u8::from_str_radix(s, 16).unwrap()).collect(); + let target_mac_bytes: Vec = target_mac_address + .split(':') + .map(|s| u8::from_str_radix(s, 16).unwrap()) + .collect(); buffer.extend_from_slice(&target_mac_bytes.iter().rev().cloned().collect::>()); buffer.extend_from_slice(&[0x7E, 0x00]); buffer.extend_from_slice(&[0x01, 0xE6, 0x5B]); @@ -980,7 +1093,10 @@ impl AACPManager { pub async fn send_hijack_reversed(&self, target_mac_address: &str) -> Result<()> { let opcode = [opcodes::SMART_ROUTING, 0x00]; let mut buffer = Vec::with_capacity(97); - let target_mac_bytes: Vec = target_mac_address.split(':').map(|s| u8::from_str_radix(s, 16).unwrap()).collect(); + let target_mac_bytes: Vec = target_mac_address + .split(':') + .map(|s| u8::from_str_radix(s, 16).unwrap()) + .collect(); buffer.extend_from_slice(&target_mac_bytes.iter().rev().cloned().collect::>()); buffer.extend_from_slice(&[0x59, 0x00]); buffer.extend_from_slice(&[0x01, 0xE3]); @@ -1003,10 +1119,17 @@ impl AACPManager { self.send_data_packet(&packet).await } - pub async fn send_add_tipi_device(&self, self_mac_address: &str, target_mac_address: &str) -> Result<()> { + pub async fn send_add_tipi_device( + &self, + self_mac_address: &str, + target_mac_address: &str, + ) -> Result<()> { let opcode = [opcodes::SMART_ROUTING, 0x00]; let mut buffer = Vec::with_capacity(86); - let target_mac_bytes: Vec = target_mac_address.split(':').map(|s| u8::from_str_radix(s, 16).unwrap()).collect(); + let target_mac_bytes: Vec = target_mac_address + .split(':') + .map(|s| u8::from_str_radix(s, 16).unwrap()) + .collect(); buffer.extend_from_slice(&target_mac_bytes.iter().rev().cloned().collect::>()); buffer.extend_from_slice(&[0x4E, 0x00]); buffer.extend_from_slice(&[0x01, 0xE5]); @@ -1031,10 +1154,8 @@ impl AACPManager { } pub async fn send_some_packet(&self) -> Result<()> { - self.send_data_packet(&[ - 0x29, 0x00, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF - ]).await + self.send_data_packet(&[0x29, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF]) + .await } } @@ -1053,7 +1174,9 @@ async fn recv_thread(manager: AACPManager, sp: Arc) { } Err(e) => { error!("Read error: {}", e); - debug!("We have probably disconnected, clearing state variables (owns=false, connected_devices=empty, control_command_status_list=empty)."); + debug!( + "We have probably disconnected, clearing state variables (owns=false, connected_devices=empty, control_command_status_list=empty)." + ); let mut state = manager.state.lock().await; state.owns = false; state.connected_devices.clear(); diff --git a/linux-rust/src/bluetooth/att.rs b/linux-rust/src/bluetooth/att.rs index 88fd37e7b..767abf490 100644 --- a/linux-rust/src/bluetooth/att.rs +++ b/linux-rust/src/bluetooth/att.rs @@ -1,12 +1,12 @@ -use bluer::l2cap::{SocketAddr, Socket, SeqPacket}; -use bluer::{Address, AddressType, Result, Error}; -use log::{info, error, debug}; +use bluer::l2cap::{SeqPacket, Socket, SocketAddr}; +use bluer::{Address, AddressType, Error, Result}; +use hex; +use log::{debug, error, info}; +use std::collections::HashMap; use std::sync::Arc; use tokio::sync::{Mutex, mpsc}; use tokio::task::JoinSet; -use tokio::time::{sleep, Duration, Instant}; -use std::collections::HashMap; -use hex; +use tokio::time::{Duration, Instant, sleep}; const PSM_ATT: u16 = 0x001F; const CONNECT_TIMEOUT: Duration = Duration::from_secs(10); @@ -25,7 +25,7 @@ pub enum ATTHandles { AirPodsLoudSoundReduction = 0x1B, AirPodsHearingAid = 0x2A, NothingEverything = 0x8002, - NothingEverythingRead = 0x8005 // for some reason, and not the same as the write handle + NothingEverythingRead = 0x8005, // for some reason, and not the same as the write handle } #[repr(u16)] @@ -43,7 +43,7 @@ impl From for ATTCCCDHandles { ATTHandles::AirPodsLoudSoundReduction => ATTCCCDHandles::LoudSoundReduction, ATTHandles::AirPodsHearingAid => ATTCCCDHandles::HearingAid, ATTHandles::NothingEverything => panic!("No CCCD for NothingEverything handle"), // we don't request it - ATTHandles::NothingEverythingRead => panic!("No CCD for NothingEverythingRead handle") // it sends notifications without CCCD + ATTHandles::NothingEverythingRead => panic!("No CCD for NothingEverythingRead handle"), // it sends notifications without CCCD } } } @@ -57,7 +57,7 @@ impl ATTManagerState { fn new() -> Self { ATTManagerState { sender: None, - listeners: HashMap::new() + listeners: HashMap::new(), } } } @@ -82,11 +82,15 @@ impl ATTManager { } pub async fn connect(&mut self, addr: Address) -> Result<()> { - info!("ATTManager connecting to {} on PSM {:#06X}...", addr, PSM_ATT); + info!( + "ATTManager connecting to {} on PSM {:#06X}...", + addr, PSM_ATT + ); let target_sa = SocketAddr::new(addr, AddressType::BrEdr, PSM_ATT); let socket = Socket::new_seq_packet()?; - let seq_packet_result = tokio::time::timeout(CONNECT_TIMEOUT, socket.connect(target_sa)).await; + let seq_packet_result = + tokio::time::timeout(CONNECT_TIMEOUT, socket.connect(target_sa)).await; let seq_packet = match seq_packet_result { Ok(Ok(s)) => Arc::new(s), Ok(Err(e)) => { @@ -95,7 +99,10 @@ impl ATTManager { } Err(_) => { error!("L2CAP connect timed out"); - return Err(Error::from(std::io::Error::new(std::io::ErrorKind::TimedOut, "Connection timeout"))); + return Err(Error::from(std::io::Error::new( + std::io::ErrorKind::TimedOut, + "Connection timeout", + ))); } }; @@ -106,7 +113,8 @@ impl ATTManager { Ok(peer) if peer.cid != 0 => break, Ok(_) => {} Err(e) => { - if e.raw_os_error() == Some(107) { // ENOTCONN + if e.raw_os_error() == Some(107) { + // ENOTCONN error!("Peer has disconnected during connection setup."); return Err(e.into()); } @@ -115,7 +123,10 @@ impl ATTManager { } if start.elapsed() >= CONNECT_TIMEOUT { error!("Timed out waiting for L2CAP connection to be fully established."); - return Err(Error::from(std::io::Error::new(std::io::ErrorKind::TimedOut, "Connection timeout"))); + return Err(Error::from(std::io::Error::new( + std::io::ErrorKind::TimedOut, + "Connection timeout", + ))); } sleep(POLL_INTERVAL).await; } @@ -180,11 +191,17 @@ impl ATTManager { if let Some(sender) = &state.sender { sender.send(data.to_vec()).await.map_err(|e| { error!("Failed to send packet to channel: {}", e); - Error::from(std::io::Error::new(std::io::ErrorKind::NotConnected, "L2CAP send channel closed")) + Error::from(std::io::Error::new( + std::io::ErrorKind::NotConnected, + "L2CAP send channel closed", + )) }) } else { error!("Cannot send packet, sender is not available."); - Err(Error::from(std::io::Error::new(std::io::ErrorKind::NotConnected, "L2CAP stream not connected"))) + Err(Error::from(std::io::Error::new( + std::io::ErrorKind::NotConnected, + "L2CAP stream not connected", + ))) } } @@ -195,11 +212,11 @@ impl ATTManager { Ok(Some(resp)) => Ok(resp), Ok(None) => Err(Error::from(std::io::Error::new( std::io::ErrorKind::UnexpectedEof, - "Response channel closed" + "Response channel closed", ))), Err(_) => Err(Error::from(std::io::Error::new( std::io::ErrorKind::TimedOut, - "Response timeout" + "Response timeout", ))), } } diff --git a/linux-rust/src/bluetooth/discovery.rs b/linux-rust/src/bluetooth/discovery.rs index a0d4c78b2..22c401099 100644 --- a/linux-rust/src/bluetooth/discovery.rs +++ b/linux-rust/src/bluetooth/discovery.rs @@ -1,6 +1,6 @@ -use std::io::Error; use bluer::Adapter; use log::debug; +use std::io::Error; pub(crate) async fn find_connected_airpods(adapter: &Adapter) -> bluer::Result { let target_uuid = uuid::Uuid::parse_str("74ec2172-0bad-4d01-8f77-997b2be0722a").unwrap(); @@ -8,20 +8,24 @@ pub(crate) async fn find_connected_airpods(adapter: &Adapter) -> bluer::Result) -> bluer::Result> { +pub async fn find_other_managed_devices( + adapter: &Adapter, + managed_macs: Vec, +) -> bluer::Result> { let addrs = adapter.device_addresses().await?; let mut devices = Vec::new(); for addr in addrs { @@ -38,5 +42,8 @@ pub async fn find_other_managed_devices(adapter: &Adapter, managed_macs: Vec [u8; 16] { let cipher = Aes128::new(&GenericArray::from(*key)); @@ -24,7 +24,8 @@ fn decrypt(key: &[u8; 16], data: &[u8; 16]) -> [u8; 16] { } fn verify_rpa(addr: &str, irk: &[u8; 16]) -> bool { - let rpa: Vec = addr.split(':') + let rpa: Vec = addr + .split(':') .map(|s| u8::from_str_radix(s, 16).unwrap()) .collect::>() .into_iter() @@ -38,7 +39,10 @@ fn verify_rpa(addr: &str, irk: &[u8; 16]) -> bool { let hash_slice = &rpa[0..3]; let hash: [u8; 3] = hash_slice.try_into().unwrap(); let computed_hash = ah(irk, &prand); - debug!("Verifying RPA: addr={}, hash={:?}, computed_hash={:?}", addr, hash, computed_hash); + debug!( + "Verifying RPA: addr={}, hash={:?}, computed_hash={:?}", + addr, hash, computed_hash + ); hash == computed_hash } @@ -47,20 +51,19 @@ pub async fn start_le_monitor(tray_handle: Option>) -> blue let adapter = session.default_adapter().await?; adapter.set_powered(true).await?; - let all_devices: HashMap = - std::fs::read_to_string(get_devices_path()) - .ok() - .and_then(|s| serde_json::from_str(&s).ok()) - .unwrap_or_default(); + let all_devices: HashMap = std::fs::read_to_string(get_devices_path()) + .ok() + .and_then(|s| serde_json::from_str(&s).ok()) + .unwrap_or_default(); let mut verified_macs: HashMap = HashMap::new(); let mut failed_macs: HashSet
= HashSet::new(); let connecting_macs = Arc::new(Mutex::new(HashSet::
::new())); let pattern = Pattern { - data_type: 0xFF, // Manufacturer specific data + data_type: 0xFF, // Manufacturer specific data start_position: 0, - content: vec![0x4C, 0x00], // Apple manufacturer ID (76) in LE + content: vec![0x4C, 0x00], // Apple manufacturer ID (76) in LE }; let mm = adapter.monitor().await?; @@ -97,20 +100,24 @@ pub async fn start_le_monitor(tray_handle: Option>) -> blue debug!("Checking RPA for device: {}", addr_str); let mut found_mac = None; for (airpods_mac, device_data) in &all_devices { - if device_data.type_ == DeviceType::AirPods { - if let Some(DeviceInformation::AirPods(info)) = &device_data.information { - if let Ok(irk_bytes) = hex::decode(&info.le_keys.irk) { - if irk_bytes.len() == 16 { - let irk: [u8; 16] = irk_bytes.as_slice().try_into().unwrap(); - debug!("Verifying RPA {} for airpods MAC {} with IRK {}", addr_str, airpods_mac, info.le_keys.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 device_data.type_ == DeviceType::AirPods + && let Some(DeviceInformation::AirPods(info)) = &device_data.information + && let Ok(irk_bytes) = hex::decode(&info.le_keys.irk) + && irk_bytes.len() == 16 + { + let irk: [u8; 16] = irk_bytes.as_slice().try_into().unwrap(); + debug!( + "Verifying RPA {} for airpods MAC {} with IRK {}", + addr_str, airpods_mac, info.le_keys.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; } } } @@ -124,16 +131,13 @@ pub async fn start_le_monitor(tray_handle: Option>) -> blue } } - if let Some(ref mac) = matched_airpods_mac { - if let Some(device_data) = all_devices.get(mac) { - if let Some(DeviceInformation::AirPods(info)) = &device_data.information { - if let Ok(enc_key_bytes) = hex::decode(&info.le_keys.enc_key) { - if enc_key_bytes.len() == 16 { - matched_enc_key = Some(enc_key_bytes.as_slice().try_into().unwrap()); - } - } - } - } + if let Some(ref mac) = matched_airpods_mac + && let Some(device_data) = all_devices.get(mac) + && let Some(DeviceInformation::AirPods(info)) = &device_data.information + && let Ok(enc_key_bytes) = hex::decode(&info.le_keys.enc_key) + && enc_key_bytes.len() == 16 + { + matched_enc_key = Some(enc_key_bytes.as_slice().try_into().unwrap()); } if matched_airpods_mac.is_some() { @@ -144,123 +148,224 @@ pub async fn start_le_monitor(tray_handle: Option>) -> blue while let Some(ev) = events.next().await { match ev { bluer::DeviceEvent::PropertyChanged(prop) => { - match prop { - bluer::DeviceProperty::ManufacturerData(data) => { - 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 airpods_mac {}: {}", - matched_airpods_mac.as_ref().unwrap_or(&"unknown".to_string()), - hex::encode(&decrypted)); + if let bluer::DeviceProperty::ManufacturerData(data) = prop { + if let Some(enc_key) = &matched_enc_key + && let Some(apple_data) = data.get(&76) + && 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 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> = - 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 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 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, + > = 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 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; + let xor_factor = primary_left ^ this_in_case; + let is_left_in_ear = if xor_factor { + (status & 0x02) != 0 + } else { + (status & 0x08) != 0 + }; + let is_right_in_ear = if xor_factor { + (status & 0x08) != 0 + } else { + (status & 0x02) != 0 + }; + let is_flipped = !primary_left; + + let left_byte_index = if is_flipped { 2 } else { 1 }; + let right_byte_index = if is_flipped { 1 } else { 2 }; + + let left_byte = decrypted[left_byte_index] as i32; + let right_byte = decrypted[right_byte_index] as i32; + let case_byte = decrypted[3] as i32; + + let (left_battery, left_charging) = if left_byte == 0xff { + (0, false) + } else { + (left_byte & 0x7F, (left_byte & 0x80) != 0) + }; + let (right_battery, right_charging) = if right_byte == 0xff + { + (0, false) + } else { + (right_byte & 0x7F, (right_byte & 0x80) != 0) + }; + let (case_battery, case_charging) = if case_byte == 0xff { + (0, false) + } else { + (case_byte & 0x7F, (case_byte & 0x80) != 0) + }; - let status = apple_data[5] as usize; - let primary_left = (status >> 5) & 0x01 == 1; - let this_in_case = (status >> 6) & 0x01 == 1; - let xor_factor = primary_left ^ this_in_case; - let is_left_in_ear = if xor_factor { (status & 0x02) != 0 } else { (status & 0x08) != 0 }; - let is_right_in_ear = if xor_factor { (status & 0x08) != 0 } else { (status & 0x02) != 0 }; - let is_flipped = !primary_left; - - let left_byte_index = if is_flipped { 2 } else { 1 }; - let right_byte_index = if is_flipped { 1 } else { 2 }; - - let left_byte = decrypted[left_byte_index] as i32; - let right_byte = decrypted[right_byte_index] as i32; - let case_byte = decrypted[3] as i32; - - let (left_battery, left_charging) = if left_byte == 0xff { - (0, false) + if let Some(handle) = &tray_handle_clone { + handle + .update(|tray: &mut MyTray| { + tray.battery_l = if left_byte == 0xff { + None } else { - (left_byte & 0x7F, (left_byte & 0x80) != 0) + Some(left_battery as u8) }; - let (right_battery, right_charging) = if right_byte == 0xff { - (0, false) + tray.battery_l_status = if left_byte == 0xff { + Some(BatteryStatus::Disconnected) + } else if left_charging { + Some(BatteryStatus::Charging) } else { - (right_byte & 0x7F, (right_byte & 0x80) != 0) + Some(BatteryStatus::NotCharging) }; - let (case_battery, case_charging) = if case_byte == 0xff { - (0, false) + tray.battery_r = if right_byte == 0xff { + None } else { - (case_byte & 0x7F, (case_byte & 0x80) != 0) + Some(right_battery as u8) }; - - if let Some(handle) = &tray_handle_clone { - handle.update(|tray: &mut MyTray| { - tray.battery_l = if left_byte == 0xff { None } else { Some(left_battery as u8) }; - tray.battery_l_status = if left_byte == 0xff { Some(BatteryStatus::Disconnected) } else if left_charging { Some(BatteryStatus::Charging) } else { Some(BatteryStatus::NotCharging) }; - tray.battery_r = if right_byte == 0xff { None } else { Some(right_battery as u8) }; - tray.battery_r_status = if right_byte == 0xff { Some(BatteryStatus::Disconnected) } else if right_charging { Some(BatteryStatus::Charging) } else { Some(BatteryStatus::NotCharging) }; - tray.battery_c = if case_byte == 0xff { None } else { Some(case_battery as u8) }; - tray.battery_c_status = if case_byte == 0xff { Some(BatteryStatus::Disconnected) } else if case_charging { Some(BatteryStatus::Charging) } else { Some(BatteryStatus::NotCharging) }; - }).await; - } - - 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) }, - is_left_in_ear, is_right_in_ear); - } - } + tray.battery_r_status = if right_byte == 0xff { + Some(BatteryStatus::Disconnected) + } else if right_charging { + Some(BatteryStatus::Charging) + } else { + Some(BatteryStatus::NotCharging) + }; + tray.battery_c = if case_byte == 0xff { + None + } else { + Some(case_battery as u8) + }; + tray.battery_c_status = if case_byte == 0xff { + Some(BatteryStatus::Disconnected) + } else if case_charging { + Some(BatteryStatus::Charging) + } else { + Some(BatteryStatus::NotCharging) + }; + }) + .await; } + + 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 + ) + }, + is_left_in_ear, + is_right_in_ear + ); } - _ => {} } } } diff --git a/linux-rust/src/bluetooth/managers.rs b/linux-rust/src/bluetooth/managers.rs index 403195aa9..078c851b3 100644 --- a/linux-rust/src/bluetooth/managers.rs +++ b/linux-rust/src/bluetooth/managers.rs @@ -1,6 +1,6 @@ -use std::sync::Arc; use crate::bluetooth::aacp::AACPManager; use crate::bluetooth::att::ATTManager; +use std::sync::Arc; pub struct DeviceManagers { att: Option>, @@ -9,16 +9,25 @@ pub struct DeviceManagers { impl DeviceManagers { pub fn with_aacp(aacp: AACPManager) -> Self { - Self { att: None, aacp: Some(Arc::new(aacp)) } + Self { + att: None, + aacp: Some(Arc::new(aacp)), + } } pub fn with_att(att: ATTManager) -> Self { - Self { att: Some(Arc::new(att)), aacp: None } + Self { + att: Some(Arc::new(att)), + aacp: None, + } } // keeping the att for airpods optional as it requires changes in system bluez config pub fn with_both(aacp: AACPManager, att: ATTManager) -> Self { - Self { att: Some(Arc::new(att)), aacp: Some(Arc::new(aacp)) } + Self { + att: Some(Arc::new(att)), + aacp: Some(Arc::new(aacp)), + } } pub fn set_aacp(&mut self, manager: AACPManager) { diff --git a/linux-rust/src/bluetooth/mod.rs b/linux-rust/src/bluetooth/mod.rs index dd4bd5570..dfd520770 100644 --- a/linux-rust/src/bluetooth/mod.rs +++ b/linux-rust/src/bluetooth/mod.rs @@ -1,5 +1,5 @@ -pub(crate) mod discovery; pub mod aacp; pub mod att; +pub(crate) mod discovery; pub mod le; -pub mod managers; \ No newline at end of file +pub mod managers; diff --git a/linux-rust/src/devices/airpods.rs b/linux-rust/src/devices/airpods.rs index e42aaf844..bbec2d425 100644 --- a/linux-rust/src/devices/airpods.rs +++ b/linux-rust/src/devices/airpods.rs @@ -1,16 +1,15 @@ -use crate::bluetooth::aacp::{AACPManager, ProximityKeyType, AACPEvent, AirPodsLEKeys}; use crate::bluetooth::aacp::ControlCommandIdentifiers; -use crate::bluetooth::att::ATTManager; +use crate::bluetooth::aacp::{AACPEvent, AACPManager, AirPodsLEKeys, ProximityKeyType}; use crate::media_controller::MediaController; +use crate::ui::messages::BluetoothUIMessage; +use crate::ui::tray::MyTray; use bluer::Address; -use log::{debug, info, error}; -use std::sync::Arc; use ksni::Handle; +use log::{debug, error, info}; use serde::{Deserialize, Serialize}; +use std::sync::Arc; use tokio::sync::Mutex; -use tokio::time::{sleep, Duration}; -use crate::ui::tray::MyTray; -use crate::ui::messages::BluetoothUIMessage; +use tokio::time::{Duration, sleep}; pub struct AirPodsDevice { pub mac_address: Address, @@ -34,7 +33,9 @@ impl AirPodsDevice { // 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; + handle + .update(|tray: &mut MyTray| tray.connected = true) + .await; } info!("Sending handshake"); @@ -62,24 +63,39 @@ impl AirPodsDevice { } info!("Requesting Proximity Keys: IRK and ENC_KEY"); - if let Err(e) = aacp_manager.send_proximity_keys_request( - vec![ProximityKeyType::Irk, ProximityKeyType::EncKey], - ).await { + if let Err(e) = aacp_manager + .send_proximity_keys_request(vec![ProximityKeyType::Irk, ProximityKeyType::EncKey]) + .await + { error!("Failed to request proximity keys: {}", e); } - let session = bluer::Session::new().await.expect("Failed to get bluer session"); - let adapter = session.default_adapter().await.expect("Failed to get default adapter"); - let local_mac = adapter.address().await.expect("Failed to get adapter address").to_string(); + let session = bluer::Session::new() + .await + .expect("Failed to get bluer session"); + let adapter = session + .default_adapter() + .await + .expect("Failed to get default adapter"); + let local_mac = adapter + .address() + .await + .expect("Failed to get adapter address") + .to_string(); - let media_controller = Arc::new(Mutex::new(MediaController::new(mac_address.to_string(), local_mac.clone()))); + let media_controller = Arc::new(Mutex::new(MediaController::new( + mac_address.to_string(), + local_mac.clone(), + ))); let mc_clone = media_controller.clone(); let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel(); let (command_tx, mut command_rx) = tokio::sync::mpsc::unbounded_channel(); aacp_manager.set_event_channel(tx).await; if let Some(handle) = &tray_handle { - handle.update(|tray: &mut MyTray| tray.command_tx = Some(command_tx.clone())).await; + handle + .update(|tray: &mut MyTray| tray.command_tx = Some(command_tx.clone())) + .await; } let aacp_manager_clone = aacp_manager.clone(); @@ -93,54 +109,80 @@ impl AirPodsDevice { let mc_listener = media_controller.lock().await; let aacp_manager_clone_listener = aacp_manager.clone(); - mc_listener.start_playback_listener(aacp_manager_clone_listener, command_tx.clone()).await; + mc_listener + .start_playback_listener(aacp_manager_clone_listener, command_tx.clone()) + .await; drop(mc_listener); let (listening_mode_tx, mut listening_mode_rx) = tokio::sync::mpsc::unbounded_channel(); - aacp_manager.subscribe_to_control_command(ControlCommandIdentifiers::ListeningMode, listening_mode_tx).await; + aacp_manager + .subscribe_to_control_command( + ControlCommandIdentifiers::ListeningMode, + listening_mode_tx, + ) + .await; let tray_handle_clone = tray_handle.clone(); tokio::spawn(async move { while let Some(value) = listening_mode_rx.recv().await { if let Some(handle) = &tray_handle_clone { - handle.update(|tray: &mut MyTray| { - tray.listening_mode = Some(value[0]); - }).await; + handle + .update(|tray: &mut MyTray| { + tray.listening_mode = Some(value[0]); + }) + .await; } } }); let (allow_off_tx, mut allow_off_rx) = tokio::sync::mpsc::unbounded_channel(); - aacp_manager.subscribe_to_control_command(ControlCommandIdentifiers::AllowOffOption, allow_off_tx).await; + aacp_manager + .subscribe_to_control_command(ControlCommandIdentifiers::AllowOffOption, allow_off_tx) + .await; let tray_handle_clone = tray_handle.clone(); tokio::spawn(async move { while let Some(value) = allow_off_rx.recv().await { if let Some(handle) = &tray_handle_clone { - handle.update(|tray: &mut MyTray| { - tray.allow_off_option = Some(value[0]); - }).await; + handle + .update(|tray: &mut MyTray| { + tray.allow_off_option = Some(value[0]); + }) + .await; } } }); - let (conversation_detect_tx, mut conversation_detect_rx) = tokio::sync::mpsc::unbounded_channel(); - aacp_manager.subscribe_to_control_command(ControlCommandIdentifiers::ConversationDetectConfig, conversation_detect_tx).await; + let (conversation_detect_tx, mut conversation_detect_rx) = + tokio::sync::mpsc::unbounded_channel(); + aacp_manager + .subscribe_to_control_command( + ControlCommandIdentifiers::ConversationDetectConfig, + conversation_detect_tx, + ) + .await; let tray_handle_clone = tray_handle.clone(); tokio::spawn(async move { while let Some(value) = conversation_detect_rx.recv().await { if let Some(handle) = &tray_handle_clone { - handle.update(|tray: &mut MyTray| { - tray.conversation_detect_enabled = Some(value[0] == 0x01); - }).await; + handle + .update(|tray: &mut MyTray| { + tray.conversation_detect_enabled = Some(value[0] == 0x01); + }) + .await; } } }); let (owns_connection_tx, mut owns_connection_rx) = tokio::sync::mpsc::unbounded_channel(); - aacp_manager.subscribe_to_control_command(ControlCommandIdentifiers::OwnsConnection, owns_connection_tx).await; + aacp_manager + .subscribe_to_control_command( + ControlCommandIdentifiers::OwnsConnection, + owns_connection_tx, + ) + .await; let mc_clone_owns = media_controller.clone(); tokio::spawn(async move { while let Some(value) = owns_connection_rx.recv().await { - let owns = value.get(0).copied().unwrap_or(0) != 0; + let owns = value.first().copied().unwrap_or(0) != 0; if !owns { info!("Lost ownership, pausing media and disconnecting audio"); let controller = mc_clone_owns.lock().await; @@ -159,46 +201,62 @@ impl AirPodsDevice { let event_clone = event.clone(); match event { AACPEvent::EarDetection(old_status, new_status) => { - debug!("Received EarDetection event: old_status={:?}, new_status={:?}", old_status, new_status); + debug!( + "Received EarDetection event: old_status={:?}, new_status={:?}", + old_status, new_status + ); let controller = mc_clone.lock().await; - debug!("Calling handle_ear_detection with old_status: {:?}, new_status: {:?}", old_status, new_status); - controller.handle_ear_detection(old_status, new_status).await; + debug!( + "Calling handle_ear_detection with old_status: {:?}, new_status: {:?}", + old_status, new_status + ); + controller + .handle_ear_detection(old_status, new_status) + .await; } AACPEvent::BatteryInfo(battery_info) => { debug!("Received BatteryInfo event: {:?}", battery_info); if let Some(handle) = &tray_handle { - handle.update(|tray: &mut MyTray| { - for b in &battery_info { - match b.component as u8 { - 0x01 => { - tray.battery_headphone = Some(b.level); - tray.battery_headphone_status = Some(b.status); + handle + .update(|tray: &mut MyTray| { + for b in &battery_info { + match b.component as u8 { + 0x01 => { + tray.battery_headphone = Some(b.level); + tray.battery_headphone_status = Some(b.status); + } + 0x02 => { + tray.battery_r = Some(b.level); + tray.battery_r_status = Some(b.status); + } + 0x04 => { + tray.battery_l = Some(b.level); + tray.battery_l_status = Some(b.status); + } + 0x08 => { + tray.battery_c = Some(b.level); + tray.battery_c_status = Some(b.status); + } + _ => {} } - 0x02 => { - tray.battery_r = Some(b.level); - tray.battery_r_status = Some(b.status); - } - 0x04 => { - tray.battery_l = Some(b.level); - tray.battery_l_status = Some(b.status); - } - 0x08 => { - tray.battery_c = Some(b.level); - tray.battery_c_status = Some(b.status); - } - _ => {} } - } - }).await; + }) + .await; } debug!("Updated tray with new battery info"); - let _ = ui_tx_clone.send(BluetoothUIMessage::AACPUIEvent(mac_address.to_string(), event_clone)); + let _ = ui_tx_clone.send(BluetoothUIMessage::AACPUIEvent( + mac_address.to_string(), + event_clone, + )); debug!("Sent BatteryInfo event to UI"); } AACPEvent::ControlCommand(status) => { debug!("Received ControlCommand event: {:?}", status); - let _ = ui_tx_clone.send(BluetoothUIMessage::AACPUIEvent(mac_address.to_string(), event_clone)); + let _ = ui_tx_clone.send(BluetoothUIMessage::AACPUIEvent( + mac_address.to_string(), + event_clone, + )); debug!("Sent ControlCommand event to UI"); } AACPEvent::ConversationalAwareness(status) => { @@ -209,37 +267,60 @@ impl AirPodsDevice { AACPEvent::ConnectedDevices(old_devices, new_devices) => { let local_mac = local_mac_events.clone(); let new_devices_filtered = new_devices.iter().filter(|new_device| { - let not_in_old = old_devices.iter().all(|old_device| old_device.mac != new_device.mac); + let not_in_old = old_devices + .iter() + .all(|old_device| old_device.mac != new_device.mac); let not_local = new_device.mac != local_mac; not_in_old && not_local }); for device in new_devices_filtered { - info!("New connected device: {}, info1: {}, info2: {}", device.mac, device.info1, device.info2); - info!("Sending new Tipi packet for device {}, and sending media info to the device", device.mac); + info!( + "New connected device: {}, info1: {}, info2: {}", + device.mac, device.info1, device.info2 + ); + info!( + "Sending new Tipi packet for device {}, and sending media info to the device", + device.mac + ); let aacp_manager_clone = aacp_manager_clone_events.clone(); let local_mac_clone = local_mac.clone(); let device_mac_clone = device.mac.clone(); tokio::spawn(async move { - if let Err(e) = aacp_manager_clone.send_media_information_new_device(&local_mac_clone, &device_mac_clone).await { + if let Err(e) = aacp_manager_clone + .send_media_information_new_device( + &local_mac_clone, + &device_mac_clone, + ) + .await + { error!("Failed to send media info new device: {}", e); } - if let Err(e) = aacp_manager_clone.send_add_tipi_device(&local_mac_clone, &device_mac_clone).await { + if let Err(e) = aacp_manager_clone + .send_add_tipi_device(&local_mac_clone, &device_mac_clone) + .await + { error!("Failed to send add tipi device: {}", e); } }); } } AACPEvent::OwnershipToFalseRequest => { - info!("Received ownership to false request. Setting ownership to false and pausing media."); - let _ = command_tx_clone.send((ControlCommandIdentifiers::OwnsConnection, vec![0x00])); + info!( + "Received ownership to false request. Setting ownership to false and pausing media." + ); + let _ = command_tx_clone + .send((ControlCommandIdentifiers::OwnsConnection, vec![0x00])); let controller = mc_clone.lock().await; controller.pause_all_media().await; controller.deactivate_a2dp_profile().await; } _ => { debug!("Received unhandled AACP event: {:?}", event); - let _ = ui_tx_clone.send(BluetoothUIMessage::AACPUIEvent(mac_address.to_string(), event_clone)); + let _ = ui_tx_clone.send(BluetoothUIMessage::AACPUIEvent( + mac_address.to_string(), + event_clone, + )); debug!("Sent unhandled AACP event to UI"); } } @@ -269,5 +350,5 @@ pub struct AirPodsInformation { pub left_serial_number: String, pub right_serial_number: String, pub version3: String, - pub le_keys: AirPodsLEKeys -} \ No newline at end of file + pub le_keys: AirPodsLEKeys, +} diff --git a/linux-rust/src/devices/enums.rs b/linux-rust/src/devices/enums.rs index cb98f9d9e..5768d1802 100644 --- a/linux-rust/src/devices/enums.rs +++ b/linux-rust/src/devices/enums.rs @@ -1,15 +1,14 @@ -use std::fmt::Display; -use iced::widget::combo_box; -use serde::{Deserialize, Serialize}; use crate::bluetooth::aacp::BatteryInfo; use crate::devices::airpods::AirPodsInformation; use crate::devices::nothing::NothingInformation; +use iced::widget::combo_box; +use serde::{Deserialize, Serialize}; +use std::fmt::Display; -#[derive(Debug, Clone, Serialize, Deserialize)] -#[derive(PartialEq)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub enum DeviceType { AirPods, - Nothing + Nothing, } impl Display for DeviceType { @@ -21,12 +20,11 @@ impl Display for DeviceType { } } - #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "kind", content = "data")] pub enum DeviceInformation { AirPods(AirPodsInformation), - Nothing(NothingInformation) + Nothing(NothingInformation), } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -36,7 +34,6 @@ pub struct DeviceData { pub information: Option, } - #[derive(Clone, Debug)] pub enum DeviceState { AirPods(AirPodsState), @@ -60,7 +57,7 @@ pub struct AirPodsState { pub conversation_awareness_enabled: bool, pub personalized_volume_enabled: bool, pub allow_off_mode: bool, - pub battery: Vec + pub battery: Vec, } #[derive(Clone, Debug)] @@ -68,7 +65,7 @@ pub enum AirPodsNoiseControlMode { Off, NoiseCancellation, Transparency, - Adaptive + Adaptive, } impl Display for AirPodsNoiseControlMode { @@ -115,7 +112,7 @@ pub enum NothingAncMode { MidNoiseCancellation, HighNoiseCancellation, AdaptiveNoiseCancellation, - Transparency + Transparency, } impl Display for NothingAncMode { @@ -152,4 +149,4 @@ impl NothingAncMode { NothingAncMode::Off => 0x05, } } -} \ No newline at end of file +} diff --git a/linux-rust/src/devices/mod.rs b/linux-rust/src/devices/mod.rs index d40c1cf3f..c5d459f7c 100644 --- a/linux-rust/src/devices/mod.rs +++ b/linux-rust/src/devices/mod.rs @@ -1,3 +1,3 @@ pub mod airpods; pub mod enums; -pub(crate) mod nothing; \ No newline at end of file +pub(crate) mod nothing; diff --git a/linux-rust/src/devices/nothing.rs b/linux-rust/src/devices/nothing.rs index 1246270b3..1f78044b7 100644 --- a/linux-rust/src/devices/nothing.rs +++ b/linux-rust/src/devices/nothing.rs @@ -1,167 +1,179 @@ -use std::collections::HashMap; -use std::time::Duration; +use crate::bluetooth::att::{ATTHandles, ATTManager}; +use crate::devices::enums::{DeviceData, DeviceInformation, DeviceType}; +use crate::ui::messages::BluetoothUIMessage; +use crate::utils::get_devices_path; use bluer::Address; use log::{debug, info}; use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::time::Duration; use tokio::sync::mpsc; use tokio::time::sleep; -use crate::bluetooth::att::{ATTHandles, ATTManager}; -use crate::devices::enums::{DeviceData, DeviceInformation, DeviceType}; -use crate::ui::messages::BluetoothUIMessage; -use crate::utils::get_devices_path; #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct NothingInformation{ +pub struct NothingInformation { pub serial_number: String, - pub firmware_version: String + pub firmware_version: String, } -pub struct NothingDevice{ +pub struct NothingDevice { pub att_manager: ATTManager, - pub information: NothingInformation + pub information: NothingInformation, } -impl NothingDevice{ +impl NothingDevice { pub async fn new( mac_address: Address, - ui_tx: mpsc::UnboundedSender + ui_tx: mpsc::UnboundedSender, ) -> Self { let mut att_manager = ATTManager::new(); - att_manager.connect(mac_address).await.expect("Failed to connect"); + att_manager + .connect(mac_address) + .await + .expect("Failed to connect"); let (tx, mut rx) = mpsc::unbounded_channel::>(); - att_manager.register_listener( - ATTHandles::NothingEverythingRead, - tx - ).await; + att_manager + .register_listener(ATTHandles::NothingEverythingRead, tx) + .await; - let devices: HashMap = - std::fs::read_to_string(get_devices_path()) - .ok() - .and_then(|s| serde_json::from_str(&s).ok()) - .unwrap_or_default(); + let devices: HashMap = std::fs::read_to_string(get_devices_path()) + .ok() + .and_then(|s| serde_json::from_str(&s).ok()) + .unwrap_or_default(); let device_key = mac_address.to_string(); let information = if let Some(device_data) = devices.get(&device_key) { let info = device_data.information.clone(); if let Some(DeviceInformation::Nothing(ref nothing_info)) = info { nothing_info.clone() } else { - NothingInformation{ + NothingInformation { serial_number: String::new(), - firmware_version: String::new() + firmware_version: String::new(), } } } else { - NothingInformation{ + NothingInformation { serial_number: String::new(), - firmware_version: String::new() + firmware_version: String::new(), } }; // Request version information - att_manager.write( - ATTHandles::NothingEverything, - &[ - 0x55, 0x20, - 0x01, 0x42, - 0xC0, 0x00, - 0x00, 0x00, - 0x00, 0x00 // something, idk - ] - ).await.expect("Failed to write"); + att_manager + .write( + ATTHandles::NothingEverything, + &[ + 0x55, 0x20, 0x01, 0x42, 0xC0, 0x00, 0x00, 0x00, 0x00, + 0x00, // something, idk + ], + ) + .await + .expect("Failed to write"); sleep(Duration::from_millis(100)).await; // Request serial number - att_manager.write( - ATTHandles::NothingEverything, - &[ - 0x55, 0x20, - 0x01, 0x06, - 0xC0, 0x00, - 0x00, 0x13, - 0x00, 0x00 - ] - ).await.expect("Failed to write"); + att_manager + .write( + ATTHandles::NothingEverything, + &[0x55, 0x20, 0x01, 0x06, 0xC0, 0x00, 0x00, 0x13, 0x00, 0x00], + ) + .await + .expect("Failed to write"); // let ui_tx_clone = ui_tx.clone(); let information_l = information.clone(); tokio::spawn(async move { while let Some(data) = rx.recv().await { - if data.starts_with(&[ - 0x55, 0x20, - 0x01, 0x42, 0x40 - ]) { + if data.starts_with(&[0x55, 0x20, 0x01, 0x42, 0x40]) { let firmware_version = String::from_utf8_lossy(&data[8..]).to_string(); - info!("Received firmware version from Nothing device {}: {}", mac_address, firmware_version); - let new_information = NothingInformation{ + info!( + "Received firmware version from Nothing device {}: {}", + mac_address, firmware_version + ); + let new_information = NothingInformation { serial_number: information_l.serial_number.clone(), - firmware_version: firmware_version.clone() + firmware_version: firmware_version.clone(), }; let mut new_devices = devices.clone(); new_devices.insert( device_key.clone(), - DeviceData{ - name: devices.get(&device_key) + DeviceData { + name: devices + .get(&device_key) .map(|d| d.name.clone()) .unwrap_or("Nothing Device".to_string()), - type_: devices.get(&device_key) + type_: devices + .get(&device_key) .map(|d| d.type_.clone()) .unwrap_or(DeviceType::Nothing), information: Some(DeviceInformation::Nothing(new_information)), - } + }, ); let json = serde_json::to_string(&new_devices).unwrap(); std::fs::write(get_devices_path(), json).expect("Failed to write devices file"); - } else if data.starts_with( - &[ - 0x55, 0x20, - 0x01, 0x06, 0x40 - ] - ) { - let serial_number_start_position = data.iter().position(|&b| b == "S".as_bytes()[0]).unwrap_or(8); - let serial_number_end = data.iter() + } else if data.starts_with(&[0x55, 0x20, 0x01, 0x06, 0x40]) { + let serial_number_start_position = data + .iter() + .position(|&b| b == "S".as_bytes()[0]) + .unwrap_or(8); + let serial_number_end = data + .iter() .skip(serial_number_start_position) .position(|&b| b == 0x0A) .map(|pos| pos + serial_number_start_position) .unwrap_or(data.len()); if data.get(serial_number_start_position + 1) == Some(&"H".as_bytes()[0]) { let serial_number = String::from_utf8_lossy( - &data[serial_number_start_position..serial_number_end] - ).to_string(); - info!("Received serial number from Nothing device {}: {}", mac_address, serial_number); - let new_information = NothingInformation{ + &data[serial_number_start_position..serial_number_end], + ) + .to_string(); + info!( + "Received serial number from Nothing device {}: {}", + mac_address, serial_number + ); + let new_information = NothingInformation { serial_number: serial_number.clone(), - firmware_version: information_l.firmware_version.clone() + firmware_version: information_l.firmware_version.clone(), }; let mut new_devices = devices.clone(); new_devices.insert( device_key.clone(), - DeviceData{ - name: devices.get(&device_key) + DeviceData { + name: devices + .get(&device_key) .map(|d| d.name.clone()) .unwrap_or("Nothing Device".to_string()), - type_: devices.get(&device_key) + type_: devices + .get(&device_key) .map(|d| d.type_.clone()) .unwrap_or(DeviceType::Nothing), information: Some(DeviceInformation::Nothing(new_information)), - } + }, ); let json = serde_json::to_string(&new_devices).unwrap(); - std::fs::write(get_devices_path(), json).expect("Failed to write devices file"); + std::fs::write(get_devices_path(), json) + .expect("Failed to write devices file"); } else { - debug!("Serial number format unexpected from Nothing device {}: {:?}", mac_address, data); + debug!( + "Serial number format unexpected from Nothing device {}: {:?}", + mac_address, data + ); } } - else {} - debug!("Received data from (Nothing) device {}, data: {:?}", mac_address, data); + + debug!( + "Received data from (Nothing) device {}, data: {:?}", + mac_address, data + ); } }); - NothingDevice{ + NothingDevice { att_manager, - information + information, } } -} \ No newline at end of file +} diff --git a/linux-rust/src/main.rs b/linux-rust/src/main.rs index 4f284f4e4..d81f077a9 100644 --- a/linux-rust/src/main.rs +++ b/linux-rust/src/main.rs @@ -1,50 +1,59 @@ mod bluetooth; +mod devices; mod media_controller; mod ui; mod utils; -mod devices; -use std::env; -use log::info; -use dbus::blocking::Connection; -use dbus::blocking::stdintf::org_freedesktop_dbus::Properties; -use dbus::message::MatchRule; -use dbus::arg::{RefArg, Variant}; -use std::collections::HashMap; -use std::sync::Arc; use crate::bluetooth::discovery::{find_connected_airpods, find_other_managed_devices}; -use devices::airpods::AirPodsDevice; -use bluer::{Address, InternalErrorKind}; -use ksni::TrayMethods; -use crate::ui::tray::MyTray; -use clap::Parser; use crate::bluetooth::le::start_le_monitor; -use tokio::sync::mpsc::unbounded_channel; -use tokio::sync::RwLock; use crate::bluetooth::managers::DeviceManagers; use crate::devices::enums::DeviceData; use crate::ui::messages::BluetoothUIMessage; +use crate::ui::tray::MyTray; use crate::utils::get_devices_path; +use bluer::{Address, InternalErrorKind}; +use clap::Parser; +use dbus::arg::{RefArg, Variant}; +use dbus::blocking::Connection; +use dbus::blocking::stdintf::org_freedesktop_dbus::Properties; +use dbus::message::MatchRule; +use devices::airpods::AirPodsDevice; +use ksni::TrayMethods; +use log::info; +use std::collections::HashMap; +use std::env; +use std::sync::Arc; +use tokio::sync::RwLock; +use tokio::sync::mpsc::unbounded_channel; #[derive(Parser)] struct Args { - #[arg(long, short='d', help="Enable debug logging")] + #[arg(long, short = 'd', help = "Enable debug logging")] debug: bool, - #[arg(long, help="Disable system tray, useful if your environment doesn't support AppIndicator or StatusNotifier")] + #[arg( + long, + help = "Disable system tray, useful if your environment doesn't support AppIndicator or StatusNotifier" + )] no_tray: bool, - #[arg(long, help="Start the application minimized to tray")] + #[arg(long, help = "Start the application minimized to tray")] start_minimized: bool, - #[arg(long, help="Enable Bluetooth LE debug logging. Only use when absolutely necessary; this produces a lot of logs.")] + #[arg( + long, + help = "Enable Bluetooth LE debug logging. Only use when absolutely necessary; this produces a lot of logs." + )] le_debug: bool, - #[arg(long, short='v', help="Show application version and exit")] - version: bool + #[arg(long, short = 'v', help = "Show application version and exit")] + version: bool, } fn main() -> iced::Result { let args = Args::parse(); if args.version { - println!("You are running LibrePods version {}", env!("CARGO_PKG_VERSION")); + println!( + "You are running LibrePods version {}", + env!("CARGO_PKG_VERSION") + ); return Ok(()); } @@ -54,23 +63,33 @@ fn main() -> iced::Result { if wayland_display { unsafe { env::set_var("WGPU_BACKEND", "gl") }; } - unsafe { env::set_var("RUST_LOG", log_level.to_owned() + &format!(",winit=warn,tracing=warn,iced_wgpu=warn,wgpu_hal=warn,wgpu_core=warn,cosmic_text=warn,naga=warn,iced_winit=warn,librepods_rust::bluetooth::le={}", if args.le_debug { "debug" } else { "warn" })) }; + unsafe { + env::set_var( + "RUST_LOG", + log_level.to_owned() + + &format!( + ",winit=warn,tracing=warn,iced_wgpu=warn,wgpu_hal=warn,wgpu_core=warn,cosmic_text=warn,naga=warn,iced_winit=warn,librepods_rust::bluetooth::le={}", + if args.le_debug { "debug" } else { "warn" } + ), + ) + }; } env_logger::init(); let (ui_tx, ui_rx) = unbounded_channel::(); - let device_managers: Arc>> = Arc::new(RwLock::new(HashMap::new())); + let device_managers: Arc>> = + Arc::new(RwLock::new(HashMap::new())); let device_managers_clone = device_managers.clone(); std::thread::spawn(|| { let rt = tokio::runtime::Runtime::new().unwrap(); - rt.block_on(async_main(ui_tx, device_managers_clone)).unwrap(); + rt.block_on(async_main(ui_tx, device_managers_clone)) + .unwrap(); }); ui::window::start_ui(ui_rx, args.start_minimized, device_managers) } - async fn async_main( ui_tx: tokio::sync::mpsc::UnboundedSender, device_managers: Arc>>, @@ -84,16 +103,14 @@ async fn async_main( log::error!("Failed to read devices file: {}", e); "{}".to_string() }); - let devices_list: HashMap = serde_json::from_str(&devices_json).unwrap_or_else(|e| { - log::error!("Deserialization failed: {}", e); - HashMap::new() - }); + let devices_list: HashMap = serde_json::from_str(&devices_json) + .unwrap_or_else(|e| { + log::error!("Deserialization failed: {}", e); + HashMap::new() + }); for (mac, device_data) in devices_list.iter() { - match device_data.type_ { - devices::enums::DeviceType::Nothing => { - managed_devices_mac.push(mac.clone()); - } - _ => {} + if device_data.type_ == devices::enums::DeviceType::Nothing { + managed_devices_mac.push(mac.clone()); } } @@ -137,9 +154,13 @@ async fn async_main( info!("Checking for connected devices..."); match find_connected_airpods(&adapter).await { 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); - let airpods_device = AirPodsDevice::new(device.address(), tray_handle.clone(), ui_tx.clone()).await; + let airpods_device = + AirPodsDevice::new(device.address(), tray_handle.clone(), ui_tx.clone()).await; let mut managers = device_managers.write().await; // let dev_managers = DeviceManagers::with_both(airpods_device.aacp_manager.clone(), airpods_device.att_manager.clone()); @@ -149,7 +170,11 @@ async fn async_main( .or_insert(dev_managers) .set_aacp(airpods_device.aacp_manager); drop(managers); - ui_tx.send(BluetoothUIMessage::DeviceConnected(device.address().to_string())).unwrap(); + ui_tx + .send(BluetoothUIMessage::DeviceConnected( + device.address().to_string(), + )) + .unwrap(); } Err(_) => { info!("No connected AirPods found."); @@ -160,23 +185,29 @@ async fn async_main( Ok(devices) => { for device in devices { let addr_str = device.address().to_string(); - info!("Found connected managed device: {}, initializing.", addr_str); + info!( + "Found connected managed device: {}, initializing.", + addr_str + ); let type_ = devices_list.get(&addr_str).unwrap().type_.clone(); let ui_tx_clone = ui_tx.clone(); let device_managers = device_managers.clone(); tokio::spawn(async move { let mut managers = device_managers.write().await; - match type_ { - devices::enums::DeviceType::Nothing => { - let dev = devices::nothing::NothingDevice::new(device.address(), ui_tx_clone.clone()).await; - let dev_managers = DeviceManagers::with_att(dev.att_manager.clone()); - managers - .entry(addr_str.clone()) - .or_insert(dev_managers) - .set_att(dev.att_manager); - ui_tx_clone.send(BluetoothUIMessage::DeviceConnected(addr_str)).unwrap(); - } - _ => {} + if type_ == devices::enums::DeviceType::Nothing { + let dev = devices::nothing::NothingDevice::new( + device.address(), + ui_tx_clone.clone(), + ) + .await; + let dev_managers = DeviceManagers::with_att(dev.att_manager.clone()); + managers + .entry(addr_str.clone()) + .or_insert(dev_managers) + .set_att(dev.att_manager); + ui_tx_clone + .send(BluetoothUIMessage::DeviceConnected(addr_str)) + .unwrap(); } drop(managers) }); @@ -184,7 +215,9 @@ async fn async_main( } Err(e) => { log::debug!("type of error: {:?}", e.kind); - if e.kind != bluer::ErrorKind::Internal(InternalErrorKind::Io(std::io::ErrorKind::NotFound)) { + if e.kind + != bluer::ErrorKind::Internal(InternalErrorKind::Io(std::io::ErrorKind::NotFound)) + { log::error!("Error finding other managed devices: {}", e); } else { info!("No other managed devices found."); @@ -195,49 +228,62 @@ async fn async_main( let conn = Connection::new_system()?; let rule = MatchRule::new_signal("org.freedesktop.DBus.Properties", "PropertiesChanged"); conn.add_match(rule, move |_: (), conn, msg| { - let Some(path) = msg.path() else { return true; }; + let Some(path) = msg.path() else { + return true; + }; if !path.contains("/org/bluez/hci") || !path.contains("/dev_") { return true; } // debug!("PropertiesChanged signal for path: {}", path); - let Ok((iface, changed, _)) = msg.read3::>>, Vec>() else { + let Ok((iface, changed, _)) = + msg.read3::>>, Vec>() + else { return true; }; if iface != "org.bluez.Device1" { return true; } - let Some(connected_var) = changed.get("Connected") else { return true; }; - let Some(is_connected) = connected_var.0.as_ref().as_u64() else { return true; }; + let Some(connected_var) = changed.get("Connected") else { + return true; + }; + let Some(is_connected) = connected_var.0.as_ref().as_u64() else { + return true; + }; if is_connected == 0 { return true; } let proxy = conn.with_proxy("org.bluez", path, std::time::Duration::from_millis(5000)); - let Ok(uuids) = proxy.get::>("org.bluez.Device1", "UUIDs") else { return true; }; + let Ok(uuids) = proxy.get::>("org.bluez.Device1", "UUIDs") else { + return true; + }; let target_uuid = "74ec2172-0bad-4d01-8f77-997b2be0722a"; - let Ok(addr_str) = proxy.get::("org.bluez.Device1", "Address") else { return true; }; - let Ok(addr) = addr_str.parse::
() else { return true; }; + let Ok(addr_str) = proxy.get::("org.bluez.Device1", "Address") else { + return true; + }; + let Ok(addr) = addr_str.parse::
() else { + return true; + }; if managed_devices_mac.contains(&addr_str) { info!("Managed device connected: {}, initializing", addr_str); let type_ = devices_list.get(&addr_str).unwrap().type_.clone(); - match type_ { - devices::enums::DeviceType::Nothing => { - let ui_tx_clone = ui_tx.clone(); - let device_managers = device_managers.clone(); - tokio::spawn(async move { - let mut managers = device_managers.write().await; - let dev = devices::nothing::NothingDevice::new(addr, ui_tx_clone.clone()).await; - let dev_managers = DeviceManagers::with_att(dev.att_manager.clone()); - managers - .entry(addr_str.clone()) - .or_insert(dev_managers) - .set_att(dev.att_manager); - drop(managers); - ui_tx_clone.send(BluetoothUIMessage::DeviceConnected(addr_str.clone())).unwrap(); - }); - } - _ => {} + if type_ == devices::enums::DeviceType::Nothing { + let ui_tx_clone = ui_tx.clone(); + let device_managers = device_managers.clone(); + tokio::spawn(async move { + let mut managers = device_managers.write().await; + let dev = devices::nothing::NothingDevice::new(addr, ui_tx_clone.clone()).await; + let dev_managers = DeviceManagers::with_att(dev.att_manager.clone()); + managers + .entry(addr_str.clone()) + .or_insert(dev_managers) + .set_att(dev.att_manager); + drop(managers); + ui_tx_clone + .send(BluetoothUIMessage::DeviceConnected(addr_str.clone())) + .unwrap(); + }); } return true; } @@ -245,7 +291,9 @@ async fn async_main( if !uuids.iter().any(|u| u.to_lowercase() == target_uuid) { return true; } - let name = proxy.get::("org.bluez.Device1", "Name").unwrap_or_else(|_| "Unknown".to_string()); + let name = proxy + .get::("org.bluez.Device1", "Name") + .unwrap_or_else(|_| "Unknown".to_string()); info!("AirPods connected: {}, initializing", name); let handle_clone = tray_handle.clone(); let ui_tx_clone = ui_tx.clone(); @@ -260,7 +308,9 @@ async fn async_main( .or_insert(dev_managers) .set_aacp(airpods_device.aacp_manager); drop(managers); - ui_tx_clone.send(BluetoothUIMessage::DeviceConnected(addr_str.clone())).unwrap(); + ui_tx_clone + .send(BluetoothUIMessage::DeviceConnected(addr_str.clone())) + .unwrap(); }); true })?; @@ -269,4 +319,4 @@ async fn async_main( loop { conn.process(std::time::Duration::from_millis(1000))?; } -} \ No newline at end of file +} diff --git a/linux-rust/src/media_controller.rs b/linux-rust/src/media_controller.rs index 5e540f0a6..adaef4aaa 100644 --- a/linux-rust/src/media_controller.rs +++ b/linux-rust/src/media_controller.rs @@ -1,25 +1,23 @@ -use log::{info, debug, warn, error}; -use std::sync::Arc; -use tokio::sync::Mutex; -use std::process::Command; +use crate::bluetooth::aacp::AACPManager; +use crate::bluetooth::aacp::EarDetectionStatus; +use dbus::arg::RefArg; use dbus::blocking::Connection; -use std::time::Duration; use dbus::blocking::stdintf::org_freedesktop_dbus::Properties; -use crate::bluetooth::aacp::EarDetectionStatus; -use libpulse_binding::mainloop::standard::Mainloop; +use libpulse_binding::callbacks::ListResult; +use libpulse_binding::context::introspect::SinkInfo; use libpulse_binding::context::{Context, FlagSet as ContextFlagSet}; +use libpulse_binding::def::Retval; +use libpulse_binding::mainloop::standard::Mainloop; use libpulse_binding::operation::State as OperationState; +use libpulse_binding::proplist::Proplist; +use libpulse_binding::volume::{ChannelVolumes, Volume}; +use log::{debug, error, info, warn}; use std::cell::RefCell; +use std::process::Command; use std::rc::Rc; -use dbus::arg::RefArg; -use libpulse_binding::def::Retval; -use libpulse_binding::callbacks::ListResult; -use libpulse_binding::context::introspect::{SinkInfo}; -use libpulse_binding::proplist::Proplist; -use libpulse_binding::{ - volume::{ChannelVolumes, Volume}, -}; -use crate::bluetooth::aacp::AACPManager; +use std::sync::Arc; +use std::time::Duration; +use tokio::sync::Mutex; #[derive(Clone)] struct OwnedCardProfileInfo { @@ -93,7 +91,14 @@ impl MediaController { } } - pub async fn start_playback_listener(&self, aacp_manager: AACPManager, control_tx: tokio::sync::mpsc::UnboundedSender<(crate::bluetooth::aacp::ControlCommandIdentifiers, Vec)>) { + pub async fn start_playback_listener( + &self, + aacp_manager: AACPManager, + control_tx: tokio::sync::mpsc::UnboundedSender<( + crate::bluetooth::aacp::ControlCommandIdentifiers, + Vec, + )>, + ) { let mut state = self.state.lock().await; if state.playback_listener_running { debug!("Playback listener already running"); @@ -104,18 +109,27 @@ impl MediaController { let controller_clone = self.clone(); tokio::spawn(async move { - controller_clone.playback_listener_loop(aacp_manager, control_tx).await; + controller_clone + .playback_listener_loop(aacp_manager, control_tx) + .await; }); } - async fn playback_listener_loop(&self, aacp_manager: AACPManager, control_tx: tokio::sync::mpsc::UnboundedSender<(crate::bluetooth::aacp::ControlCommandIdentifiers, Vec)>) { + async fn playback_listener_loop( + &self, + aacp_manager: AACPManager, + control_tx: tokio::sync::mpsc::UnboundedSender<( + crate::bluetooth::aacp::ControlCommandIdentifiers, + Vec, + )>, + ) { info!("Starting playback listener loop"); loop { tokio::time::sleep(Duration::from_millis(500)).await; - - let is_playing = tokio::task::spawn_blocking(|| { - Self::check_if_playing() - }).await.unwrap_or(false); + + let is_playing = tokio::task::spawn_blocking(|| Self::check_if_playing()) + .await + .unwrap_or(false); let mut state = self.state.lock().await; let was_playing = state.is_playing; @@ -125,12 +139,18 @@ impl MediaController { if !was_playing && is_playing { let aacp_state = aacp_manager.state.lock().await; - if !aacp_state.ear_detection_status.contains(&EarDetectionStatus::InEar) { + 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])); + 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"); @@ -138,11 +158,17 @@ impl MediaController { 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 { + if let Err(e) = aacp_manager + .send_media_information(&local_mac, &device.mac, true) + .await + { error!("Failed to send media information to {}: {}", device.mac, e); } if let Err(e) = aacp_manager.send_smart_routing_show_ui(&device.mac).await { - error!("Failed to send smart routing show ui to {}: {}", device.mac, e); + error!( + "Failed to send smart routing show ui to {}: {}", + device.mac, e + ); } if let Err(e) = aacp_manager.send_hijack_request(&device.mac).await { error!("Failed to send hijack request to {}: {}", device.mac, e); @@ -160,12 +186,17 @@ impl MediaController { Ok(c) => c, Err(_) => return false, }; - - let proxy = conn.with_proxy("org.freedesktop.DBus", "/org/freedesktop/DBus", Duration::from_secs(5)); - let (names,): (Vec,) = match proxy.method_call("org.freedesktop.DBus", "ListNames", ()) { - Ok(n) => n, - Err(_) => return false, - }; + + let proxy = conn.with_proxy( + "org.freedesktop.DBus", + "/org/freedesktop/DBus", + Duration::from_secs(5), + ); + let (names,): (Vec,) = + match proxy.method_call("org.freedesktop.DBus", "ListNames", ()) { + Ok(n) => n, + Err(_) => return false, + }; for service in names { if !service.starts_with("org.mpris.MediaPlayer2.") { @@ -174,12 +205,14 @@ impl MediaController { if Self::is_kdeconnect_service(&service) { continue; } - - let proxy = conn.with_proxy(&service, "/org/mpris/MediaPlayer2", Duration::from_secs(5)); - if let Ok(playback_status) = proxy.get::("org.mpris.MediaPlayer2.Player", "PlaybackStatus") { - if playback_status == "Playing" { - return true; - } + + let proxy = + conn.with_proxy(&service, "/org/mpris/MediaPlayer2", Duration::from_secs(5)); + if let Ok(playback_status) = + proxy.get::("org.mpris.MediaPlayer2.Player", "PlaybackStatus") + && playback_status == "Playing" + { + return true; } } false @@ -189,11 +222,24 @@ impl MediaController { service.starts_with("org.mpris.MediaPlayer2.kdeconnect.mpris_") } - pub async fn handle_ear_detection(&self, old_statuses: Vec, new_statuses: Vec) { - debug!("Entering handle_ear_detection with old_statuses: {:?}, new_statuses: {:?}", old_statuses, new_statuses); - - let old_in_ear_data: Vec = old_statuses.iter().map(|s| *s == EarDetectionStatus::InEar).collect(); - let new_in_ear_data: Vec = new_statuses.iter().map(|s| *s == EarDetectionStatus::InEar).collect(); + pub async fn handle_ear_detection( + &self, + old_statuses: Vec, + new_statuses: Vec, + ) { + debug!( + "Entering handle_ear_detection with old_statuses: {:?}, new_statuses: {:?}", + old_statuses, new_statuses + ); + + let old_in_ear_data: Vec = old_statuses + .iter() + .map(|s| *s == EarDetectionStatus::InEar) + .collect(); + let new_in_ear_data: Vec = new_statuses + .iter() + .map(|s| *s == EarDetectionStatus::InEar) + .collect(); let in_ear = new_in_ear_data.iter().all(|&b| b); @@ -201,7 +247,10 @@ impl MediaController { let new_has_at_least_one_in = new_in_ear_data.iter().any(|&b| b); let new_all_out = new_in_ear_data.iter().all(|&b| !b); - debug!("Computed states: in_ear={}, old_all_out={}, new_has_at_least_one_in={}, new_all_out={}", in_ear, old_all_out, new_has_at_least_one_in, new_all_out); + debug!( + "Computed states: in_ear={}, old_all_out={}, new_has_at_least_one_in={}, new_all_out={}", + in_ear, old_all_out, new_has_at_least_one_in, new_all_out + ); { let state = self.state.lock().await; @@ -234,15 +283,19 @@ impl MediaController { } } - let reset_user_played = (old_in_ear_data.iter().any(|&b| !b) && new_in_ear_data.iter().all(|&b| b)) || - (new_in_ear_data.iter().any(|&b| !b) && old_in_ear_data.iter().all(|&b| b)); + let reset_user_played = (old_in_ear_data.iter().any(|&b| !b) + && new_in_ear_data.iter().all(|&b| b)) + || (new_in_ear_data.iter().any(|&b| !b) && old_in_ear_data.iter().all(|&b| b)); if reset_user_played { debug!("Transition detected, resetting user_played_the_media"); let mut state = self.state.lock().await; state.user_played_the_media = false; } - info!("Ear Detection - old_in_ear_data: {:?}, new_in_ear_data: {:?}", old_in_ear_data, new_in_ear_data); + info!( + "Ear Detection - old_in_ear_data: {:?}, new_in_ear_data: {:?}", + old_in_ear_data, new_in_ear_data + ); let mut old_sorted = old_in_ear_data.clone(); old_sorted.sort(); @@ -257,21 +310,19 @@ impl MediaController { let mut state = self.state.lock().await; state.i_paused_the_media = false; } + } else if !old_all_out { + debug!("Pausing media as buds are not fully in ear"); + self.pause().await; + { + let mut state = self.state.lock().await; + state.i_paused_the_media = true; + } } else { - if !old_all_out { - debug!("Pausing media as buds are not fully in ear"); - self.pause().await; - { - let mut state = self.state.lock().await; - state.i_paused_the_media = true; - } - } else { - debug!("Playing media"); - self.resume().await; - { - let mut state = self.state.lock().await; - state.i_paused_the_media = false; - } + debug!("Playing media"); + self.resume().await; + { + let mut state = self.state.lock().await; + state.i_paused_the_media = false; } } } @@ -314,8 +365,13 @@ impl MediaController { warn!("A2DP profile not available, attempting to restart WirePlumber"); if self.restart_wire_plumber().await { let mut state = self.state.lock().await; - state.device_index = self.get_audio_device_index(&state.connected_device_mac).await; - debug!("Updated device_index after WirePlumber restart: {:?}", state.device_index); + state.device_index = self + .get_audio_device_index(&state.connected_device_mac) + .await; + debug!( + "Updated device_index after WirePlumber restart: {:?}", + state.device_index + ); if !self.is_a2dp_profile_available().await { error!("A2DP profile still not available after WirePlumber restart"); return; @@ -339,9 +395,10 @@ impl MediaController { if let Some(idx) = device_index { let profile_name = preferred_profile.clone(); - let success = tokio::task::spawn_blocking(move || { - set_card_profile_sync(idx, &profile_name) - }).await.unwrap_or(false); + let success = + tokio::task::spawn_blocking(move || set_card_profile_sync(idx, &profile_name)) + .await + .unwrap_or(false); if success { info!("Successfully activated A2DP profile: {}", preferred_profile); @@ -359,8 +416,14 @@ impl MediaController { let paused_services = tokio::task::spawn_blocking(|| { debug!("Listing DBus names for media players"); let conn = Connection::new_session().unwrap(); - let proxy = conn.with_proxy("org.freedesktop.DBus", "/org/freedesktop/DBus", Duration::from_secs(5)); - let (names,): (Vec,) = proxy.method_call("org.freedesktop.DBus", "ListNames", ()).unwrap(); + let proxy = conn.with_proxy( + "org.freedesktop.DBus", + "/org/freedesktop/DBus", + Duration::from_secs(5), + ); + let (names,): (Vec,) = proxy + .method_call("org.freedesktop.DBus", "ListNames", ()) + .unwrap(); let mut paused_services = Vec::new(); for service in names { @@ -371,24 +434,35 @@ impl MediaController { debug!("Skipping kdeconnect service: {}", service); continue; } - + debug!("Checking playback status for service: {}", service); - let proxy = conn.with_proxy(&service, "/org/mpris/MediaPlayer2", Duration::from_secs(5)); - if let Ok(playback_status) = proxy.get::("org.mpris.MediaPlayer2.Player", "PlaybackStatus") { - if playback_status == "Playing" { - debug!("Service {} is playing, attempting to pause", service); - if proxy.method_call::<(), _, &str, &str>("org.mpris.MediaPlayer2.Player", "Pause", ()).is_ok() { - info!("Paused playback for: {}", service); - paused_services.push(service); - } else { - debug!("Failed to pause service: {}", service); - error!("Failed to pause {}", service); - } + let proxy = + conn.with_proxy(&service, "/org/mpris/MediaPlayer2", Duration::from_secs(5)); + if let Ok(playback_status) = + proxy.get::("org.mpris.MediaPlayer2.Player", "PlaybackStatus") + && playback_status == "Playing" + { + debug!("Service {} is playing, attempting to pause", service); + if proxy + .method_call::<(), _, &str, &str>( + "org.mpris.MediaPlayer2.Player", + "Pause", + (), + ) + .is_ok() + { + info!("Paused playback for: {}", service); + paused_services.push(service); + } else { + debug!("Failed to pause service: {}", service); + error!("Failed to pause {}", service); } } } paused_services - }).await.unwrap(); + }) + .await + .unwrap(); if !paused_services.is_empty() { debug!("Paused services: {:?}", paused_services); @@ -408,8 +482,14 @@ impl MediaController { let paused_count = tokio::task::spawn_blocking(|| { debug!("Listing DBus names for media players"); let conn = Connection::new_session().unwrap(); - let proxy = conn.with_proxy("org.freedesktop.DBus", "/org/freedesktop/DBus", Duration::from_secs(5)); - let (names,): (Vec,) = proxy.method_call("org.freedesktop.DBus", "ListNames", ()).unwrap(); + let proxy = conn.with_proxy( + "org.freedesktop.DBus", + "/org/freedesktop/DBus", + Duration::from_secs(5), + ); + let (names,): (Vec,) = proxy + .method_call("org.freedesktop.DBus", "ListNames", ()) + .unwrap(); let mut paused_count = 0; for service in names { @@ -420,27 +500,41 @@ impl MediaController { debug!("Skipping kdeconnect service: {}", service); continue; } - + debug!("Checking playback status for service: {}", service); - let proxy = conn.with_proxy(&service, "/org/mpris/MediaPlayer2", Duration::from_secs(5)); - if let Ok(playback_status) = proxy.get::("org.mpris.MediaPlayer2.Player", "PlaybackStatus") { - if playback_status == "Playing" { - debug!("Service {} is playing, attempting to pause", service); - if proxy.method_call::<(), _, &str, &str>("org.mpris.MediaPlayer2.Player", "Pause", ()).is_ok() { - info!("Paused playback for: {}", service); - paused_count += 1; - } else { - debug!("Failed to pause service: {}", service); - error!("Failed to pause {}", service); - } + let proxy = + conn.with_proxy(&service, "/org/mpris/MediaPlayer2", Duration::from_secs(5)); + if let Ok(playback_status) = + proxy.get::("org.mpris.MediaPlayer2.Player", "PlaybackStatus") + && playback_status == "Playing" + { + debug!("Service {} is playing, attempting to pause", service); + if proxy + .method_call::<(), _, &str, &str>( + "org.mpris.MediaPlayer2.Player", + "Pause", + (), + ) + .is_ok() + { + info!("Paused playback for: {}", service); + paused_count += 1; + } else { + debug!("Failed to pause service: {}", service); + error!("Failed to pause {}", service); } } } paused_count - }).await.unwrap(); + }) + .await + .unwrap(); if paused_count > 0 { - info!("Paused {} media player(s) due to ownership loss", paused_count); + info!( + "Paused {} media player(s) due to ownership loss", + paused_count + ); let mut state = self.state.lock().await; state.is_playing = false; } else { @@ -469,10 +563,14 @@ impl MediaController { debug!("Skipping kdeconnect service: {}", service); continue; } - + debug!("Attempting to resume service: {}", service); - let proxy = conn.with_proxy(&service, "/org/mpris/MediaPlayer2", Duration::from_secs(5)); - if proxy.method_call::<(), _, &str, &str>("org.mpris.MediaPlayer2.Player", "Play", ()).is_ok() { + let proxy = + conn.with_proxy(&service, "/org/mpris/MediaPlayer2", Duration::from_secs(5)); + if proxy + .method_call::<(), _, &str, &str>("org.mpris.MediaPlayer2.Player", "Play", ()) + .is_ok() + { info!("Resumed playback for: {}", service); resumed_count += 1; } else { @@ -481,7 +579,9 @@ impl MediaController { } } resumed_count - }).await.unwrap(); + }) + .await + .unwrap(); if resumed_count > 0 { debug!("Resumed {} services", resumed_count); @@ -510,13 +610,20 @@ impl MediaController { tokio::task::spawn_blocking(move || { let mut mainloop = Mainloop::new().unwrap(); - let mut context = Context::new(&mut mainloop, "LibrePods-is_a2dp_profile_available").unwrap(); - context.connect(None, ContextFlagSet::NOAUTOSPAWN, None).unwrap(); + let mut context = + Context::new(&mainloop, "LibrePods-is_a2dp_profile_available").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 false, - _ => {}, + _ if context.get_state() == libpulse_binding::context::State::Failed + || context.get_state() == libpulse_binding::context::State::Terminated => + { + return false; + } + _ => {} } } @@ -525,21 +632,23 @@ impl MediaController { let op = introspector.get_card_info_list({ let card_info_list = card_info_list.clone(); let mut list = Vec::new(); - move |result| { - match result { - ListResult::Item(item) => { - let profiles = item.profiles.iter().map(|p| OwnedCardProfileInfo { + move |result| match result { + ListResult::Item(item) => { + let profiles = item + .profiles + .iter() + .map(|p| OwnedCardProfileInfo { name: p.name.as_ref().map(|n| n.to_string()), - }).collect(); - list.push(OwnedCardInfo { - index: item.index, - proplist: item.proplist.clone(), - profiles, - }); - }, - ListResult::End => *card_info_list.borrow_mut() = Some(list.clone()), - ListResult::Error => *card_info_list.borrow_mut() = None, + }) + .collect(); + list.push(OwnedCardInfo { + index: item.index, + proplist: item.proplist.clone(), + profiles, + }); } + ListResult::End => *card_info_list.borrow_mut() = Some(list.clone()), + ListResult::Error => *card_info_list.borrow_mut() = None, } }); @@ -548,20 +657,22 @@ impl MediaController { } mainloop.quit(Retval(0)); - if let Some(list) = card_info_list.borrow().as_ref() { - if let Some(card) = list.iter().find(|c| c.index == index) { - let available = card.profiles.iter().any(|p| { - p.name.as_ref().map_or(false, |name| { - name.starts_with("a2dp-sink") - }) - }); - debug!("A2DP profile available: {}", available); - return available; - } + if let Some(list) = card_info_list.borrow().as_ref() + && let Some(card) = list.iter().find(|c| c.index == index) + { + let available = card.profiles.iter().any(|p| { + p.name + .as_ref() + .is_some_and(|name| name.starts_with("a2dp-sink")) + }); + debug!("A2DP profile available: {}", available); + return available; } debug!("A2DP profile not available"); false - }).await.unwrap_or(false) + }) + .await + .unwrap_or(false) } async fn get_preferred_a2dp_profile(&self) -> String { @@ -600,40 +711,51 @@ impl MediaController { } async fn is_profile_available(&self, card_index: u32, profile: &str) -> bool { - debug!("Entering is_profile_available for card index: {}, profile: {}", card_index, profile); + debug!( + "Entering is_profile_available for card index: {}, profile: {}", + card_index, profile + ); let profile_name = profile.to_string(); tokio::task::spawn_blocking(move || { let mut mainloop = Mainloop::new().unwrap(); - let mut context = Context::new(&mut mainloop, "LibrePods-is_profile_available").unwrap(); - context.connect(None, ContextFlagSet::NOAUTOSPAWN, None).unwrap(); + let mut context = Context::new(&mainloop, "LibrePods-is_profile_available").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 false, - _ => {}, + _ if context.get_state() == libpulse_binding::context::State::Failed + || context.get_state() == libpulse_binding::context::State::Terminated => + { + return false; + } + _ => {} } } - let mut introspector = context.introspect(); + let introspector = context.introspect(); let card_info_list = Rc::new(RefCell::new(None)); let op = introspector.get_card_info_list({ let card_info_list = card_info_list.clone(); let mut list = Vec::new(); - move |result| { - match result { - ListResult::Item(item) => { - let profiles = item.profiles.iter().map(|p| OwnedCardProfileInfo { + move |result| match result { + ListResult::Item(item) => { + let profiles = item + .profiles + .iter() + .map(|p| OwnedCardProfileInfo { name: p.name.as_ref().map(|n| n.to_string()), - }).collect(); - list.push(OwnedCardInfo { - index: item.index, - proplist: item.proplist.clone(), - profiles, - }); - }, - ListResult::End => *card_info_list.borrow_mut() = Some(list.clone()), - ListResult::Error => *card_info_list.borrow_mut() = None, + }) + .collect(); + list.push(OwnedCardInfo { + index: item.index, + proplist: item.proplist.clone(), + profiles, + }); } + ListResult::End => *card_info_list.borrow_mut() = Some(list.clone()), + ListResult::Error => *card_info_list.borrow_mut() = None, } }); @@ -642,23 +764,28 @@ impl MediaController { } mainloop.quit(Retval(0)); - if let Some(list) = card_info_list.borrow().as_ref() { - if let Some(card) = list.iter().find(|c| c.index == card_index) { - let available = card.profiles.iter().any(|p| p.name.as_ref().map_or(false, |n| n == &profile_name)); - debug!("Profile {} available: {}", profile_name, available); - return available; - } + if let Some(list) = card_info_list.borrow().as_ref() + && let Some(card) = list.iter().find(|c| c.index == card_index) + { + let available = card + .profiles + .iter() + .any(|p| p.name.as_ref() == Some(&profile_name)); + debug!("Profile {} available: {}", profile_name, available); + return available; } debug!("Profile {} not available", profile_name); false - }).await.unwrap_or(false) + }) + .await + .unwrap_or(false) } async fn restart_wire_plumber(&self) -> bool { debug!("Entering restart_wire_plumber"); info!("Restarting WirePlumber to rediscover A2DP profiles"); let result = Command::new("systemctl") - .args(&["--user", "restart", "wireplumber"]) + .args(["--user", "restart", "wireplumber"]) .output(); match result { @@ -684,14 +811,20 @@ impl MediaController { tokio::task::spawn_blocking(move || { let mut mainloop = Mainloop::new().unwrap(); - let mut context = Context::new(&mut mainloop, "LibrePods-get_audio_device_index").unwrap(); - context.connect(None, ContextFlagSet::NOAUTOSPAWN, None).unwrap(); + let mut context = Context::new(&mainloop, "LibrePods-get_audio_device_index").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, - _ => {}, + _ if context.get_state() == libpulse_binding::context::State::Failed + || context.get_state() == libpulse_binding::context::State::Terminated => + { + return None; + } + _ => {} } } @@ -700,21 +833,23 @@ impl MediaController { let op = introspector.get_card_info_list({ let card_info_list = card_info_list.clone(); let mut list = Vec::new(); - move |result| { - match result { - ListResult::Item(item) => { - let profiles = item.profiles.iter().map(|p| OwnedCardProfileInfo { + move |result| match result { + ListResult::Item(item) => { + let profiles = item + .profiles + .iter() + .map(|p| OwnedCardProfileInfo { name: p.name.as_ref().map(|n| n.to_string()), - }).collect(); - list.push(OwnedCardInfo { - index: item.index, - proplist: item.proplist.clone(), - profiles, - }); - }, - ListResult::End => *card_info_list.borrow_mut() = Some(list.clone()), - ListResult::Error => *card_info_list.borrow_mut() = None, + }) + .collect(); + list.push(OwnedCardInfo { + index: item.index, + proplist: item.proplist.clone(), + profiles, + }); } + ListResult::End => *card_info_list.borrow_mut() = Some(list.clone()), + ListResult::Error => *card_info_list.borrow_mut() = None, } }); @@ -727,17 +862,25 @@ impl MediaController { 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) { - info!("Found audio device index for MAC {}: {}", mac_clone, card.index); - return Some(card.index); - } + if let Some(device_string) = props.get_str("device.string") + && device_string.contains(&mac_clone) + { + info!( + "Found audio device index for MAC {}: {}", + mac_clone, card.index + ); + return Some(card.index); } } } - error!("No matching Bluetooth card found for MAC address: {}", mac_clone); + error!( + "No matching Bluetooth card found for MAC address: {}", + mac_clone + ); None - }).await.unwrap_or(None) + }) + .await + .unwrap_or(None) } pub async fn deactivate_a2dp_profile(&self) { @@ -745,7 +888,9 @@ impl MediaController { let mut state = self.state.lock().await; if state.device_index.is_none() { - state.device_index = self.get_audio_device_index(&state.connected_device_mac).await; + state.device_index = self + .get_audio_device_index(&state.connected_device_mac) + .await; } if state.connected_device_mac.is_empty() || state.device_index.is_none() { @@ -757,9 +902,10 @@ impl MediaController { info!("Deactivating A2DP profile for AirPods by setting to off"); - let success = tokio::task::spawn_blocking(move || { - set_card_profile_sync(device_index, "off") - }).await.unwrap_or(false); + let success = + tokio::task::spawn_blocking(move || set_card_profile_sync(device_index, "off")) + .await + .unwrap_or(false); if success { info!("Successfully deactivated A2DP profile"); @@ -769,7 +915,10 @@ impl MediaController { } pub async fn handle_conversational_awareness(&self, status: u8) { - debug!("Entering handle_conversational_awareness with status: {}", status); + debug!( + "Entering handle_conversational_awareness with status: {}", + status + ); let mac; { @@ -785,17 +934,20 @@ impl MediaController { let sink = match sink_name { Some(s) => s, None => { - warn!("Could not find sink for MAC {}, skipping conversational awareness", mac); + warn!( + "Could not find sink for MAC {}, skipping conversational awareness", + mac + ); return; } }; let current_volume_opt = tokio::task::spawn_blocking({ let sink = sink.clone(); - move || { - get_sink_volume_percent_by_name_sync(&sink) - } - }).await.unwrap_or(None); + move || get_sink_volume_percent_by_name_sync(&sink) + }) + .await + .unwrap_or(None); match status { 1 => { @@ -807,15 +959,20 @@ impl MediaController { state.conv_original_volume = Some(original); state.conv_conversation_started = true; } else { - debug!("Conversation already started; not overwriting conv_original_volume"); + debug!( + "Conversation already started; not overwriting conv_original_volume" + ); } } if original > 25 { let sink_clone = sink.clone(); - tokio::task::spawn_blocking(move || { - transition_sink_volume(&sink_clone, 25) - }).await.unwrap_or(false); - info!("Conversation start: lowered volume to 25% (original {})", original); + tokio::task::spawn_blocking(move || transition_sink_volume(&sink_clone, 25)) + .await + .unwrap_or(false); + info!( + "Conversation start: lowered volume to 25% (original {})", + original + ); } else { debug!("Original volume {} <= 25, not reducing to 25", original); } @@ -824,15 +981,20 @@ impl MediaController { let original = { let state = self.state.lock().await; state.conv_original_volume - }.clone(); + }; if let Some(orig) = original { debug!("Conversation reduce (2). Original: {}", orig); if orig > 15 { let sink_clone = sink.clone(); tokio::task::spawn_blocking(move || { transition_sink_volume(&sink_clone, 15) - }).await.unwrap_or(false); - info!("Conversation reduce: lowered volume to 15% (original {})", orig); + }) + .await + .unwrap_or(false); + info!( + "Conversation reduce: lowered volume to 15% (original {})", + orig + ); } else { debug!("Original {} <= 15, not reducing to 15", orig); } @@ -854,15 +1016,29 @@ impl MediaController { let sink_clone = sink.clone(); tokio::task::spawn_blocking(move || { transition_sink_volume(&sink_clone, target) - }).await.unwrap_or(false); - info!("Conversation partial increase (3): set volume to {} (original {})", target, orig); + }) + .await + .unwrap_or(false); + info!( + "Conversation partial increase (3): set volume to {} (original {})", + target, orig + ); } else if let Some(orig_from_current) = current_volume_opt { - let target = if orig_from_current > 25 { 25 } else { orig_from_current }; + let target = if orig_from_current > 25 { + 25 + } else { + orig_from_current + }; let sink_clone = sink.clone(); tokio::task::spawn_blocking(move || { transition_sink_volume(&sink_clone, target) - }).await.unwrap_or(false); - info!("Conversation partial increase (3) with fallback current: set volume to {} (measured {})", target, orig_from_current); + }) + .await + .unwrap_or(false); + info!( + "Conversation partial increase (3) with fallback current: set volume to {} (measured {})", + target, orig_from_current + ); } else { debug!("No original volume known for status 3, skipping"); } @@ -876,15 +1052,17 @@ impl MediaController { state.conv_original_volume = None; state.conv_conversation_started = false; } else { - debug!("Received status 4 but conversation was not started; ignoring restore"); + debug!( + "Received status 4 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); + tokio::task::spawn_blocking(move || transition_sink_volume(&sink_clone, orig)) + .await + .unwrap_or(false); info!("Conversation end (4): restored volume to original {}", orig); } else { debug!("No stored original volume to restore to on status 4"); @@ -899,15 +1077,17 @@ impl MediaController { state.conv_original_volume = None; state.conv_conversation_started = false; } else { - debug!("Received status 6 but conversation was not started; ignoring restore"); + debug!( + "Received status 6 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); + tokio::task::spawn_blocking(move || transition_sink_volume(&sink_clone, orig)) + .await + .unwrap_or(false); info!("Conversation end (6): restored volume to original {}", orig); } else { debug!("No stored original volume to restore to on status 6"); @@ -922,15 +1102,17 @@ impl MediaController { state.conv_original_volume = None; state.conv_conversation_started = false; } else { - debug!("Received status 7 but conversation was not started; ignoring restore"); + debug!( + "Received status 7 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); + tokio::task::spawn_blocking(move || transition_sink_volume(&sink_clone, orig)) + .await + .unwrap_or(false); info!("Conversation end (7): restored volume to original {}", orig); } else { debug!("No stored original volume to restore to on status 7"); @@ -945,13 +1127,19 @@ impl MediaController { fn get_sink_volume_percent_by_name_sync(sink_name: &str) -> Option { 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(); + let mut context = Context::new(&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, - _ => {}, + _ if context.get_state() == libpulse_binding::context::State::Failed + || context.get_state() == libpulse_binding::context::State::Terminated => + { + return None; + } + _ => {} } } @@ -991,14 +1179,20 @@ fn get_sink_volume_percent_by_name_sync(sink_name: &str) -> Option { fn set_card_profile_sync(card_index: u32, profile_name: &str) -> bool { let mut mainloop = Mainloop::new().unwrap(); - let mut context = Context::new(&mut mainloop, "LibrePods-set_card_profile").unwrap(); - context.connect(None, ContextFlagSet::NOAUTOSPAWN, None).unwrap(); + let mut context = Context::new(&mainloop, "LibrePods-set_card_profile").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 false, - _ => {}, + _ if context.get_state() == libpulse_binding::context::State::Failed + || context.get_state() == libpulse_binding::context::State::Terminated => + { + return false; + } + _ => {} } } @@ -1015,13 +1209,19 @@ fn set_card_profile_sync(card_index: u32, profile_name: &str) -> bool { pub fn transition_sink_volume(sink_name: &str, target_volume: u32) -> bool { let mut mainloop = Mainloop::new().unwrap(); - let mut context = Context::new(&mut mainloop, "LibrePods-transition_sink_volume").unwrap(); - context.connect(None, ContextFlagSet::NOAUTOSPAWN, None).unwrap(); + let mut context = Context::new(&mainloop, "LibrePods-transition_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 false, - _ => {}, + _ if context.get_state() == libpulse_binding::context::State::Failed + || context.get_state() == libpulse_binding::context::State::Terminated => + { + return false; + } + _ => {} } } @@ -1046,7 +1246,8 @@ pub fn transition_sink_volume(sink_name: &str, target_volume: u32) -> bool { if let Some(sink_info) = sink_info_option.borrow().as_ref() { let channels = sink_info.volume.len(); let mut new_volumes = ChannelVolumes::default(); - let raw = (((target_volume as f64) / 100.0) * Volume::NORMAL.0.as_f64().unwrap()).round() as u32; + let raw = + (((target_volume as f64) / 100.0) * Volume::NORMAL.0.as_f64().unwrap()).round() as u32; let vol = Volume(raw); new_volumes.set(channels, vol); @@ -1072,14 +1273,20 @@ async fn get_sink_name_by_mac(mac: &str) -> Option { tokio::task::spawn_blocking(move || { let mut mainloop = Mainloop::new().unwrap(); - let mut context = Context::new(&mut mainloop, "LibrePods-get_sink_name_by_mac").unwrap(); - context.connect(None, ContextFlagSet::NOAUTOSPAWN, None).unwrap(); + let mut context = Context::new(&mainloop, "LibrePods-get_sink_name_by_mac").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, - _ => {}, + _ if context.get_state() == libpulse_binding::context::State::Failed + || context.get_state() == libpulse_binding::context::State::Terminated => + { + return None; + } + _ => {} } } @@ -1094,7 +1301,11 @@ async fn get_sink_name_by_mac(mac: &str) -> Option { proplist: item.proplist.clone(), volume: item.volume, }; - sink_info_list.borrow_mut().as_mut().unwrap().push(owned_item); + sink_info_list + .borrow_mut() + .as_mut() + .unwrap() + .push(owned_item); } } }); @@ -1106,26 +1317,34 @@ async fn get_sink_name_by_mac(mac: &str) -> Option { if let Some(list) = sink_info_list.borrow().as_ref() { for sink in list { - if let Some(device_string) = sink.proplist.get_str("device.string") { - if device_string.to_uppercase().contains(&mac_clone.to_uppercase()) { - if let Some(name) = &sink.name { - info!("Found sink name for MAC {}: {}", mac_clone, name); - return Some(name.to_string()); - } - } + if let Some(device_string) = sink.proplist.get_str("device.string") + && device_string + .to_uppercase() + .contains(&mac_clone.to_uppercase()) + && let Some(name) = &sink.name + { + info!("Found sink name for MAC {}: {}", mac_clone, name); + return Some(name.to_string()); } if let Some(bluez_path) = sink.proplist.get_str("bluez.path") { - let mac_from_path = bluez_path.split('/').last().unwrap_or("").replace("dev_", "").replace('_', ":"); - if mac_from_path.eq_ignore_ascii_case(&mac_clone) { - if let Some(name) = &sink.name { - info!("Found sink name for MAC {}: {}", mac_clone, name); - return Some(name.to_string()); - } + let mac_from_path = bluez_path + .split('/') + .next_back() + .unwrap_or("") + .replace("dev_", "") + .replace('_', ":"); + if mac_from_path.eq_ignore_ascii_case(&mac_clone) + && let Some(name) = &sink.name + { + info!("Found sink name for MAC {}: {}", mac_clone, name); + return Some(name.to_string()); } } } } error!("No matching sink found for MAC address: {}", mac_clone); None - }).await.unwrap_or(None) -} \ No newline at end of file + }) + .await + .unwrap_or(None) +} diff --git a/linux-rust/src/ui/airpods.rs b/linux-rust/src/ui/airpods.rs index 6610922d8..e68469acd 100644 --- a/linux-rust/src/ui/airpods.rs +++ b/linux-rust/src/ui/airpods.rs @@ -1,16 +1,18 @@ -use std::collections::HashMap; -use std::sync::Arc; -use std::thread; -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 crate::bluetooth::aacp::{AACPManager, ControlCommandIdentifiers}; use iced::Alignment::End; use iced::border::Radius; use iced::overlay::menu; use iced::widget::button::Style; use iced::widget::rule::FillMode; +use iced::widget::{ + Rule, Space, button, column, combo_box, container, row, rule, text, text_input, toggler, +}; +use iced::{Background, Border, Center, Color, Length, Padding, Theme}; use log::error; +use std::collections::HashMap; +use std::sync::Arc; +use std::thread; 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; @@ -29,26 +31,20 @@ pub fn airpods_view<'a>( let rename_input = container( row![ Space::with_width(10), - text("Name").size(16).style( - |theme: &Theme| { - let mut style = text::Style::default(); - style.color = Some(theme.palette().text); - style - } - ), + text("Name").size(16).style(|theme: &Theme| { + let mut style = text::Style::default(); + style.color = Some(theme.palette().text); + style + }), Space::with_width(Length::Fill), - text_input( - "", - &state.device_name - ) - .padding(Padding{ - top: 5.0, - bottom: 5.0, - left: 10.0, - right: 10.0, - }) - .style( - |theme: &Theme, _status| { + text_input("", &state.device_name) + .padding(Padding { + top: 5.0, + bottom: 5.0, + left: 10.0, + right: 10.0, + }) + .style(|theme: &Theme, _status| { text_input::Style { background: Background::Color(Color::TRANSPARENT), border: Default::default(), @@ -57,56 +53,52 @@ pub fn airpods_view<'a>( value: theme.palette().text, selection: Default::default(), } - } - ) - .align_x(End) - .on_input({ + }) + .align_x(End) + .on_input({ let mac = mac.clone(); let state = state.clone(); - move|new_name| { + 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"); - } + 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)) } - } - ) + }) ] - .align_y(Center) + .align_y(Center), ) - .padding(Padding{ - top: 5.0, - bottom: 5.0, - left: 10.0, - right: 10.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 - } - ); + .padding(Padding { + top: 5.0, + bottom: 5.0, + left: 10.0, + right: 10.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 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 - } - ), + 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(); @@ -121,78 +113,71 @@ pub fn airpods_view<'a>( 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( + 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"); - } - ); + &[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{ + .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)), - } - } - ) + .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) + .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 - } - ); + .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(); @@ -381,126 +366,102 @@ pub fn airpods_view<'a>( if let Some(DeviceInformation::AirPods(ref airpods_info)) = device.information { let info_rows = column![ row![ - text("Model Number").size(16).style( - |theme: &Theme| { - let mut style = text::Style::default(); - style.color = Some(theme.palette().text); - style - } - ), + text("Model Number").size(16).style(|theme: &Theme| { + let mut style = text::Style::default(); + style.color = Some(theme.palette().text); + style + }), Space::with_width(Length::Fill), text(airpods_info.model_number.clone()).size(16) ], row![ - text("Manufacturer").size(16).style( - |theme: &Theme| { - let mut style = text::Style::default(); - style.color = Some(theme.palette().text); - style - } - ), + text("Manufacturer").size(16).style(|theme: &Theme| { + let mut style = text::Style::default(); + style.color = Some(theme.palette().text); + style + }), Space::with_width(Length::Fill), text(airpods_info.manufacturer.clone()).size(16) ], row![ - text("Serial Number").size(16).style( - |theme: &Theme| { - let mut style = text::Style::default(); - style.color = Some(theme.palette().text); - style - } - ), + text("Serial Number").size(16).style(|theme: &Theme| { + let mut style = text::Style::default(); + style.color = Some(theme.palette().text); + style + }), Space::with_width(Length::Fill), - button( - text(airpods_info.serial_number.clone()).size(16) - ) - .style( - |theme: &Theme, _status| { - let mut style = Style::default(); - style.text_color = theme.palette().text; - style.background = Some(Background::Color(Color::TRANSPARENT)); - style - } - ) + button(text(airpods_info.serial_number.clone()).size(16)) + .style(|theme: &Theme, _status| { + let mut style = Style::default(); + style.text_color = theme.palette().text; + style.background = Some(Background::Color(Color::TRANSPARENT)); + style + }) .padding(0) .on_press(Message::CopyToClipboard(airpods_info.serial_number.clone())) ], row![ - text("Left Serial Number").size(16).style( - |theme: &Theme| { - let mut style = text::Style::default(); - style.color = Some(theme.palette().text); - style - } - ), + text("Left Serial Number").size(16).style(|theme: &Theme| { + let mut style = text::Style::default(); + style.color = Some(theme.palette().text); + style + }), Space::with_width(Length::Fill), - button( - text(airpods_info.left_serial_number.clone()).size(16) - ) - .style( - |theme: &Theme, _status| { - let mut style = Style::default(); - style.text_color = theme.palette().text; - style.background = Some(Background::Color(Color::TRANSPARENT)); - style - } - ) + button(text(airpods_info.left_serial_number.clone()).size(16)) + .style(|theme: &Theme, _status| { + let mut style = Style::default(); + style.text_color = theme.palette().text; + style.background = Some(Background::Color(Color::TRANSPARENT)); + style + }) .padding(0) - .on_press(Message::CopyToClipboard(airpods_info.left_serial_number.clone())) + .on_press(Message::CopyToClipboard( + airpods_info.left_serial_number.clone() + )) ], row![ - text("Right Serial Number").size(16).style( - |theme: &Theme| { - let mut style = text::Style::default(); - style.color = Some(theme.palette().text); - style - } - ), + text("Right Serial Number").size(16).style(|theme: &Theme| { + let mut style = text::Style::default(); + style.color = Some(theme.palette().text); + style + }), Space::with_width(Length::Fill), - button( - text(airpods_info.right_serial_number.clone()).size(16) - ) - .style( - |theme: &Theme, _status| { - let mut style = Style::default(); - style.text_color = theme.palette().text; - style.background = Some(Background::Color(Color::TRANSPARENT)); - style - } - ) + button(text(airpods_info.right_serial_number.clone()).size(16)) + .style(|theme: &Theme, _status| { + let mut style = Style::default(); + style.text_color = theme.palette().text; + style.background = Some(Background::Color(Color::TRANSPARENT)); + style + }) .padding(0) - .on_press(Message::CopyToClipboard(airpods_info.right_serial_number.clone())) + .on_press(Message::CopyToClipboard( + airpods_info.right_serial_number.clone() + )) ], row![ - text("Version 1").size(16).style( - |theme: &Theme| { - let mut style = text::Style::default(); - style.color = Some(theme.palette().text); - style - } - ), + text("Version 1").size(16).style(|theme: &Theme| { + let mut style = text::Style::default(); + style.color = Some(theme.palette().text); + style + }), Space::with_width(Length::Fill), text(airpods_info.version1.clone()).size(16) ], row![ - text("Version 2").size(16).style( - |theme: &Theme| { - let mut style = text::Style::default(); - style.color = Some(theme.palette().text); - style - } - ), + text("Version 2").size(16).style(|theme: &Theme| { + let mut style = text::Style::default(); + style.color = Some(theme.palette().text); + style + }), Space::with_width(Length::Fill), text(airpods_info.version2.clone()).size(16) ], row![ - text("Version 3").size(16).style( - |theme: &Theme| { - let mut style = text::Style::default(); - style.color = Some(theme.palette().text); - style - } - ), + text("Version 3").size(16).style(|theme: &Theme| { + let mut style = text::Style::default(); + style.color = Some(theme.palette().text); + style + }), Space::with_width(Length::Fill), text(airpods_info.version3.clone()).size(16) ] @@ -509,56 +470,53 @@ pub fn airpods_view<'a>( .padding(8); information_col = column![ - container( - text("Device Information").size(18).style( - |theme: &Theme| { - let mut style = text::Style::default(); - style.color = Some(theme.palette().primary); - style - } - ) - ).padding(Padding{ + container(text("Device Information").size(18).style(|theme: &Theme| { + let mut style = text::Style::default(); + style.color = Some(theme.palette().primary); + style + })) + .padding(Padding { top: 5.0, bottom: 5.0, left: 18.0, right: 18.0, }), container(info_rows) - .padding(Padding{ - top: 5.0, - bottom: 5.0, - left: 10.0, - right: 10.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 - } - ) + .padding(Padding { + top: 5.0, + bottom: 5.0, + left: 10.0, + right: 10.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 + }) ]; } else { - error!("Expected AirPodsInformation for device {}, got something else", mac.clone()); + error!( + "Expected AirPodsInformation for device {}, got something else", + mac.clone() + ); } } - container( - 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(20)), - off_listening_mode_toggle, - Space::with_height(Length::from(20)), - information_col - ] - ) + container(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(20)), + off_listening_mode_toggle, + Space::with_height(Length::from(20)), + information_col + ]) .padding(20) .center_x(Length::Fill) .height(Length::Fill) @@ -572,4 +530,4 @@ where let rt = Runtime::new().unwrap(); rt.block_on(fut); }); -} \ No newline at end of file +} diff --git a/linux-rust/src/ui/messages.rs b/linux-rust/src/ui/messages.rs index 9d5c82887..c72aeb9c6 100644 --- a/linux-rust/src/ui/messages.rs +++ b/linux-rust/src/ui/messages.rs @@ -3,9 +3,9 @@ use crate::bluetooth::aacp::AACPEvent; #[derive(Debug, Clone)] pub enum BluetoothUIMessage { OpenWindow, - DeviceConnected(String), // mac - DeviceDisconnected(String), // mac - AACPUIEvent(String, AACPEvent), // mac, event + DeviceConnected(String), // mac + DeviceDisconnected(String), // mac + AACPUIEvent(String, AACPEvent), // mac, event ATTNotification(String, u16, Vec), // mac, handle, data - NoOp -} \ No newline at end of file + NoOp, +} diff --git a/linux-rust/src/ui/mod.rs b/linux-rust/src/ui/mod.rs index 1f22d99b0..0fff86303 100644 --- a/linux-rust/src/ui/mod.rs +++ b/linux-rust/src/ui/mod.rs @@ -1,5 +1,5 @@ +mod airpods; +pub mod messages; +mod nothing; pub mod tray; pub mod window; -pub mod messages; -mod airpods; -mod nothing; \ No newline at end of file diff --git a/linux-rust/src/ui/nothing.rs b/linux-rust/src/ui/nothing.rs index f40e70bd4..072931d78 100644 --- a/linux-rust/src/ui/nothing.rs +++ b/linux-rust/src/ui/nothing.rs @@ -1,73 +1,62 @@ -use std::collections::HashMap; -use std::sync::Arc; -use iced::{Background, Border, Length, Theme}; -use iced::widget::{container, text, column, row, Space}; -use iced::widget::combo_box; +use crate::bluetooth::att::{ATTHandles, ATTManager}; +use crate::devices::enums::{DeviceData, DeviceInformation, DeviceState, NothingState}; +use crate::ui::window::Message; use iced::border::Radius; use iced::overlay::menu; +use iced::widget::combo_box; use iced::widget::text_input; -use tokio::runtime::Runtime; +use iced::widget::{Space, column, container, row, text}; +use iced::{Background, Border, Length, Theme}; +use std::collections::HashMap; +use std::sync::Arc; use std::thread; -use crate::bluetooth::att::{ATTManager, ATTHandles}; -use crate::devices::enums::{DeviceData, DeviceInformation, NothingState, DeviceState}; -use crate::ui::window::Message; +use tokio::runtime::Runtime; pub fn nothing_view<'a>( mac: &'a str, devices_list: &HashMap, state: &'a NothingState, - att_manager: Arc + att_manager: Arc, ) -> iced::widget::Container<'a, Message> { let mut information_col = iced::widget::column![]; let mac = mac.to_string(); - if let Some(device) = devices_list.get(mac.as_str()) { - if let Some(DeviceInformation::Nothing(ref nothing_info)) = device.information { - information_col = information_col - .push(text("Device Information").size(18).style( - |theme: &Theme| { - let mut style = text::Style::default(); - style.color = Some(theme.palette().primary); - style - } - )) - .push(Space::with_height(iced::Length::from(10))) - .push( - iced::widget::row![ - text("Serial Number").size(16).style( - |theme: &Theme| { - let mut style = text::Style::default(); - style.color = Some(theme.palette().text); - style - } - ), - Space::with_width(Length::Fill), - text(nothing_info.serial_number.clone()).size(16) - ] - ) - .push( - iced::widget::row![ - text("Firmware Version").size(16).style( - |theme: &Theme| { - let mut style = text::Style::default(); - style.color = Some(theme.palette().text); - style - } - ), - Space::with_width(Length::Fill), - text(nothing_info.firmware_version.clone()).size(16) - ] - ); - } - } - - let noise_control_mode = container(row![ - text("Noise Control Mode").size(16).style( - |theme: &Theme| { + if let Some(device) = devices_list.get(mac.as_str()) + && let Some(DeviceInformation::Nothing(ref nothing_info)) = device.information + { + information_col = information_col + .push(text("Device Information").size(18).style(|theme: &Theme| { + let mut style = text::Style::default(); + style.color = Some(theme.palette().primary); + style + })) + .push(Space::with_height(iced::Length::from(10))) + .push(iced::widget::row![ + text("Serial Number").size(16).style(|theme: &Theme| { let mut style = text::Style::default(); style.color = Some(theme.palette().text); style - } - ), + }), + Space::with_width(Length::Fill), + text(nothing_info.serial_number.clone()).size(16) + ]) + .push(iced::widget::row![ + text("Firmware Version").size(16).style(|theme: &Theme| { + let mut style = text::Style::default(); + style.color = Some(theme.palette().text); + style + }), + Space::with_width(Length::Fill), + text(nothing_info.firmware_version.clone()).size(16) + ]); + } + + let noise_control_mode = container( + row![ + text("Noise Control 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(); @@ -82,110 +71,110 @@ pub fn nothing_view<'a>( let att_manager = att_manager_clone.clone(); let selected_mode_c = selected_mode.clone(); let mac_s = mac.clone(); - run_async_in_thread( - async move { - if let Err(e) = att_manager.write( + run_async_in_thread(async move { + if let Err(e) = att_manager + .write( ATTHandles::NothingEverything, &[ 0x55, - 0x60, 0x01, - 0x0F, 0xF0, - 0x03, 0x00, - 0x00, 0x01, - selected_mode_c.to_byte(), 0x00, - 0x00, 0x00 - ] - ).await { - log::error!("Failed to set noise cancellation mode for device {}: {}", mac_s, e); - } + 0x60, + 0x01, + 0x0F, + 0xF0, + 0x03, + 0x00, + 0x00, + 0x01, + selected_mode_c.to_byte(), + 0x00, + 0x00, + 0x00, + ], + ) + .await + { + log::error!( + "Failed to set noise cancellation mode for device {}: {}", + mac_s, + e + ); } - ); + }); let mut state = state_clone.clone(); state.anc_mode = selected_mode.clone(); Message::StateChanged(mac.to_string(), DeviceState::Nothing(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(iced::Padding{ + .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(iced::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)), - } - } - ) + .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(iced::Alignment::Center) + .align_y(iced::Alignment::Center), ) - .padding(iced::Padding{ - top: 5.0, - bottom: 5.0, - left: 18.0, - right: 18.0, - }) - .style( - |theme: &Theme| { + .padding(iced::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 + }); + + container(column![ + noise_control_mode, + Space::with_height(Length::from(20)), + container(information_col) + .style(|theme: &Theme| { let mut style = container::Style::default(); - style.background = Some(Background::Color(theme.palette().primary.scale_alpha(0.1))); + 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); + border.color = theme.palette().text; + style.border = border.rounded(20); style - } - ); - - container( - column![ - noise_control_mode, - Space::with_height(Length::from(20)), - container(information_col) - .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().text; - style.border = border.rounded(20); - style - } - ) - .padding(20) - ] - ) - .padding(20) - .center_x(Length::Fill) - .height(Length::Fill) + }) + .padding(20) + ]) + .padding(20) + .center_x(Length::Fill) + .height(Length::Fill) } fn run_async_in_thread(fut: F) diff --git a/linux-rust/src/ui/tray.rs b/linux-rust/src/ui/tray.rs index af9713077..b3adbc53a 100644 --- a/linux-rust/src/ui/tray.rs +++ b/linux-rust/src/ui/tray.rs @@ -41,15 +41,15 @@ impl ksni::Tray for MyTray { levels.push(h); } } else { - if let Some(l) = self.battery_l { - if self.battery_l_status != Some(BatteryStatus::Disconnected) { - levels.push(l); - } + if let Some(l) = self.battery_l + && self.battery_l_status != Some(BatteryStatus::Disconnected) + { + levels.push(l); } - if let Some(r) = self.battery_r { - if self.battery_r_status != Some(BatteryStatus::Disconnected) { - levels.push(r); - } + if let Some(r) = self.battery_r + && self.battery_r_status != Some(BatteryStatus::Disconnected) + { + levels.push(r); } // if let Some(c) = self.battery_c { // if self.battery_c_status != Some(BatteryStatus::Disconnected) { @@ -70,7 +70,8 @@ impl ksni::Tray for MyTray { let settings = std::fs::read_to_string(&app_settings_path) .ok() .and_then(|s| serde_json::from_str::(&s).ok()); - let text_mode = settings.clone() + let text_mode = settings + .clone() .and_then(|v| v.get("tray_text_mode").cloned()) .and_then(|ttm| serde_json::from_value(ttm).ok()) .unwrap_or(false); @@ -78,20 +79,21 @@ impl ksni::Tray for MyTray { vec![icon] } fn tool_tip(&self) -> ToolTip { - let format_component = |label: &str, level: Option, status: Option| -> String { - match status { - Some(BatteryStatus::Disconnected) => format!("{}: -", label), - _ => { - let pct = level.map(|b| format!("{}%", b)).unwrap_or("?".to_string()); - let suffix = if status == Some(BatteryStatus::Charging) { - "⚡" - } else { - "" - }; - format!("{}: {}{}", label, pct, suffix) + let format_component = + |label: &str, level: Option, status: Option| -> String { + match status { + Some(BatteryStatus::Disconnected) => format!("{}: -", label), + _ => { + let pct = level.map(|b| format!("{}%", b)).unwrap_or("?".to_string()); + let suffix = if status == Some(BatteryStatus::Charging) { + "⚡" + } else { + "" + }; + format!("{}: {}{}", label, pct, suffix) + } } - } - }; + }; let l = format_component("L", self.battery_l, self.battery_l_status); let r = format_component("R", self.battery_r, self.battery_r_status); @@ -121,9 +123,10 @@ impl ksni::Tray for MyTray { ("Adaptive", 0x04), ] }; - let selected = self.listening_mode.and_then(|mode| { - options.iter().position(|&(_, val)| val == mode) - }).unwrap_or(0); + let selected = self + .listening_mode + .and_then(|mode| options.iter().position(|&(_, val)| val == mode)) + .unwrap_or(0); let options_clone = options.clone(); vec![ StandardItem { @@ -135,19 +138,26 @@ impl ksni::Tray for MyTray { } }), ..Default::default() - }.into(), + } + .into(), RadioGroup { selected, select: Box::new(move |this: &mut Self, current| { if let Some(tx) = &this.command_tx { - let value = options_clone.get(current).map(|&(_, val)| val).unwrap_or(0x02); + let value = options_clone + .get(current) + .map(|&(_, val)| val) + .unwrap_or(0x02); let _ = tx.send((ControlCommandIdentifiers::ListeningMode, vec![value])); } }), - options: options.into_iter().map(|(label, _)| RadioItem { - label: label.into(), - ..Default::default() - }).collect(), + options: options + .into_iter() + .map(|(label, _)| RadioItem { + label: label.into(), + ..Default::default() + }) + .collect(), ..Default::default() } .into(), @@ -157,13 +167,16 @@ impl ksni::Tray for MyTray { checked: self.conversation_detect_enabled.unwrap_or(false), enabled: self.conversation_detect_enabled.is_some(), activate: Box::new(|this: &mut Self| { - if let Some(tx) = &this.command_tx { - if let Some(is_enabled) = this.conversation_detect_enabled { - let new_state = !is_enabled; - let value = if !new_state { 0x02 } else { 0x01 }; - let _ = tx.send((ControlCommandIdentifiers::ConversationDetectConfig, vec![value])); - this.conversation_detect_enabled = Some(new_state); - } + if let Some(tx) = &this.command_tx + && let Some(is_enabled) = this.conversation_detect_enabled + { + let new_state = !is_enabled; + let value = if !new_state { 0x02 } else { 0x01 }; + let _ = tx.send(( + ControlCommandIdentifiers::ConversationDetectConfig, + vec![value], + )); + this.conversation_detect_enabled = Some(new_state); } }), ..Default::default() @@ -229,7 +242,8 @@ fn generate_icon(text: &str, text_mode: bool, charging: bool) -> Icon { let dist = (dx * dx + dy * dy).sqrt(); if dist > inner_radius && dist <= outer_radius { let angle = dy.atan2(dx); - let angle_from_top = (angle + std::f32::consts::PI / 2.0).rem_euclid(2.0 * std::f32::consts::PI); + let angle_from_top = + (angle + std::f32::consts::PI / 2.0).rem_euclid(2.0 * std::f32::consts::PI); if angle_from_top <= percentage * 2.0 * std::f32::consts::PI { img.put_pixel(x, y, Rgba([0u8, 255u8, 0u8, 255u8])); } @@ -284,4 +298,4 @@ fn generate_icon(text: &str, text_mode: bool, charging: bool) -> Icon { height: height as i32, data, } -} \ No newline at end of file +} diff --git a/linux-rust/src/ui/window.rs b/linux-rust/src/ui/window.rs index 45f493330..4f89faa9e 100644 --- a/linux-rust/src/ui/window.rs +++ b/linux-rust/src/ui/window.rs @@ -1,22 +1,33 @@ -use std::collections::HashMap; -use iced::widget::button::Style; -use iced::widget::{button, column, container, pane_grid, text, Space, combo_box, row, text_input, scrollable, vertical_rule, rule, toggler}; -use iced::{daemon, window, Background, Border, Center, Color, Element, Font, Length, Padding, Size, Subscription, Task, Theme}; -use std::sync::Arc; +use crate::bluetooth::aacp::{ + AACPEvent, BatteryComponent, BatteryStatus, ControlCommandIdentifiers, +}; +use crate::bluetooth::managers::DeviceManagers; +use crate::devices::enums::{ + AirPodsNoiseControlMode, AirPodsState, DeviceData, DeviceState, DeviceType, NothingAncMode, + NothingState, +}; +use crate::ui::airpods::airpods_view; +use crate::ui::messages::BluetoothUIMessage; +use crate::ui::nothing::nothing_view; +use crate::utils::{MyTheme, get_app_settings_path, get_devices_path}; use bluer::{Address, Session}; use iced::border::Radius; use iced::overlay::menu; +use iced::widget::button::Style; use iced::widget::rule::FillMode; +use iced::widget::{ + Space, button, column, combo_box, container, pane_grid, row, rule, scrollable, text, + text_input, toggler, vertical_rule, +}; +use iced::{ + Background, Border, Center, Element, Font, Length, Padding, Size, Subscription, Task, Theme, + daemon, window, +}; use log::{debug, error}; +use std::collections::HashMap; +use std::sync::Arc; use tokio::sync::mpsc::UnboundedReceiver; use tokio::sync::{Mutex, RwLock}; -use crate::bluetooth::aacp::{AACPEvent, ControlCommandIdentifiers, BatteryComponent, BatteryStatus}; -use crate::bluetooth::managers::DeviceManagers; -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; -use crate::ui::nothing::nothing_view; pub fn start_ui( ui_rx: UnboundedReceiver, @@ -45,11 +56,11 @@ pub struct App { pending_add_device: Option<(String, Address)>, device_type_state: combo_box::State, selected_device_type: Option, - tray_text_mode: bool + tray_text_mode: bool, } pub struct BluetoothState { - connected_devices: Vec + connected_devices: Vec, } impl BluetoothState { @@ -76,14 +87,14 @@ pub enum Message { ConfirmAddDevice, CancelAddDevice, StateChanged(String, DeviceState), - TrayTextModeChanged(bool) // yes, I know I should add all settings to a struct, but I'm lazy + TrayTextModeChanged(bool), // yes, I know I should add all settings to a struct, but I'm lazy } #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub enum Tab { Device(String), Settings, - AddDevice + AddDevice, } #[derive(Clone, Copy)] @@ -104,10 +115,7 @@ impl App { let ui_rx = Arc::new(Mutex::new(ui_rx)); - let wait_task = Task::perform( - wait_for_message(Arc::clone(&ui_rx)), - |msg| msg, - ); + let wait_task = Task::perform(wait_for_message(Arc::clone(&ui_rx)), |msg| msg); let (window, open_task) = if start_minimized { (None, Task::none()) @@ -123,11 +131,13 @@ impl App { let settings = std::fs::read_to_string(&app_settings_path) .ok() .and_then(|s| serde_json::from_str::(&s).ok()); - let selected_theme = settings.clone() + let selected_theme = settings + .clone() .and_then(|v| v.get("theme").cloned()) .and_then(|t| serde_json::from_value(t).ok()) .unwrap_or(MyTheme::Dark); - let tray_text_mode = settings.clone() + let tray_text_mode = settings + .clone() .and_then(|v| v.get("tray_text_mode").cloned()) .and_then(|ttm| serde_json::from_value(ttm).ok()) .unwrap_or(false); @@ -141,7 +151,6 @@ impl App { // ("28:2D:7F:C2:05:5B".to_string(), dummy_device_state), // ]); - let device_states = HashMap::new(); ( Self { @@ -178,14 +187,12 @@ impl App { paired_devices: HashMap::new(), device_states, pending_add_device: None, - device_type_state: combo_box::State::new(vec![ - DeviceType::Nothing - ]), + device_type_state: combo_box::State::new(vec![DeviceType::Nothing]), selected_device_type: None, device_managers, - tray_text_mode + tray_text_mode, }, - Task::batch(vec![open_task, wait_task]) + Task::batch(vec![open_task, wait_task]), ) } @@ -217,54 +224,44 @@ impl App { self.selected_theme = theme; let app_settings_path = get_app_settings_path(); let settings = serde_json::json!({"theme": self.selected_theme, "tray_text_mode": self.tray_text_mode}); - debug!("Writing settings to {}: {}", app_settings_path.to_str().unwrap() , settings); + debug!( + "Writing settings to {}: {}", + app_settings_path.to_str().unwrap(), + settings + ); std::fs::write(app_settings_path, settings.to_string()).ok(); Task::none() } - Message::CopyToClipboard(data) => { - iced::clipboard::write(data) - } + Message::CopyToClipboard(data) => iced::clipboard::write(data), Message::BluetoothMessage(ui_message) => { match ui_message { BluetoothUIMessage::NoOp => { let ui_rx = Arc::clone(&self.ui_rx); - let wait_task = Task::perform( - wait_for_message(ui_rx), - |msg| msg, - ); - wait_task + + Task::perform(wait_for_message(ui_rx), |msg| msg) } BluetoothUIMessage::OpenWindow => { let ui_rx = Arc::clone(&self.ui_rx); - let wait_task = Task::perform( - wait_for_message(ui_rx), - |msg| msg, - ); + let wait_task = Task::perform(wait_for_message(ui_rx), |msg| msg); debug!("Opening main window..."); if let Some(window_id) = self.window { - Task::batch(vec![ - window::gain_focus(window_id), - wait_task, - ]) + Task::batch(vec![window::gain_focus(window_id), wait_task]) } else { let mut settings = window::Settings::default(); settings.min_size = Some(Size::new(400.0, 300.0)); settings.icon = window::icon::from_file("../../assets/icon.png").ok(); let (new_window_task, open_task) = window::open(settings); self.window = Some(new_window_task); - Task::batch(vec![ - open_task.map(Message::WindowOpened), - wait_task, - ]) + Task::batch(vec![open_task.map(Message::WindowOpened), wait_task]) } } BluetoothUIMessage::DeviceConnected(mac) => { let ui_rx = Arc::clone(&self.ui_rx); - let wait_task = Task::perform( - wait_for_message(ui_rx), - |msg| msg, + let wait_task = Task::perform(wait_for_message(ui_rx), |msg| msg); + debug!( + "Device connected: {}. Adding to connected devices list", + mac ); - debug!("Device connected: {}. Adding to connected devices list", mac); let mut already_connected = false; for device in &self.bluetooth_state.connected_devices { if device == &mac { @@ -281,14 +278,16 @@ impl App { // })); 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 = serde_json::from_str(&devices_json).unwrap_or_else(|e| { - error!("Deserialization failed: {}", e); - HashMap::new() - }); + 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 = + 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_ { @@ -300,22 +299,27 @@ impl App { let state = aacp_manager_state.blocking_lock(); debug!("AACP manager found for AirPods device {}", mac); let device_name = { - 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 = serde_json::from_str(&devices_json).unwrap_or_else(|e| { - error!("Deserialization failed: {}", e); - HashMap::new() - }); - devices_list.get(&mac).map(|d| d.name.clone()).unwrap_or_else(|| "Unknown Device".to_string()) + 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 = + serde_json::from_str(&devices_json).unwrap_or_else(|e| { + error!("Deserialization failed: {}", e); + HashMap::new() + }); + devices_list + .get(&mac) + .map(|d| d.name.clone()) + .unwrap_or_else(|| "Unknown Device".to_string()) }; self.device_states.insert(mac.clone(), DeviceState::AirPods(AirPodsState { device_name, battery: state.battery_info.clone(), 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)) + status.value.first().map(AirPodsNoiseControlMode::from_byte) } else { None } @@ -351,136 +355,144 @@ impl App { })); } Some(DeviceType::Nothing) => { - self.device_states.insert(mac.clone(), DeviceState::Nothing(NothingState { - anc_mode: NothingAncMode::Off, - anc_mode_state: combo_box::State::new(vec![ - NothingAncMode::Off, - NothingAncMode::Transparency, - NothingAncMode::AdaptiveNoiseCancellation, - NothingAncMode::LowNoiseCancellation, - NothingAncMode::MidNoiseCancellation, - NothingAncMode::HighNoiseCancellation - ]), - })); + self.device_states.insert( + mac.clone(), + DeviceState::Nothing(NothingState { + anc_mode: NothingAncMode::Off, + anc_mode_state: combo_box::State::new(vec![ + NothingAncMode::Off, + NothingAncMode::Transparency, + NothingAncMode::AdaptiveNoiseCancellation, + NothingAncMode::LowNoiseCancellation, + NothingAncMode::MidNoiseCancellation, + NothingAncMode::HighNoiseCancellation, + ]), + }), + ); } _ => {} } - Task::batch(vec![ - wait_task, - ]) + Task::batch(vec![wait_task]) } BluetoothUIMessage::DeviceDisconnected(mac) => { let ui_rx = Arc::clone(&self.ui_rx); - let wait_task = Task::perform( - wait_for_message(ui_rx), - |msg| msg, - ); + let wait_task = Task::perform(wait_for_message(ui_rx), |msg| msg); debug!("Device disconnected: {}", mac); self.device_states.remove(&mac); - Task::batch(vec![ - wait_task, - ]) + Task::batch(vec![wait_task]) } BluetoothUIMessage::AACPUIEvent(mac, event) => { let ui_rx = Arc::clone(&self.ui_rx); - let wait_task = Task::perform( - wait_for_message(ui_rx), - |msg| msg, - ); + let wait_task = Task::perform(wait_for_message(ui_rx), |msg| msg); debug!("AACP UI Event for {}: {:?}", mac, event); 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; - } + AACPEvent::ControlCommand(status) => match status.identifier { + ControlCommandIdentifiers::ListeningMode => { + let mode = status + .value + .first() + .map(AirPodsNoiseControlMode::from_byte) + .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, - [0x02] => false, - _ => { - error!("Unknown Conversation Detect Config value: {:?}", status.value); - false - } - }; - if let Some(DeviceState::AirPods(state)) = self.device_states.get_mut(&mac) { - state.conversation_awareness_enabled = is_enabled; + } + ControlCommandIdentifiers::ConversationDetectConfig => { + let is_enabled = match status.value.as_slice() { + [0x01] => true, + [0x02] => false, + _ => { + error!( + "Unknown Conversation Detect Config value: {:?}", + status.value + ); + false } + }; + if let Some(DeviceState::AirPods(state)) = + self.device_states.get_mut(&mac) + { + state.conversation_awareness_enabled = is_enabled; } - ControlCommandIdentifiers::AdaptiveVolumeConfig => { - let is_enabled = match status.value.as_slice() { - [0x01] => true, - [0x02] => false, - _ => { - error!("Unknown Adaptive Volume Config value: {:?}", status.value); - false - } - }; - if let Some(DeviceState::AirPods(state)) = self.device_states.get_mut(&mac) { - state.personalized_volume_enabled = is_enabled; + } + ControlCommandIdentifiers::AdaptiveVolumeConfig => { + let is_enabled = match status.value.as_slice() { + [0x01] => true, + [0x02] => false, + _ => { + error!( + "Unknown Adaptive Volume Config value: {:?}", + status.value + ); + false } + }; + if let Some(DeviceState::AirPods(state)) = + self.device_states.get_mut(&mac) + { + 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 - } + } + ControlCommandIdentifiers::AllowOffOption => { + let is_enabled = match status.value.as_slice() { + [0x01] => true, + [0x02] => false, + _ => { + error!( + "Unknown Allow Off Option value: {:?}", + status.value ); + false } - } - _ => { - debug!("Unhandled Control Command Status: {:?}", status); + }; + 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); + } + }, AACPEvent::BatteryInfo(battery_info) => { - if let Some(DeviceState::AirPods(state)) = self.device_states.get_mut(&mac) { + if let Some(DeviceState::AirPods(state)) = + self.device_states.get_mut(&mac) + { state.battery = battery_info; debug!("Updated battery info for {}: {:?}", mac, state.battery); } } _ => {} } - Task::batch(vec![ - wait_task, - ]) + Task::batch(vec![wait_task]) } BluetoothUIMessage::ATTNotification(mac, handle, value) => { - debug!("ATT Notification for {}: handle=0x{:04X}, value={:?}", mac, handle, value); + debug!( + "ATT Notification for {}: handle=0x{:04X}, value={:?}", + mac, handle, value + ); // TODO: Handle Nothing's ANC Mode changes here let ui_rx = Arc::clone(&self.ui_rx); - let wait_task = Task::perform( - wait_for_message(ui_rx), - |msg| msg, - ); - Task::batch(vec![ - wait_task, - ]) + let wait_task = Task::perform(wait_for_message(ui_rx), |msg| msg); + Task::batch(vec![wait_task]) } } } @@ -503,31 +515,35 @@ impl App { Task::none() } Message::ConfirmAddDevice => { - if let Some((name, addr)) = self.pending_add_device.take() { - if let Some(type_) = self.selected_device_type.take() { - let devices_path = get_devices_path(); - let devices_json = std::fs::read_to_string(&devices_path).unwrap_or_else(|e| { - error!("Failed to read devices file: {}", e); - "{}".to_string() - }); - let mut devices_list: HashMap = serde_json::from_str(&devices_json).unwrap_or_else(|e| { + if let Some((name, addr)) = self.pending_add_device.take() + && let Some(type_) = self.selected_device_type.take() + { + let devices_path = get_devices_path(); + let devices_json = std::fs::read_to_string(&devices_path).unwrap_or_else(|e| { + error!("Failed to read devices file: {}", e); + "{}".to_string() + }); + let mut devices_list: HashMap = + serde_json::from_str(&devices_json).unwrap_or_else(|e| { error!("Deserialization failed: {}", e); HashMap::new() }); - devices_list.insert(addr.to_string(), DeviceData { + devices_list.insert( + addr.to_string(), + DeviceData { name, type_: type_.clone(), - information: None - }); - let updated_json = serde_json::to_string(&devices_list).unwrap_or_else(|e| { - error!("Serialization failed: {}", e); - "{}".to_string() - }); - if let Err(e) = std::fs::write(&devices_path, updated_json) { - error!("Failed to write devices file: {}", e); - } - self.selected_tab = Tab::Device(addr.to_string()); + information: None, + }, + ); + let updated_json = serde_json::to_string(&devices_list).unwrap_or_else(|e| { + error!("Serialization failed: {}", e); + "{}".to_string() + }); + if let Err(e) = std::fs::write(&devices_path, updated_json) { + error!("Failed to write devices file: {}", e); } + self.selected_tab = Tab::Device(addr.to_string()); } Task::none() } @@ -540,35 +556,32 @@ impl App { 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 = serde_json::from_str(&devices_json).unwrap_or_else(|e| { - error!("Deserialization failed: {}", e); - HashMap::new() - }); + 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 = + 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 - } - ); + if let Some(DeviceType::AirPods) = type_ + && 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() } @@ -576,7 +589,11 @@ impl App { self.tray_text_mode = is_enabled; let app_settings_path = get_app_settings_path(); let settings = serde_json::json!({"theme": self.selected_theme, "tray_text_mode": self.tray_text_mode}); - debug!("Writing settings to {}: {}", app_settings_path.to_str().unwrap() , settings); + debug!( + "Writing settings to {}: {}", + app_settings_path.to_str().unwrap(), + settings + ); std::fs::write(app_settings_path, settings.to_string()).ok(); Task::none() } @@ -588,10 +605,11 @@ impl App { error!("Failed to read devices file: {}", e); "{}".to_string() }); - let devices_list: HashMap = serde_json::from_str(&devices_json).unwrap_or_else(|e| { - error!("Deserialization failed: {}", e); - HashMap::new() - }); + let devices_list: HashMap = serde_json::from_str(&devices_json) + .unwrap_or_else(|e| { + error!("Deserialization failed: {}", e); + HashMap::new() + }); let pane_grid = pane_grid::PaneGrid::new(&self.panes, |_pane_id, pane, _is_maximized| { match pane { Pane::Sidebar => { @@ -771,7 +789,7 @@ impl App { ] ) } - + Pane::Content => { let device_managers = self.device_managers.blocking_read(); let content = match &self.selected_tab { @@ -788,22 +806,17 @@ impl App { debug!("Rendering device view for {}: type={:?}, state={:?}", id, device_type, device_state); match device_type { Some(DeviceType::AirPods) => { - let view = device_state.as_ref().and_then(|state| { + + 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( + managers.get_aacp().map(|aacp_manager| airpods_view( id, &devices_list, state, aacp_manager.clone() - ), - // att_manager.clone(), - ) - // }) - }) + )) }) } _ => None, @@ -814,8 +827,7 @@ impl App { ) .center_x(Length::Fill) .center_y(Length::Fill) - }); - view + }) } Some(DeviceType::Nothing) => { if let Some(DeviceState::Nothing(state)) = device_state { @@ -1029,14 +1041,14 @@ impl App { } ) .padding(8) - .on_press(Message::StartAddDevice(device.0.clone(), device.1.clone())) + .on_press(Message::StartAddDevice(device.0.clone(), device.1)) .into() ); } item_col = item_col.push(row(row_elements).align_y(Center)); - - if let Some((_, pending_addr)) = &self.pending_add_device { - if pending_addr == &device.1 { + + if let Some((_, pending_addr)) = &self.pending_add_device + && pending_addr == &device.1 { item_col = item_col.push( row![ text("Device Type:").size(16), @@ -1112,8 +1124,6 @@ impl App { .width(Length::Fill) ); } - } - list_col = list_col.push( container(item_col) .padding(8) @@ -1170,9 +1180,7 @@ impl App { } } -async fn wait_for_message( - ui_rx: Arc>>, -) -> Message { +async fn wait_for_message(ui_rx: Arc>>) -> Message { let mut rx = ui_rx.lock().await; match rx.recv().await { Some(msg) => Message::BluetoothMessage(msg), @@ -1189,10 +1197,15 @@ async fn load_paired_devices() -> HashMap { let adapter = session.default_adapter().await.ok().unwrap(); let addresses = adapter.device_addresses().await.ok().unwrap(); for addr in addresses { - let device = adapter.device(addr.clone()).ok().unwrap(); + let device = adapter.device(addr).ok().unwrap(); let paired = device.is_paired().await.ok().unwrap(); if paired { - let name = device.name().await.ok().flatten().unwrap_or_else(|| "Unknown".to_string()); + let name = device + .name() + .await + .ok() + .flatten() + .unwrap_or_else(|| "Unknown".to_string()); devices.insert(name, addr); } } diff --git a/linux-rust/src/utils.rs b/linux-rust/src/utils.rs index e561e788b..0a57910ca 100644 --- a/linux-rust/src/utils.rs +++ b/linux-rust/src/utils.rs @@ -1,6 +1,6 @@ +use aes::Aes128; use aes::cipher::generic_array::GenericArray; use aes::cipher::{BlockEncrypt, KeyInit}; -use aes::Aes128; use iced::Theme; use serde::{Deserialize, Serialize}; use std::path::PathBuf; @@ -8,19 +8,25 @@ use std::path::PathBuf; pub fn get_devices_path() -> PathBuf { let data_dir = std::env::var("XDG_DATA_HOME") .unwrap_or_else(|_| format!("{}/.local/share", std::env::var("HOME").unwrap_or_default())); - PathBuf::from(data_dir).join("librepods").join("devices.json") + PathBuf::from(data_dir) + .join("librepods") + .join("devices.json") } pub fn get_preferences_path() -> PathBuf { let config_dir = std::env::var("XDG_CONFIG_HOME") .unwrap_or_else(|_| format!("{}/.local/share", std::env::var("HOME").unwrap_or_default())); - PathBuf::from(config_dir).join("librepods").join("preferences.json") + PathBuf::from(config_dir) + .join("librepods") + .join("preferences.json") } pub fn get_app_settings_path() -> PathBuf { let config_dir = std::env::var("XDG_CONFIG_HOME") .unwrap_or_else(|_| format!("{}/.local/share", std::env::var("HOME").unwrap_or_default())); - PathBuf::from(config_dir).join("librepods").join("app_settings.json") + PathBuf::from(config_dir) + .join("librepods") + .join("app_settings.json") } fn e(key: &[u8; 16], data: &[u8; 16]) -> [u8; 16] { @@ -127,4 +133,4 @@ impl From for Theme { MyTheme::Ferra => Theme::Ferra, } } -} \ No newline at end of file +} diff --git a/shell.nix b/shell.nix new file mode 100644 index 000000000..459e1526e --- /dev/null +++ b/shell.nix @@ -0,0 +1,12 @@ +(import ( + let + lock = builtins.fromJSON (builtins.readFile ./flake.lock); + nodeName = lock.nodes.root.inputs.flake-compat; + in + fetchTarball { + url = + lock.nodes.${nodeName}.locked.url + or "https://github.com/edolstra/flake-compat/archive/${lock.nodes.${nodeName}.locked.rev}.tar.gz"; + sha256 = lock.nodes.${nodeName}.locked.narHash; + } +) { src = ./.; }).shellNix