rust/bt-ascs: Implement Release operation

This change implements the client-side "Release" operation as defined
in ASCS v1.0.1, Section 5.8. This allows a client to release all
resources associated with one or more ASEs.

 - Added the `release` method to the `AudioStreamControlServiceClient`.
 - Implemented the encoding logic for the `Release` control operation.
 - Added unit tests to verify the success path and failure due to
   invalid ASE state.

Test: cargo test -p bt-ascs
Bug: b/431814103
Change-Id: I0cef17323e1d0b2c9cfd16a0d978bb086b81144e
Reviewed-on: https://bluetooth-review.googlesource.com/c/bluetooth/+/2483
diff --git a/rust/bt-ascs/src/client.rs b/rust/bt-ascs/src/client.rs
index fbd50b1..ce1e1e8 100644
--- a/rust/bt-ascs/src/client.rs
+++ b/rust/bt-ascs/src/client.rs
@@ -306,7 +306,7 @@
     /// `UnknownAseId` or `InvalidStartState`) if any target ASE fails
     /// validation.
     fn validate_arguments<'a>(
-        &mut self,
+        &self,
         opcode: AseControlPointOpcode,
         pending_ases: impl IntoIterator<Item = &'a AseId>,
     ) -> Result<(), Error> {
@@ -375,9 +375,10 @@
 
     async fn collect_operation_responses(
         &mut self,
-        opcode: u8,
-        mut remaining_ases: HashSet<AseId>,
+        operation: &AseControlOperation,
     ) -> Result<HashMap<AseId, ResponseCode>, Error> {
+        let opcode = u8::try_from(operation)?;
+        let mut remaining_ases: HashSet<AseId> = operation.ase_ids().copied().collect();
         let mut responses = HashMap::new();
 
         while !remaining_ases.is_empty() {
@@ -414,12 +415,10 @@
     async fn perform_and_verify_operation(
         &mut self,
         operation: AseControlOperation,
-        pending_ases: HashSet<AseId>,
     ) -> Result<AseControlOperationOutcome, Error> {
         self.write_operation(&operation).await?;
 
-        let opcode_u8 = u8::try_from(&operation)?;
-        let responses = self.collect_operation_responses(opcode_u8, pending_ases).await?;
+        let responses = self.collect_operation_responses(&operation).await?;
 
         let mut successful_endpoints = HashMap::new();
         let opcode = AseControlPointOpcode::try_from(&operation).map_err(|e| {
@@ -489,12 +488,14 @@
         &mut self,
         codec_configurations: Vec<CodecConfiguration>,
     ) -> Result<AseControlOperationOutcome, Error> {
-        let pending_ases: HashSet<AseId> = codec_configurations.iter().map(|c| c.ase_id).collect();
-        self.validate_arguments(AseControlPointOpcode::ConfigCodec, &pending_ases)?;
+        self.validate_arguments(
+            AseControlPointOpcode::ConfigCodec,
+            codec_configurations.iter().map(|c| &c.ase_id),
+        )?;
 
         let op = AseControlOperation::ConfigCodec { codec_configurations, responses: vec![] };
 
-        self.perform_and_verify_operation(op, pending_ases).await
+        self.perform_and_verify_operation(op).await
     }
 
     /// Performs the Configure QoS operation on one or more ASEs.
@@ -515,15 +516,17 @@
             .map(|req| req.try_into_config(self))
             .collect::<Result<Vec<_>, Error>>()?;
 
-        let pending_ases: HashSet<AseId> = qos_configurations.iter().map(|q| q.ase_id).collect();
-        self.validate_arguments(AseControlPointOpcode::ConfigQos, &pending_ases)?;
+        self.validate_arguments(
+            AseControlPointOpcode::ConfigQos,
+            qos_configurations.iter().map(|q| &q.ase_id),
+        )?;
 
         // TODO(b/431814103): Consider ATT MTU limits when configuring multiple ASEs.
         // If the encoded request exceeds the current MTU, we may need to break it into
         // multiple Control Point writes.
         let op = AseControlOperation::ConfigQos { qos_configurations, responses: vec![] };
 
-        self.perform_and_verify_operation(op, pending_ases).await
+        self.perform_and_verify_operation(op).await
     }
 
     /// Performs the Enable operation on one or more ASEs.
@@ -539,12 +542,14 @@
         &mut self,
         ases_with_metadata: Vec<AseIdWithMetadata>,
     ) -> Result<AseControlOperationOutcome, Error> {
-        let pending_ases: HashSet<AseId> = ases_with_metadata.iter().map(|a| a.ase_id).collect();
-        self.validate_arguments(AseControlPointOpcode::Enable, &pending_ases)?;
+        self.validate_arguments(
+            AseControlPointOpcode::Enable,
+            ases_with_metadata.iter().map(|a| &a.ase_id),
+        )?;
 
         let op = AseControlOperation::Enable { ases_with_metadata, responses: vec![] };
 
-        self.perform_and_verify_operation(op, pending_ases).await
+        self.perform_and_verify_operation(op).await
     }
 
     /// Performs the Receiver Start Ready operation on one or more Source ASEs.
@@ -559,17 +564,16 @@
         &mut self,
         ases: Vec<AseId>,
     ) -> Result<AseControlOperationOutcome, Error> {
-        let pending_ases: HashSet<AseId> = ases.iter().cloned().collect();
-        self.validate_arguments(AseControlPointOpcode::ReceiverStartReady, &pending_ases)?;
+        self.validate_arguments(AseControlPointOpcode::ReceiverStartReady, &ases)?;
         self.verify_ase_directions(
-            &pending_ases,
+            &ases,
             AseControlPointOpcode::ReceiverStartReady,
             AudioDirection::Source,
         )?;
 
         let op = AseControlOperation::ReceiverStartReady { ases };
 
-        self.perform_and_verify_operation(op, pending_ases).await
+        self.perform_and_verify_operation(op).await
     }
 
     /// Performs the Disable operation on one or more ASEs.
@@ -581,12 +585,11 @@
     /// On success, returns an [`AseControlOperationOutcome`] containing the
     /// results of the operation.
     pub async fn disable(&mut self, ases: Vec<AseId>) -> Result<AseControlOperationOutcome, Error> {
-        let pending_ases: HashSet<AseId> = ases.iter().cloned().collect();
-        self.validate_arguments(AseControlPointOpcode::Disable, &pending_ases)?;
+        self.validate_arguments(AseControlPointOpcode::Disable, &ases)?;
 
         let op = AseControlOperation::Disable { ases };
 
-        self.perform_and_verify_operation(op, pending_ases).await
+        self.perform_and_verify_operation(op).await
     }
 
     /// Performs the Receiver Stop Ready operation on one or more Source ASEs.
@@ -601,17 +604,16 @@
         &mut self,
         ases: Vec<AseId>,
     ) -> Result<AseControlOperationOutcome, Error> {
-        let pending_ases: HashSet<AseId> = ases.iter().cloned().collect();
-        self.validate_arguments(AseControlPointOpcode::ReceiverStopReady, &pending_ases)?;
+        self.validate_arguments(AseControlPointOpcode::ReceiverStopReady, &ases)?;
         self.verify_ase_directions(
-            &pending_ases,
+            &ases,
             AseControlPointOpcode::ReceiverStopReady,
             AudioDirection::Source,
         )?;
 
         let op = AseControlOperation::ReceiverStopReady { ases };
 
-        self.perform_and_verify_operation(op, pending_ases).await
+        self.perform_and_verify_operation(op).await
     }
 
     /// Performs the Update Metadata operation on one or more ASEs.
@@ -632,10 +634,25 @@
             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
+        self.perform_and_verify_operation(op).await
+    }
+
+    /// Performs the Release operation on one or more ASEs.
+    ///
+    /// # Arguments
+    /// * `ases` - A vector of `AseId`s to release.
+    ///
+    /// # Returns
+    /// On success, returns an [`AseControlOperationOutcome`] containing the
+    /// results of the operation.
+    pub async fn release(&mut self, ases: Vec<AseId>) -> Result<AseControlOperationOutcome, Error> {
+        self.validate_arguments(AseControlPointOpcode::Release, &ases)?;
+
+        let op = AseControlOperation::Release { ases };
+
+        self.perform_and_verify_operation(op).await
     }
 }
 
