// Copyright 2024 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/kiosk_controller_impl.h"
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
#include "ash/constants/ash_features.h"
#include "ash/constants/ash_switches.h"
#include "ash/public/cpp/login_accelerators.h"
#include "base/check.h"
#include "base/check_deref.h"
#include "base/check_is_test.h"
#include "base/check_op.h"
#include "base/command_line.h"
#include "base/functional/bind.h"
#include "base/location.h"
#include "base/logging.h"
#include "base/notimplemented.h"
#include "base/notreached.h"
#include "base/ranges/algorithm.h"
#include "base/sequence_checker.h"
#include "base/task/sequenced_task_runner.h"
#include "chrome/browser/ash/app_mode/app_launch_utils.h"
#include "chrome/browser/ash/app_mode/crash_recovery_launcher.h"
#include "chrome/browser/ash/app_mode/kiosk_app.h"
#include "chrome/browser/ash/app_mode/kiosk_app_launch_error.h"
#include "chrome/browser/ash/app_mode/kiosk_app_manager_base.h"
#include "chrome/browser/ash/app_mode/kiosk_app_types.h"
#include "chrome/browser/ash/app_mode/kiosk_chrome_app_manager.h"
#include "chrome/browser/ash/app_mode/kiosk_controller.h"
#include "chrome/browser/ash/app_mode/kiosk_system_session.h"
#include "chrome/browser/ash/app_mode/web_app/web_kiosk_app_data.h"
#include "chrome/browser/ash/app_mode/web_app/web_kiosk_app_manager.h"
#include "chrome/browser/ash/login/app_mode/kiosk_launch_controller.h"
#include "chrome/browser/ash/login/ui/login_display_host.h"
#include "chrome/browser/ash/policy/core/device_local_account.h"
#include "chrome/browser/lifetime/application_lifetime.h"
#include "chrome/common/chrome_switches.h"
#include "chromeos/ash/components/kiosk/vision/internals_page_processor.h"
#include "chromeos/ash/components/kiosk/vision/kiosk_vision.h"
#include "chromeos/ash/components/settings/cros_settings.h"
#include "components/account_id/account_id.h"
#include "components/user_manager/user.h"
#include "components/user_manager/user_manager.h"
#include "ui/ozone/public/input_controller.h"
#include "ui/ozone/public/ozone_platform.h"
#include "ui/wm/core/wm_core_switches.h"
namespace ash {
namespace {
std::optional<KioskApp> WebAppById(const WebKioskAppManager& manager,
const AccountId& account_id) {
const WebKioskAppData* data = manager.GetAppByAccountId(account_id);
if (!data) {
return std::nullopt;
}
return KioskApp(KioskAppId::ForWebApp(account_id), data->name(), data->icon(),
data->install_url());
}
std::optional<KioskApp> ChromeAppById(const KioskChromeAppManager& manager,
std::string_view chrome_app_id) {
KioskChromeAppManager::App manager_app;
if (!manager.GetApp(std::string(chrome_app_id), &manager_app)) {
return std::nullopt;
}
return KioskApp(
KioskAppId::ForChromeApp(chrome_app_id, manager_app.account_id),
manager_app.name, manager_app.icon);
}
KioskApp EmptyKioskApp(const KioskAppId& app_id) {
switch (app_id.type) {
case KioskAppType::kChromeApp:
case KioskAppType::kIsolatedWebApp:
return KioskApp{app_id,
/*name=*/"",
/*icon=*/gfx::ImageSkia(),
/*url=*/std::nullopt};
case KioskAppType::kWebApp:
return KioskApp{app_id,
/*name=*/"",
/*icon=*/gfx::ImageSkia(),
/*url=*/GURL()};
}
NOTREACHED();
}
} // namespace
KioskControllerImpl::KioskControllerImpl(
user_manager::UserManager* user_manager) {
user_manager_observation_.Observe(user_manager);
}
KioskControllerImpl::~KioskControllerImpl() = default;
std::vector<KioskApp> KioskControllerImpl::GetApps() const {
std::vector<KioskApp> apps;
AppendWebApps(apps);
AppendChromeApps(apps);
if (ash::features::IsIsolatedWebAppKioskEnabled()) {
AppendIsolatedWebApps(apps);
}
return apps;
}
std::optional<KioskApp> KioskControllerImpl::GetAppById(
const KioskAppId& app_id) const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
switch (app_id.type) {
case KioskAppType::kWebApp:
return WebAppById(web_app_manager_, app_id.account_id);
case KioskAppType::kChromeApp:
return ChromeAppById(chrome_app_manager_, app_id.app_id.value());
case KioskAppType::kIsolatedWebApp:
// TODO(crbug.com/359774056): add IsolatedWebAppById.
return EmptyKioskApp(app_id);
}
}
std::optional<KioskApp> KioskControllerImpl::GetAutoLaunchApp() const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (const auto& web_account_id = web_app_manager_.GetAutoLaunchAccountId();
web_account_id.is_valid()) {
return WebAppById(web_app_manager_, web_account_id);
} else if (std::string chrome_app_id = chrome_app_manager_.GetAutoLaunchApp();
!chrome_app_id.empty()) {
return ChromeAppById(chrome_app_manager_, chrome_app_id);
}
return std::nullopt;
}
void KioskControllerImpl::InitializeKioskSystemSession(
const KioskAppId& kiosk_app_id,
Profile* profile,
const std::optional<std::string>& app_name) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
CHECK(!system_session_.has_value())
<< "KioskSystemSession is already initialized";
system_session_.emplace(profile, kiosk_app_id, app_name);
switch (kiosk_app_id.type) {
case KioskAppType::kWebApp:
web_app_manager_.OnKioskSessionStarted(kiosk_app_id);
break;
case KioskAppType::kChromeApp:
chrome_app_manager_.OnKioskSessionStarted(kiosk_app_id);
break;
case KioskAppType::kIsolatedWebApp:
// TODO(crbug.com/361017701): add iwa_manager_.OnKioskSessionStarted.
NOTIMPLEMENTED();
break;
}
}
void KioskControllerImpl::StartSession(const KioskAppId& app_id,
bool is_auto_launch,
LoginDisplayHost* host) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
CHECK_EQ(launch_controller_, nullptr);
CHECK(!system_session_.has_value());
std::optional<KioskApp> app_maybe = GetAppById(app_id);
// TODO(b/306117645) change to CHECK and drop `value_or`.
DUMP_WILL_BE_CHECK(app_maybe.has_value());
KioskApp app = std::move(app_maybe).value_or(EmptyKioskApp(app_id));
launch_controller_ = std::make_unique<KioskLaunchController>(
host, host->GetOobeUI(),
/*app_launched_callback=*/
base::BindOnce(&KioskControllerImpl::OnAppLaunched,
base::Unretained(this)),
/*done_callback=*/
base::BindOnce(&KioskControllerImpl::OnLaunchComplete,
base::Unretained(this)));
launch_controller_->Start(std::move(app), is_auto_launch);
}
void KioskControllerImpl::StartSessionAfterCrash(const KioskAppId& app,
Profile* profile) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
crash_recovery_launcher_ =
std::make_unique<CrashRecoveryLauncher>(CHECK_DEREF(profile), app);
crash_recovery_launcher_->Start(
base::BindOnce(&KioskControllerImpl::OnLaunchCompleteAfterCrash,
// Safe since `this` owns the `crash_recovery_launcher_`.
base::Unretained(this), app, profile));
}
bool KioskControllerImpl::IsSessionStarting() const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return launch_controller_ != nullptr || crash_recovery_launcher_ != nullptr;
}
void KioskControllerImpl::CancelSessionStart() {
DeleteLaunchControllerAsync();
}
void KioskControllerImpl::AddProfileLoadFailedObserver(
KioskProfileLoadFailedObserver* observer) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
CHECK_NE(launch_controller_, nullptr);
launch_controller_->AddKioskProfileLoadFailedObserver(observer);
}
void KioskControllerImpl::RemoveProfileLoadFailedObserver(
KioskProfileLoadFailedObserver* observer) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (launch_controller_) {
launch_controller_->RemoveKioskProfileLoadFailedObserver(observer);
}
}
bool KioskControllerImpl::HandleAccelerator(LoginAcceleratorAction action) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return launch_controller_ && launch_controller_->HandleAccelerator(action);
}
void KioskControllerImpl::OnGuestAdded(
content::WebContents* guest_web_contents) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (system_session_.has_value()) {
system_session_->OnGuestAdded(guest_web_contents);
}
}
KioskSystemSession* KioskControllerImpl::GetKioskSystemSession() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!system_session_.has_value()) {
return nullptr;
}
return &system_session_.value();
}
kiosk_vision::TelemetryProcessor*
KioskControllerImpl::GetKioskVisionTelemetryProcessor() {
auto* kiosk_system_session = GetKioskSystemSession();
if (!kiosk_system_session) {
return nullptr;
}
return kiosk_system_session->kiosk_vision().GetTelemetryProcessor();
}
kiosk_vision::InternalsPageProcessor*
KioskControllerImpl::GetKioskVisionInternalsPageProcessor() {
auto* kiosk_system_session = GetKioskSystemSession();
if (!kiosk_system_session) {
return nullptr;
}
return kiosk_system_session->kiosk_vision().GetInternalsPageProcessor();
}
void KioskControllerImpl::OnUserLoggedIn(const user_manager::User& user) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!user.IsKioskType()) {
return;
}
const AccountId& kiosk_app_account_id = user.GetAccountId();
// TODO(bartfab): Add KioskAppUsers to the users_ list and keep metadata like
// the kiosk_app_id in these objects, removing the need to re-parse the
// device-local account list here to extract the kiosk_app_id.
const std::vector<policy::DeviceLocalAccount> device_local_accounts =
policy::GetDeviceLocalAccounts(CrosSettings::Get());
const auto account = base::ranges::find(device_local_accounts,
kiosk_app_account_id.GetUserEmail(),
&policy::DeviceLocalAccount::user_id);
std::string kiosk_app_id;
if (account != device_local_accounts.end()) {
kiosk_app_id = account->kiosk_app_id;
} else {
LOG(ERROR) << "Logged into nonexistent kiosk-app account: "
<< kiosk_app_account_id.GetUserEmail();
CHECK_IS_TEST();
}
base::CommandLine* command_line = base::CommandLine::ForCurrentProcess();
command_line->AppendSwitch(::switches::kForceAppMode);
// This happens in Web kiosks.
if (!kiosk_app_id.empty()) {
command_line->AppendSwitchASCII(::switches::kAppId, kiosk_app_id);
}
// Disable window animation since kiosk app runs in a single full screen
// window and window animation causes start-up janks.
command_line->AppendSwitch(wm::switches::kWindowAnimationsDisabled);
// If restoring auto-launched kiosk session, make sure the app is marked
// as auto-launched.
if (command_line->HasSwitch(switches::kLoginUser) &&
command_line->HasSwitch(switches::kAppAutoLaunched) &&
!kiosk_app_id.empty()) {
chrome_app_manager_.SetAppWasAutoLaunchedWithZeroDelay(kiosk_app_id);
}
}
void KioskControllerImpl::OnAppLaunched(
const KioskAppId& kiosk_app_id,
Profile* profile,
const std::optional<std::string>& app_name) {
InitializeKioskSystemSession(kiosk_app_id, profile, app_name);
}
void KioskControllerImpl::OnLaunchComplete(KioskAppLaunchError::Error error) {
if (auto* input_controller =
ui::OzonePlatform::GetInstance()->GetInputController()) {
input_controller->DisableKeyboardImposterCheck();
}
// Delete the launcher so it doesn't end up with dangling references.
DeleteLaunchControllerAsync();
}
void KioskControllerImpl::DeleteLaunchControllerAsync() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// Deleted asynchronously since this method is invoked in a callback called by
// the launcher itself, but don't use `DeleteSoon` to prevent the launcher
// from outliving `this`.
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, base::BindOnce(&KioskControllerImpl::DeleteLaunchController,
weak_factory_.GetWeakPtr()));
}
void KioskControllerImpl::DeleteLaunchController() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
launch_controller_.reset();
}
void KioskControllerImpl::OnLaunchCompleteAfterCrash(
const KioskAppId& app,
Profile* profile,
bool success,
const std::optional<std::string>& app_name) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (success) {
if (auto* input_controller =
ui::OzonePlatform::GetInstance()->GetInputController()) {
input_controller->DisableKeyboardImposterCheck();
}
InitializeKioskSystemSession(app, profile, app_name);
} else {
chrome::AttemptUserExit();
}
// Delete launcher so it doesn't end up with dangling references.
crash_recovery_launcher_.reset();
}
void KioskControllerImpl::AppendWebApps(std::vector<KioskApp>& apps) const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
for (const KioskAppManagerBase::App& web_app : web_app_manager_.GetApps()) {
apps.emplace_back(KioskAppId::ForWebApp(web_app.account_id), web_app.name,
web_app.icon, web_app.url);
}
}
void KioskControllerImpl::AppendChromeApps(std::vector<KioskApp>& apps) const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
for (const KioskAppManagerBase::App& chrome_app :
chrome_app_manager_.GetApps()) {
apps.emplace_back(
KioskAppId::ForChromeApp(chrome_app.app_id, chrome_app.account_id),
chrome_app.name, chrome_app.icon);
}
}
void KioskControllerImpl::AppendIsolatedWebApps(
std::vector<KioskApp>& apps) const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
for (const KioskAppManagerBase::App& iwa_app : iwa_manager_.GetApps()) {
apps.emplace_back(KioskAppId::ForIsolatedWebApp(iwa_app.account_id),
iwa_app.name, iwa_app.icon);
}
}
} // namespace ash