rust/bt-ascs: Implement Receiver Start Ready operation

This change implements the client-side "Receiver Start Ready" operation
as defined in ASCS v1.0.1, Section 5.4.

This also includes a major refactoring of the ASE control operations in
the `AudioStreamEndpointHandle` to use a common `perform_operation`
helper method. This reduces code duplication and improves
maintainability.

 - The `configure_codec`, `configure_qos`, and `enable` operations were
   updated to use this new helper.
 - The `AseControlOperation` enum was improved by renaming `is_valid_state`
   to `is_valid_operation` and `next_state` to `expected_next_state` to
   better reflect their purpose.

Test: cargo test -p bt-ascs
Bug: b/431814103

Change-Id: I916d9553eb529d6940635bcc14098e948f9a0cad
Reviewed-on: https://bluetooth-review.googlesource.com/c/bluetooth/+/2460
diff --git a/rust/bt-ascs/src/client.rs b/rust/bt-ascs/src/client.rs
index 6b7fac7..b20abb4 100644
--- a/rust/bt-ascs/src/client.rs
+++ b/rust/bt-ascs/src/client.rs
@@ -38,6 +38,8 @@
     InvalidStartState { ase_id: AseId, opcode: AseControlPointOpcode, actual: AseState },
     #[error("Remote server rejected operation: {0:?}")]
     RejectedOperation(Vec<ResponseCode>),
+    #[error("ASE {ase_id:?} has invalid direction {direction:?} for opcode {opcode:?}")]
+    InvalidDirection { ase_id: AseId, opcode: AseControlPointOpcode, direction: AudioDirection },
 }
 
 /// Represents a single source/sink ASE state characteristic.
@@ -276,6 +278,8 @@
         Ok(DiscoveredEndpoints { sink: sink_endpoints, source: source_endpoints })
     }
 
+    /// Reads the current value of an ASE characteristic from the remote server
+    /// and decodes it into an [`AudioStreamEndpoint`].
     async fn read_and_create_endpoint(
         gatt_client: &T::PeerService,
         handle: Handle,
@@ -294,35 +298,28 @@
         Ok(endpoint)
     }
 