@@ -1976,4 +1993,159 @@
             })
         ));
     }
+
+    #[test]
+    fn release_success_to_idle() {
+        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();
+
+        #[rustfmt::skip]
+        service.expect_characteristic_value(
+            &CONTROL_POINT_HANDLE,
+            vec![
+                0x08, // Opcode: Release
+                0x01, // Num ASEs
+                0x02, // ASE ID: 2
+            ],
+        );
+
+        #[rustfmt::skip]
+        service.notify(
+            &CONTROL_POINT_HANDLE,
+            Ok(CharacteristicNotification {
+                handle: CONTROL_POINT_HANDLE,
+                value: vec![
+                    0x08, // Opcode: Release
+                    0x01, // Num ASEs
+                    0x02, 0x00, 0x00, // ASE ID: 2, Success
+                ],
+                maybe_truncated: false,
+            }),
+        );
+
+        // Expected state transitions back to Idle
+        #[rustfmt::skip]
+        service.notify(
+            &SOURCE_ASE_HANDLE,
+            Ok(CharacteristicNotification {
+                handle: SOURCE_ASE_HANDLE,
+                value: vec![
+                    0x02, // ASE ID: 2
+                    0x00, // ASE State: Idle
+                ],
+                maybe_truncated: false,
+            }),
+        );
+
+        let release_fut = client.release(vec![AseId(2)]);
+        let outcome = run_to_completion(release_fut).expect("release should succeed");
+
+        assert_eq!(outcome.rejected().len(), 0);
+        assert_eq!(client.endpoints.source[&SOURCE_ASE_HANDLE].endpoint.state, AseState::Idle);
+    }
+
+    #[test]
+    fn release_success_to_codec_configured() {
+        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();
+
+        #[rustfmt::skip]
+        service.expect_characteristic_value(
+            &CONTROL_POINT_HANDLE,
+            vec![
+                0x08, // Opcode: Release
+                0x01, // Num ASEs
+                0x02, // ASE ID: 2
+            ],
+        );
+
+        #[rustfmt::skip]
+        service.notify(
+            &CONTROL_POINT_HANDLE,
+            Ok(CharacteristicNotification {
+                handle: CONTROL_POINT_HANDLE,
+                value: vec![
+                    0x08, // Opcode: Release
+                    0x01, // Num ASEs
+                    0x02, 0x00, 0x00, // ASE ID: 2, Success
+                ],
+                maybe_truncated: false,
+            }),
+        );
+
+        // Expected state transitions back to Codec Configured
+        #[rustfmt::skip]
+        service.notify(
+            &SOURCE_ASE_HANDLE,
+            Ok(CharacteristicNotification {
+                handle: SOURCE_ASE_HANDLE,
+                value: vec![
+                    0x02, // ASE ID: 2
+                    0x01, // ASE State: Codec Configured
+                    0x00, 0x01, 0x02, 0x0A, 0x00, 0x10, 0x27, 0x00, 0x40, 0x9C, 0x00, 0x00, 0x00, 0x00,
+                    0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00,
+                ],
+                maybe_truncated: false,
+            }),
+        );
+
+        let release_fut = client.release(vec![AseId(2)]);
+        let outcome = run_to_completion(release_fut).expect("release should succeed");
+
+        assert_eq!(outcome.rejected().len(), 0);
+        assert_eq!(
+            client.endpoints.source[&SOURCE_ASE_HANDLE].endpoint.state,
+            AseState::CodecConfigured
+        );
+    }
+
+    #[test]
+    fn release_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 Release
+        let release_fut = client.release(vec![AseId(2)]);
+        let err = run_to_completion(release_fut).expect_err("should fail client-side validation");
+
+        assert!(matches!(
+            err,
+            Error::Client(ClientError::InvalidStartState {
+                ase_id: AseId(2),
+                opcode: AseControlPointOpcode::Release,
+                actual: AseState::Idle,
+            })
+        ));
+    }
 }
