| // Copyright 2021 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/chromeos/app_mode/chrome_kiosk_app_installer.h" |
| |
| #include "base/metrics/histogram_functions.h" |
| #include "base/syslog_logging.h" |
| #include "chrome/browser/chromeos/app_mode/chrome_kiosk_external_loader_broker.h" |
| #include "chrome/browser/chromeos/app_mode/startup_app_launcher_update_checker.h" |
| #include "chrome/browser/extensions/extension_service.h" |
| #include "chrome/browser/extensions/forced_extensions/install_stage_tracker.h" |
| #include "chrome/browser/extensions/install_tracker_factory.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "extensions/browser/extension_system.h" |
| #include "extensions/common/file_util.h" |
| #include "extensions/common/manifest_handlers/kiosk_mode_info.h" |
| |
| namespace ash { |
| |
| namespace { |
| |
| const char kChromeKioskExtensionUpdateErrorHistogram[] = |
| "Kiosk.ChromeApp.ExtensionUpdateError"; |
| const char kChromeKioskExtensionHasUpdateDurationHistogram[] = |
| "Kiosk.ChromeApp.ExtensionUpdateDuration.HasUpdate"; |
| const char kChromeKioskExtensionNoUpdateDurationHistogram[] = |
| "Kiosk.ChromeApp.ExtensionUpdateDuration.NoUpdate"; |
| |
| } // namespace |
| |
| ChromeKioskAppInstaller::ChromeKioskAppInstaller( |
| Profile* profile, |
| const AppInstallParams& install_data) |
| : profile_(profile), primary_app_install_data_(install_data) {} |
| |
| ChromeKioskAppInstaller::~ChromeKioskAppInstaller() {} |
| |
| void ChromeKioskAppInstaller::BeginInstall(InstallCallback callback) { |
| DCHECK(!install_complete_); |
| |
| SYSLOG(INFO) << "BeginInstall"; |
| |
| on_ready_callback_ = std::move(callback); |
| |
| extensions::file_util::SetUseSafeInstallation(true); |
| |
| if (primary_app_install_data_.crx_file_location.empty() && |
| !GetPrimaryAppExtension()) { |
| ReportInstallFailure(InstallResult::kPrimaryAppNotCached); |
| return; |
| } |
| |
| ChromeKioskExternalLoaderBroker::Get()->TriggerPrimaryAppInstall( |
| primary_app_install_data_); |
| if (IsAppInstallPending(primary_app_install_data_.id)) { |
| ObserveActiveInstallations(); |
| return; |
| } |
| |
| const extensions::Extension* primary_app = GetPrimaryAppExtension(); |
| if (!primary_app) { |
| // The extension is skipped for installation due to some error. |
| ReportInstallFailure(InstallResult::kPrimaryAppInstallFailed); |
| return; |
| } |
| |
| if (!extensions::KioskModeInfo::IsKioskEnabled(primary_app)) { |
| // The installed primary app is not kiosk enabled. |
| ReportInstallFailure(InstallResult::kPrimaryAppNotKioskEnabled); |
| return; |
| } |
| |
| // Install secondary apps. |
| MaybeInstallSecondaryApps(); |
| } |
| |
| void ChromeKioskAppInstaller::MaybeInstallSecondaryApps() { |
| if (install_complete_) { |
| return; |
| } |
| |
| secondary_apps_installing_ = true; |
| extensions::KioskModeInfo* info = |
| extensions::KioskModeInfo::Get(GetPrimaryAppExtension()); |
| |
| std::vector<std::string> secondary_app_ids; |
| for (const auto& app : info->secondary_apps) { |
| secondary_app_ids.push_back(app.id); |
| } |
| |
| ChromeKioskExternalLoaderBroker::Get()->TriggerSecondaryAppInstall( |
| secondary_app_ids); |
| if (IsAnySecondaryAppPending()) { |
| ObserveActiveInstallations(); |
| return; |
| } |
| |
| if (AreSecondaryAppsInstalled()) { |
| // Check extension update before launching the primary kiosk app. |
| MaybeCheckExtensionUpdate(); |
| } else { |
| ReportInstallFailure(InstallResult::kSecondaryAppInstallFailed); |
| } |
| } |
| |
| void ChromeKioskAppInstaller::MaybeCheckExtensionUpdate() { |
| DCHECK(!install_complete_); |
| |
| SYSLOG(INFO) << "MaybeCheckExtensionUpdate"; |
| |
| // Record update start time to calculate time consumed by update check. When |
| // |OnExtensionUpdateCheckFinished| is called the update is already finished |
| // because |extensions::ExtensionUpdater::CheckParams::install_immediately| is |
| // set to true. |
| extension_update_start_time_ = base::Time::Now(); |
| |
| // Observe installation failures. |
| install_stage_observation_.Observe( |
| extensions::InstallStageTracker::Get(profile_)); |
| |
| // Enforce an immediate version update check for all extensions before |
| // launching the primary app. After the chromeos is updated, the shared |
| // module(e.g. ARC runtime) may need to be updated to a newer version |
| // compatible with the new chromeos. See crbug.com/555083. |
| update_checker_ = std::make_unique<StartupAppLauncherUpdateChecker>(profile_); |
| if (!update_checker_->Run(base::BindOnce( |
| &ChromeKioskAppInstaller::OnExtensionUpdateCheckFinished, |
| weak_ptr_factory_.GetWeakPtr()))) { |
| update_checker_.reset(); |
| install_stage_observation_.Reset(); |
| FinalizeAppInstall(); |
| return; |
| } |
| |
| SYSLOG(INFO) << "Extension update check run."; |
| } |
| |
| void ChromeKioskAppInstaller::OnExtensionUpdateCheckFinished( |
| bool update_found) { |
| DCHECK(!install_complete_); |
| |
| SYSLOG(INFO) << "OnExtensionUpdateCheckFinished"; |
| update_checker_.reset(); |
| install_stage_observation_.Reset(); |
| if (update_found) { |
| SYSLOG(INFO) << "Start to reload extension with id " |
| << primary_app_install_data_.id; |
| |
| // Reload the primary app to make sure any reference to the previous version |
| // of the shared module, extension, etc will be cleaned up and the new |
| // version will be loaded. |
| extensions::ExtensionSystem::Get(profile_) |
| ->extension_service() |
| ->ReloadExtension(primary_app_install_data_.id); |
| |
| SYSLOG(INFO) << "Finish to reload extension with id " |
| << primary_app_install_data_.id; |
| } |
| |
| base::UmaHistogramMediumTimes( |
| update_found ? kChromeKioskExtensionHasUpdateDurationHistogram |
| : kChromeKioskExtensionNoUpdateDurationHistogram, |
| base::Time::Now() - extension_update_start_time_); |
| |
| FinalizeAppInstall(); |
| } |
| |
| void ChromeKioskAppInstaller::FinalizeAppInstall() { |
| DCHECK(!install_complete_); |
| |
| install_complete_ = true; |
| |
| ReportInstallSuccess(); |
| } |
| |
| void ChromeKioskAppInstaller::OnFinishCrxInstall( |
| const std::string& extension_id, |
| bool success) { |
| DCHECK(!install_complete_); |
| |
| SYSLOG(INFO) << "OnFinishCrxInstall, id=" << extension_id |
| << ", success=" << success; |
| |
| if (DidPrimaryOrSecondaryAppFailedToInstall(success, extension_id)) { |
| install_observation_.Reset(); |
| ReportInstallFailure((extension_id == primary_app_install_data_.id) |
| ? InstallResult::kPrimaryAppInstallFailed |
| : InstallResult::kSecondaryAppInstallFailed); |
| return; |
| } |
| |
| // Wait for pending updates or dependent extensions to download. |
| if (extensions::ExtensionSystem::Get(profile_) |
| ->extension_service() |
| ->pending_extension_manager() |
| ->HasPendingExtensions()) { |
| return; |
| } |
| |
| install_observation_.Reset(); |
| |
| const extensions::Extension* primary_app = GetPrimaryAppExtension(); |
| if (!primary_app) { |
| ReportInstallFailure(InstallResult::kPrimaryAppInstallFailed); |
| return; |
| } |
| |
| if (!extensions::KioskModeInfo::IsKioskEnabled(primary_app)) { |
| ReportInstallFailure(InstallResult::kPrimaryAppNotKioskEnabled); |
| return; |
| } |
| |
| if (!secondary_apps_installing_) { |
| MaybeInstallSecondaryApps(); |
| } else { |
| MaybeCheckExtensionUpdate(); |
| } |
| } |
| |
| void ChromeKioskAppInstaller::OnExtensionInstallationFailed( |
| const extensions::ExtensionId& id, |
| extensions::InstallStageTracker::FailureReason reason) { |
| base::UmaHistogramEnumeration(kChromeKioskExtensionUpdateErrorHistogram, |
| reason); |
| } |
| |
| void ChromeKioskAppInstaller::ReportInstallSuccess() { |
| DCHECK(install_complete_); |
| SYSLOG(INFO) << "Kiosk app is ready to launch."; |
| |
| std::move(on_ready_callback_) |
| .Run(ChromeKioskAppInstaller::InstallResult::kSuccess); |
| } |
| |
| void ChromeKioskAppInstaller::ReportInstallFailure( |
| ChromeKioskAppInstaller::InstallResult error) { |
| SYSLOG(ERROR) << "App install failed, error: " << static_cast<int>(error); |
| DCHECK_NE(ChromeKioskAppInstaller::InstallResult::kSuccess, error); |
| |
| std::move(on_ready_callback_).Run(error); |
| } |
| |
| void ChromeKioskAppInstaller::ObserveActiveInstallations() { |
| install_observation_.Observe( |
| extensions::InstallTrackerFactory::GetForBrowserContext(profile_)); |
| } |
| |
| const extensions::Extension* ChromeKioskAppInstaller::GetPrimaryAppExtension() |
| const { |
| return extensions::ExtensionRegistry::Get(profile_)->GetInstalledExtension( |
| primary_app_install_data_.id); |
| } |
| |
| bool ChromeKioskAppInstaller::AreSecondaryAppsInstalled() const { |
| const extensions::Extension* extension = GetPrimaryAppExtension(); |
| DCHECK(extension); |
| extensions::KioskModeInfo* info = extensions::KioskModeInfo::Get(extension); |
| for (const auto& app : info->secondary_apps) { |
| if (!extensions::ExtensionRegistry::Get(profile_)->GetInstalledExtension( |
| app.id)) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| bool ChromeKioskAppInstaller::IsAppInstallPending(const std::string& id) const { |
| return extensions::ExtensionSystem::Get(profile_) |
| ->extension_service() |
| ->pending_extension_manager() |
| ->IsIdPending(id); |
| } |
| |
| bool ChromeKioskAppInstaller::IsAnySecondaryAppPending() const { |
| const extensions::Extension* extension = GetPrimaryAppExtension(); |
| DCHECK(extension); |
| extensions::KioskModeInfo* info = extensions::KioskModeInfo::Get(extension); |
| for (const auto& app : info->secondary_apps) { |
| if (IsAppInstallPending(app.id)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| bool ChromeKioskAppInstaller::PrimaryAppHasPendingUpdate() const { |
| return extensions::ExtensionSystem::Get(profile_) |
| ->extension_service() |
| ->GetPendingExtensionUpdate(primary_app_install_data_.id); |
| } |
| |
| bool ChromeKioskAppInstaller::DidPrimaryOrSecondaryAppFailedToInstall( |
| bool success, |
| const std::string& id) const { |
| if (success) { |
| return false; |
| } |
| |
| if (id == primary_app_install_data_.id) { |
| SYSLOG(ERROR) << "Failed to install crx file of the primary app id=" << id; |
| return true; |
| } |
| |
| const extensions::Extension* extension = GetPrimaryAppExtension(); |
| if (!extension) { |
| return false; |
| } |
| |
| extensions::KioskModeInfo* info = extensions::KioskModeInfo::Get(extension); |
| for (const auto& app : info->secondary_apps) { |
| if (app.id == id) { |
| SYSLOG(ERROR) << "Failed to install a secondary app id=" << id; |
| return true; |
| } |
| } |
| |
| SYSLOG(WARNING) << "Failed to install crx file for an app id=" << id; |
| return false; |
| } |
| |
| } // namespace ash |