-    fn verify_ase_ids<I>(&self, ase_ids: I) -> Result<(), Error>
-    where
-        I: IntoIterator<Item = AseId>,
-    {
-        for ase_id in ase_ids {
-            if self.endpoints.lookup_by_ase_id(ase_id).is_none() {
-                return Err(Error::Client(ClientError::UnknownAseId(ase_id)));
-            }
-        }
-        Ok(())
-    }
-
-    fn validate_arguments(
+    /// Validates that the target ASE IDs are known to this client and that
+    /// the requested `opcode` is allowed in their current state according
+    /// to the ASCS state machine.
+    ///
+    /// Returns `Ok(())` if validation passes, or a `ClientError` (such as
+    /// `UnknownAseId` or `InvalidStartState`) if any target ASE fails
+    /// validation.
+    fn validate_arguments<'a>(
         &mut self,
         opcode: AseControlPointOpcode,
-        pending_ases: &HashSet<AseId>,
+        pending_ases: impl IntoIterator<Item = &'a AseId>,
     ) -> Result<(), Error> {
-        self.verify_ase_ids(pending_ases.iter().cloned())?;
-
-        for ase_id in pending_ases {
+        for &ase_id in pending_ases {
             let endpoint = &self
                 .endpoints
-                .lookup_by_ase_id(*ase_id)
-                .ok_or(Error::Client(ClientError::UnknownAseId(*ase_id)))?
+                .lookup_by_ase_id(ase_id)
+                .ok_or(Error::Client(ClientError::UnknownAseId(ase_id)))?
                 .endpoint;
 
             if !opcode.allowed_in_state(&endpoint.state) {
                 return Err(Error::Client(ClientError::InvalidStartState {
-                    ase_id: *ase_id,
+                    ase_id,
                     opcode,
                     actual: endpoint.state,
                 }));
@@ -331,19 +328,8 @@
         Ok(())
     }
 
-    /// Reads the current value of an ASE characteristic from the remote server,
-    /// decodes it as an [`AudioStreamEndpoint`], updates the local cache of
-    /// sink or source endpoints, and returns the updated endpoint.
-    ///
-    /// # Arguments
-    /// * `handle` - The GATT characteristic handle of the ASE to read.
-    /// * `direction` - The direction (Sink or Source) of the ASE.
-    ///
-    /// # Returns
-    /// * `Ok(AudioStreamEndpoint)` - The newly read and updated endpoint
-    ///   struct.
-    /// * `Err(Error)` - If reading from the GATT client fails, or if the read
-    ///   value cannot be decoded.
+    /// Encodes an ASE control operation and writes it to the ASE Control Point
+    /// characteristic.
     async fn write_operation(&mut self, operation: &AseControlOperation) -> Result<(), Error> {
         use bt_common::packet_encoding::Encodable;
 
@@ -468,21 +454,13 @@
 
     /// Performs the Configure Codec operation on one or more ASEs.
     ///
-    /// This method sends the Configure Codec request to the remote server,
-    /// waits for control point notifications for all involved ASEs, and
-    /// verifies that the successfully configured ASEs have transitioned to
-    /// the `CodecConfigured` state.
-    ///
     /// # Arguments
     /// * `codec_configurations` - A vector of `CodecConfiguration` structs
     ///   containing the parameters for each ASE to be configured.
     ///
     /// # Returns
-    /// On success, returns a tuple containing:
-    /// * A `HashMap` mapping each successfully configured `AseId` to its
-    ///   supported `QosParameters` published by the server.
-    /// * A `Vec` containing the `ResponseCode` for any ASEs where the
-    ///   configuration failed.
+    /// On success, returns an [`AseControlOperationOutcome`] containing the
+    /// results of the operation.
     pub async fn configure_codec(
         &mut self,
         codec_configurations: Vec<CodecConfiguration>,
@@ -497,32 +475,13 @@
 
     /// Performs the Configure QoS operation on one or more ASEs.
     ///
-    /// This method sends the Configure QoS request to the remote server,
-    /// waits for control point notifications for all involved ASEs, and
-    /// verifies that the successfully configured ASEs have transitioned to
-    /// the `QosConfigured` state.
-    ///
-    /// # Arguments
-    /// * `qos_configurations` - A vector of `QosConfiguration` structs
-    ///   containing the QoS parameters for each ASE.
-    ///
-    /// # Returns
-    /// On success, returns a `Vec<ResponseCode>` containing the response
-    /// codes for any ASEs where the configuration failed.
-    /// Performs the Configure QoS operation on one or more ASEs.
-    ///
-    /// This method sends the Configure QoS request to the remote server,
-    /// waits for control point notifications for all involved ASEs, and
-    /// verifies that the successfully configured ASEs have transitioned to
-    /// the `QosConfigured` state.
-    ///
     /// # Arguments
     /// * `requests` - A vector of `QosConfigurationRequest` enums specifying
     ///   either custom or server-preferred configurations for each ASE.
     ///
     /// # Returns
-    /// On success, returns a `Vec<ResponseCode>` containing the response
-    /// codes for any ASEs where the configuration failed.
+    /// On success, returns an [`AseControlOperationOutcome`] containing the
+    /// results of the operation.
     pub async fn configure_qos(
         &mut self,
         requests: Vec<QosConfigurationRequest>,
@@ -545,18 +504,13 @@
 
     /// Performs the Enable operation on one or more ASEs.
     ///
-    /// This method sends the Enable request to the remote server,
-    /// waits for control point notifications for all involved ASEs, and
-    /// verifies that the successfully enabled ASEs have transitioned to
-    /// the `Enabling` state.
-    ///
     /// # Arguments
     /// * `ases_with_metadata` - A vector of `AseIdWithMetadata` structs
     ///   specifying the target ASE IDs and their codec metadata.
     ///
     /// # Returns
-    /// On success, returns a `Vec<ResponseCode>` containing the response
-    /// codes for any ASEs where the enable operation failed.
+    /// On success, returns an [`AseControlOperationOutcome`] containing the
+    /// results of the operation.
     pub async fn enable(
         &mut self,
         ases_with_metadata: Vec<AseIdWithMetadata>,
@@ -568,6 +522,40 @@
 
         self.perform_and_verify_operation(op, pending_ases).await
     }
+
+    /// Performs the Receiver Start Ready operation on one or more Source ASEs.
+    ///
+    /// # Arguments
+    /// * `ases` - A vector of `AseId`s to start.
+    ///
+    /// # Returns
+    /// On success, returns an [`AseControlOperationOutcome`] containing the
+    /// results of the operation.
+    pub async fn receiver_start_ready(
+        &mut self,
+        ases: Vec<AseId>,
+    ) -> Result<AseControlOperationOutcome, Error> {
+        let pending_ases: HashSet<AseId> = ases.iter().cloned().collect();
+        self.validate_arguments(AseControlPointOpcode::ReceiverStartReady, &pending_ases)?;
+
+        for ase_id in &pending_ases {
+            let endpoint = self
+                .endpoints
+                .lookup_by_ase_id(*ase_id)
+                .expect("ASE ID verified by validate_arguments");
+            if endpoint.endpoint.direction != AudioDirection::Source {
+                return Err(Error::Client(ClientError::InvalidDirection {
+                    ase_id: *ase_id,
+                    opcode: AseControlPointOpcode::ReceiverStartReady,
+                    direction: endpoint.endpoint.direction,
+                }));
+            }
+        }
+
+        let op = AseControlOperation::ReceiverStartReady { ases };
+
+        self.perform_and_verify_operation(op, pending_ases).await
+    }
 }
 
 #[cfg(test)]
@@ -1378,4 +1366,182 @@
             })
         ));
     }
