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,
+            })
+        ));
+    }
 }