// Copyright 2013 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/ash/app_mode/startup_app_launcher.h"
#include <memory>
#include <utility>
#include <vector>
#include "base/functional/bind.h"
#include "base/functional/callback_forward.h"
#include "base/location.h"
#include "base/notreached.h"
#include "base/syslog_logging.h"
#include "base/task/single_thread_task_runner.h"
#include "base/types/cxx23_to_underlying.h"
#include "chrome/browser/ash/app_mode/kiosk_app_launch_error.h"
#include "chrome/browser/ash/app_mode/kiosk_app_launcher.h"
#include "chrome/browser/ash/app_mode/kiosk_chrome_app_manager.h"
#include "chrome/browser/ash/crosapi/browser_manager.h"
#include "chrome/browser/ash/crosapi/browser_manager_observer.h"
#include "chrome/browser/ash/crosapi/browser_util.h"
#include "chrome/browser/ash/crosapi/chrome_app_kiosk_service_ash.h"
#include "chrome/browser/ash/crosapi/crosapi_ash.h"
#include "chrome/browser/ash/crosapi/crosapi_manager.h"
#include "chrome/browser/chromeos/app_mode/chrome_kiosk_app_installer.h"
#include "chrome/browser/chromeos/app_mode/chrome_kiosk_app_launcher.h"
#include "chrome/browser/profiles/profile.h"
#include "chromeos/crosapi/mojom/chrome_app_kiosk_service.mojom.h"
#include "components/crx_file/id_util.h"
using chromeos::ChromeKioskAppInstaller;
using chromeos::ChromeKioskAppLauncher;
namespace ash {
namespace {
const int kMaxLaunchAttempt = 5;
// Reduced backoff policy for extension downloader while Kiosk is launching.
const net::BackoffEntry::Policy kKioskLaunchExtensionBackoffPolicy = {
.num_errors_to_ignore = 0,
.initial_delay_ms = 2000,
.multiply_factor = 2,
.jitter_factor = 0.1,
.maximum_backoff_ms = 3000,
.entry_lifetime_ms = -1,
.always_use_initial_delay = false,
};
crosapi::ChromeAppKioskServiceAsh* crosapi_chrome_app_kiosk_service() {
return crosapi::CrosapiManager::Get()
->crosapi_ash()
->chrome_app_kiosk_service();
}
} // namespace
StartupAppLauncher::StartupAppLauncher(
Profile* profile,
const std::string& app_id,
bool should_skip_install,
StartupAppLauncher::NetworkDelegate* network_delegate)
: KioskAppLauncher(network_delegate),
profile_(profile),
app_id_(app_id),
should_skip_install_(should_skip_install) {
CHECK(profile_);
DCHECK(crx_file::id_util::IdIsValid(app_id_));
// Reduce extension downloader retry backoff to avoid waiting on splash screen
// for a long time.
KioskChromeAppManager::Get()->SetExtensionDownloaderBackoffPolicy(
kKioskLaunchExtensionBackoffPolicy);
}
StartupAppLauncher::~StartupAppLauncher() {
// Restore to default extension downloader backoff policy.
KioskChromeAppManager::Get()->SetExtensionDownloaderBackoffPolicy(
std::nullopt);
}
void StartupAppLauncher::AddObserver(KioskAppLauncher::Observer* observer) {
observers_.AddObserver(observer);
}
void StartupAppLauncher::RemoveObserver(KioskAppLauncher::Observer* observer) {
observers_.RemoveObserver(observer);
}
void StartupAppLauncher::Initialize() {
CHECK(state_ != LaunchState::kReadyToLaunch &&
state_ != LaunchState::kWaitingForWindow &&
state_ != LaunchState::kLaunchSucceeded);
if (should_skip_install_) {
OnInstallSuccess();
return;
}
// Update the offline enabled crx cache if the network is ready;
// or just install the app.
if (delegate_->IsNetworkReady()) {
ContinueWithNetworkReady();
} else {
BeginInstall();
}
}
void StartupAppLauncher::ContinueWithNetworkReady() {
SYSLOG(INFO) << "ContinueWithNetworkReady"
<< ", state_=" << base::to_underlying(state_);
if (state_ != LaunchState::kInitializingNetwork &&
state_ != LaunchState::kNotStarted) {
return;
}
if (should_skip_install_) {
OnInstallSuccess();
return;
}
// The network might not be ready when KioskChromeAppManager tries to update
// external cache initially. Update the external cache now that the network
// is ready for sure.
state_ = LaunchState::kWaitingForCache;
kiosk_app_manager_observation_.Observe(KioskChromeAppManager::Get());
KioskChromeAppManager::Get()->UpdateExternalCache();
}
bool StartupAppLauncher::RetryWhenNetworkIsAvailable() {
++launch_attempt_;
if (launch_attempt_ < kMaxLaunchAttempt) {
state_ = LaunchState::kInitializingNetwork;
delegate_->InitializeNetwork();
return true;
}
return false;
}
void StartupAppLauncher::OnKioskExtensionLoadedInCache(
const std::string& app_id) {
OnKioskAppDataLoadStatusChanged(app_id);
}
void StartupAppLauncher::OnKioskExtensionDownloadFailed(
const std::string& app_id) {
OnKioskAppDataLoadStatusChanged(app_id);
}
void StartupAppLauncher::OnKioskAppDataLoadStatusChanged(
const std::string& app_id) {
CHECK_EQ(state_, LaunchState::kWaitingForCache);
if (app_id != app_id_) {
return;
}
kiosk_app_manager_observation_.Reset();
if (KioskChromeAppManager::Get()->HasCachedCrx(app_id_)) {
BeginInstall();
} else {
OnLaunchFailure(KioskAppLaunchError::Error::kUnableToDownload);
}
}
void StartupAppLauncher::BeginInstall() {
state_ = LaunchState::kInstallingApp;
observers_.NotifyAppInstalling();
if (crosapi::browser_util::IsLacrosEnabledInChromeKioskSession()) {
InstallAppInLacros();
} else {
InstallAppInAsh();
}
}
void StartupAppLauncher::InstallAppInAsh() {
installer_ = std::make_unique<ChromeKioskAppInstaller>(
profile_,
KioskChromeAppManager::Get()->CreatePrimaryAppInstallData(app_id_));
installer_->BeginInstall(base::BindOnce(
&StartupAppLauncher::OnInstallComplete, weak_ptr_factory_.GetWeakPtr()));
}
void StartupAppLauncher::InstallAppInLacros() {
crosapi_chrome_app_kiosk_service()->InstallKioskApp(
KioskChromeAppManager::Get()->CreatePrimaryAppInstallData(app_id_),
base::BindOnce(&StartupAppLauncher::OnInstallComplete,
weak_ptr_factory_.GetWeakPtr()));
}
void StartupAppLauncher::OnInstallComplete(
ChromeKioskAppInstaller::InstallResult result) {
CHECK_EQ(state_, LaunchState::kInstallingApp);
installer_.reset();
switch (result) {
case ChromeKioskAppInstaller::InstallResult::kSuccess:
OnInstallSuccess();
return;
case ChromeKioskAppInstaller::InstallResult::kPrimaryAppInstallFailed:
OnLaunchFailure(KioskAppLaunchError::Error::kUnableToInstall);
return;
case ChromeKioskAppInstaller::InstallResult::kPrimaryAppNotKioskEnabled:
OnLaunchFailure(KioskAppLaunchError::Error::kNotKioskEnabled);
return;
case ChromeKioskAppInstaller::InstallResult::kPrimaryAppNotCached:
case ChromeKioskAppInstaller::InstallResult::kSecondaryAppInstallFailed:
if (!RetryWhenNetworkIsAvailable()) {
OnLaunchFailure(KioskAppLaunchError::Error::kUnableToInstall);
}
return;
case ChromeKioskAppInstaller::InstallResult::kUnknown:
SYSLOG(ERROR) << "Received unknown InstallResult";
OnLaunchFailure(KioskAppLaunchError::Error::kUnableToInstall);
return;
}
}
void StartupAppLauncher::OnInstallSuccess() {
state_ = LaunchState::kReadyToLaunch;
observers_.NotifyAppPrepared();
}
void StartupAppLauncher::LaunchApp() {
if (state_ != LaunchState::kReadyToLaunch) {
NOTREACHED_IN_MIGRATION();
SYSLOG(ERROR) << "LaunchApp() called but launcher is not initialized.";
}
if (crosapi::browser_util::IsLacrosEnabledInChromeKioskSession()) {
crosapi_chrome_app_kiosk_service()->LaunchKioskApp(
app_id_, delegate_->IsNetworkReady(),
base::BindOnce(&StartupAppLauncher::OnLaunchComplete,
weak_ptr_factory_.GetWeakPtr()));
} else {
launcher_ = std::make_unique<ChromeKioskAppLauncher>(
profile_, app_id_, delegate_->IsNetworkReady());
launcher_->LaunchApp(base::BindOnce(&StartupAppLauncher::OnLaunchComplete,
weak_ptr_factory_.GetWeakPtr()));
}
}
void StartupAppLauncher::OnLaunchComplete(
ChromeKioskAppLauncher::LaunchResult result) {
CHECK_EQ(state_, LaunchState::kReadyToLaunch);
launcher_.reset();
switch (result) {
case ChromeKioskAppLauncher::LaunchResult::kSuccess:
OnLaunchSuccess();
return;
case ChromeKioskAppLauncher::LaunchResult::kUnableToLaunch:
OnLaunchFailure(KioskAppLaunchError::Error::kUnableToLaunch);
return;
case ChromeKioskAppLauncher::LaunchResult::kNetworkMissing:
if (!RetryWhenNetworkIsAvailable()) {
OnLaunchFailure(KioskAppLaunchError::Error::kUnableToLaunch);
}
return;
case ChromeKioskAppLauncher::LaunchResult::kUnknown:
SYSLOG(ERROR) << "Received unknown LaunchResult";
OnLaunchFailure(KioskAppLaunchError::Error::kUnableToLaunch);
return;
}
}
void StartupAppLauncher::OnLaunchSuccess() {
state_ = LaunchState::kLaunchSucceeded;
observers_.NotifyAppLaunched();
observers_.NotifyAppWindowCreated();
}
void StartupAppLauncher::OnLaunchFailure(KioskAppLaunchError::Error error) {
SYSLOG(ERROR) << "App launch failed, error: " << static_cast<int>(error);
CHECK_NE(KioskAppLaunchError::Error::kNone, error);
observers_.NotifyLaunchFailed(error);
}
} // namespace ash