+
+    #[test]
+    fn receiver_start_ready_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 to Enabling state
+        let source_value = vec![
+            0x02, // ASE ID: 2
+            0x03, // ASE State: Enabling
+            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![
+                0x04, // Opcode: Receiver Start Ready
+                0x01, // Num ASEs
+                0x02, // ASE ID: 2
+            ],
+        );
+
+        #[rustfmt::skip]
+        service.notify(
+            &CONTROL_POINT_HANDLE,
+            Ok(CharacteristicNotification {
+                handle: CONTROL_POINT_HANDLE,
+                value: vec![
+                    0x04, // Opcode: Receiver Start Ready
+                    0x01, // Num ASEs
+                    0x02, 0x00, 0x00, // ASE ID: 2, Success
+                ],
+                maybe_truncated: false,
+            }),
+        );
+
+        #[rustfmt::skip]
+        service.notify(
+            &SOURCE_ASE_HANDLE,
+            Ok(CharacteristicNotification {
+                handle: SOURCE_ASE_HANDLE,
+                value: vec![
+                    0x02, // ASE ID: 2
+                    0x04, // ASE State: Streaming
+                    0x01, // CIG ID: 1
+                    0x01, // CIS ID: 1
+                    0x00, // Metadata Length: 0
+                ],
+                maybe_truncated: false,
+            }),
+        );
+
+        let start_fut = client.receiver_start_ready(vec![AseId(2)]);
+        let outcome = run_to_completion(start_fut).expect("receiver start ready should succeed");
+
+        assert_eq!(outcome.rejected().len(), 0);
+        assert_eq!(client.endpoints.source[&SOURCE_ASE_HANDLE].endpoint.state, AseState::Streaming);
+    }
+
+    #[test]
+    fn receiver_start_ready_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");
+
+        // ASE 2 (Source) is in Idle state, which is invalid for ReceiverStartReady
+        let start_fut = client.receiver_start_ready(vec![AseId(2)]);
+        let err = run_to_completion(start_fut).expect_err("should fail client-side validation");
+
+        assert!(matches!(
+            err,
+            Error::Client(ClientError::InvalidStartState {
+                ase_id: AseId(2),
+                opcode: AseControlPointOpcode::ReceiverStartReady,
+                actual: AseState::Idle,
+            })
+        ));
+    }
+
+    #[test]
+    fn receiver_start_ready_fail_invalid_direction() {
+        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");
+
+        // Pre-condition SINK_ASE_HANDLE (ASE 1) to Enabling state
+        let sink_value = vec![
+            0x01, // ASE ID: 1
+            0x03, // ASE State: Enabling
+            0x01, 0x01, 0x00,
+        ];
+        client.endpoints.sink.get_mut(&SINK_ASE_HANDLE).unwrap().endpoint =
+            AudioStreamEndpoint::from_char_value(
+                SINK_ASE_HANDLE,
+                AudioDirection::Sink,
+                &sink_value,
+            )
+            .unwrap();
+
+        let start_fut = client.receiver_start_ready(vec![AseId(1)]);
+        let err = run_to_completion(start_fut).expect_err("should fail client-side validation");
+
+        assert!(matches!(
+            err,
+            Error::Client(ClientError::InvalidDirection {
+                ase_id: AseId(1),
+                opcode: AseControlPointOpcode::ReceiverStartReady,
+                direction: AudioDirection::Sink,
+            })
+        ));
+    }
+
+    #[test]
+    fn receiver_start_ready_rejected() {
+        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 to Enabling state
+        let source_value = vec![
+            0x02, // ASE ID: 2
+            0x03, // ASE State: Enabling
+            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![
+                0x04, // Opcode: Receiver Start Ready
+                0x01, // Num ASEs
+                0x02, // ASE ID: 2
+            ],
+        );
+
+        // Notify rejection from server due to Insufficient Resources (Response Code
+        // 0x0D)
+        #[rustfmt::skip]
+        service.notify(
+            &CONTROL_POINT_HANDLE,
+            Ok(CharacteristicNotification {
+                handle: CONTROL_POINT_HANDLE,
+                value: vec![
+                    0x04, // Opcode: Receiver Start Ready
+                    0x01, // Num ASEs
+                    0x02, 0x0D, 0x00, // ASE ID: 2, Insufficient Resources
+                ],
+                maybe_truncated: false,
+            }),
+        );
+
+        let start_fut = client.receiver_start_ready(vec![AseId(2)]);
+        let outcome = run_to_completion(start_fut).expect("receiver start ready should succeed");
+
+        assert_eq!(outcome.rejected().len(), 1);
+        assert_eq!(
+            outcome.rejected()[&AseId(2)],
+            ResponseCode::InsufficientResources { ase_id: AseId(2) }
+        );
+        // The state should remain Enabling
+        assert_eq!(client.endpoints.source[&SOURCE_ASE_HANDLE].endpoint.state, AseState::Enabling);
+    }
 }