diff --git a/rust/bt-ascs/src/types.rs b/rust/bt-ascs/src/types.rs
index fc330d2..27b81d3 100644
--- a/rust/bt-ascs/src/types.rs
+++ b/rust/bt-ascs/src/types.rs
@@ -739,6 +739,27 @@
 impl AseControlOperation {
     const MIN_BYTE_SIZE: usize = 3;
 
+    pub(crate) fn ase_ids(&self) -> impl Iterator<Item = &AseId> {
+        match self {
+            Self::ConfigCodec { codec_configurations, .. } => {
+                Box::new(codec_configurations.iter().map(|c| &c.ase_id))
+                    as Box<dyn Iterator<Item = &AseId>>
+            }
+            Self::ConfigQos { qos_configurations, .. } => {
+                Box::new(qos_configurations.iter().map(|q| &q.ase_id))
+            }
+            Self::Enable { ases_with_metadata, .. }
+            | Self::UpdateMetadata { ases_with_metadata, .. } => {
+                Box::new(ases_with_metadata.iter().map(|a| &a.ase_id))
+            }
+            Self::ReceiverStartReady { ases }
+            | Self::Disable { ases }
+            | Self::ReceiverStopReady { ases }
+            | Self::Release { ases } => Box::new(ases.iter()),
+            Self::Released { ase_id } => Box::new(std::iter::once(ase_id)),
+        }
+    }
+
     fn contains_invalid_length(&self) -> bool {
         match self {
             Self::ConfigCodec { responses, .. }
@@ -1897,12 +1918,47 @@
 mod tests {
     use super::*;
 
-    use bt_common::packet_encoding::Encodable;
+    use std::collections::HashSet;
 
-    use bt_common::core::ltv::LtValue;
     use bt_common::generic_audio::{codec_configuration, AudioLocation};
 
     #[test]
+    fn ase_control_operation_ase_ids() {
+        let op = AseControlOperation::ConfigCodec {
+            codec_configurations: vec![
+                CodecConfiguration {
+                    ase_id: AseId(1),
+                    target_latency: TargetLatency::TargetLowLatency,
+                    target_phy: TargetPhy::Le1MPhy,
+                    codec_id: CodecId::Assigned(bt_common::core::CodingFormat::Lc3),
+                    codec_specific_configuration: vec![],
+                },
+                CodecConfiguration {
+                    ase_id: AseId(2),
+                    target_latency: TargetLatency::TargetLowLatency,
+                    target_phy: TargetPhy::Le1MPhy,
+                    codec_id: CodecId::Assigned(bt_common::core::CodingFormat::Lc3),
+                    codec_specific_configuration: vec![],
+                },
+            ],
+            responses: vec![],
+        };
+        assert_eq!(
+            op.ase_ids().copied().collect::<HashSet<_>>(),
+            HashSet::from([AseId(1), AseId(2)])
+        );
+
+        let op = AseControlOperation::ReceiverStartReady { ases: vec![AseId(3), AseId(4)] };
+        assert_eq!(
+            op.ase_ids().copied().collect::<HashSet<_>>(),
+            HashSet::from([AseId(3), AseId(4)])
+        );
+
+        let op = AseControlOperation::Released { ase_id: AseId(5) };
+        assert_eq!(op.ase_ids().copied().collect::<HashSet<_>>(), HashSet::from([AseId(5)]));
+    }
+
+    #[test]
     fn opcode_state_rules() {
         // 1. allowed_in_state tests
         assert!(AseControlPointOpcode::ConfigCodec.allowed_in_state(&AseState::Idle));