chromium/chrome/browser/chromeos/app_mode/chrome_kiosk_app_installer.cc

// 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 chromeos {

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() = default;

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(
    content::BrowserContext* context,
    const extensions::CrxInstaller& installer,
    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 chromeos