blob: f2fdf21e78051ba74334f52b369ed31526327992 [file] [log] [blame]
// Copyright 2021 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use {
crate::log_if_err,
crate::message::Message,
crate::node::Node,
anyhow::{format_err, Context, Result},
async_trait::async_trait,
async_utils::hanging_get::client::HangingGetStream,
fidl_fuchsia_settings as fsettings,
fuchsia_component::client::connect_to_protocol,
fuchsia_inspect::{self as inspect, Property},
futures::future::{FutureExt as _, LocalBoxFuture},
futures::stream::FuturesUnordered,
futures::StreamExt as _,
serde_derive::Deserialize,
serde_json as json,
std::collections::HashMap,
std::rc::Rc,
};
/// Node: InputSettingsHandler
///
/// Summary: Connects to the fuchsia.settings.Input service to monitor for input settings changes.
/// (Initially, the node is only concerned with monitoring the enabled state of the
/// microphone). The node relays these settings changes to the SystemProfileHandler node.
///
/// Handles Messages: N/A
///
/// Sends Messages:
/// - NotifyMicEnabledChanged
///
/// FIDL dependencies:
/// - fuchsia.settings.Input: the node connects to this service to monitor for changes to input
/// settings
pub struct InputSettingsHandlerBuilder<'a> {
profile_handler_node: Rc<dyn Node>,
input_settings_proxy: Option<fsettings::InputProxy>,
inspect_root: Option<&'a inspect::Node>,
}
impl<'a> InputSettingsHandlerBuilder<'a> {
pub fn new_from_json(json_data: json::Value, nodes: &HashMap<String, Rc<dyn Node>>) -> Self {
#[derive(Deserialize)]
struct Dependencies {
system_profile_handler_node: String,
}
#[derive(Deserialize)]
struct JsonData {
dependencies: Dependencies,
}
let data: JsonData = json::from_value(json_data).unwrap();
Self {
profile_handler_node: nodes[&data.dependencies.system_profile_handler_node].clone(),
input_settings_proxy: None,
inspect_root: None,
}
}
#[cfg(test)]
fn new(profile_handler_node: Rc<dyn Node>) -> Self {
Self { profile_handler_node, input_settings_proxy: None, inspect_root: None }
}
#[cfg(test)]
fn with_inspect_root(mut self, root: &'a inspect::Node) -> Self {
self.inspect_root = Some(root);
self
}
#[cfg(test)]
fn with_proxy(mut self, proxy: fsettings::InputProxy) -> Self {
self.input_settings_proxy = Some(proxy);
self
}
pub fn build(
self,
futures_out: &FuturesUnordered<LocalBoxFuture<'_, ()>>,
) -> Result<Rc<InputSettingsHandler>> {
// Allow test to override
let inspect_root =
self.inspect_root.unwrap_or_else(|| inspect::component::inspector().root());
let inspect = InspectData::new(inspect_root, "InputSettingsHandler".to_string());
// Allow test to override
let input_settings_proxy = if let Some(proxy) = self.input_settings_proxy {
proxy
} else {
connect_to_protocol::<fsettings::InputMarker>()?
};
let node = Rc::new(InputSettingsHandler {
input_settings_proxy,
profile_handler_node: self.profile_handler_node,
inspect,
});
futures_out.push(node.clone().watch_input_settings()?);
Ok(node)
}
}
pub struct InputSettingsHandler {
/// Proxy to the fuchsia.settings.Input service.
input_settings_proxy: fsettings::InputProxy,
/// Node that we send the NotifyMicEnabledChanged message to once we observe changes to the
/// input settings.
profile_handler_node: Rc<dyn Node>,
inspect: InspectData,
}
impl InputSettingsHandler {
/// Watch the Input settings service for changes. When changes to the microphone enabled state
/// are observed, a NotifyMicEnabledChanged message is sent to `profile_handler_node`. The
/// method returns a Future that performs these steps in an infinite loop.
fn watch_input_settings<'a>(self: Rc<Self>) -> Result<LocalBoxFuture<'a, ()>> {
// Create a HangingGetStream wrapper to abstract the details of the hanging-get pattern that
// is used by the InputSettings service.
let proxy = self.input_settings_proxy.clone();
let mut stream = HangingGetStream::new(proxy, fsettings::InputProxy::watch);
Ok(async move {
self.inspect.set_handler_enabled(true);
let mut prev_mic_enabled = None;
loop {
match stream.next().await {
// Got a settings change event
Some(Ok(settings)) => match Self::parse_is_mic_enabled(settings) {
Ok(enabled) => {
if prev_mic_enabled != Some(enabled) {
self.inspect.set_mic_enabled(enabled);
prev_mic_enabled = Some(enabled);
log_if_err!(
self.send_message(
&self.profile_handler_node,
&Message::NotifyMicEnabledChanged(enabled),
)
.await,
"Failed to send NotifyMicEnabledChanged"
);
}
}
Err(e) => log::error!("Failed to parse mic settings: {:?}", e),
},
// Stream gave an unexpected error. This should only happen if the InputSettings
// service is not available (likely because it isn't running on this build
// variant), so exit the loop.
Some(Err(e)) => {
log::error!("Failed to monitor fuchsia.settings.Input ({:?})", e);
break;
}
// Stream will never close because the HangingGetStream always polls for new
// data.
None => unreachable!(),
}
}
log::error!("InputSettingsHandler is disabled");
self.inspect.set_handler_enabled(false);
}
.boxed_local())
}
/// Parses the InputSettings struct to retrieve microphone enabled state.
fn parse_is_mic_enabled(settings: fsettings::InputSettings) -> Result<bool> {
let mic_settings = settings
.devices
.context("Missing 'devices' in settings")?
.into_iter()
.filter(|device| device.device_type == Some(fsettings::DeviceType::Microphone))
.collect::<Vec<_>>();
match mic_settings.len() {
0 => Err(format_err!("Missing microphone settings")),
1 => Ok(()),
n => Err(format_err!("Invalid microphone settings length {} (expected 1)", n)),
}?;
let is_enabled = mic_settings[0]
.state
.as_ref()
.ok_or(format_err!("Microphone DeviceState is None"))?
.toggle_flags
.ok_or(format_err!("Microphone ToggleStateFlags is None"))?
.contains(fsettings::ToggleStateFlags::AVAILABLE);
Ok(is_enabled)
}
}
#[async_trait(?Send)]
impl Node for InputSettingsHandler {
fn name(&self) -> String {
"InputSettingsHandler".to_string()
}
}
struct InspectData {
handler_enabled: inspect::StringProperty,
mic_enabled: inspect::StringProperty,
}
impl InspectData {
fn new(parent: &inspect::Node, name: String) -> Self {
let root = parent.create_child(name);
let handler_enabled = root.create_string("handler_enabled", "");
let mic_enabled = root.create_string("mic_enabled", "");
parent.record(root);
Self { handler_enabled, mic_enabled }
}
fn set_handler_enabled(&self, enabled: bool) {
self.handler_enabled.set(&enabled.to_string());
}
fn set_mic_enabled(&self, enabled: bool) {
self.mic_enabled.set(&enabled.to_string());
}
}
#[cfg(test)]
mod tests {
use {
super::*,
crate::test::mock_node::{create_dummy_node, MessageMatcher, MockNodeMaker},
crate::{msg_eq, msg_ok_return},
fuchsia_async as fasync,
fuchsia_inspect::assert_data_tree,
};
// A fake Settings service implementation for testing
struct FakeSettingsSvc {
stream: fsettings::InputRequestStream,
pending_request: Option<fsettings::InputWatchResponder>,
}
impl FakeSettingsSvc {
fn new() -> (fsettings::InputProxy, Self) {
let (proxy, stream) =
fidl::endpoints::create_proxy_and_stream::<fsettings::InputMarker>()
.expect("Failed to create Input proxy and stream");
(proxy, Self { stream, pending_request: None })
}
// Generates the required InputSettings struct based on some short parameters for testing
fn generate_device_settings(mic_enabled: bool) -> fsettings::InputSettings {
fsettings::InputSettings {
devices: Some(vec![fsettings::InputDevice {
device_type: Some(fsettings::DeviceType::Microphone),
state: Some(fsettings::DeviceState {
toggle_flags: Some(if mic_enabled {
fsettings::ToggleStateFlags::AVAILABLE
} else {
fsettings::ToggleStateFlags::MUTED
}),
..fsettings::DeviceState::EMPTY
}),
..fsettings::InputDevice::EMPTY
}]),
..fsettings::InputSettings::EMPTY
}
}
// Gets the pending hanging-get request and completes the request with the specified device
// settings. Waits for the next hanging-get request to arrive before returning to ensure the
// node has processed the response.
async fn set_mic_enabled(&mut self, enabled: bool) {
// Make sure there is a pending hanging-get
self.ensure_request_pending().await;
// Complete the pending hanging-get with the mic_enabled value
self.pending_request
.take()
.unwrap()
.send(Self::generate_device_settings(enabled))
.expect("Failed to send mic state update to client");
// Wait for the next hanging-get request to arrive so we can be sure the node has
// processed the mic_enabled result we just provided
self.ensure_request_pending().await;
}
async fn ensure_request_pending(&mut self) {
if self.pending_request.is_none() {
self.pending_request = Some(self.get_next_request().await);
}
}
// Retrieves the next hanging-get request
async fn get_next_request(&mut self) -> fsettings::InputWatchResponder {
match self
.stream
.next()
.await
.expect(
"Input request stream yielded Some(None)
(Input channel closed without receiving hanging-get request)",
)
.expect("Input request stream yielded Some(Err)")
{
fsettings::InputRequest::Watch { responder } => responder,
request => panic!("Unexpected request: {:?}", request),
}
}
}
/// Tests for the presence and correctness of dynamically-added inspect data
#[fasync::run_singlethreaded(test)]
async fn test_inspect_data() {
let inspector = inspect::Inspector::new();
let (proxy, mut fake_settings) = FakeSettingsSvc::new();
let futures_out = FuturesUnordered::new();
let _node = InputSettingsHandlerBuilder::new(create_dummy_node())
.with_inspect_root(inspector.root())
.with_proxy(proxy)
.build(&futures_out)
.unwrap();
futures::select! {
_ = futures_out.collect::<()>() => {},
_ = async {
fake_settings.set_mic_enabled(true).await;
assert_data_tree!(
inspector,
root: {
"InputSettingsHandler": {
"handler_enabled": "true",
"mic_enabled": "true"
}
}
);
fake_settings.set_mic_enabled(false).await;
assert_data_tree!(
inspector,
root: {
"InputSettingsHandler": {
"handler_enabled": "true",
"mic_enabled": "false"
}
}
);
}.fuse() => {}
};
}
/// Tests that the InputSettingsHandler relays NotifyMicEnabledChanged messages to the
/// ProfileHandler node when it observes changes to input settings.
#[fasync::run_singlethreaded(test)]
async fn test_settings_monitor() {
let mut mock_maker = MockNodeMaker::new();
// For this test, the ProfileHandler should receive two NotifyMicEnabledChanged messages
let profile_handler_node = mock_maker.make(
"ProfileHandler",
vec![
(msg_eq!(NotifyMicEnabledChanged(true)), msg_ok_return!(NotifyMicEnabledChanged)),
(msg_eq!(NotifyMicEnabledChanged(false)), msg_ok_return!(NotifyMicEnabledChanged)),
],
);
// Create the node
let (proxy, mut fake_settings) = FakeSettingsSvc::new();
let futures_out = FuturesUnordered::new();
let _node = InputSettingsHandlerBuilder::new(profile_handler_node)
.with_proxy(proxy)
.build(&futures_out);
// Use `select!` here so that the node future is polled concurrently with our fake_settings
// changes. This lets the node's `watch_input_settings` future run while we simulate input
// settings changes.
futures::select! {
_ = futures_out.collect::<()>() => {},
_ = async {
fake_settings.set_mic_enabled(true).await;
fake_settings.set_mic_enabled(false).await;
}.fuse() => {}
};
// When mock_maker goes out of scope it verifies the two NotifyMicEnabledChanged messages
// were received
}
}