diff --git a/cras/client/audio_diagnostics/src/settings.rs b/cras/client/audio_diagnostics/src/settings.rs index c4300e1a7..f78aba38c 100644 --- a/cras/client/audio_diagnostics/src/settings.rs +++ b/cras/client/audio_diagnostics/src/settings.rs @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +use std::collections::BTreeSet; use std::collections::HashMap; use anyhow::Context; @@ -26,11 +27,13 @@ struct Audio { devices: Option, input_user_priority: Option>, output_user_priority: Option>, + input_preference_set: Option>, + output_preference_set: Option>, // The last_seen field is intentionally dropped to avoid // joining feedback reports. See b/279545748#comment11. } -#[derive(PartialEq, Eq, Hash, Debug)] +#[derive(PartialEq, Eq, Hash, Debug, Ord, PartialOrd)] struct AudioNode { stable_id: u32, is_input: bool, @@ -42,6 +45,16 @@ enum AudioNodeParseError { InvalidIsInput, } +impl AudioNodeParseError { + fn as_str(&self) -> &'static str { + match self { + AudioNodeParseError::MissingDelimiter => "missing delimiter", + AudioNodeParseError::StableIdNotU32 => "stable id is not u32", + AudioNodeParseError::InvalidIsInput => "invalid is_input", + } + } +} + impl AudioNode { fn try_from_str(v: &str) -> Result { let (stable_id_str, is_input_str) = v @@ -62,16 +75,20 @@ impl AudioNode { } } +fn format_audio_node(node: &AudioNode) -> String { + format!( + "0x{:x} : {}", + Salt::instance().pseudonymize_stable_id(node.stable_id), + node.is_input as i32 + ) +} + impl Serialize for AudioNode { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, { - serializer.serialize_str(&format!( - "0x{:x} : {}", - Salt::instance().pseudonymize_stable_id(self.stable_id), - self.is_input as i32 - )) + serializer.serialize_str(&format_audio_node(self)) } } @@ -88,14 +105,9 @@ impl<'de> Visitor<'de> for AudioNodeVisitor { where E: serde::de::Error, { - use AudioNodeParseError::*; match AudioNode::try_from_str(v) { Ok(node) => Ok(node), - Err(err) => Err(match err { - MissingDelimiter => E::custom("missing delimiter"), - StableIdNotU32 => E::custom("stable id is not u32"), - InvalidIsInput => E::custom("invalid is_input"), - }), + Err(err) => Err(E::custom(err.as_str())), } } } @@ -109,6 +121,43 @@ impl<'de> Deserialize<'de> for AudioNode { } } +#[derive(PartialEq, Eq, Hash, Debug)] +struct AudioNodeSet { + audio_nodes: BTreeSet, +} + +impl<'de> Deserialize<'de> for AudioNodeSet { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let mut audio_nodes = BTreeSet::new(); + let audio_nodes_string = String::deserialize(deserializer)?; + for part in audio_nodes_string.split(',') { + let audio_node = AudioNode::try_from_str(part.trim()) + .map_err(|err| serde::de::Error::custom(err.as_str()))?; + audio_nodes.insert(audio_node); + } + Ok(AudioNodeSet { audio_nodes }) + } +} + +impl Serialize for AudioNodeSet { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let mut audio_nodes_string = String::new(); + for (i, node) in self.audio_nodes.iter().enumerate() { + if i > 0 { + audio_nodes_string.push_str(", "); + } + audio_nodes_string.push_str(&format_audio_node(node)); + } + serializer.serialize_str(&audio_nodes_string) + } +} + #[derive(Serialize, Deserialize, Debug, PartialEq)] struct DeviceState { activate_by_user: Option, @@ -140,9 +189,12 @@ pub fn print_salted_audio_settings(path: &str) { #[cfg(test)] mod tests { + use std::collections::BTreeSet; + use cras_common::pseudonymization::Salt; use super::AudioNode; + use super::AudioNodeSet; use super::DeviceState; use super::LocalState; @@ -169,6 +221,67 @@ mod tests { assert_eq!(serde_json::from_str::(b_json).unwrap(), b); } + #[test] + fn audio_node_set() { + let stable_id1 = 1; + let stable_id2 = 2; + let input_set = AudioNodeSet { + audio_nodes: BTreeSet::from([ + AudioNode { + stable_id: stable_id1, + is_input: true, + }, + AudioNode { + stable_id: stable_id2, + is_input: true, + }, + ]), + }; + let input_set_json = format!("\"{:x} : 1, {:x} : 1\"", stable_id1, stable_id2); + let input_set_json_salted = format!( + "\"0x{:x} : 1, 0x{:x} : 1\"", + Salt::instance().pseudonymize_stable_id(stable_id1), + Salt::instance().pseudonymize_stable_id(stable_id2) + ); + assert_eq!( + serde_json::to_string(&input_set).unwrap(), + input_set_json_salted + ); + assert_eq!( + serde_json::from_str::(&input_set_json).unwrap(), + input_set + ); + + let stable_id3 = 3; + let stable_id4 = 4; + let output_set = AudioNodeSet { + audio_nodes: BTreeSet::from([ + AudioNode { + stable_id: stable_id3, + is_input: false, + }, + AudioNode { + stable_id: stable_id4, + is_input: false, + }, + ]), + }; + let output_set_json = format!("\"{:x} : 0, {:x} : 0\"", stable_id3, stable_id4); + let output_set_json_salted = format!( + "\"0x{:x} : 0, 0x{:x} : 0\"", + Salt::instance().pseudonymize_stable_id(stable_id3), + Salt::instance().pseudonymize_stable_id(stable_id4) + ); + assert_eq!( + serde_json::to_string(&output_set).unwrap(), + output_set_json_salted + ); + assert_eq!( + serde_json::from_str::(&output_set_json).unwrap(), + output_set + ); + } + #[test] fn deserialize() { let audio_settings_str = r#"{ @@ -198,6 +311,9 @@ mod tests { "input_user_priority": { "3787040 : 1": 1 }, + "input_preference_set": { + "3787040 : 1, 2315562897 : 1, 3962083865 : 1": "2315562897 : 1" + }, "last_seen": { "1923447123 : 0": 1687158819.326577, "2315562897 : 1": 1687158819.326577, @@ -207,6 +323,9 @@ mod tests { }, "output_user_priority": { "1923447123 : 0": 1 + }, + "output_preference_set": { + "2356475750 : 0, 1923447123 : 0, 2315562897 : 0": "1923447123 : 0" } }"#; let local_state_string = r#"{"settings":{"audio":@AUDIO_SETTINGS@}}"# @@ -302,6 +421,33 @@ mod tests { .unwrap(), 1 ); + assert_eq!( + *audio + .input_preference_set + .as_ref() + .unwrap() + .get(&AudioNodeSet { + audio_nodes: BTreeSet::from([ + AudioNode { + stable_id: 3962083865, + is_input: true, + }, + AudioNode { + stable_id: 3787040, + is_input: true, + }, + AudioNode { + stable_id: 2315562897, + is_input: true, + }, + ]) + }) + .unwrap(), + AudioNode { + stable_id: 2315562897, + is_input: true + } + ); assert_eq!( *audio .output_user_priority @@ -313,6 +459,33 @@ mod tests { }) .unwrap(), 1 - ) + ); + assert_eq!( + *audio + .output_preference_set + .as_ref() + .unwrap() + .get(&AudioNodeSet { + audio_nodes: BTreeSet::from([ + AudioNode { + stable_id: 2315562897, + is_input: false, + }, + AudioNode { + stable_id: 2356475750, + is_input: false, + }, + AudioNode { + stable_id: 1923447123, + is_input: false, + }, + ]) + }) + .unwrap(), + AudioNode { + stable_id: 1923447123, + is_input: false + } + ); } }