use crate::consumer_controls_binding::ConsumerControlsEvent;
use crate::input_handler::{InputHandlerStatus, UnhandledInputHandler};
use crate::{input_device, metrics};
use anyhow::{anyhow, Context as _, Error};
use async_trait::async_trait;
use async_utils::hanging_get::server::HangingGet;
use fidl::endpoints::DiscoverableProtocolMarker as _;
use fidl_fuchsia_media::AudioRenderUsage;
use fidl_fuchsia_media_sounds::{PlaySoundError, PlayerMarker};
use fidl_fuchsia_recovery::FactoryResetMarker;
use fidl_fuchsia_recovery_policy::{DeviceRequest, DeviceRequestStream};
use fidl_fuchsia_recovery_ui::{
FactoryResetCountdownRequestStream, FactoryResetCountdownState,
FactoryResetCountdownWatchResponder,
};
use fuchsia_async::{MonotonicDuration, MonotonicInstant, Task, TimeoutExt, Timer};
use fuchsia_inspect::health::Reporter;
use futures::StreamExt;
use metrics_registry::*;
use std::cell::RefCell;
use std::fs::{self, File};
use std::path::Path;
use std::rc::Rc;
use {fidl_fuchsia_input_report as fidl_input_report, fidl_fuchsia_io as fio};
#[derive(Clone, Copy, Debug, PartialEq)]
enum FactoryResetState {
Disallowed,
Idle,
ButtonCountdown { deadline: MonotonicInstant },
ResetCountdown { deadline: MonotonicInstant },
Resetting,
}
const FACTORY_RESET_DISALLOWED_PATH: &'static str = "/data/factory_reset_disallowed";
const FACTORY_RESET_SOUND_PATH: &'static str = "/config/data/chirp-start-tone.wav";
const BUTTON_TIMEOUT: MonotonicDuration = MonotonicDuration::from_millis(500);
const RESET_TIMEOUT: MonotonicDuration = MonotonicDuration::from_seconds(10);
const EARCON_TIMEOUT: MonotonicDuration = MonotonicDuration::from_millis(2000);
type NotifyFn = Box<
dyn Fn(
&(FactoryResetState, metrics::MetricsLogger),
FactoryResetCountdownWatchResponder,
) -> bool
+ Send,
>;
type ResetCountdownHangingGet = HangingGet<
(FactoryResetState, metrics::MetricsLogger),
FactoryResetCountdownWatchResponder,
NotifyFn,
>;
pub struct FactoryResetHandler {
factory_reset_state: RefCell<FactoryResetState>,
countdown_hanging_get: RefCell<ResetCountdownHangingGet>,
pub inspect_status: InputHandlerStatus,
metrics_logger: metrics::MetricsLogger,
}
fn is_reset_requested(event: &ConsumerControlsEvent) -> bool {
event.pressed_buttons.iter().any(|button| match button {
fidl_input_report::ConsumerControlButton::FactoryReset => true,
_ => false,
})
}
impl FactoryResetHandler {
pub fn new(
input_handlers_node: &fuchsia_inspect::Node,
metrics_logger: metrics::MetricsLogger,
) -> Rc<Self> {
let initial_state = if Path::new(FACTORY_RESET_DISALLOWED_PATH).exists() {
FactoryResetState::Disallowed
} else {
FactoryResetState::Idle
};
let countdown_hanging_get =
FactoryResetHandler::init_hanging_get(initial_state, metrics_logger.clone());
let inspect_status = InputHandlerStatus::new(
input_handlers_node,
"factory_reset_handler",
false,
);
Rc::new(Self {
factory_reset_state: RefCell::new(initial_state),
countdown_hanging_get: RefCell::new(countdown_hanging_get),
inspect_status,
metrics_logger,
})
}
pub fn handle_factory_reset_countdown_request_stream(
self: Rc<Self>,
mut stream: FactoryResetCountdownRequestStream,
) -> impl futures::Future<Output = Result<(), Error>> {
let subscriber = self.countdown_hanging_get.borrow_mut().new_subscriber();
async move {
while let Some(request_result) = stream.next().await {
let watcher = request_result?
.into_watch()
.ok_or_else(|| anyhow!("Failed to get FactoryResetCoundown Watcher"))?;
subscriber.register(watcher)?;
}
Ok(())
}
}
pub fn handle_recovery_policy_device_request_stream(
self: Rc<Self>,
mut stream: DeviceRequestStream,
) -> impl futures::Future<Output = Result<(), Error>> {
async move {
while let Some(request_result) = stream.next().await {
let DeviceRequest::SetIsLocalResetAllowed { allowed, .. } = request_result?;
match self.factory_reset_state() {
FactoryResetState::Disallowed if allowed => {
self.set_factory_reset_state(FactoryResetState::Idle);
fs::remove_file(FACTORY_RESET_DISALLOWED_PATH).with_context(|| {
format!("failed to remove {}", FACTORY_RESET_DISALLOWED_PATH)
})?
}
_ if !allowed => {
self.set_factory_reset_state(FactoryResetState::Disallowed);
let _: File =
File::create(FACTORY_RESET_DISALLOWED_PATH).with_context(|| {
format!("failed to create {}", FACTORY_RESET_DISALLOWED_PATH)
})?;
}
_ => (),
}
}
Ok(())
}
}
async fn handle_allowed_event(self: &Rc<Self>, event: &ConsumerControlsEvent) {
if is_reset_requested(event) {
if let Err(error) = self.start_button_countdown().await {
self.metrics_logger.log_error(
InputPipelineErrorMetricDimensionEvent::FactoryResetFailedToReset,
std::format!("Failed to factory reset device: {:?}", error),
);
}
}
}
fn handle_disallowed_event(self: &Rc<Self>, event: &ConsumerControlsEvent) {
if is_reset_requested(event) {
self.metrics_logger.log_error(
InputPipelineErrorMetricDimensionEvent::FactoryResetNotAllowedReset,
"Attempted to factory reset a device that is not allowed to reset",
);
}
}
fn handle_button_countdown_event(self: &Rc<Self>, event: &ConsumerControlsEvent) {
if !is_reset_requested(event) {
self.set_factory_reset_state(FactoryResetState::Idle);
}
}
fn handle_reset_countdown_event(self: &Rc<Self>, event: &ConsumerControlsEvent) {
if !is_reset_requested(event) {
self.set_factory_reset_state(FactoryResetState::Idle);
}
}
fn init_hanging_get(
initial_state: FactoryResetState,
metrics_logger: metrics::MetricsLogger,
) -> ResetCountdownHangingGet {
let notify_fn: NotifyFn = Box::new(|(state, metrics_logger), responder| {
let deadline = match state {
FactoryResetState::ResetCountdown { deadline } => {
Some(deadline.into_nanos() as i64)
}
_ => None,
};
let countdown_state =
FactoryResetCountdownState { scheduled_reset_time: deadline, ..Default::default() };
if responder.send(&countdown_state).is_err() {
metrics_logger.log_error(
InputPipelineErrorMetricDimensionEvent::FactoryResetFailedToSendCountdown,
"Failed to send factory reset countdown state",
);
}
true
});
ResetCountdownHangingGet::new((initial_state, metrics_logger), notify_fn)
}
fn set_factory_reset_state(self: &Rc<Self>, state: FactoryResetState) {
*self.factory_reset_state.borrow_mut() = state;
self.countdown_hanging_get
.borrow_mut()
.new_publisher()
.set((state, self.metrics_logger.clone()));
}
fn factory_reset_state(self: &Rc<Self>) -> FactoryResetState {
*self.factory_reset_state.borrow()
}
async fn start_button_countdown(self: &Rc<Self>) -> Result<(), Error> {
let deadline = MonotonicInstant::after(BUTTON_TIMEOUT);
self.set_factory_reset_state(FactoryResetState::ButtonCountdown { deadline });
Timer::new(MonotonicInstant::after(BUTTON_TIMEOUT)).await;
match self.factory_reset_state() {
FactoryResetState::ButtonCountdown { deadline: state_deadline }
if state_deadline == deadline =>
{
self.start_reset_countdown().await?;
}
_ => {
tracing::info!("Factory reset request cancelled");
}
}
Ok(())
}
async fn start_reset_countdown(self: &Rc<Self>) -> Result<(), Error> {
let deadline = MonotonicInstant::after(RESET_TIMEOUT);
self.set_factory_reset_state(FactoryResetState::ResetCountdown { deadline });
Timer::new(MonotonicInstant::after(RESET_TIMEOUT)).await;
match self.factory_reset_state() {
FactoryResetState::ResetCountdown { deadline: state_deadline }
if state_deadline == deadline =>
{
self.reset().await?;
}
_ => {
tracing::info!("Factory reset request cancelled");
}
}
Ok(())
}
async fn play_reset_sound(self: &Rc<Self>) -> Result<(), Error> {
tracing::debug!("Getting sound");
let (sound_endpoint, server_end) = fidl::endpoints::create_endpoints::<fio::FileMarker>();
let () = fuchsia_fs::file::open_channel_in_namespace(
FACTORY_RESET_SOUND_PATH,
fuchsia_fs::PERM_READABLE,
server_end,
)
.context("Failed to open factory reset sound file")?;
tracing::debug!("Playing sound");
let sound_player = fuchsia_component::client::connect_to_protocol::<PlayerMarker>()
.with_context(|| format!("failed to connect to {}", PlayerMarker::PROTOCOL_NAME))?;
tracing::debug!("Connected to player");
let sound_id = 0;
let _duration: i64 = sound_player
.add_sound_from_file(sound_id, sound_endpoint)
.await
.context("AddSoundFromFile error")?
.map_err(zx::Status::from_raw)
.context("AddSoundFromFile failed")?;
tracing::debug!("Added sound from file");
sound_player
.play_sound(sound_id, AudioRenderUsage::Media)
.await
.context("PlaySound error")?
.map_err(|err: PlaySoundError| anyhow!("PlaySound failed: {:?}", err))?;
tracing::debug!("Played sound");
Ok(())
}
async fn reset(self: &Rc<Self>) -> Result<(), Error> {
tracing::info!("Beginning reset sequence");
if let Err(error) = self
.play_reset_sound()
.on_timeout(EARCON_TIMEOUT, || Err(anyhow!("play_reset_sound took too long")))
.await
{
tracing::warn!("Failed to play reset sound: {:?}", error);
}
self.set_factory_reset_state(FactoryResetState::Resetting);
tracing::info!("Calling {}.Reset", FactoryResetMarker::PROTOCOL_NAME);
let factory_reset = fuchsia_component::client::connect_to_protocol::<FactoryResetMarker>()
.with_context(|| {
format!("failed to connect to {}", FactoryResetMarker::PROTOCOL_NAME)
})?;
factory_reset.reset().await.context("failed while calling Reset")?;
Ok(())
}
}
#[async_trait(?Send)]
impl UnhandledInputHandler for FactoryResetHandler {
async fn handle_unhandled_input_event(
self: Rc<Self>,
unhandled_input_event: input_device::UnhandledInputEvent,
) -> Vec<input_device::InputEvent> {
match unhandled_input_event {
input_device::UnhandledInputEvent {
device_event: input_device::InputDeviceEvent::ConsumerControls(ref event),
device_descriptor: input_device::InputDeviceDescriptor::ConsumerControls(_),
event_time: _,
trace_id: _,
} => {
self.inspect_status.count_received_event(input_device::InputEvent::from(
unhandled_input_event.clone(),
));
match self.factory_reset_state() {
FactoryResetState::Idle => {
let event_clone = event.clone();
Task::local(async move { self.handle_allowed_event(&event_clone).await })
.detach()
}
FactoryResetState::Disallowed => self.handle_disallowed_event(event),
FactoryResetState::ButtonCountdown { deadline: _ } => {
self.handle_button_countdown_event(event)
}
FactoryResetState::ResetCountdown { deadline: _ } => {
self.handle_reset_countdown_event(event)
}
FactoryResetState::Resetting => {
tracing::warn!("Recieved an input event while factory resetting the device")
}
};
}
_ => (),
};
vec![input_device::InputEvent::from(unhandled_input_event)]
}
fn set_handler_healthy(self: std::rc::Rc<Self>) {
self.inspect_status.health_node.borrow_mut().set_ok();
}
fn set_handler_unhealthy(self: std::rc::Rc<Self>, msg: &str) {
self.inspect_status.health_node.borrow_mut().set_unhealthy(msg);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::consumer_controls_binding::ConsumerControlsDeviceDescriptor;
use crate::input_handler::InputHandler;
use assert_matches::assert_matches;
use fidl::endpoints::create_proxy_and_stream;
use fidl_fuchsia_recovery_policy::{DeviceMarker, DeviceProxy};
use fidl_fuchsia_recovery_ui::{FactoryResetCountdownMarker, FactoryResetCountdownProxy};
use fuchsia_async::TestExecutor;
use pretty_assertions::assert_eq;
use std::pin::pin;
use std::task::Poll;
fn create_factory_reset_countdown_proxy(
reset_handler: Rc<FactoryResetHandler>,
) -> FactoryResetCountdownProxy {
let (countdown_proxy, countdown_stream) =
create_proxy_and_stream::<FactoryResetCountdownMarker>();
let stream_fut =
reset_handler.clone().handle_factory_reset_countdown_request_stream(countdown_stream);
Task::local(async move {
if stream_fut.await.is_err() {
tracing::warn!("Failed to handle factory reset countdown request stream");
}
})
.detach();
countdown_proxy
}
fn create_recovery_policy_proxy(reset_handler: Rc<FactoryResetHandler>) -> DeviceProxy {
let (device_proxy, device_stream) = create_proxy_and_stream::<DeviceMarker>();
Task::local(async move {
if reset_handler
.handle_recovery_policy_device_request_stream(device_stream)
.await
.is_err()
{
tracing::warn!("Failed to handle recovery policy device request stream");
}
})
.detach();
device_proxy
}
fn create_input_device_descriptor() -> input_device::InputDeviceDescriptor {
input_device::InputDeviceDescriptor::ConsumerControls(ConsumerControlsDeviceDescriptor {
buttons: vec![
fidl_input_report::ConsumerControlButton::CameraDisable,
fidl_input_report::ConsumerControlButton::FactoryReset,
fidl_input_report::ConsumerControlButton::MicMute,
fidl_input_report::ConsumerControlButton::Pause,
fidl_input_report::ConsumerControlButton::VolumeDown,
fidl_input_report::ConsumerControlButton::VolumeUp,
],
device_id: 0,
})
}
fn create_reset_consumer_controls_event() -> ConsumerControlsEvent {
ConsumerControlsEvent::new(vec![fidl_input_report::ConsumerControlButton::FactoryReset])
}
fn create_non_reset_consumer_controls_event() -> ConsumerControlsEvent {
ConsumerControlsEvent::new(vec![
fidl_input_report::ConsumerControlButton::CameraDisable,
fidl_input_report::ConsumerControlButton::MicMute,
fidl_input_report::ConsumerControlButton::Pause,
fidl_input_report::ConsumerControlButton::VolumeDown,
fidl_input_report::ConsumerControlButton::VolumeUp,
])
}
fn create_non_reset_input_event() -> input_device::UnhandledInputEvent {
let device_event = input_device::InputDeviceEvent::ConsumerControls(
create_non_reset_consumer_controls_event(),
);
input_device::UnhandledInputEvent {
device_event,
device_descriptor: create_input_device_descriptor(),
event_time: zx::MonotonicInstant::get(),
trace_id: None,
}
}
fn create_reset_input_event() -> input_device::UnhandledInputEvent {
let device_event = input_device::InputDeviceEvent::ConsumerControls(
create_reset_consumer_controls_event(),
);
input_device::UnhandledInputEvent {
device_event,
device_descriptor: create_input_device_descriptor(),
event_time: zx::MonotonicInstant::get(),
trace_id: None,
}
}
#[fuchsia::test]
async fn is_reset_requested_looks_for_reset_signal() {
let reset_event = create_reset_consumer_controls_event();
let non_reset_event = create_non_reset_consumer_controls_event();
assert!(
is_reset_requested(&reset_event),
"Should reset when the reset signal is received."
);
assert!(
!is_reset_requested(&non_reset_event),
"Should only reset when the reset signal is received."
);
}
#[fuchsia::test]
async fn factory_reset_countdown_listener_gets_initial_state() {
let inspector = fuchsia_inspect::Inspector::default();
let test_node = inspector.root().create_child("test_node");
let reset_handler = FactoryResetHandler::new(&test_node, metrics::MetricsLogger::default());
let countdown_proxy = create_factory_reset_countdown_proxy(reset_handler.clone());
let reset_state = countdown_proxy.watch().await.expect("Failed to get countdown state");
assert_eq!(reset_state.scheduled_reset_time, None);
assert_eq!(reset_handler.factory_reset_state(), FactoryResetState::Idle);
}
#[fuchsia::test]
fn factory_reset_countdown_listener_is_notified_on_state_change() -> Result<(), Error> {
let mut executor = TestExecutor::new_with_fake_time();
let inspector = fuchsia_inspect::Inspector::default();
let test_node = inspector.root().create_child("test_node");
let reset_handler = FactoryResetHandler::new(&test_node, metrics::MetricsLogger::default());
let countdown_proxy = create_factory_reset_countdown_proxy(reset_handler.clone());
let get_countdown_state = |executor: &mut TestExecutor| {
let mut fut = countdown_proxy.watch();
loop {
match executor.run_until_stalled(&mut fut) {
Poll::Pending => continue,
Poll::Ready(state) => {
return state.expect("Failed to get countdown state");
}
}
}
};
let countdown_state = get_countdown_state(&mut executor);
let handler_state = reset_handler.factory_reset_state();
assert_eq!(countdown_state.scheduled_reset_time, None);
assert_eq!(handler_state, FactoryResetState::Idle);
let reset_event = create_reset_input_event();
let mut handle_input_event_fut =
pin!(reset_handler.clone().handle_unhandled_input_event(reset_event));
assert_matches!(executor.run_until_stalled(&mut handle_input_event_fut), Poll::Ready(events) => {
assert_matches!(events.as_slice(), [input_device::InputEvent { .. }]);
});
let countdown_state = get_countdown_state(&mut executor);
let handler_state = reset_handler.factory_reset_state();
assert_eq!(countdown_state.scheduled_reset_time, None);
assert_matches!(handler_state, FactoryResetState::ButtonCountdown { deadline: _ });
executor.set_fake_time(MonotonicInstant::after(MonotonicDuration::from_millis(500)));
executor.wake_expired_timers();
let countdown_state = get_countdown_state(&mut executor);
let handler_state = reset_handler.factory_reset_state();
assert_matches!(countdown_state.scheduled_reset_time, Some(_));
assert_matches!(handler_state, FactoryResetState::ResetCountdown { deadline: _ });
executor.set_fake_time(MonotonicInstant::after(MonotonicDuration::from_seconds(10)));
executor.wake_expired_timers();
let countdown_state = get_countdown_state(&mut executor);
let handler_state = reset_handler.factory_reset_state();
assert_eq!(countdown_state.scheduled_reset_time, None);
assert_eq!(handler_state, FactoryResetState::Resetting);
Ok(())
}
#[fuchsia::test]
async fn recovery_policy_requests_update_reset_handler_state() {
let inspector = fuchsia_inspect::Inspector::default();
let test_node = inspector.root().create_child("test_node");
let reset_handler = FactoryResetHandler::new(&test_node, metrics::MetricsLogger::default());
let countdown_proxy = create_factory_reset_countdown_proxy(reset_handler.clone());
let reset_state = countdown_proxy.watch().await.expect("Failed to get countdown state");
assert_eq!(reset_state.scheduled_reset_time, None);
assert_eq!(reset_handler.factory_reset_state(), FactoryResetState::Idle);
let device_proxy = create_recovery_policy_proxy(reset_handler.clone());
device_proxy.set_is_local_reset_allowed(false).expect("Failed to set recovery policy");
let reset_state = countdown_proxy.watch().await.expect("Failed to get countdown state");
assert_eq!(reset_state.scheduled_reset_time, None);
assert_eq!(reset_handler.factory_reset_state(), FactoryResetState::Disallowed);
let reset_event = create_reset_input_event();
reset_handler.clone().handle_unhandled_input_event(reset_event).await;
assert_eq!(reset_handler.factory_reset_state(), FactoryResetState::Disallowed);
let device_proxy = create_recovery_policy_proxy(reset_handler.clone());
device_proxy.set_is_local_reset_allowed(true).expect("Failed to set recovery policy");
let reset_state = countdown_proxy.watch().await.expect("Failed to get countdown state");
assert_eq!(reset_state.scheduled_reset_time, None);
assert_eq!(reset_handler.factory_reset_state(), FactoryResetState::Idle);
}
#[fuchsia::test]
fn handle_allowed_event_changes_state_with_reset() {
let mut executor = TestExecutor::new();
let reset_event = create_reset_consumer_controls_event();
let inspector = fuchsia_inspect::Inspector::default();
let test_node = inspector.root().create_child("test_node");
let reset_handler = FactoryResetHandler::new(&test_node, metrics::MetricsLogger::default());
let countdown_proxy = create_factory_reset_countdown_proxy(reset_handler.clone());
let reset_state = executor
.run_singlethreaded(countdown_proxy.watch())
.expect("Failed to get countdown state");
assert_eq!(reset_state.scheduled_reset_time, None);
assert_eq!(reset_handler.factory_reset_state(), FactoryResetState::Idle);
let handle_allowed_event_fut = reset_handler.handle_allowed_event(&reset_event);
futures::pin_mut!(handle_allowed_event_fut);
assert_eq!(executor.run_until_stalled(&mut handle_allowed_event_fut), Poll::Pending);
assert_matches!(
executor.run_singlethreaded(countdown_proxy.watch()),
Ok(FactoryResetCountdownState { scheduled_reset_time: None, .. })
);
assert_matches!(
reset_handler.factory_reset_state(),
FactoryResetState::ButtonCountdown { deadline: _ }
);
}
#[fuchsia::test]
async fn handle_allowed_event_wont_change_state_without_reset() {
let inspector = fuchsia_inspect::Inspector::default();
let test_node = inspector.root().create_child("test_node");
let reset_handler = FactoryResetHandler::new(&test_node, metrics::MetricsLogger::default());
let countdown_proxy = create_factory_reset_countdown_proxy(reset_handler.clone());
let reset_state = countdown_proxy.watch().await.expect("Failed to get countdown state");
assert_eq!(reset_state.scheduled_reset_time, None);
assert_eq!(reset_handler.factory_reset_state(), FactoryResetState::Idle);
let non_reset_event = create_non_reset_consumer_controls_event();
reset_handler.clone().handle_allowed_event(&non_reset_event).await;
assert_eq!(reset_handler.factory_reset_state(), FactoryResetState::Idle);
}
#[fuchsia::test]
async fn handle_disallowed_event_wont_change_state() {
let inspector = fuchsia_inspect::Inspector::default();
let test_node = inspector.root().create_child("test_node");
let reset_handler = FactoryResetHandler::new(&test_node, metrics::MetricsLogger::default());
*reset_handler.factory_reset_state.borrow_mut() = FactoryResetState::Disallowed;
let reset_event = create_reset_consumer_controls_event();
reset_handler.handle_disallowed_event(&reset_event);
assert_eq!(reset_handler.factory_reset_state(), FactoryResetState::Disallowed);
let non_reset_event = create_non_reset_consumer_controls_event();
reset_handler.handle_disallowed_event(&non_reset_event);
assert_eq!(reset_handler.factory_reset_state(), FactoryResetState::Disallowed);
}
#[fuchsia::test]
async fn handle_button_countdown_event_changes_state_when_reset_no_longer_requested() {
let inspector = fuchsia_inspect::Inspector::default();
let test_node = inspector.root().create_child("test_node");
let reset_handler = FactoryResetHandler::new(&test_node, metrics::MetricsLogger::default());
let deadline = MonotonicInstant::after(BUTTON_TIMEOUT);
*reset_handler.factory_reset_state.borrow_mut() =
FactoryResetState::ButtonCountdown { deadline };
let non_reset_event = create_non_reset_consumer_controls_event();
reset_handler.handle_button_countdown_event(&non_reset_event);
assert_eq!(reset_handler.factory_reset_state(), FactoryResetState::Idle);
}
#[fuchsia::test]
async fn handle_reset_countdown_event_changes_state_when_reset_no_longer_requested() {
let inspector = fuchsia_inspect::Inspector::default();
let test_node = inspector.root().create_child("test_node");
let reset_handler = FactoryResetHandler::new(&test_node, metrics::MetricsLogger::default());
*reset_handler.factory_reset_state.borrow_mut() =
FactoryResetState::ResetCountdown { deadline: MonotonicInstant::now() };
let non_reset_event = create_non_reset_consumer_controls_event();
reset_handler.handle_reset_countdown_event(&non_reset_event);
assert_eq!(reset_handler.factory_reset_state(), FactoryResetState::Idle);
}
#[fuchsia::test]
async fn factory_reset_disallowed_during_button_countdown() {
let inspector = fuchsia_inspect::Inspector::default();
let test_node = inspector.root().create_child("test_node");
let reset_handler = FactoryResetHandler::new(&test_node, metrics::MetricsLogger::default());
let countdown_proxy = create_factory_reset_countdown_proxy(reset_handler.clone());
let reset_state = countdown_proxy.watch().await.expect("Failed to get countdown state");
assert_eq!(reset_state.scheduled_reset_time, None);
assert_eq!(reset_handler.factory_reset_state(), FactoryResetState::Idle);
let reset_event = create_reset_input_event();
reset_handler.clone().handle_unhandled_input_event(reset_event).await;
let reset_state = countdown_proxy.watch().await.expect("Failed to get countdown state");
assert_eq!(reset_state.scheduled_reset_time, None);
assert_matches!(
reset_handler.factory_reset_state(),
FactoryResetState::ButtonCountdown { deadline: _ }
);
let device_proxy = create_recovery_policy_proxy(reset_handler.clone());
device_proxy.set_is_local_reset_allowed(false).expect("Failed to set recovery policy");
let reset_state = countdown_proxy.watch().await.expect("Failed to get countdown state");
assert_eq!(reset_state.scheduled_reset_time, None);
assert_eq!(reset_handler.factory_reset_state(), FactoryResetState::Disallowed);
}
#[fuchsia::test]
async fn factory_reset_disallowed_during_reset_countdown() {
let inspector = fuchsia_inspect::Inspector::default();
let test_node = inspector.root().create_child("test_node");
let reset_handler = FactoryResetHandler::new(&test_node, metrics::MetricsLogger::default());
let countdown_proxy = create_factory_reset_countdown_proxy(reset_handler.clone());
let reset_state = countdown_proxy.watch().await.expect("Failed to get countdown state");
assert_eq!(reset_state.scheduled_reset_time, None);
assert_eq!(reset_handler.factory_reset_state(), FactoryResetState::Idle);
let reset_event = create_reset_input_event();
reset_handler.clone().handle_unhandled_input_event(reset_event).await;
let reset_state = countdown_proxy.watch().await.expect("Failed to get countdown state");
assert_eq!(reset_state.scheduled_reset_time, None);
assert_matches!(
reset_handler.factory_reset_state(),
FactoryResetState::ButtonCountdown { deadline: _ }
);
let reset_state = countdown_proxy.watch().await.expect("Failed to get countdown state");
assert_matches!(reset_state.scheduled_reset_time, Some(_));
assert_matches!(
reset_handler.factory_reset_state(),
FactoryResetState::ResetCountdown { deadline: _ }
);
let device_proxy = create_recovery_policy_proxy(reset_handler.clone());
device_proxy.set_is_local_reset_allowed(false).expect("Failed to set recovery policy");
let reset_state = countdown_proxy.watch().await.expect("Failed to get countdown state");
assert_eq!(reset_state.scheduled_reset_time, None);
assert_eq!(reset_handler.factory_reset_state(), FactoryResetState::Disallowed);
}
#[fuchsia::test]
async fn factory_reset_cancelled_during_button_countdown() {
let inspector = fuchsia_inspect::Inspector::default();
let test_node = inspector.root().create_child("test_node");
let reset_handler = FactoryResetHandler::new(&test_node, metrics::MetricsLogger::default());
let countdown_proxy = create_factory_reset_countdown_proxy(reset_handler.clone());
let reset_state = countdown_proxy.watch().await.expect("Failed to get countdown state");
assert_eq!(reset_state.scheduled_reset_time, None);
assert_eq!(reset_handler.factory_reset_state(), FactoryResetState::Idle);
let reset_event = create_reset_input_event();
reset_handler.clone().handle_unhandled_input_event(reset_event).await;
let reset_state = countdown_proxy.watch().await.expect("Failed to get countdown state");
assert_eq!(reset_state.scheduled_reset_time, None);
assert_matches!(
reset_handler.factory_reset_state(),
FactoryResetState::ButtonCountdown { deadline: _ }
);
let non_reset_event = create_non_reset_input_event();
reset_handler.clone().handle_unhandled_input_event(non_reset_event).await;
let reset_state = countdown_proxy.watch().await.expect("Failed to get countdown state");
assert_eq!(reset_state.scheduled_reset_time, None);
assert_eq!(reset_handler.factory_reset_state(), FactoryResetState::Idle);
}
#[fuchsia::test]
async fn factory_reset_cancelled_during_reset_countdown() {
let inspector = fuchsia_inspect::Inspector::default();
let test_node = inspector.root().create_child("test_node");
let reset_handler = FactoryResetHandler::new(&test_node, metrics::MetricsLogger::default());
let countdown_proxy = create_factory_reset_countdown_proxy(reset_handler.clone());
let reset_state = countdown_proxy.watch().await.expect("Failed to get countdown state");
assert_eq!(reset_state.scheduled_reset_time, None);
assert_eq!(reset_handler.factory_reset_state(), FactoryResetState::Idle);
let reset_event = create_reset_input_event();
reset_handler.clone().handle_unhandled_input_event(reset_event).await;
let reset_state = countdown_proxy.watch().await.expect("Failed to get countdown state");
assert_eq!(reset_state.scheduled_reset_time, None);
assert_matches!(
reset_handler.factory_reset_state(),
FactoryResetState::ButtonCountdown { deadline: _ }
);
let reset_state = countdown_proxy.watch().await.expect("Failed to get countdown state");
assert_matches!(reset_state.scheduled_reset_time, Some(_));
assert_matches!(
reset_handler.factory_reset_state(),
FactoryResetState::ResetCountdown { deadline: _ }
);
let non_reset_event = create_non_reset_input_event();
reset_handler.clone().handle_unhandled_input_event(non_reset_event).await;
let reset_state = countdown_proxy.watch().await.expect("Failed to get countdown state");
assert_eq!(reset_state.scheduled_reset_time, None);
assert_eq!(reset_handler.factory_reset_state(), FactoryResetState::Idle);
}
#[fuchsia::test]
fn factory_reset_handler_initialized_with_inspect_node() {
let inspector = fuchsia_inspect::Inspector::default();
let fake_handlers_node = inspector.root().create_child("input_handlers_node");
let _handler =
FactoryResetHandler::new(&fake_handlers_node, metrics::MetricsLogger::default());
diagnostics_assertions::assert_data_tree!(inspector, root: {
input_handlers_node: {
factory_reset_handler: {
events_received_count: 0u64,
events_handled_count: 0u64,
last_received_timestamp_ns: 0u64,
"fuchsia.inspect.Health": {
status: "STARTING_UP",
start_timestamp_nanos: diagnostics_assertions::AnyProperty
},
}
}
});
}
#[fuchsia::test]
async fn factory_reset_handler_inspect_counts_events() {
let inspector = fuchsia_inspect::Inspector::default();
let fake_handlers_node = inspector.root().create_child("input_handlers_node");
let reset_handler =
FactoryResetHandler::new(&fake_handlers_node, metrics::MetricsLogger::default());
let reset_event = create_reset_input_event();
reset_handler.clone().handle_unhandled_input_event(reset_event).await;
let mut handled_event = input_device::InputEvent::from(create_reset_input_event());
handled_event.handled = input_device::Handled::Yes;
reset_handler.clone().handle_input_event(handled_event).await;
let non_reset_event = create_non_reset_input_event();
let last_event_timestamp: u64 =
non_reset_event.clone().event_time.into_nanos().try_into().unwrap();
reset_handler.clone().handle_unhandled_input_event(non_reset_event).await;
diagnostics_assertions::assert_data_tree!(inspector, root: {
input_handlers_node: {
factory_reset_handler: {
events_received_count: 2u64,
events_handled_count: 0u64,
last_received_timestamp_ns: last_event_timestamp,
"fuchsia.inspect.Health": {
status: "STARTING_UP",
start_timestamp_nanos: diagnostics_assertions::AnyProperty
},
}
}
});
}
}