blob: aa51e8eb097d79114d9de3393fba5d682cfb1cf8 [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::consumer_controls_binding::ConsumerControlsEvent,
crate::input_device,
crate::input_handler::UnhandledInputHandler,
anyhow::{anyhow, Context as _, Error},
async_trait::async_trait,
async_utils::hanging_get::server::HangingGet,
fidl_fuchsia_input_report as fidl_input_report, fidl_fuchsia_io as fio,
fidl_fuchsia_media::AudioRenderUsage,
fidl_fuchsia_media_sounds::PlayerMarker,
fidl_fuchsia_recovery::FactoryResetMarker,
fidl_fuchsia_recovery_policy::{DeviceRequest, DeviceRequestStream},
fidl_fuchsia_recovery_ui::{
FactoryResetCountdownRequestStream, FactoryResetCountdownState,
FactoryResetCountdownWatchResponder,
},
fuchsia_async::{Duration, Task, Time, Timer},
fuchsia_component,
fuchsia_zircon::Channel,
futures::StreamExt,
std::{
cell::RefCell,
fs::{self, File},
path::Path,
rc::Rc,
},
};
/// FactoryResetState tracks the state of the device through the factory reset
/// process.
///
/// # Values
/// ## Disallowed
/// Factory reset of the device is not allowed. This is used to
/// keep public devices from being reset, such as when being used in kiosk mode.
///
/// ### Transitions
/// Disallowed → Idle
///
/// ## Idle
/// This is the default state for a device when factory resets are allowed but
/// is not currently being reset.
///
/// ### Transitions
/// Idle → Disallowed
/// Idle → ButtonCountdown
///
/// ## ButtonCountdown
/// This state represents the fact that the reset button has been pressed and a
/// countdown has started to verify that the button was pressed intentionally.
///
/// ### Transitions
/// ButtonCountdown → Disallowed
/// ButtonCountdown → Idle
/// ButtonCountdown → ResetCountdown
///
/// ## ResetCountdown
/// The button countdown has completed indicating that this was a purposeful
/// action so a reset countdown is started to give the user a chance to cancel
/// the factory reset.
///
/// ### Transitions
/// ResetCountdown → Disallowed
/// ResetCountdown → Idle
/// ResetCountdown → Resetting
///
/// ## Resetting
/// Once the device is in this state a factory reset is imminent and can no
/// longer be cancelled.
#[derive(Clone, Copy, Debug, PartialEq)]
enum FactoryResetState {
Disallowed,
Idle,
ButtonCountdown { deadline: Time },
ResetCountdown { deadline: Time },
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: Duration = Duration::from_millis(500);
const RESET_TIMEOUT: Duration = Duration::from_seconds(10);
type NotifyFn = Box<dyn Fn(&FactoryResetState, FactoryResetCountdownWatchResponder) -> bool + Send>;
type ResetCountdownHangingGet =
HangingGet<FactoryResetState, FactoryResetCountdownWatchResponder, NotifyFn>;
/// A [`FactoryResetHandler`] tracks the state of the consumer control buttons
/// and starts the factory reset process after appropriate timeouts.
pub struct FactoryResetHandler {
factory_reset_state: RefCell<FactoryResetState>,
countdown_hanging_get: RefCell<ResetCountdownHangingGet>,
}
/// Uses the `ConsumerControlsEvent` to determine whether the device should
/// start the Factory Reset process. The driver will turn special button
/// combinations into a `FactoryReset` signal so this code only needs to
/// listen for that.
fn is_reset_requested(event: &ConsumerControlsEvent) -> bool {
event.pressed_buttons.iter().any(|button| match button {
fidl_input_report::ConsumerControlButton::FactoryReset => true,
_ => false,
})
}
impl FactoryResetHandler {
/// Creates a new [`FactoryResetHandler`] that listens for the reset button
/// and handles timing down and, ultimately, factory resetting the device.
pub fn new() -> 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);
Rc::new(Self {
factory_reset_state: RefCell::new(initial_state),
countdown_hanging_get: RefCell::new(countdown_hanging_get),
})
}
/// Handles the request stream for FactoryResetCountdown
///
/// # Parameters
/// `stream`: The `FactoryResetCountdownRequestStream` to be handled.
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(anyhow!("Failed to get FactoryResetCoundown Watcher"))?;
subscriber.register(watcher)?;
}
Ok(())
}
}
/// Handles the request stream for fuchsia.recovery.policy.Device
///
/// # Parameters
/// `stream`: The `DeviceRequestStream` to be handled.
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 => {
// Update state and delete file
self.set_factory_reset_state(FactoryResetState::Idle);
fs::remove_file(FACTORY_RESET_DISALLOWED_PATH).map_err(|error| {
anyhow!("Failed to SetIsLocalResetAllowed to true: {:?}", error)
})?;
}
_ if !allowed => {
// Update state and create file
self.set_factory_reset_state(FactoryResetState::Disallowed);
File::create(FACTORY_RESET_DISALLOWED_PATH).map_err(|error| {
anyhow!("Failed to SetIsLocalResetAllowed to false: {:?}", error)
})?;
}
_ => (),
}
}
Ok(())
}
}
/// Handles `ConsumerControlEvent`s when the device is in the
/// `FactoryResetState::Idle` state
async fn handle_allowed_event(self: &Rc<Self>, event: &ConsumerControlsEvent) {
if is_reset_requested(event) {
if let Err(error) = self.start_button_countdown().await {
tracing::error!("Failed to factory reset device: {:?}", error);
}
}
}
/// Handles `ConsumerControlEvent`s when the device is in the
/// `FactoryResetState::Disallowed` state
fn handle_disallowed_event(self: &Rc<Self>, event: &ConsumerControlsEvent) {
if is_reset_requested(event) {
tracing::error!("Attempted to factory reset a device that is not allowed to reset");
}
}
/// Handles `ConsumerControlEvent`s when the device is in the
/// `FactoryResetState::ButtonCountdown` state
fn handle_button_countdown_event(self: &Rc<Self>, event: &ConsumerControlsEvent) {
if !is_reset_requested(event) {
// Cancel button timeout
self.set_factory_reset_state(FactoryResetState::Idle);
}
}
/// Handles `ConsumerControlEvent`s when the device is in the
/// `FactoryResetState::ResetCountdown` state
fn handle_reset_countdown_event(self: &Rc<Self>, event: &ConsumerControlsEvent) {
if !is_reset_requested(event) {
// Cancel reset timeout
self.set_factory_reset_state(FactoryResetState::Idle);
}
}
fn init_hanging_get(initial_state: FactoryResetState) -> ResetCountdownHangingGet {
let notify_fn: NotifyFn = Box::new(|state, responder| {
let deadline = match state {
FactoryResetState::ResetCountdown { deadline } => {
Some(deadline.into_nanos() as i64)
}
_ => None,
};
let countdown_state = FactoryResetCountdownState {
scheduled_reset_time: deadline,
..FactoryResetCountdownState::EMPTY
};
if responder.send(countdown_state).is_err() {
tracing::error!("Failed to send factory reset countdown state");
}
true
});
ResetCountdownHangingGet::new(initial_state, notify_fn)
}
/// Sets the state of FactoryResetHandler and notifies watchers of the updated state.
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);
}
fn factory_reset_state(self: &Rc<Self>) -> FactoryResetState {
*self.factory_reset_state.borrow()
}
/// Handles waiting for the reset button to be held down long enough to start
/// the factory reset countdown.
async fn start_button_countdown(self: &Rc<Self>) -> Result<(), Error> {
let deadline = Time::after(BUTTON_TIMEOUT);
self.set_factory_reset_state(FactoryResetState::ButtonCountdown { deadline });
// Wait for button timeout
Timer::new(Time::after(BUTTON_TIMEOUT)).await;
// Make sure the buttons are still held
match self.factory_reset_state() {
FactoryResetState::ButtonCountdown { deadline: state_deadline }
if state_deadline == deadline =>
{
// Proceed with reset.
self.start_reset_countdown().await?;
}
_ => {
tracing::info!("Factory reset request cancelled");
}
}
Ok(())
}
/// Handles waiting for the reset countdown to complete before resetting the
/// device.
async fn start_reset_countdown(self: &Rc<Self>) -> Result<(), Error> {
let deadline = Time::after(RESET_TIMEOUT);
self.set_factory_reset_state(FactoryResetState::ResetCountdown { deadline });
// Wait for reset timeout
Timer::new(Time::after(RESET_TIMEOUT)).await;
// Make sure the buttons are still held
match self.factory_reset_state() {
FactoryResetState::ResetCountdown { deadline: state_deadline }
if state_deadline == deadline =>
{
// Proceed with reset.
self.reset().await?;
}
_ => {
tracing::info!("Factory reset request cancelled");
}
}
Ok(())
}
/// Retrieves and plays the sound associated with factory resetting the device.
async fn play_reset_sound(self: &Rc<Self>) -> Result<(), Error> {
// Get sound
let sound_file = File::open(FACTORY_RESET_SOUND_PATH)
.context("Failed to open factory reset sound file")?;
let sound_channel = Channel::from(fdio::transfer_fd(sound_file)?);
let sound_endpoint = fidl::endpoints::ClientEnd::<fio::FileMarker>::new(sound_channel);
// Play sound
let sound_player = fuchsia_component::client::connect_to_protocol::<PlayerMarker>()?;
let sound_id = 0;
let _duration = sound_player
.add_sound_from_file(sound_id, sound_endpoint)
.await?
.map_err(|status| anyhow::format_err!("AddSoundFromFile failed {}", status))?;
sound_player
.play_sound(sound_id, AudioRenderUsage::Media)
.await?
.map_err(|err| anyhow::format_err!("PlaySound failed: {:?}", err))?;
Ok(())
}
/// Performs the actual factory reset.
async fn reset(self: &Rc<Self>) -> Result<(), Error> {
if let Err(error) = self.play_reset_sound().await {
tracing::info!("Failed to play reset sound: {:?}", error);
}
// Trigger reset
self.set_factory_reset_state(FactoryResetState::Resetting);
tracing::info!("Triggering factory reset");
let factory_reset = fuchsia_component::client::connect_to_protocol::<FactoryResetMarker>()?;
factory_reset.reset().await?;
Ok(())
}
}
#[async_trait(?Send)]
impl UnhandledInputHandler for FactoryResetHandler {
/// This InputHandler doesn't consume any input events. It just passes them on to the next handler in the pipeline.
/// Since it doesn't need exclusive access to the events this seems like the best way to avoid handlers further
/// down the pipeline missing events that they need.
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: _,
} => {
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)]
}
}
#[cfg(test)]
mod tests {
use {
super::*,
crate::consumer_controls_binding::ConsumerControlsDeviceDescriptor,
assert_matches::assert_matches,
fidl::endpoints::create_proxy_and_stream,
fidl_fuchsia_recovery_policy::{DeviceMarker, DeviceProxy},
fidl_fuchsia_recovery_ui::{FactoryResetCountdownMarker, FactoryResetCountdownProxy},
fuchsia_async::TestExecutor,
fuchsia_zircon as zx,
pin_utils::pin_mut,
pretty_assertions::assert_eq,
std::task::Poll,
};
fn create_factory_reset_countdown_proxy(
reset_handler: Rc<FactoryResetHandler>,
) -> FactoryResetCountdownProxy {
let (countdown_proxy, countdown_stream) =
create_proxy_and_stream::<FactoryResetCountdownMarker>()
.expect("Failed to create countdown proxy");
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>()
.expect("Failed to create recovery policy device proxy");
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,
],
})
}
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::Time::get_monotonic(),
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::Time::get_monotonic(),
trace_id: None,
}
}
fn get_countdown_state(
proxy: &FactoryResetCountdownProxy,
executor: &mut TestExecutor,
) -> FactoryResetCountdownState {
let countdown_proxy_clone = proxy.clone();
let countdown_state_fut = async move {
countdown_proxy_clone.watch().await.expect("Failed to get countdown state")
};
pin_mut!(countdown_state_fut);
match executor.run_until_stalled(&mut countdown_state_fut) {
Poll::Ready(countdown_state) => countdown_state,
_ => panic!("Failed to get countdown state"),
}
}
#[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 reset_handler = FactoryResetHandler::new();
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!(reset_state.scheduled_reset_time.is_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().unwrap();
let reset_handler = FactoryResetHandler::new();
let countdown_proxy = create_factory_reset_countdown_proxy(reset_handler.clone());
// The initial state should be no scheduled reset time and the
// FactoryRestHandler state should be FactoryResetState::Idle
let countdown_state = get_countdown_state(&countdown_proxy, &mut executor);
let handler_state = reset_handler.factory_reset_state();
assert!(countdown_state.scheduled_reset_time.is_none());
assert_eq!(handler_state, FactoryResetState::Idle);
// Send a reset event
let reset_event = create_reset_input_event();
let handle_input_event_fut =
reset_handler.clone().handle_unhandled_input_event(reset_event);
pin_mut!(handle_input_event_fut);
assert!(executor.run_until_stalled(&mut handle_input_event_fut).is_ready());
// The next state will be FactoryResetState::ButtonCountdown with no scheduled reset
let countdown_state = get_countdown_state(&countdown_proxy, &mut executor);
let handler_state = reset_handler.factory_reset_state();
assert!(countdown_state.scheduled_reset_time.is_none());
assert_matches!(handler_state, FactoryResetState::ButtonCountdown { deadline: _ });
// Skip ahead 500ms for the ButtonCountdown
executor.set_fake_time(Time::after(Duration::from_millis(500)));
executor.wake_expired_timers();
// After the ButtonCountdown the reset_handler enters the
// FactoryResetState::ResetCountdown state WITH a scheduled reset time.
let countdown_state = get_countdown_state(&countdown_proxy, &mut executor);
let handler_state = reset_handler.factory_reset_state();
assert!(countdown_state.scheduled_reset_time.is_some());
assert_matches!(handler_state, FactoryResetState::ResetCountdown { deadline: _ });
// Skip ahead 10s for the ResetCountdown
executor.set_fake_time(Time::after(Duration::from_seconds(10)));
executor.wake_expired_timers();
// After the ResetCountdown the reset_handler enters the
// FactoryResetState::Resetting state with no scheduled reset time.
let countdown_state = get_countdown_state(&countdown_proxy, &mut executor);
let handler_state = reset_handler.factory_reset_state();
assert!(countdown_state.scheduled_reset_time.is_none());
assert_eq!(handler_state, FactoryResetState::Resetting);
Ok(())
}
#[fuchsia::test]
async fn recovery_policy_requests_update_reset_handler_state() {
let reset_handler = FactoryResetHandler::new();
let countdown_proxy = create_factory_reset_countdown_proxy(reset_handler.clone());
// Initial state should be FactoryResetState::Idle with no scheduled reset
let reset_state = countdown_proxy.watch().await.expect("Failed to get countdown state");
assert!(reset_state.scheduled_reset_time.is_none());
assert_eq!(reset_handler.factory_reset_state(), FactoryResetState::Idle);
// Set FactoryResetState to Disallow
let device_proxy = create_recovery_policy_proxy(reset_handler.clone());
device_proxy.set_is_local_reset_allowed(false).expect("Failed to set recovery policy");
// State should now be in Disallow and scheduled_reset_time should be None
let reset_state = countdown_proxy.watch().await.expect("Failed to get countdown state");
assert!(reset_state.scheduled_reset_time.is_none());
assert_eq!(reset_handler.factory_reset_state(), FactoryResetState::Disallowed);
// Send reset event
let reset_event = create_reset_input_event();
reset_handler.clone().handle_unhandled_input_event(reset_event).await;
// State should still be Disallow
assert_eq!(reset_handler.factory_reset_state(), FactoryResetState::Disallowed);
// Set the state back to Allow
let device_proxy = create_recovery_policy_proxy(reset_handler.clone());
device_proxy.set_is_local_reset_allowed(true).expect("Failed to set recovery policy");
// State should be FactoryResetState::Idle with no scheduled reset
let reset_state = countdown_proxy.watch().await.expect("Failed to get countdown state");
assert!(reset_state.scheduled_reset_time.is_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().unwrap();
let reset_event = create_reset_consumer_controls_event();
let reset_handler = FactoryResetHandler::new();
let countdown_proxy = create_factory_reset_countdown_proxy(reset_handler.clone());
// Initial state should be FactoryResetState::Idle with no scheduled reset
let reset_state = executor.run_singlethreaded(async {
countdown_proxy.watch().await.expect("Failed to get countdown state")
});
assert!(reset_state.scheduled_reset_time.is_none());
assert_eq!(reset_handler.factory_reset_state(), FactoryResetState::Idle);
let reset_handler_clone = reset_handler.clone();
let handle_allowed_event_fut = reset_handler_clone.handle_allowed_event(&reset_event);
futures::pin_mut!(handle_allowed_event_fut);
let _ = executor.run_until_stalled(&mut handle_allowed_event_fut);
let watch_res = executor.run_singlethreaded(countdown_proxy.watch());
// This should result in the reset handler entering the ButtonCountdown state
assert!(watch_res.is_ok());
assert!(watch_res.unwrap().scheduled_reset_time.is_none());
assert_matches!(
reset_handler.factory_reset_state(),
FactoryResetState::ButtonCountdown { deadline: _ }
);
}
#[fuchsia::test]
async fn handle_allowed_event_wont_change_state_without_reset() {
let reset_handler = FactoryResetHandler::new();
let countdown_proxy = create_factory_reset_countdown_proxy(reset_handler.clone());
// Initial state should be FactoryResetState::Idle with no scheduled reset
let reset_state = countdown_proxy.watch().await.expect("Failed to get countdown state");
assert!(reset_state.scheduled_reset_time.is_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;
// This should result in the reset handler staying in the Allowed state
assert_eq!(reset_handler.factory_reset_state(), FactoryResetState::Idle);
}
#[fuchsia::test]
async fn handle_disallowed_event_wont_change_state() {
let reset_handler = FactoryResetHandler::new();
*reset_handler.factory_reset_state.borrow_mut() = FactoryResetState::Disallowed;
// Calling handle_disallowed_event shouldn't change the state no matter
// what the contents of the event are
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 reset_handler = FactoryResetHandler::new();
let deadline = Time::after(BUTTON_TIMEOUT);
*reset_handler.factory_reset_state.borrow_mut() =
FactoryResetState::ButtonCountdown { deadline };
// Calling handle_button_countdown_event should reset the handler
// to the idle state
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 reset_handler = FactoryResetHandler::new();
*reset_handler.factory_reset_state.borrow_mut() =
FactoryResetState::ResetCountdown { deadline: Time::now() };
// Calling handle_reset_countdown_event should reset the handler
// to the idle state
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 reset_handler = FactoryResetHandler::new();
let countdown_proxy = create_factory_reset_countdown_proxy(reset_handler.clone());
// Initial state should be FactoryResetState::Idle with no scheduled reset
let reset_state = countdown_proxy.watch().await.expect("Failed to get countdown state");
assert!(reset_state.scheduled_reset_time.is_none());
assert_eq!(reset_handler.factory_reset_state(), FactoryResetState::Idle);
// Send reset event
let reset_event = create_reset_input_event();
reset_handler.clone().handle_unhandled_input_event(reset_event).await;
// State should now be ButtonCountdown and scheduled_reset_time should be None
let reset_state = countdown_proxy.watch().await.expect("Failed to get countdown state");
assert!(reset_state.scheduled_reset_time.is_none());
assert_matches!(
reset_handler.factory_reset_state(),
FactoryResetState::ButtonCountdown { deadline: _ }
);
// Set FactoryResetState to Disallow
let device_proxy = create_recovery_policy_proxy(reset_handler.clone());
device_proxy.set_is_local_reset_allowed(false).expect("Failed to set recovery policy");
// State should now be in Disallow and scheduled_reset_time should be None
let reset_state = countdown_proxy.watch().await.expect("Failed to get countdown state");
assert!(reset_state.scheduled_reset_time.is_none());
assert_eq!(reset_handler.factory_reset_state(), FactoryResetState::Disallowed);
}
#[fuchsia::test]
async fn factory_reset_disallowed_during_reset_countdown() {
let reset_handler = FactoryResetHandler::new();
let countdown_proxy = create_factory_reset_countdown_proxy(reset_handler.clone());
// Initial state should be FactoryResetState::Idle with no scheduled reset
let reset_state = countdown_proxy.watch().await.expect("Failed to get countdown state");
assert!(reset_state.scheduled_reset_time.is_none());
assert_eq!(reset_handler.factory_reset_state(), FactoryResetState::Idle);
// Send reset event
let reset_event = create_reset_input_event();
reset_handler.clone().handle_unhandled_input_event(reset_event).await;
// State should now be ButtonCountdown and scheduled_reset_time should be None
let reset_state = countdown_proxy.watch().await.expect("Failed to get countdown state");
assert!(reset_state.scheduled_reset_time.is_none());
assert_matches!(
reset_handler.factory_reset_state(),
FactoryResetState::ButtonCountdown { deadline: _ }
);
// State should now be ResetCountdown and scheduled_reset_time should be Some
let reset_state = countdown_proxy.watch().await.expect("Failed to get countdown state");
assert!(reset_state.scheduled_reset_time.is_some());
assert_matches!(
reset_handler.factory_reset_state(),
FactoryResetState::ResetCountdown { deadline: _ }
);
// Set FactoryResetState to Disallow
let device_proxy = create_recovery_policy_proxy(reset_handler.clone());
device_proxy.set_is_local_reset_allowed(false).expect("Failed to set recovery policy");
// State should now be in Disallow and scheduled_reset_time should be None
let reset_state = countdown_proxy.watch().await.expect("Failed to get countdown state");
assert!(reset_state.scheduled_reset_time.is_none());
assert_eq!(reset_handler.factory_reset_state(), FactoryResetState::Disallowed);
}
#[fuchsia::test]
async fn factory_reset_cancelled_during_button_countdown() {
let reset_handler = FactoryResetHandler::new();
let countdown_proxy = create_factory_reset_countdown_proxy(reset_handler.clone());
// Initial state should be FactoryResetState::Idle with no scheduled reset
let reset_state = countdown_proxy.watch().await.expect("Failed to get countdown state");
assert!(reset_state.scheduled_reset_time.is_none());
assert_eq!(reset_handler.factory_reset_state(), FactoryResetState::Idle);
// Send reset event
let reset_event = create_reset_input_event();
reset_handler.clone().handle_unhandled_input_event(reset_event).await;
// State should now be ButtonCountdown and scheduled_reset_time should be None
let reset_state = countdown_proxy.watch().await.expect("Failed to get countdown state");
assert!(reset_state.scheduled_reset_time.is_none());
assert_matches!(
reset_handler.factory_reset_state(),
FactoryResetState::ButtonCountdown { deadline: _ }
);
// Pass in an event to simulate releasing the reset button
let non_reset_event = create_non_reset_input_event();
reset_handler.clone().handle_unhandled_input_event(non_reset_event).await;
// State should now be in Idle and scheduled_reset_time should be None
let reset_state = countdown_proxy.watch().await.expect("Failed to get countdown state");
assert!(reset_state.scheduled_reset_time.is_none());
assert_eq!(reset_handler.factory_reset_state(), FactoryResetState::Idle);
}
#[fuchsia::test]
async fn factory_reset_cancelled_during_reset_countdown() {
let reset_handler = FactoryResetHandler::new();
let countdown_proxy = create_factory_reset_countdown_proxy(reset_handler.clone());
// Initial state should be FactoryResetState::Idle with no scheduled reset
let reset_state = countdown_proxy.watch().await.expect("Failed to get countdown state");
assert!(reset_state.scheduled_reset_time.is_none());
assert_eq!(reset_handler.factory_reset_state(), FactoryResetState::Idle);
// Send reset event
let reset_event = create_reset_input_event();
reset_handler.clone().handle_unhandled_input_event(reset_event).await;
// State should now be ButtonCountdown and scheduled_reset_time should be None
let reset_state = countdown_proxy.watch().await.expect("Failed to get countdown state");
assert!(reset_state.scheduled_reset_time.is_none());
assert_matches!(
reset_handler.factory_reset_state(),
FactoryResetState::ButtonCountdown { deadline: _ }
);
// State should now be ResetCountdown and scheduled_reset_time should be Some
let reset_state = countdown_proxy.watch().await.expect("Failed to get countdown state");
assert!(reset_state.scheduled_reset_time.is_some());
assert_matches!(
reset_handler.factory_reset_state(),
FactoryResetState::ResetCountdown { deadline: _ }
);
// Pass in an event to simulate releasing the reset button
let non_reset_event = create_non_reset_input_event();
reset_handler.clone().handle_unhandled_input_event(non_reset_event).await;
// State should now be in Idle and scheduled_reset_time should be None
let reset_state = countdown_proxy.watch().await.expect("Failed to get countdown state");
assert!(reset_state.scheduled_reset_time.is_none());
assert_eq!(reset_handler.factory_reset_state(), FactoryResetState::Idle);
}
}