| // 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; |
| } |
| } |