blob: cb055792c06c8ee0ef4b3d4c87a63951f40b9807 [file] [log] [blame]
// Copyright 2019 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 anyhow::{format_err, Error};
use serde_json::{from_value, to_value, Value};
use crate::setui::types::{IntlInfo, MicStates, NetworkType, SetUiResult};
use fidl_fuchsia_media::AudioRenderUsage;
use fidl_fuchsia_settings::{
self as fsettings, AudioMarker, AudioStreamSettingSource, AudioStreamSettings,
ConfigurationInterfaces, DeviceState, DisplayMarker, DisplaySettings, InputMarker, InputState,
IntlMarker, SetupMarker, SetupSettings, Volume,
};
use fuchsia_component::client::connect_to_protocol;
use fuchsia_syslog::macros::fx_log_info;
/// Facade providing access to SetUi interfaces.
#[derive(Debug)]
pub struct SetUiFacade {
/// Audio proxy that may be optionally provided for testing. The proxy is not cached during
/// normal operation.
audio_proxy: Option<fsettings::AudioProxy>,
/// Optional Display proxy for testing, similar to `audio_proxy`.
display_proxy: Option<fsettings::DisplayProxy>,
/// Optional Input proxy for testing, similar to `audio_proxy`.
input_proxy: Option<fsettings::InputProxy>,
}
impl SetUiFacade {
pub fn new() -> SetUiFacade {
SetUiFacade { audio_proxy: None, display_proxy: None, input_proxy: None }
}
/// Sets network option used by device setup.
/// Same action as choosing "Setup over Ethernet [enabled|disabled]" in "Developer options"
///
/// args: accepted args are "ethernet" or "wifi". ex: {"params": "ethernet"}
pub async fn set_network(&self, args: Value) -> Result<Value, Error> {
let network_type: NetworkType = from_value(args)?;
fx_log_info!("set_network input {:?}", network_type);
let setup_service_proxy = match connect_to_protocol::<SetupMarker>() {
Ok(proxy) => proxy,
Err(e) => bail!("Failed to connect to Setup service {:?}.", e),
};
let mut settings = SetupSettings::EMPTY;
match network_type {
NetworkType::Ethernet => {
settings.enabled_configuration_interfaces = Some(ConfigurationInterfaces::ETHERNET);
}
NetworkType::Wifi => {
settings.enabled_configuration_interfaces = Some(ConfigurationInterfaces::WIFI);
}
_ => return Err(format_err!("Network type must either be ethernet or wifi.")),
}
// Update network configuration without automatic device reboot.
// For changes to take effect, either restart basemgr component or reboot device.
match setup_service_proxy.set(settings, false).await? {
Ok(_) => Ok(to_value(SetUiResult::Success)?),
Err(err) => Err(format_err!("Update network settings failed with err {:?}", err)),
}
}
/// Reports the network option used for setup
///
/// Returns either "ethernet", "wifi" or "unknown".
pub async fn get_network_setting(&self) -> Result<Value, Error> {
let setup_service_proxy = match connect_to_protocol::<SetupMarker>() {
Ok(proxy) => proxy,
Err(e) => bail!("Failed to connect to Setup service {:?}.", e),
};
let setting = setup_service_proxy.watch().await?;
match setting.enabled_configuration_interfaces {
Some(ConfigurationInterfaces::ETHERNET) => Ok(to_value(NetworkType::Ethernet)?),
Some(ConfigurationInterfaces::WIFI) => Ok(to_value(NetworkType::Wifi)?),
_ => Ok(to_value(NetworkType::Unknown)?),
}
}
/// Sets Internationalization values.
///
/// Input should is expected to be type IntlInfo in json.
/// Example:
/// {
/// "hour_cycle":"H12",
/// "locales":[{"id":"he-FR"}],
/// "temperature_unit":"Celsius",
/// "time_zone_id":"UTC"
/// }
pub async fn set_intl_setting(&self, args: Value) -> Result<Value, Error> {
let intl_info: IntlInfo = from_value(args)?;
fx_log_info!("Received Intl Settings Request {:?}", intl_info);
let intl_service_proxy = match connect_to_protocol::<IntlMarker>() {
Ok(proxy) => proxy,
Err(e) => bail!("Failed to connect to Intl service {:?}.", e),
};
match intl_service_proxy.set(intl_info.into()).await? {
Ok(_) => Ok(to_value(SetUiResult::Success)?),
Err(err) => Err(format_err!("Update intl settings failed with err {:?}", err)),
}
}
/// Reads the Internationalization setting.
///
/// Returns IntlInfo in json.
pub async fn get_intl_setting(&self) -> Result<Value, Error> {
let intl_service_proxy = match connect_to_protocol::<IntlMarker>() {
Ok(proxy) => proxy,
Err(e) => bail!("Failed to connect to Intl service {:?}.", e),
};
let intl_info: IntlInfo = intl_service_proxy.watch().await?.into();
return Ok(to_value(&intl_info)?);
}
/// Reports the microphone DeviceState.
///
/// Returns true if mic is muted or false if mic is unmuted.
pub async fn is_mic_muted(&self) -> Result<Value, Error> {
let input_proxy = match self.input_proxy.as_ref() {
Some(proxy) => proxy.clone(),
None => match connect_to_protocol::<InputMarker>() {
Ok(proxy) => proxy,
Err(e) => bail!("isMicMuted - failed to connect to Input service {:?}.", e),
},
};
match input_proxy.watch().await?.devices {
Some(input_device) => {
let mut muted = false;
let device = input_device
.into_iter()
.find(|device| device.device_type == Some(fsettings::DeviceType::Microphone))
.unwrap();
match device.state {
Some(state) => {
muted = state.toggle_flags == Some(fsettings::ToggleStateFlags::MUTED);
}
_ => (),
}
return Ok(to_value(muted)?);
}
_ => Err(format_err!("isMicMuted - cannot read input state.")),
}
}
/// Sets the display brightness via `fuchsia.settings.Display.Set`.
///
/// # Arguments
/// * `args`: JSON value containing the desired brightness level as f32.
pub async fn set_brightness(&self, args: Value) -> Result<Value, Error> {
let brightness: f32 = from_value(args)?;
// Use the test proxy if one was provided, otherwise connect to the discoverable Display
// service.
let display_proxy = match self.display_proxy.as_ref() {
Some(proxy) => proxy.clone(),
None => match connect_to_protocol::<DisplayMarker>() {
Ok(proxy) => proxy,
Err(e) => bail!("Failed to connect to Display service {:?}.", e),
},
};
let settings = DisplaySettings {
auto_brightness: Some(false),
brightness_value: Some(brightness),
..DisplaySettings::EMPTY
};
match display_proxy.set(settings).await? {
Ok(_) => Ok(to_value(SetUiResult::Success)?),
Err(e) => Err(format_err!("SetBrightness failed with err {:?}", e)),
}
}
/// Sets the media volume level via `fuchsia.settings.Audio.Set`.
///
/// # Arguments
/// * `args`: JSON value containing the desired volume level as f32.
pub async fn set_media_volume(&self, args: Value) -> Result<Value, Error> {
let volume: f32 = from_value(args)?;
// Use the test proxy if one was provided, otherwise connect to the discoverable Audio
// service.
let audio_proxy = match self.audio_proxy.as_ref() {
Some(proxy) => proxy.clone(),
None => match connect_to_protocol::<AudioMarker>() {
Ok(proxy) => proxy,
Err(e) => bail!("Failed to connect to Display service {:?}.", e),
},
};
let stream_settings = AudioStreamSettings {
stream: Some(AudioRenderUsage::Media),
source: Some(AudioStreamSettingSource::User),
user_volume: Some(Volume { level: Some(volume), muted: Some(false), ..Volume::EMPTY }),
..AudioStreamSettings::EMPTY
};
let settings = fsettings::AudioSettings {
streams: Some(vec![stream_settings]),
input: None,
..fsettings::AudioSettings::EMPTY
};
fx_log_info!("Setting audio settings {:?}", settings);
match audio_proxy.set(settings).await? {
Ok(_) => Ok(to_value(SetUiResult::Success)?),
Err(e) => Err(format_err!("SetVolume failed with err {:?}", e)),
}
}
/// Sets the AudioInput mic to (not)muted depending on input.
///
/// # Arguments
/// * args: accepted args are "muted" or "available". ex: {"params": "muted"}
pub async fn set_mic_mute(&self, args: Value) -> Result<Value, Error> {
let mic_state: MicStates = from_value(args)?;
// If mic is already in desired state, then nothing left to execute.
let is_muted = self.is_mic_muted().await?.as_bool().unwrap();
let mut mute_mic: bool = false;
match mic_state {
MicStates::Muted => {
if is_muted {
return Ok(to_value(SetUiResult::Success)?);
}
mute_mic = true;
}
MicStates::Available => {
if !is_muted {
return Ok(to_value(SetUiResult::Success)?);
}
}
_ => return Err(format_err!("Mic state must either be muted or available.")),
}
// Use given proxy (if possible), else connect to protocol.
let input_proxy = match self.input_proxy.as_ref() {
Some(proxy) => proxy.clone(),
None => match connect_to_protocol::<InputMarker>() {
Ok(proxy) => proxy,
Err(e) => bail!("Failed to connect to Microphone {:?}.", e),
},
};
// Initialize the InputState struct.
let mic_device_name = "microphone";
let mut input_states = InputState {
name: Some(mic_device_name.to_string()),
device_type: Some(fsettings::DeviceType::Microphone),
state: Some(DeviceState {
toggle_flags: Some(fsettings::ToggleStateFlags::AVAILABLE),
..DeviceState::EMPTY
}),
..InputState::EMPTY
};
// Change DeviceState if microphone should be muted- dependent on input enum.
if mute_mic {
input_states.state = Some(DeviceState {
toggle_flags: Some(fsettings::ToggleStateFlags::MUTED),
..DeviceState::EMPTY
});
}
fx_log_info!("SetMicMute: setting input state {:?}", input_states);
match input_proxy.set(&mut vec![input_states].into_iter()).await? {
Ok(_) => Ok(to_value(SetUiResult::Success)?),
Err(e) => Err(format_err!("SetMicMute failed with err {:?}", e)),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::common_utils::test::assert_value_round_trips_as;
use crate::setui::types::{
HourCycle, IntlInfo, LocaleId, MicStates, MicStates::Muted, TemperatureUnit,
};
use fidl::endpoints::create_proxy_and_stream;
use fidl_fuchsia_settings::InputDevice;
use fuchsia_async as fasync;
use futures::TryStreamExt;
use serde_json::json;
fn make_intl_info() -> IntlInfo {
return IntlInfo {
locales: Some(vec![LocaleId { id: "en-US".into() }]),
temperature_unit: Some(TemperatureUnit::Celsius),
time_zone_id: Some("UTC".into()),
hour_cycle: Some(HourCycle::H12),
};
}
#[test]
fn serde_intl_set() {
let intl_request = make_intl_info();
assert_value_round_trips_as(
intl_request,
json!(
{
"locales": [{"id": "en-US"}],
"temperature_unit":"Celsius",
"time_zone_id": "UTC",
"hour_cycle": "H12",
}),
);
}
// Tests that `set_brightness` correctly sends a request to the Display service.
#[fasync::run_singlethreaded(test)]
async fn test_set_brightness() {
let brightness = 0.5f32;
let (proxy, mut stream) = create_proxy_and_stream::<DisplayMarker>().unwrap();
// Create a facade future that sends a request to `proxy`.
let facade =
SetUiFacade { audio_proxy: None, display_proxy: Some(proxy), input_proxy: None };
let facade_fut = async move {
assert_eq!(
facade.set_brightness(to_value(brightness).unwrap()).await.unwrap(),
to_value(SetUiResult::Success).unwrap()
);
};
// Create a future to service the request stream.
let stream_fut = async move {
match stream.try_next().await {
Ok(Some(fsettings::DisplayRequest::Set { settings, responder })) => {
assert_eq!(
settings,
DisplaySettings {
auto_brightness: Some(false),
brightness_value: Some(brightness),
..DisplaySettings::EMPTY
}
);
responder.send(&mut Ok(())).unwrap();
}
other => panic!("Unexpected stream item: {:?}", other),
}
};
futures::future::join(facade_fut, stream_fut).await;
}
// Tests that `set_media_volume` correctly sends a request to the Audio service.
#[fasync::run_singlethreaded(test)]
async fn test_set_media_volume() {
let volume = 0.5f32;
let (proxy, mut stream) = create_proxy_and_stream::<AudioMarker>().unwrap();
// Create a facade future that sends a request to `proxy`.
let facade =
SetUiFacade { audio_proxy: Some(proxy), display_proxy: None, input_proxy: None };
let facade_fut = async move {
assert_eq!(
facade.set_media_volume(to_value(volume).unwrap()).await.unwrap(),
to_value(SetUiResult::Success).unwrap()
);
};
// Create a future to service the request stream.
let stream_fut = async move {
match stream.try_next().await {
Ok(Some(fsettings::AudioRequest::Set { settings, responder })) => {
let mut streams = settings.streams.unwrap();
assert_eq!(1, streams.len());
assert_eq!(
streams.pop().unwrap(),
AudioStreamSettings {
stream: Some(AudioRenderUsage::Media),
source: Some(AudioStreamSettingSource::User),
user_volume: Some(Volume {
level: Some(volume),
muted: Some(false),
..Volume::EMPTY
}),
..AudioStreamSettings::EMPTY
}
);
responder.send(&mut Ok(())).unwrap();
}
other => panic!("Unexpected stream item: {:?}", other),
}
};
futures::future::join(facade_fut, stream_fut).await;
}
// Tests that `set_mic_mute` correctly sends a request to the Input service to
// mute the device mic.
#[fasync::run_singlethreaded(test)]
async fn test_set_mic_mute() {
let mic_state: MicStates = Muted;
let (proxy, mut stream) = create_proxy_and_stream::<InputMarker>().unwrap();
// Create a facade future that sends a request to `proxy`.
let facade =
SetUiFacade { audio_proxy: None, display_proxy: None, input_proxy: Some(proxy) };
let facade_fut = async move {
assert_eq!(
facade.set_mic_mute(to_value(mic_state).unwrap()).await.unwrap(),
to_value(SetUiResult::Success).unwrap()
);
};
// Create a future to service the request stream.
let input_stream_fut = async move {
match stream.try_next().await {
Ok(Some(fsettings::InputRequest::Watch { responder })) => {
let device = InputDevice {
device_name: None,
device_type: Some(fsettings::DeviceType::Microphone),
source_states: None,
mutable_toggle_state: None,
state: Some(DeviceState {
toggle_flags: Some(fsettings::ToggleStateFlags::AVAILABLE),
..DeviceState::EMPTY
}),
unknown_data: None,
..InputDevice::EMPTY
};
let settings = fsettings::InputSettings {
devices: Some(vec![device]),
unknown_data: None,
..fsettings::InputSettings::EMPTY
};
responder.send(settings).unwrap();
}
other => panic!("Unexpected Watch request: {:?}", other),
}
match stream.try_next().await {
Ok(Some(fsettings::InputRequest::Set { input_states, responder })) => {
assert_eq!(
input_states[0],
InputState {
name: Some("microphone".to_string()),
device_type: Some(fsettings::DeviceType::Microphone),
state: Some(DeviceState {
toggle_flags: Some(fsettings::ToggleStateFlags::MUTED),
..DeviceState::EMPTY
}),
..InputState::EMPTY
}
);
responder.send(&mut Ok(())).unwrap();
}
other => panic!("Unexpected stream item: {:?}", other),
}
};
futures::future::join(facade_fut, input_stream_fut).await;
}
// Tests that `set_mic_mute` does not send a request to the Input service if the mic is already in desired state.
#[fasync::run_singlethreaded(test)]
async fn test_set_mic_mute_in_desired_state() {
let mic_state: MicStates = Muted;
let (proxy, mut stream) = create_proxy_and_stream::<InputMarker>().unwrap();
// Create a facade future that sends a request to `proxy`.
let facade =
SetUiFacade { audio_proxy: None, display_proxy: None, input_proxy: Some(proxy) };
let facade_fut = async move {
assert_eq!(
facade.set_mic_mute(to_value(mic_state).unwrap()).await.unwrap(),
to_value(SetUiResult::Success).unwrap()
);
};
// Create a future to check that the request stream using Set is never called (due to early termination).
let input_stream_fut = async move {
match stream.try_next().await {
Ok(Some(fsettings::InputRequest::Watch { responder })) => {
let device = InputDevice {
device_name: None,
device_type: Some(fsettings::DeviceType::Microphone),
source_states: None,
mutable_toggle_state: None,
state: Some(DeviceState {
toggle_flags: Some(fsettings::ToggleStateFlags::MUTED),
..DeviceState::EMPTY
}),
unknown_data: None,
..InputDevice::EMPTY
};
let settings = fsettings::InputSettings {
devices: Some(vec![device]),
unknown_data: None,
..fsettings::InputSettings::EMPTY
};
responder.send(settings).unwrap();
}
other => panic!("Unexpected Watch request: {:?}", other),
}
match stream.try_next().await {
Ok(Some(fsettings::InputRequest::Set { input_states, responder: _ })) => {
panic!("Unexpected stream item: {:?}", input_states[0]);
}
_ => (),
}
};
futures::future::join(facade_fut, input_stream_fut).await;
}
// Tests that `is_mic_muted` correctly returns the mic state.
#[fasync::run_singlethreaded(test)]
async fn test_is_mic_muted() {
let is_muted = true;
let (proxy, mut stream) = create_proxy_and_stream::<InputMarker>().unwrap();
// Create a facade future that sends a request to `proxy`.
let facade =
SetUiFacade { audio_proxy: None, display_proxy: None, input_proxy: Some(proxy) };
let facade_fut = async move {
assert_eq!(facade.is_mic_muted().await.unwrap(), to_value(is_muted).unwrap());
};
// Create a future to service the request stream.
let input_stream_fut = async move {
match stream.try_next().await {
Ok(Some(fsettings::InputRequest::Watch { responder })) => {
let device = InputDevice {
device_name: None,
device_type: Some(fsettings::DeviceType::Microphone),
source_states: None,
mutable_toggle_state: None,
state: Some(DeviceState {
toggle_flags: Some(fsettings::ToggleStateFlags::MUTED),
..DeviceState::EMPTY
}),
unknown_data: None,
..InputDevice::EMPTY
};
let settings = fsettings::InputSettings {
devices: Some(vec![device]),
unknown_data: None,
..fsettings::InputSettings::EMPTY
};
responder.send(settings).unwrap();
}
other => panic!("Unexpected Watch request: {:?}", other),
}
};
futures::future::join(facade_fut, input_stream_fut).await;
}
}