blob: 3d35f9a8ffa38531bb4782a13d3c4d0db6c7d477 [file] [log] [blame]
// Copyright 2022 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::Error;
use carnelian::{
app::Config, drawing::DisplayRotation, App, AppAssistant, AppAssistantPtr, AppSender,
AssistantCreator, AssistantCreatorFunc, ViewAssistantPtr, ViewKey,
};
use fidl::endpoints::{DiscoverableProtocolMarker, RequestStream};
use fidl_fuchsia_recovery_ui as frui;
use fuchsia_async as fasync;
use futures::TryStreamExt;
use ota_lib::OtaComponent;
use recovery_ui::proxy_view_assistant::ProxyViewAssistant;
use recovery_ui::screens::Screens;
use recovery_ui_config::Config as UiConfig;
use recovery_util::crash::{CrashReportBuilder, CrashReporter};
use recovery_util::ota::action::Action;
use recovery_util::ota::controller::{Controller, ControllerImpl, SendEvent};
use recovery_util::ota::state_machine::{Event, OtaStatus, State, StateMachine};
use std::rc::Rc;
use std::sync::Arc;
#[cfg(feature = "debug_console")]
use {
carnelian::{make_message, MessageTarget},
futures::channel::mpsc::{self, Sender},
futures::StreamExt,
ota_lib::{OtaLogListener, OtaLogListenerImpl},
recovery_ui::console::{ConsoleMessages, ConsoleViewAssistant},
recovery_ui::font,
recovery_util::ota::controller::EventSender,
};
struct RecoveryAppAssistant {
app_sender: AppSender,
display_rotation: DisplayRotation,
controller: Box<dyn Controller>,
crash_reporter: Option<Rc<CrashReporter>>,
}
impl RecoveryAppAssistant {
pub fn new(app_sender: AppSender) -> Self {
let display_rotation = Self::get_display_rotation();
let crash_reporter = CrashReportBuilder::new().build().unwrap();
Self::new_with_params(
app_sender,
display_rotation,
Box::new(ControllerImpl::new()),
Some(crash_reporter),
)
}
pub fn new_with_params(
app_sender: AppSender,
display_rotation: DisplayRotation,
controller: Box<dyn Controller>,
crash_reporter: Option<Rc<CrashReporter>>,
) -> Self {
Self { app_sender, display_rotation, controller, crash_reporter }
}
fn get_display_rotation() -> DisplayRotation {
let config = UiConfig::take_from_startup_handle();
match config.display_rotation {
0 => DisplayRotation::Deg0,
180 => DisplayRotation::Deg180,
// Carnelian uses an inverted z-axis for rotation
90 => DisplayRotation::Deg270,
270 => DisplayRotation::Deg90,
val => {
eprintln!("Invalid display_rotation {}, defaulting to 90 degrees", val);
DisplayRotation::Deg90
}
}
}
// TODO(b/253084947) Swap out interface when we switch to the official progress fidl from SWD
fn send_ota_status(mut event_sender: Box<dyn SendEvent>, percent: f32, status: frui::Status) {
match status {
frui::Status::Active => {
println!("OTA update is now in progress... ({}%)", percent);
event_sender.send(Event::Progress(percent as u8));
}
frui::Status::Complete => {
event_sender.send(Event::ProgressComplete(OtaStatus::Succeeded))
}
frui::Status::Cancelled | frui::Status::Error => {
// Cancelled and Error are both considered failed, since you can't cancel recovery installation
event_sender.send(Event::ProgressComplete(OtaStatus::Failed))
}
frui::Status::Paused => (), //ignore
}
}
}
#[cfg(feature = "debug_console")]
impl RecoveryAppAssistant {
fn setup_event_logging_observer(&self, view_key: ViewKey) -> Sender<Event> {
let app_sender = self.app_sender.clone();
let view_key = view_key.clone();
let (observer_sender, mut observer_receiver) = mpsc::channel::<Event>(10);
fasync::Task::local(async move {
loop {
match observer_receiver.next().await {
Some(Event::DebugLog(msg)) => {
app_sender.queue_message(
MessageTarget::View(view_key),
make_message(ConsoleMessages::AddText(msg)),
);
}
Some(Event::Error(msg)) => {
app_sender.queue_message(
MessageTarget::View(view_key),
make_message(ConsoleMessages::AddText(format!("Error: {}", msg))),
);
}
_ => (), // Ignore other Events
}
}
})
.detach();
observer_sender
}
fn set_up_ota_logger(&mut self, mut event_sender: EventSender) -> Result<(), Error> {
let ota_log_listener = OtaLogListenerImpl::new().unwrap();
fasync::Task::local(async move {
let mut local_event_sender = event_sender.clone();
let result = ota_log_listener
.listen(Box::new(move |line| {
local_event_sender.send(Event::DebugLog(format!("OTA: {}", line)))
}))
.await;
if let Err(e) = result {
let msg = format!("failed to subscribe to OTA syslog: {:?}", e);
eprintln!("{}", &msg);
event_sender.send(Event::Error(msg));
}
})
.detach();
Ok(())
}
}
impl AppAssistant for RecoveryAppAssistant {
fn setup(&mut self) -> Result<(), Error> {
// This line is required for the CQ build and test system, specifically boot_test.go
println!("recovery: AppAssistant setup");
Ok(())
}
// Create the State Machine
fn create_view_assistant(&mut self, view_key: ViewKey) -> Result<ViewAssistantPtr, Error> {
// TODO(b/244744635) Add a structured initialization flow for the recovery component
let ota_manager =
Arc::new(OtaComponent::new().expect("failed to create OTA component manager"));
let action = Action::new(
self.controller.get_event_sender(),
ota_manager,
self.crash_reporter.clone(),
);
self.controller.add_state_handler(Box::new(action));
let screens = Screens::new(self.app_sender.clone(), view_key);
let first_screen = screens.initial_screen();
self.controller.add_state_handler(Box::new(screens));
let state_machine = Box::new(StateMachine::new(State::Home));
#[cfg(feature = "debug_console")]
let console_view_assistant_ptr: Option<ViewAssistantPtr> = {
// Debug Console setup
self.controller.add_event_observer(self.setup_event_logging_observer(view_key));
self.set_up_ota_logger(self.controller.get_event_sender().clone())?;
let font_face = font::get_default_font_face();
Some(Box::new(ConsoleViewAssistant::new(font_face.clone())?))
};
#[cfg(not(feature = "debug_console"))]
let console_view_assistant_ptr: Option<ViewAssistantPtr> = None;
let proxy_ptr = Box::new(ProxyViewAssistant::new(
Some(Box::new(self.controller.get_event_sender())),
console_view_assistant_ptr,
first_screen,
)?);
self.controller.start(state_machine);
Ok(proxy_ptr)
}
fn filter_config(&mut self, config: &mut Config) {
config.display_rotation = self.display_rotation;
}
fn outgoing_services_names(&self) -> Vec<&'static str> {
vec![frui::ProgressRendererMarker::PROTOCOL_NAME]
}
fn handle_service_connection_request(
&mut self,
service_name: &str,
channel: fasync::Channel,
) -> Result<(), Error> {
match service_name {
frui::ProgressRendererMarker::PROTOCOL_NAME => {
let event_sender = self.controller.get_event_sender();
fasync::Task::local(async move {
let mut stream = frui::ProgressRendererRequestStream::from_channel(channel);
while let Ok(Some(request)) = stream.try_next().await {
match request {
frui::ProgressRendererRequest::Render {
status,
percent_complete,
responder,
} => {
println!("Ota status event {:?}", &status);
Self::send_ota_status(
Box::new(event_sender.clone()),
percent_complete,
status,
);
responder.send().expect("Error replying to progress update");
}
frui::ProgressRendererRequest::Render2 { payload, responder } => {
println!("Ota progress event: {:?}", &payload);
if let Some(status) = payload.status {
let progress = payload.percent_complete.unwrap_or(0f32);
Self::send_ota_status(
Box::new(event_sender.clone()),
progress,
status,
);
}
responder.send().expect("Error replying to progress update");
}
}
}
})
.detach();
}
_ => panic!("Error: Unexpected service: {}", service_name),
}
Ok(())
}
}
fn make_app_assistant_fut() -> impl FnOnce(&AppSender) -> AssistantCreator<'_> {
move |app_sender: &AppSender| {
let f = async move {
// Route stdout to debuglog, allowing log lines to appear over serial.
stdout_to_debuglog::init().await.unwrap_or_else(|error| {
eprintln!("Failed to initialize debuglog: {:?}", error);
});
if let Err(e) = recovery_util::regulatory::set_region_code_from_factory().await {
eprintln!("error setting region code: {:?}", e);
}
println!("Starting New recovery UI");
let assistant = Box::new(RecoveryAppAssistant::new(app_sender.clone()));
Ok::<AppAssistantPtr, Error>(assistant)
};
Box::pin(f)
}
}
fn make_app_assistant() -> AssistantCreatorFunc {
Box::new(make_app_assistant_fut())
}
#[allow(unused)]
pub fn main() -> Result<(), Error> {
App::run(make_app_assistant())
}
#[cfg(test)]
mod tests {
use super::*;
use fidl_fuchsia_recovery_ui::{
ProgressRendererMarker, ProgressRendererRender2Request, Status,
};
use fuchsia_async as fasync;
use recovery_util::ota::controller::MockController;
#[fuchsia::test]
async fn test_create_view_assistant_sets_up_state_handlers_and_starts() {
let (sender, _) = mpsc::channel::<Event>(10);
let mut controller = MockController::new();
controller.expect_get_event_sender().return_const(EventSender::new(sender));
controller.expect_add_state_handler().times(2).return_const(());
controller.expect_start().once().return_const(());
#[cfg(feature = "debug_console")]
controller.expect_add_event_observer().once().return_const(());
let mut recovery_app_assistant = RecoveryAppAssistant::new_with_params(
AppSender::new_for_testing_purposes_only(),
DisplayRotation::Deg0,
Box::new(controller),
None,
);
recovery_app_assistant.create_view_assistant(ViewKey::default()).unwrap();
}
#[fuchsia::test]
async fn test_progress_updates_reports_success_to_ota_manager() {
let (sender, mut on_handle_event) = mpsc::channel::<Event>(10);
let mut controller = MockController::new();
controller.expect_get_event_sender().return_const(EventSender::new(sender));
let mut recovery_app_assistant = RecoveryAppAssistant::new_with_params(
AppSender::new_for_testing_purposes_only(),
DisplayRotation::Deg0,
Box::new(controller),
None,
);
assert_eq!(
vec![ProgressRendererMarker::PROTOCOL_NAME],
recovery_app_assistant.outgoing_services_names()
);
let (progress_proxy, progress_server) =
fidl::endpoints::create_proxy::<ProgressRendererMarker>().unwrap();
recovery_app_assistant
.handle_service_connection_request(
ProgressRendererMarker::PROTOCOL_NAME,
fasync::Channel::from_channel(progress_server.into_channel()),
)
.unwrap();
progress_proxy
.render2(&ProgressRendererRender2Request {
status: Some(Status::Active),
percent_complete: Some(0.0),
..Default::default()
})
.await
.unwrap();
progress_proxy
.render2(&ProgressRendererRender2Request {
status: Some(Status::Complete),
percent_complete: Some(100.0),
..Default::default()
})
.await
.unwrap();
assert_eq!(Event::Progress(0), on_handle_event.try_next().unwrap().unwrap());
assert_eq!(
Event::ProgressComplete(OtaStatus::Succeeded),
on_handle_event.try_next().unwrap().unwrap()
);
}
#[fuchsia::test]
async fn test_progress_updates_reports_error_to_ota_manager() {
let (sender, mut on_handle_event) = mpsc::channel::<Event>(10);
let mut controller = MockController::new();
controller.expect_get_event_sender().return_const(EventSender::new(sender));
let mut recovery_app_assistant = RecoveryAppAssistant::new_with_params(
AppSender::new_for_testing_purposes_only(),
DisplayRotation::Deg0,
Box::new(controller),
None,
);
assert_eq!(
vec![ProgressRendererMarker::PROTOCOL_NAME],
recovery_app_assistant.outgoing_services_names()
);
let (progress_proxy, progress_server) =
fidl::endpoints::create_proxy::<ProgressRendererMarker>().unwrap();
recovery_app_assistant
.handle_service_connection_request(
ProgressRendererMarker::PROTOCOL_NAME,
fasync::Channel::from_channel(progress_server.into_channel()),
)
.unwrap();
progress_proxy
.render2(&ProgressRendererRender2Request {
status: Some(Status::Active),
percent_complete: Some(0.0),
..Default::default()
})
.await
.unwrap();
progress_proxy
.render2(&ProgressRendererRender2Request {
status: Some(Status::Error),
..Default::default()
})
.await
.unwrap();
assert_eq!(Event::Progress(0), on_handle_event.try_next().unwrap().unwrap());
assert_eq!(
Event::ProgressComplete(OtaStatus::Failed),
on_handle_event.try_next().unwrap().unwrap()
);
}
}