// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ash/booting/booting_animation_controller.h"
#include <memory>
#include "ash/booting/booting_animation_view.h"
#include "ash/constants/ash_features.h"
#include "ash/public/cpp/shell_window_ids.h"
#include "ash/shell.h"
#include "base/files/file_util.h"
#include "base/location.h"
#include "base/system/sys_info.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "base/time/time.h"
#include "ui/views/widget/widget.h"
#include "ui/views/widget/widget_delegate.h"
namespace ash {
namespace {
constexpr base::FilePath::CharType kAnimationPath[] = FILE_PATH_LITERAL(
"/usr/share/chromeos-assets/animated_splash_screen/splash_animation.json");
std::string ReadFileToString(const base::FilePath& path) {
std::string result;
if (!base::ReadFileToString(path, &result)) {
LOG(WARNING) << "Failed reading file";
result.clear();
}
return result;
}
} // namespace
BootingAnimationController::BootingAnimationController() {
CHECK(ash::Shell::Get()->display_configurator());
scoped_display_configurator_observer_.Observe(
ash::Shell::Get()->display_configurator());
base::ThreadPool::PostTaskAndReplyWithResult(
FROM_HERE,
{base::MayBlock(), base::TaskPriority::USER_VISIBLE,
base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN},
base::BindOnce(&ReadFileToString, base::FilePath(kAnimationPath)),
base::BindOnce(&BootingAnimationController::OnAnimationDataFetched,
weak_factory_.GetWeakPtr()));
}
BootingAnimationController::~BootingAnimationController() = default;
void BootingAnimationController::Show() {
// If data fetch failed, notify caller immediately without showing the widget.
if (data_fetch_failed_.has_value() && data_fetch_failed_.value()) {
std::move(animation_played_callback_).Run();
return;
}
widget_ = std::make_unique<views::Widget>();
views::Widget::InitParams params(
views::Widget::InitParams::NATIVE_WIDGET_OWNS_WIDGET,
views::Widget::InitParams::TYPE_WINDOW_FRAMELESS);
params.delegate = new views::WidgetDelegate; // Takes ownership.
params.delegate->SetOwnedByWidget(true);
// Allow maximize so the booting container's FillLayoutManager can
// fill the screen with the widget. This is required even for
// fullscreen widgets.
params.delegate->SetCanMaximize(true);
params.delegate->SetCanFullscreen(true);
params.name = "BootingAnimationWidget";
params.show_state = ui::SHOW_STATE_FULLSCREEN;
// Create the Booting Animation widget on the primary display.
auto* animation_window = Shell::GetContainer(
Shell::GetPrimaryRootWindow(), kShellWindowId_BootingAnimationContainer);
params.parent = animation_window;
params.ownership = views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET;
// Make the opacity `kTranslucent` so the OOBE WebUI will be rendered in the
// background.
params.opacity = views::Widget::InitParams::WindowOpacity::kTranslucent;
widget_->Init(std::move(params));
widget_->SetContentsView(std::make_unique<BootingAnimationView>());
// Show widget even if the animation isn't ready yet. This prevents other UI
// to be shown.
widget_->Show();
}
void BootingAnimationController::ShowAnimationWithEndCallback(
base::OnceClosure callback) {
animation_played_callback_ = std::move(callback);
// Show the widget early to prevent UI blinks. The animation will start once
// its data is fetched and device is ready.
Show();
// Don't wait for GPU to be ready in non-ChromeOS environment.
if (!base::SysInfo::IsRunningOnChromeOS()) {
IgnoreGpuReadiness();
return;
}
// If we are still waiting for the signal from DisplayConfigurator wait for
// not more than a few seconds and play the animation anyway.
if (!IsDeviceReady()) {
base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
FROM_HERE,
base::BindOnce(&BootingAnimationController::IgnoreGpuReadiness,
weak_factory_.GetWeakPtr()),
base::TimeDelta(base::Seconds(5)));
return;
}
StartAnimation();
}
void BootingAnimationController::Finish() {
widget_.reset();
animation_played_callback_.Reset();
}
base::WeakPtr<BootingAnimationController>
BootingAnimationController::GetWeakPtr() {
return weak_factory_.GetWeakPtr();
}
void BootingAnimationController::OnDisplayConfigurationChanged(
const display::DisplayConfigurator::DisplayStateList& displays) {
if (!is_gpu_ready_) {
return;
}
scoped_display_configurator_observer_.Reset();
CHECK(IsDeviceReady());
if (!animation_played_callback_.is_null()) {
StartAnimation();
}
}
void BootingAnimationController::OnDisplaySnapshotsInvalidated() {
// This call represents that GPU has returned us valid display snapshots, but
// they are not still applied. Starting the animation before modeset happens
// is too early and we need to wait for the next `OnDisplayModeChanged` call.
is_gpu_ready_ = true;
}
void BootingAnimationController::AnimationCycleEnded(
const lottie::Animation* animation) {
// Once animation has finished playing we might delete it. Stop observation
// here explicitly.
scoped_animation_observer_.Reset();
if (!animation_played_callback_.is_null()) {
std::move(animation_played_callback_).Run();
}
}
void BootingAnimationController::OnAnimationDataFetched(std::string data) {
if (data.empty()) {
LOG(ERROR) << "No booting animation file available.";
data_fetch_failed_ = true;
// Notify caller immediately that there is no animation file.
if (!animation_played_callback_.is_null()) {
std::move(animation_played_callback_).Run();
}
return;
}
data_fetch_failed_ = false;
animation_data_ = std::move(data);
// Only start if we haven't exited earlier already and the device is ready to
// show.
if (!animation_played_callback_.is_null() && IsDeviceReady()) {
StartAnimation();
}
}
void BootingAnimationController::StartAnimation() {
if (!data_fetch_failed_.has_value()) {
LOG(ERROR) << "Booting animation isn't ready yet.";
return;
}
CHECK(!animation_played_callback_.is_null() && IsDeviceReady());
if (was_shown_) {
return;
}
was_shown_ = true;
BootingAnimationView* view =
static_cast<BootingAnimationView*>(widget_->GetContentsView());
view->SetAnimatedImage(animation_data_);
// If there is no animated image set at this point it means that data file
// is invalid and we need to finish the animation immediately.
auto* animated_image = view->GetAnimatedImage();
if (!animated_image) {
std::move(animation_played_callback_).Run();
return;
}
// Observe animation to know when it finishes playing.
scoped_animation_observer_.Observe(animated_image);
view->Play();
}
void BootingAnimationController::IgnoreGpuReadiness() {
// Don't do anything if the device is ready.
if (IsDeviceReady()) {
return;
}
LOG(ERROR) << "Ignore the readiness of the GPU and play the animation.";
is_gpu_ready_ = true;
scoped_display_configurator_observer_.Reset();
if (!animation_played_callback_.is_null()) {
StartAnimation();
}
}
bool BootingAnimationController::IsDeviceReady() const {
return is_gpu_ready_ && !scoped_display_configurator_observer_.IsObserving();
}
} // namespace ash