blob: 6c41f0047ccaa6e28057d44db4fd015e637fbea6 [file] [log] [blame]
// 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 <algorithm>
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
#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/notreached.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/arcvm_app/kiosk_arcvm_app_data.h"
#include "chrome/browser/ash/app_mode/arcvm_app/kiosk_arcvm_app_manager.h"
#include "chrome/browser/ash/app_mode/crash_recovery_launcher.h"
#include "chrome/browser/ash/app_mode/isolated_web_app/kiosk_iwa_data.h"
#include "chrome/browser/ash/app_mode/isolated_web_app/kiosk_iwa_manager.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/kiosk_web_app_data.h"
#include "chrome/browser/ash/app_mode/web_app/kiosk_web_app_manager.h"
#include "chrome/browser/ash/login/app_mode/kiosk_launch_controller.h"
#include "chrome/browser/ash/login/screens/app_launch_splash_screen.h"
#include "chrome/browser/ash/login/wizard_controller.h"
#include "chrome/browser/ash/policy/core/device_local_account.h"
#include "chrome/browser/chromeos/app_mode/kiosk_app_level_logs_manager_wrapper.h"
#include "chrome/browser/lifetime/application_lifetime.h"
#include "chrome/browser/ui/ash/login/login_display_host.h"
#include "chrome/common/chrome_switches.h"
#include "chromeos/ash/components/settings/cros_settings.h"
#include "components/account_id/account_id.h"
#include "components/prefs/pref_service.h"
#include "components/user_manager/user.h"
#include "components/user_manager/user_manager.h"
#include "services/network/public/cpp/shared_url_loader_factory.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 KioskWebAppManager& manager,
const AccountId& account_id) {
const KioskWebAppData* 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) {
auto manager_app = manager.GetApp(std::string(chrome_app_id));
if (!manager_app.has_value()) {
return std::nullopt;
}
return KioskApp(
KioskAppId::ForChromeApp(chrome_app_id, manager_app->account_id),
manager_app->name, manager_app->icon);
}
std::optional<KioskApp> IsolatedWebAppById(const KioskIwaManager& manager,
const AccountId& account_id) {
const KioskIwaData* app_data = manager.GetApp(account_id);
if (!app_data) {
return std::nullopt;
}
return KioskApp(KioskAppId::ForIsolatedWebApp(account_id), app_data->name(),
app_data->icon());
}
std::optional<KioskApp> ArcvmAppById(const KioskArcvmAppManager& manager,
const AccountId& account_id) {
const KioskArcvmAppData* data = manager.GetAppByAccountId(account_id);
if (!data) {
return std::nullopt;
}
return KioskApp(KioskAppId::ForArcvmApp(account_id), data->name(),
data->icon());
}
KioskApp EmptyKioskApp(const KioskAppId& app_id) {
switch (app_id.type) {
case KioskAppType::kChromeApp:
case KioskAppType::kIsolatedWebApp:
case KioskAppType::kArcvmApp:
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(
PrefService& local_state,
scoped_refptr<network::SharedURLLoaderFactory> shared_url_loader_factory,
user_manager::UserManager* user_manager)
: local_state_(local_state),
cryptohome_remover_(&local_state),
iwa_manager_(local_state, &cryptohome_remover_),
web_app_manager_(&local_state,
shared_url_loader_factory,
&cryptohome_remover_),
chrome_app_manager_(&local_state,
shared_url_loader_factory,
&cryptohome_remover_),
arcvm_app_manager_(&local_state, &cryptohome_remover_) {
user_manager_observation_.Observe(user_manager);
}
KioskControllerImpl::~KioskControllerImpl() = default;
std::vector<KioskApp> KioskControllerImpl::GetApps() const {
std::vector<KioskApp> apps;
AppendWebApps(apps);
AppendChromeApps(apps);
AppendIsolatedWebApps(apps);
AppendArcvmApps(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:
return IsolatedWebAppById(iwa_manager_, app_id.account_id);
case KioskAppType::kArcvmApp:
return ArcvmAppById(arcvm_app_manager_, app_id.account_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);
}
if (const auto& chrome_app_id = chrome_app_manager_.GetAutoLaunchApp();
!chrome_app_id.empty()) {
return ChromeAppById(chrome_app_manager_, chrome_app_id);
}
if (const auto& iwa_account_id = iwa_manager_.GetAutoLaunchAccountId();
iwa_account_id.has_value()) {
return IsolatedWebAppById(iwa_manager_, *iwa_account_id);
}
if (const auto& arc_account_id = arcvm_app_manager_.GetAutoLaunchAccountId();
arc_account_id.is_valid()) {
return ArcvmAppById(arcvm_app_manager_, arc_account_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(local_state_.get(), 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:
iwa_manager_.OnKioskSessionStarted(kiosk_app_id);
break;
case KioskAppType::kArcvmApp:
arcvm_app_manager_.OnKioskSessionStarted(kiosk_app_id);
break;
}
}
void KioskControllerImpl::StartSession(const KioskAppId& app_id,
bool is_auto_launch,
LoginDisplayHost* host,
AppLaunchSplashScreen* splash_screen) {
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));
kiosk_log_manager_wrapper_ =
std::make_unique<chromeos::KioskAppLevelLogsManagerWrapper>(app_id);
launch_controller_ = std::make_unique<KioskLaunchController>(
&local_state_.get(), host,
/*app_launched_callback=*/
base::BindOnce(&KioskControllerImpl::OnAppLaunched,
base::Unretained(this)),
/*splash_screen=*/splash_screen,
/*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_);
if (base::CommandLine::ForCurrentProcess()->HasSwitch(
ash::switches::kPreventKioskAutolaunchForTesting)) {
LOG(WARNING) << "Skipping to launch " << app << " for "
<< profile->GetPath() << " due to --"
<< ash::switches::kPreventKioskAutolaunchForTesting
<< " flag.";
return;
}
kiosk_log_manager_wrapper_ =
std::make_unique<chromeos::KioskAppLevelLogsManagerWrapper>(profile, app);
crash_recovery_launcher_ = std::make_unique<CrashRecoveryLauncher>(
&local_state_.get(), 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();
}
void KioskControllerImpl::RemoveObsoleteCryptohomes() {
cryptohome_remover_.RemoveObsoleteCryptohomes();
}
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 = std::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);
// Disables installation of preinstalled apps in kiosk sessions as
// `UserManager::Observer::OnUserLoggedIn` is called before `Profile` creation
// and `WebAppProvider::Start`.
// TODO(crbug.com/385072112): Replace cmd line switch with proper filtering.
command_line->AppendSwitch(::switches::kDisableDefaultApps);
// 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);
}
}
void KioskControllerImpl::AppendArcvmApps(std::vector<KioskApp>& apps) const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
for (const KioskAppManagerBase::App& iwa_app : arcvm_app_manager_.GetApps()) {
apps.emplace_back(KioskAppId::ForArcvmApp(iwa_app.account_id), iwa_app.name,
iwa_app.icon);
}
}
} // namespace ash