rust/bt-ascs: Add the Update Metadata operation This adds the `update_metadata` method to the ASCS client, which allows a client to update the metadata for one or more ASEs. - Add `UpdateMetadata` to `AseControlOperation` - Implement encoding and decoding for the operation - Add a unit test for the new operation Test: cargo test -p bt-ascs Bug: b/431814103 Change-Id: Ic898bdbc862a735423729d4525bf83af19784256 Reviewed-on: https://bluetooth-review.googlesource.com/c/bluetooth/+/2482
diff --git a/rust/bt-ascs/src/client.rs b/rust/bt-ascs/src/client.rs index 20131be..fbd50b1 100644 --- a/rust/bt-ascs/src/client.rs +++ b/rust/bt-ascs/src/client.rs
@@ -613,12 +613,37 @@ self.perform_and_verify_operation(op, pending_ases).await } + + /// Performs the Update Metadata operation on one or more ASEs. + /// + /// # Arguments + /// * `ases_with_metadata` - A vector of `AseIdWithMetadata` structs + /// specifying the target ASE IDs and their new codec metadata. + /// + /// # Returns + /// On success, returns an [`AseControlOperationOutcome`] containing the + /// results of the operation. + pub async fn update_metadata( + &mut self, + ases_with_metadata: Vec<AseIdWithMetadata>, + ) -> Result<AseControlOperationOutcome, Error> { + self.validate_arguments( + AseControlPointOpcode::UpdateMetadata, + ases_with_metadata.iter().map(|a| &a.ase_id), + )?; + + let pending_ases: HashSet<AseId> = ases_with_metadata.iter().map(|a| a.ase_id).collect(); + let op = AseControlOperation::UpdateMetadata { ases_with_metadata, responses: vec![] }; + + self.perform_and_verify_operation(op, pending_ases).await + } } #[cfg(test)] mod tests { use super::*; use bt_common::core::CodecId; + use bt_common::generic_audio::metadata_ltv::Metadata; use bt_gatt::test_utils::{FakePeerService, FakeTypes}; use bt_gatt::types::{AttributePermissions, CharacteristicProperty}; use bt_gatt::Characteristic; @@ -1855,4 +1880,100 @@ assert!(matches!(err, Error::Client(ClientError::UnknownAseId(AseId(99))))); } + + #[test] + fn update_metadata_success() { + let mut service = setup_fake_service(); + let client_fut = AudioStreamControlServiceClient::<FakeTypes>::create(service.clone()); + let mut client = run_to_completion(client_fut).expect("client creation should succeed"); + + // Pre-condition SOURCE_ASE_HANDLE (ASE 2) to Streaming state + let source_value = vec![ + 0x02, // ASE ID: 2 + 0x04, // ASE State: Streaming + 0x01, 0x01, 0x00, + ]; + client.endpoints.source.get_mut(&SOURCE_ASE_HANDLE).unwrap().endpoint = + AudioStreamEndpoint::from_char_value( + SOURCE_ASE_HANDLE, + AudioDirection::Source, + &source_value, + ) + .unwrap(); + + let update_req = AseIdWithMetadata { + ase_id: AseId(2), + metadata: vec![Metadata::AudioActiveState(true)], + }; + + #[rustfmt::skip] + service.expect_characteristic_value( + &CONTROL_POINT_HANDLE, + vec![ + 0x07, // Opcode: Update Metadata + 0x01, // Num ASEs + 0x02, // ASE ID: 2 + 0x03, // Metadata Length: 3 + 0x02, 0x08, 0x01, // Metadata: AudioActiveState(true) + ], + ); + + #[rustfmt::skip] + service.notify( + &CONTROL_POINT_HANDLE, + Ok(CharacteristicNotification { + handle: CONTROL_POINT_HANDLE, + value: vec![ + 0x07, // Opcode: Update Metadata + 0x01, // Num ASEs + 0x02, 0x00, 0x00, // ASE ID: 2, Success + ], + maybe_truncated: false, + }), + ); + + // ASE remains in Streaming state + #[rustfmt::skip] + service.notify( + &SOURCE_ASE_HANDLE, + Ok(CharacteristicNotification { + handle: SOURCE_ASE_HANDLE, + value: vec![ + 0x02, // ASE ID: 2 + 0x04, // ASE State: Streaming + 0x01, 0x01, 0x00, + ], + maybe_truncated: false, + }), + ); + + let update_fut = client.update_metadata(vec![update_req]); + let outcome = run_to_completion(update_fut).expect("update metadata should succeed"); + + assert_eq!(outcome.rejected().len(), 0); + assert_eq!(client.endpoints.source[&SOURCE_ASE_HANDLE].endpoint.state, AseState::Streaming); + } + + #[test] + fn update_metadata_fail_invalid_start_state() { + let service = setup_fake_service(); + let client_fut = AudioStreamControlServiceClient::<FakeTypes>::create(service.clone()); + let mut client = run_to_completion(client_fut).expect("client creation should succeed"); + + // Source ASE is in Idle state, which is invalid starting state for + // UpdateMetadata + let update_req = AseIdWithMetadata { ase_id: AseId(2), metadata: vec![] }; + + let update_fut = client.update_metadata(vec![update_req]); + let err = run_to_completion(update_fut).expect_err("should fail client-side validation"); + + assert!(matches!( + err, + Error::Client(ClientError::InvalidStartState { + ase_id: AseId(2), + opcode: AseControlPointOpcode::UpdateMetadata, + actual: AseState::Idle, + }) + )); + } }