diff --git a/linux-rust/src/airpods.rs b/linux-rust/src/airpods.rs index bfdb686..b52bfab 100644 --- a/linux-rust/src/airpods.rs +++ b/linux-rust/src/airpods.rs @@ -3,7 +3,7 @@ use crate::bluetooth::aacp::ControlCommandIdentifiers; use crate::bluetooth::att::ATTManager; use crate::media_controller::MediaController; use bluer::Address; -use log::{debug, info}; +use log::{debug, info, error}; use std::sync::Arc; use ksni::Handle; use tokio::sync::Mutex; @@ -47,19 +47,29 @@ impl AirPodsDevice { "Failed to request notifications", ); + info!("sending some packet"); + aacp_manager.send_some_packet().await.expect( + "Failed to send some packet", + ); + info!("Requesting Proximity Keys: IRK and ENC_KEY"); aacp_manager.send_proximity_keys_request( vec![ProximityKeyType::Irk, ProximityKeyType::EncKey], ).await.expect( "Failed to request proximity keys", ); - let media_controller = Arc::new(Mutex::new(MediaController::new(mac_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 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; - tray_handle.update(|tray: &mut MyTray| tray.command_tx = Some(command_tx)).await; + tray_handle.update(|tray: &mut MyTray| tray.command_tx = Some(command_tx.clone())).await; let aacp_manager_clone = aacp_manager.clone(); tokio::spawn(async move { @@ -70,6 +80,11 @@ 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; + 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; let tray_handle_clone = tray_handle.clone(); @@ -103,6 +118,23 @@ impl AirPodsDevice { } }); + 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; + 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; + if !owns { + info!("Lost ownership, pausing media and disconnecting audio"); + let controller = mc_clone_owns.lock().await; + controller.pause_all_media().await; + controller.deactivate_a2dp_profile().await; + } + } + }); + + let aacp_manager_clone_events = aacp_manager.clone(); + let local_mac_events = local_mac.clone(); tokio::spawn(async move { while let Some(event) = rx.recv().await { match event { @@ -143,6 +175,37 @@ impl AirPodsDevice { let controller = mc_clone.lock().await; controller.handle_conversational_awareness(status).await; } + 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_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); + 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 { + 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 { + 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.send((ControlCommandIdentifiers::OwnsConnection, vec![0x00])); + let controller = mc_clone.lock().await; + controller.pause_all_media().await; + controller.deactivate_a2dp_profile().await; + } _ => {} } } diff --git a/linux-rust/src/bluetooth/aacp.rs b/linux-rust/src/bluetooth/aacp.rs index 47a4eb0..6b45b4a 100644 --- a/linux-rust/src/bluetooth/aacp.rs +++ b/linux-rust/src/bluetooth/aacp.rs @@ -30,6 +30,8 @@ pub mod opcodes { pub const SMART_ROUTING: u8 = 0x10; pub const SMART_ROUTING_RESP: u8 = 0x11; pub const SEND_CONNECTED_MAC: u8 = 0x14; + pub const HEADTRACKING: u8 = 0x17; + pub const TIPI_3: u8 = 0x0C; } #[derive(Debug, Clone, PartialEq, Eq, Hash)] @@ -217,7 +219,8 @@ pub enum AACPEvent { ConversationalAwareness(u8), ProximityKeys(Vec<(u8, Vec)>), AudioSource(AudioSource), - ConnectedDevices(Vec), + ConnectedDevices(Vec, Vec), + OwnershipToFalseRequest, } struct AACPManagerState { @@ -225,6 +228,7 @@ struct AACPManagerState { control_command_status_list: Vec, control_command_subscribers: HashMap>>>, owns: bool, + old_connected_devices: Vec, connected_devices: Vec, audio_source: Option, battery_info: Vec, @@ -241,6 +245,7 @@ impl AACPManagerState { control_command_status_list: Vec::new(), control_command_subscribers: HashMap::new(), owns: false, + old_connected_devices: Vec::new(), connected_devices: Vec::new(), audio_source: None, battery_info: Vec::new(), @@ -355,6 +360,10 @@ impl AACPManager { state.event_tx = Some(tx); } + pub async fn get_connected_devices(&self) -> Vec { + self.state.lock().await.connected_devices.clone() + } + 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); @@ -578,14 +587,22 @@ impl AACPManager { 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(devices)); + let _ = tx.send(AACPEvent::ConnectedDevices(state.old_connected_devices.clone(), devices)); } info!("Received Connected Devices: {:?}", state.connected_devices); } opcodes::SMART_ROUTING_RESP => { - info!("Received Smart Routing Response: {:?}", &payload[1..]); + let packet_string = String::from_utf8_lossy(&payload[2..]); + info!("Received Smart Routing Response: {}", packet_string); + if packet_string.contains("SetOwnershipToFalse") { + info!("Received OwnershipToFalse request"); + if let Some(ref tx) = self.state.lock().await.event_tx { + let _ = tx.send(AACPEvent::OwnershipToFalseRequest); + } + } } opcodes::EQ_DATA => { debug!("Received EQ Data"); @@ -648,6 +665,195 @@ impl AACPManager { let packet = [opcode.as_slice(), data.as_slice()].concat(); self.send_data_packet(&packet).await } + + 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(); + buffer.extend_from_slice(&target_mac_bytes.iter().rev().cloned().collect::>()); + + buffer.extend_from_slice(&[0x68, 0x00]); + buffer.extend_from_slice(&[0x01, 0xE5, 0x4A]); + buffer.extend_from_slice(b"playingApp"); + buffer.push(0x42); + buffer.extend_from_slice(b"NA");; + buffer.push(0x52); + buffer.extend_from_slice(b"hostStreamingState"); + buffer.push(0x42); + buffer.extend_from_slice(b"NO");; + buffer.push(0x49); + buffer.extend_from_slice(b"btAddress"); + buffer.push(0x51); + buffer.extend_from_slice(self_mac_address.as_bytes()); + buffer.push(0x46); + buffer.extend_from_slice(b"btName"); + buffer.push(0x43); + buffer.extend_from_slice(b"Mac");; + buffer.push(0x58); + buffer.extend_from_slice(b"otherDevice"); + buffer.extend_from_slice(b"AudioCategory"); + buffer.extend_from_slice(&[0x30, 0x64]); + + let packet = [opcode.as_slice(), buffer.as_slice()].concat(); + self.send_data_packet(&packet).await + } + + 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(); + buffer.extend_from_slice(&target_mac_bytes.iter().rev().cloned().collect::>()); + buffer.extend_from_slice(&[0x62, 0x00]); + buffer.extend_from_slice(&[0x01, 0xE5]); + buffer.push(0x4A); + buffer.extend_from_slice(b"localscore"); + buffer.extend_from_slice(&[0x30, 0x64]); + buffer.push(0x46); + buffer.extend_from_slice(b"reason"); + buffer.push(0x48); + buffer.extend_from_slice(b"Hijackv2"); + buffer.push(0x51); + buffer.extend_from_slice(b"audioRoutingScore"); + buffer.extend_from_slice(&[0x31, 0x2D, 0x01, 0x5F]); + buffer.extend_from_slice(b"audioRoutingSetOwnershipToFalse"); + buffer.push(0x01); + buffer.push(0x4B); + buffer.extend_from_slice(b"remotescore"); + buffer.push(0xA5); + + while buffer.len() < 106 { + buffer.push(0x00); + } + + let packet = [opcode.as_slice(), buffer.as_slice()].concat(); + 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<()> { + 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(); + buffer.extend_from_slice(&target_mac_bytes.iter().rev().cloned().collect::>()); + buffer.extend_from_slice(&[0x82, 0x00]); + buffer.extend_from_slice(&[0x01, 0xE5, 0x4A]); + buffer.extend_from_slice(b"PlayingApp"); + buffer.push(0x56); + buffer.extend_from_slice(b"com.google.ios.youtube"); + buffer.push(0x52); + buffer.extend_from_slice(b"HostStreamingState"); + buffer.push(0x42); + buffer.extend_from_slice(if streaming_state { b"YES" } else { b"NO" }); + buffer.push(0x49); + buffer.extend_from_slice(b"btAddress"); + buffer.push(0x51); + buffer.extend_from_slice(self_mac_address.as_bytes()); + buffer.extend_from_slice(b"btName"); + buffer.push(0x43); + buffer.extend_from_slice(b"Mac"); + buffer.push(0x58); + buffer.extend_from_slice(b"otherDevice"); + buffer.extend_from_slice(b"AudioCategory"); + buffer.extend_from_slice(&[0x31, 0x2D, 0x01]); + + while buffer.len() < 138 { + buffer.push(0x00); + } + let packet = [opcode.as_slice(), buffer.as_slice()].concat(); + self.send_data_packet(&packet).await + } + + 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(); + buffer.extend_from_slice(&target_mac_bytes.iter().rev().cloned().collect::>()); + buffer.extend_from_slice(&[0x7E, 0x00]); + buffer.extend_from_slice(&[0x01, 0xE6, 0x5B]); + buffer.extend_from_slice(b"SmartRoutingKeyShowNearbyUI"); + buffer.push(0x01); + buffer.push(0x4A); + buffer.extend_from_slice(b"localscore"); + buffer.extend_from_slice(&[0x31, 0x2D]); + buffer.push(0x01); + buffer.push(0x46); + buffer.extend_from_slice(b"reasonHhijackv2"); + buffer.push(0x51); + buffer.extend_from_slice(b"audioRoutingScore"); + buffer.push(0xA2); + buffer.push(0x5F); + buffer.extend_from_slice(b"audioRoutingSetOwnershipToFalse"); + buffer.push(0x01); + buffer.push(0x4B); + buffer.extend_from_slice(b"remotescore"); + buffer.push(0xA2); + + while buffer.len() < 134 { + buffer.push(0x00); + } + + let packet = [opcode.as_slice(), buffer.as_slice()].concat(); + self.send_data_packet(&packet).await + } + + 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(); + buffer.extend_from_slice(&target_mac_bytes.iter().rev().cloned().collect::>()); + buffer.extend_from_slice(&[0x59, 0x00]); + buffer.extend_from_slice(&[0x01, 0xE3]); + buffer.push(0x5F); + buffer.extend_from_slice(b"audioRoutingSetOwnershipToFalse"); + buffer.push(0x01); + buffer.push(0x59); + buffer.extend_from_slice(b"audioRoutingShowReverseUI"); + buffer.push(0x01); + buffer.push(0x46); + buffer.extend_from_slice(b"reason"); + buffer.push(0x53); + buffer.extend_from_slice(b"ReverseBannerTapped"); + + while buffer.len() < 97 { + buffer.push(0x00); + } + + let packet = [opcode.as_slice(), buffer.as_slice()].concat(); + self.send_data_packet(&packet).await + } + + 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(); + buffer.extend_from_slice(&target_mac_bytes.iter().rev().cloned().collect::>()); + buffer.extend_from_slice(&[0x4E, 0x00]); + buffer.extend_from_slice(&[0x01, 0xE5]); + buffer.extend_from_slice(&[0x48, 0x69]); + buffer.extend_from_slice(b"idleTime"); + buffer.extend_from_slice(&[0x08, 0x47]); + buffer.extend_from_slice(b"newTipi"); + buffer.extend_from_slice(&[0x01, 0x49]); + buffer.extend_from_slice(b"btAddress"); + buffer.push(0x51); + buffer.extend_from_slice(self_mac_address.as_bytes()); + buffer.push(0x46); + buffer.extend_from_slice(b"btName"); + buffer.push(0x43); + buffer.extend_from_slice(b"Mac"); + buffer.push(0x50); + buffer.extend_from_slice(b"nearbyAudioScore"); + buffer.push(0x0E); + + let packet = [opcode.as_slice(), buffer.as_slice()].concat(); + self.send_data_packet(&packet).await + } + + pub async fn send_some_packet(&self) -> Result<()> { + self.send_data_packet(&[ + 0x29, 0x00, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF + ]).await + } } async fn recv_thread(manager: AACPManager, sp: Arc) { @@ -665,6 +871,11 @@ 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)."); + let mut state = manager.state.lock().await; + state.owns = false; + state.connected_devices.clear(); + state.control_command_status_list.clear(); break; } } diff --git a/linux-rust/src/main.rs b/linux-rust/src/main.rs index 01b8dc5..09fef94 100644 --- a/linux-rust/src/main.rs +++ b/linux-rust/src/main.rs @@ -19,7 +19,7 @@ use crate::ui::tray::MyTray; #[tokio::main] async fn main() -> bluer::Result<()> { if env::var("RUST_LOG").is_err() { - unsafe { env::set_var("RUST_LOG", "info"); } + unsafe { env::set_var("RUST_LOG", "debug"); } } env_logger::init(); diff --git a/linux-rust/src/media_controller.rs b/linux-rust/src/media_controller.rs index c42f2d9..8d9b844 100644 --- a/linux-rust/src/media_controller.rs +++ b/linux-rust/src/media_controller.rs @@ -19,6 +19,7 @@ use libpulse_binding::proplist::Proplist; use libpulse_binding::{ volume::{ChannelVolumes, Volume}, }; +use crate::bluetooth::aacp::AACPManager; #[derive(Clone)] struct OwnedCardProfileInfo { @@ -41,6 +42,7 @@ struct OwnedSinkInfo { struct MediaControllerState { connected_device_mac: String, + local_mac: String, is_playing: bool, paused_by_app_services: Vec, device_index: Option, @@ -52,12 +54,14 @@ struct MediaControllerState { disconnect_when_not_wearing: bool, conv_original_volume: Option, conv_conversation_started: bool, + playback_listener_running: bool, } impl MediaControllerState { fn new() -> Self { MediaControllerState { connected_device_mac: String::new(), + local_mac: String::new(), is_playing: false, paused_by_app_services: Vec::new(), device_index: None, @@ -69,6 +73,7 @@ impl MediaControllerState { disconnect_when_not_wearing: true, conv_original_volume: None, conv_conversation_started: false, + playback_listener_running: false, } } } @@ -79,14 +84,104 @@ pub struct MediaController { } impl MediaController { - pub fn new(connected_mac: String) -> Self { + pub fn new(connected_mac: String, local_mac: String) -> Self { let mut state = MediaControllerState::new(); state.connected_device_mac = connected_mac; + state.local_mac = local_mac; MediaController { state: Arc::new(Mutex::new(state)), } } + 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"); + return; + } + state.playback_listener_running = true; + drop(state); + + let controller_clone = self.clone(); + tokio::spawn(async move { + 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)>) { + 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 mut state = self.state.lock().await; + let was_playing = state.is_playing; + state.is_playing = is_playing; + let local_mac = state.local_mac.clone(); + drop(state); + + if !was_playing && is_playing { + info!("Media playback started, taking ownership and activating a2dp"); + let _ = control_tx.send((crate::bluetooth::aacp::ControlCommandIdentifiers::OwnsConnection, vec![0x01])); + self.activate_a2dp_profile().await; + + info!("already connected locally, hijacking connection by asking AirPods"); + + let connected_devices = aacp_manager.get_connected_devices().await; + 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 { + 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); + } + if let Err(e) = aacp_manager.send_hijack_request(&device.mac).await { + error!("Failed to send hijack request to {}: {}", device.mac, e); + } + } + } + } + } + } + + fn check_if_playing() -> bool { + let conn = match Connection::new_session() { + 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, + }; + + for service in names { + if !service.starts_with("org.mpris.MediaPlayer2.") { + continue; + } + 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; + } + } + } + false + } + + fn is_kdeconnect_service(service: &str) -> bool { + 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); @@ -262,19 +357,25 @@ impl MediaController { let mut paused_services = Vec::new(); for service in names { - if service.starts_with("org.mpris.MediaPlayer2.") { - 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); - } + if !service.starts_with("org.mpris.MediaPlayer2.") { + continue; + } + if Self::is_kdeconnect_service(&service) { + 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); } } } @@ -287,12 +388,59 @@ impl MediaController { info!("Paused {} media player(s) via DBus", paused_services.len()); let mut state = self.state.lock().await; state.paused_by_app_services = paused_services; + state.is_playing = false; } else { debug!("No playing media players found"); info!("No playing media players found to pause"); } } + pub async fn pause_all_media(&self) { + debug!("Pausing all media (without tracking for resume)"); + + 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 mut paused_count = 0; + + for service in names { + if !service.starts_with("org.mpris.MediaPlayer2.") { + continue; + } + if Self::is_kdeconnect_service(&service) { + 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); + } + } + } + } + paused_count + }).await.unwrap(); + + if paused_count > 0 { + info!("Paused {} media player(s) due to ownership loss", paused_count); + let mut state = self.state.lock().await; + state.is_playing = false; + } else { + debug!("No playing media players found to pause"); + } + } + async fn resume(&self) { debug!("Entering resume method"); debug!("Resuming playback"); @@ -310,6 +458,11 @@ impl MediaController { let conn = Connection::new_session().unwrap(); let mut resumed_count = 0; for service in services { + if Self::is_kdeconnect_service(&service) { + 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() { @@ -454,7 +607,7 @@ impl MediaController { } } - let introspector = context.introspect(); + let mut 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();