rust/bt-ascs: Implement Disable operation

This change implements the client-side "Disable" operation as defined
in ASCS v1.0.1, Section 5.5.

 - The `disable` method was added to the `AudioStreamEndpointHandle`.
 - The implementation follows the established pattern of using the
   `perform_operation` helper method.
 - Unit tests have been added to verify the new functionality.

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

Change-Id: Ibbb39c2a8caa39cdd3805fd90983e1965eb46708
Reviewed-on: https://bluetooth-review.googlesource.com/c/bluetooth/+/2480
diff --git a/rust/bt-ascs/src/client.rs b/rust/bt-ascs/src/client.rs
index b20abb4..71a4192 100644
--- a/rust/bt-ascs/src/client.rs
+++ b/rust/bt-ascs/src/client.rs
@@ -556,6 +556,23 @@
 
         self.perform_and_verify_operation(op, pending_ases).await
     }
+
+    /// Performs the Disable operation on one or more ASEs.
+    ///
+    /// # Arguments
+    /// * `ases` - A vector of `AseId`s to disable.
+    ///
+    /// # Returns
+    /// 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)?;
+
+        let op = AseControlOperation::Disable { ases };
+
+        self.perform_and_verify_operation(op, pending_ases).await
+    }
 }
 
 #[cfg(test)]
@@ -1544,4 +1561,123 @@
         // The state should remain Enabling
         assert_eq!(client.endpoints.source[&SOURCE_ASE_HANDLE].endpoint.state, AseState::Enabling);
     }
+
+    #[test]
+    fn disable_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 SINK_ASE_HANDLE (ASE 1) to Streaming state
+        let sink_value = vec![
+            0x01, // ASE ID: 1
+            0x04, // ASE State: Streaming
+            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();
+
+        // 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![
+                0x05, // Opcode: Disable
+                0x02, // Num ASEs
+                0x01, // ASE ID: 1
+                0x02, // ASE ID: 2
+            ],
+        );
+
+        #[rustfmt::skip]
+        service.notify(
+            &CONTROL_POINT_HANDLE,
+            Ok(CharacteristicNotification {
+                handle: CONTROL_POINT_HANDLE,
+                value: vec![
+                    0x05, // Opcode: Disable
+                    0x02, // Num ASEs
+                    0x01, 0x00, 0x00, // ASE ID: 1, Success
+                    0x02, 0x00, 0x00, // ASE ID: 2, Success
+                ],
+                maybe_truncated: false,
+            }),
+        );
+
+        // Expected state updates:
+        // Sink ASE transitions back to QoS Configured (no Disabling state)
+        #[rustfmt::skip]
+        service.notify(
+            &SINK_ASE_HANDLE,
+            Ok(CharacteristicNotification {
+                handle: SINK_ASE_HANDLE,
+                value: vec![
+                    0x01, // ASE ID: 1
+                    0x02, // ASE State: QoS Configured
+                    0x01, 0x01, 0x10, 0x27, 0x00, 0x00, 0x01, 0x64, 0x00, 0x02, 0x0A, 0x00, 0x40, 0x9C, 0x00,
+                ],
+                maybe_truncated: false,
+            }),
+        );
+
+        // Source ASE transitions to Disabling
+        #[rustfmt::skip]
+        service.notify(
+            &SOURCE_ASE_HANDLE,
+            Ok(CharacteristicNotification {
+                handle: SOURCE_ASE_HANDLE,
+                value: vec![
+                    0x02, // ASE ID: 2
+                    0x05, // ASE State: Disabling
+                    0x01, 0x01, 0x00,
+                ],
+                maybe_truncated: false,
+            }),
+        );
+
+        let disable_fut = client.disable(vec![AseId(1), AseId(2)]);
+        let outcome = run_to_completion(disable_fut).expect("disable should succeed");
+
+        assert_eq!(outcome.rejected().len(), 0);
+        assert_eq!(client.endpoints.sink[&SINK_ASE_HANDLE].endpoint.state, AseState::QosConfigured);
+        assert_eq!(client.endpoints.source[&SOURCE_ASE_HANDLE].endpoint.state, AseState::Disabling);
+    }
+
+    #[test]
+    fn disable_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 1 is in Idle state, which is an invalid starting state for Disable
+        let disable_fut = client.disable(vec![AseId(1)]);
+        let err = run_to_completion(disable_fut).expect_err("should fail client-side validation");
+
+        assert!(matches!(
+            err,
+            Error::Client(ClientError::InvalidStartState {
+                ase_id: AseId(1),
+                opcode: AseControlPointOpcode::Disable,
+                actual: AseState::Idle,
+            })
+        ));
+    }
 }