text: // Copyright 2013 The Chromium Authors. All rights reserved.
  // 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/startup_app_launcher.h"
  
  #include <utility>
  #include <vector>
  
  #include "base/bind.h"
  #include "base/command_line.h"
  #include "base/syslog_logging.h"
  #include "base/threading/thread_task_runner_handle.h"
  #include "base/time/time.h"
  #include "base/values.h"
  #include "chrome/browser/chromeos/app_mode/kiosk_app_manager.h"
  #include "chrome/browser/chromeos/app_mode/kiosk_diagnosis_runner.h"
  #include "chrome/browser/chromeos/app_mode/startup_app_launcher_update_checker.h"
  #include "chrome/browser/chromeos/net/delay_network_call.h"
  #include "chrome/browser/extensions/extension_service.h"
  #include "chrome/browser/extensions/install_tracker.h"
  #include "chrome/browser/extensions/install_tracker_factory.h"
  #include "chrome/browser/lifetime/application_lifetime.h"
  #include "chrome/browser/profiles/profile.h"
  #include "chrome/browser/ui/extensions/app_launch_params.h"
  #include "chrome/browser/ui/extensions/application_launch.h"
  #include "chrome/common/chrome_switches.h"
  #include "components/crx_file/id_util.h"
  #include "components/session_manager/core/session_manager.h"
  #include "extensions/browser/extension_prefs.h"
  #include "extensions/browser/extension_system.h"
  #include "extensions/common/constants.h"
  #include "extensions/common/extension.h"
  #include "extensions/common/file_util.h"
  #include "extensions/common/manifest_handlers/kiosk_mode_info.h"
  #include "extensions/common/manifest_handlers/offline_enabled_info.h"
  #include "extensions/common/manifest_url_handlers.h"
  #include "net/base/load_flags.h"
  
  using extensions::Extension;
  
  namespace chromeos {
  
  namespace {
  
  const int kMaxLaunchAttempt = 5;
  
  }  // namespace
  
  StartupAppLauncher::StartupAppLauncher(Profile* profile,
                                         const std::string& app_id,
                                         bool diagnostic_mode,
                                         StartupAppLauncher::Delegate* delegate)
      : profile_(profile),
        app_id_(app_id),
        diagnostic_mode_(diagnostic_mode),
        delegate_(delegate),
        kiosk_app_manager_observer_(this),
        install_observer_(this),
        weak_ptr_factory_(this) {
    DCHECK(profile_);
    DCHECK(crx_file::id_util::IdIsValid(app_id_));
    kiosk_app_manager_observer_.Add(KioskAppManager::Get());
  }
  
  StartupAppLauncher::~StartupAppLauncher() = default;
  
  void StartupAppLauncher::Initialize() {
    MaybeInitializeNetwork();
  }
  
  void StartupAppLauncher::ContinueWithNetworkReady() {
    SYSLOG(INFO) << "ContinueWithNetworkReady"
                 << ", network_ready_handled_=" << network_ready_handled_
                 << ", ready_to_launch_=" << ready_to_launch_;
  
    if (ready_to_launch_ || network_ready_handled_)
      return;
  
    network_ready_handled_ = true;
  
    if (delegate_->ShouldSkipAppInstallation()) {
      MaybeLaunchApp();
      return;
    }
  
    // The network might not be ready when KioskAppManager tries to update
    // external cache initially. Update the external cache now that the network
    // is ready for sure.
    wait_for_crx_update_ = true;
    KioskAppManager::Get()->UpdateExternalCache();
  }
  
  void StartupAppLauncher::RestartLauncher() {
    // Do not allow restarts after the launcher finishes kiosk apps installation.
    if (ready_to_launch_)
      return;
  
    // If the installer is still running in the background, we don't need to
    // restart the launch process. We will just wait until it completes and
    // launches the kiosk app.
    if (extensions::ExtensionSystem::Get(profile_)
            ->extension_service()
            ->pending_extension_manager()
            ->IsIdPending(app_id_)) {
      SYSLOG(WARNING) << "Installer still running";
      return;
    }
  
    MaybeInitializeNetwork();
  }
  
  void StartupAppLauncher::MaybeInitializeNetwork() {
    DCHECK(!ready_to_launch_);
  
    network_ready_handled_ = false;
  
    const Extension* extension = GetPrimaryAppExtension();
    bool crx_cached = KioskAppManager::Get()->HasCachedCrx(app_id_);
    const bool requires_network =
        (!extension && !crx_cached) ||
        (extension &&
         !extensions::OfflineEnabledInfo::IsOfflineEnabled(extension));
  
    SYSLOG(INFO) << "MaybeInitializeNetwork"
                 << ", requires_network=" << requires_network
                 << ", network_ready=" << delegate_->IsNetworkReady();
  
    if (requires_network) {
      delegate_->InitializeNetwork();
      return;
    }
  
    if (delegate_->ShouldSkipAppInstallation()) {
      MaybeLaunchApp();
      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::SetSecondaryAppsEnabledState(
      const extensions::Extension* primary_app) {
    extensions::KioskModeInfo* info = extensions::KioskModeInfo::Get(primary_app);
    ExtensionService* service =
        extensions::ExtensionSystem::Get(profile_)->extension_service();
    extensions::ExtensionPrefs* prefs = extensions::ExtensionPrefs::Get(profile_);
    for (const auto& app_info : info->secondary_apps) {
      // If the enabled on launch is not specified in the manifest, the apps
      // enabled state should be kept as is.
      if (!app_info.enabled_on_launch.has_value())
        continue;
  
      // If the app is already enabled, and should not be disabled, there is
      // nothing to do for the app.
      if (app_info.enabled_on_launch.value() &&
          service->IsExtensionEnabled(app_info.id)) {
        continue;
      }
  
      if (!app_info.enabled_on_launch.value()) {
        service->DisableExtension(
            app_info.id, extensions::disable_reason::DISABLE_USER_ACTION);
      } else {
        // Remove USER_ACTION disable reason - if the app was disabled only due to
        // user action, enable it.
        if (prefs->GetDisableReasons(app_info.id) ==
            extensions::disable_reason::DISABLE_USER_ACTION) {
          service->EnableExtension(app_info.id);
        } else {
          prefs->RemoveDisableReason(
              app_info.id, extensions::disable_reason::DISABLE_USER_ACTION);
        }
      }
    }
  }
  
  void StartupAppLauncher::MaybeLaunchApp() {
    DCHECK(!ready_to_launch_);
  
    SYSLOG(INFO) << "MaybeLaunchApp";
    const Extension* extension = GetPrimaryAppExtension();
    // Verify that requred apps are installed. While the apps should be
    // present at this point, crash recovery flow skips app installation steps -
    // this means that the kiosk app might not yet be downloaded. If that is
    // the case, bail out from the app launch.
    if (!extension || !AreSecondaryAppsInstalled()) {
      OnLaunchFailure(KioskAppLaunchError::UNABLE_TO_LAUNCH);
      return;
    }
  
    const bool offline_enabled =
        extensions::OfflineEnabledInfo::IsOfflineEnabled(extension);
    // If the app is not offline enabled, make sure the network is ready before
    // launching.
    if (offline_enabled || delegate_->IsNetworkReady()) {
      ready_to_launch_ = true;
      // Updates to cached primary app crx will be ignored after this point, so
      // there is no need to observe the kiosk app manager any longer.
      kiosk_app_manager_observer_.RemoveAll();
  
      SetSecondaryAppsEnabledState(extension);
  
      base::ThreadTaskRunnerHandle::Get()->PostTask(
          FROM_HERE, base::BindOnce(&StartupAppLauncher::OnReadyToLaunch,
                                    weak_ptr_factory_.GetWeakPtr()));
    } else {
      ++launch_attempt_;
      if (launch_attempt_ < kMaxLaunchAttempt) {
        base::ThreadTaskRunnerHandle::Get()->PostTask(
            FROM_HERE, base::BindOnce(&StartupAppLauncher::MaybeInitializeNetwork,
                                      weak_ptr_factory_.GetWeakPtr()));
        return;
      }
      OnLaunchFailure(KioskAppLaunchError::UNABLE_TO_LAUNCH);
    }
  }
  
  void StartupAppLauncher::MaybeCheckExtensionUpdate() {
    DCHECK(!ready_to_launch_);
  
    SYSLOG(INFO) << "MaybeCheckExtensionUpdate";
    if (!delegate_->IsNetworkReady()) {
      MaybeLaunchApp();
      return;
    }
  
    // 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(&StartupAppLauncher::OnExtensionUpdateCheckFinished,
                           weak_ptr_factory_.GetWeakPtr()))) {
      update_checker_.reset();
      MaybeLaunchApp();
      return;
    }
  
    SYSLOG(INFO) << "Extension update check run.";
  }
  
  void StartupAppLauncher::OnExtensionUpdateCheckFinished(bool update_found) {
    DCHECK(!ready_to_launch_);
  
    SYSLOG(INFO) << "OnExtensionUpdateCheckFinished";
    update_checker_.reset();
    if (update_found) {
      // Reload the primary app to make sure any reference to the previous version
      // of the shared module, extension, etc will be cleaned up andthe new
      // version will be loaded.
      extensions::ExtensionSystem::Get(profile_)
              ->extension_service()
              ->ReloadExtension(app_id_);
    }
  
    MaybeLaunchApp();
  }
  
  void StartupAppLauncher::OnFinishCrxInstall(const std::string& extension_id,
                                              bool success) {
    DCHECK(!ready_to_launch_);
  
    SYSLOG(INFO) << "OnFinishCrxInstall, id=" << extension_id
                 << ", success=" << success;
  
    if (DidPrimaryOrSecondaryAppFailedToInstall(success, extension_id)) {
      install_observer_.RemoveAll();
      OnLaunchFailure(KioskAppLaunchError::UNABLE_TO_INSTALL);
      return;
    }
  
    // Wait for pending updates or dependent extensions to download.
    if (extensions::ExtensionSystem::Get(profile_)
            ->extension_service()
            ->pending_extension_manager()
            ->HasPendingExtensions()) {
      return;
    }
  
    install_observer_.RemoveAll();
    if (delegate_->IsShowingNetworkConfigScreen()) {
      SYSLOG(WARNING) << "Showing network config screen";
      return;
    }
  
    const extensions::Extension* primary_app = GetPrimaryAppExtension();
    if (!primary_app) {
      OnLaunchFailure(KioskAppLaunchError::UNABLE_TO_INSTALL);
      return;
    }
  
    if (!extensions::KioskModeInfo::IsKioskEnabled(primary_app)) {
      OnLaunchFailure(KioskAppLaunchError::NOT_KIOSK_ENABLED);
      return;
    }
  
    if (!secondary_apps_installed_)
      MaybeInstallSecondaryApps();
    else
      MaybeCheckExtensionUpdate();
  }
  
  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) {
    DCHECK(!ready_to_launch_);
  
    if (app_id != app_id_ || !wait_for_crx_update_)
      return;
  
    wait_for_crx_update_ = false;
    if (KioskAppManager::Get()->HasCachedCrx(app_id_))
      BeginInstall();
    else
      OnLaunchFailure(KioskAppLaunchError::UNABLE_TO_DOWNLOAD);
  }
  
  bool StartupAppLauncher::IsAnySecondaryAppPending() const {
    const extensions::Extension* extension = GetPrimaryAppExtension();
    DCHECK(extension);
    extensions::KioskModeInfo* info = extensions::KioskModeInfo::Get(extension);
    for (const auto& app : info->secondary_apps) {
      if (extensions::ExtensionSystem::Get(profile_)
              ->extension_service()
              ->pending_extension_manager()
              ->IsIdPending(app.id)) {
        return true;
      }
    }
    return false;
  }
  
  bool StartupAppLauncher::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::ExtensionSystem::Get(profile_)
               ->extension_service()
               ->GetInstalledExtension(app.id)) {
        return false;
      }
    }
    return true;
  }
  
  bool StartupAppLauncher::HasSecondaryApps() const {
    const extensions::Extension* extension = GetPrimaryAppExtension();
    DCHECK(extension);
    return extensions::KioskModeInfo::HasSecondaryApps(extension);
  }
  
  bool StartupAppLauncher::PrimaryAppHasPendingUpdate() const {
    return !!extensions::ExtensionSystem::Get(profile_)
                 ->extension_service()
                 ->GetPendingExtensionUpdate(app_id_);
  }
  
  bool StartupAppLauncher::DidPrimaryOrSecondaryAppFailedToInstall(
      bool success,
      const std::string& id) const {
    if (success)
      return false;
  
    if (id == app_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;
  }
  
  const extensions::Extension* StartupAppLauncher::GetPrimaryAppExtension()
      const {
    return extensions::ExtensionSystem::Get(profile_)
        ->extension_service()
        ->GetInstalledExtension(app_id_);
  }
  
  void StartupAppLauncher::LaunchApp() {
    if (!ready_to_launch_) {
      NOTREACHED();
      SYSLOG(ERROR) << "LaunchApp() called but launcher is not initialized.";
    }
  
    const Extension* extension = GetPrimaryAppExtension();
    CHECK(extension);
  
    if (!extensions::KioskModeInfo::IsKioskEnabled(extension)) {
      OnLaunchFailure(KioskAppLaunchError::NOT_KIOSK_ENABLED);
      return;
    }
  
    SYSLOG(INFO) << "Attempt to launch app.";
  
    // Always open the app in a window.
    OpenApplication(AppLaunchParams(
        profile_, extension, extensions::LAUNCH_CONTAINER_WINDOW,
        WindowOpenDisposition::NEW_WINDOW, extensions::SOURCE_KIOSK));
  
    KioskAppManager::Get()->InitSession(profile_, app_id_);
    session_manager::SessionManager::Get()->SessionStarted();
  
    if (diagnostic_mode_)
      KioskDiagnosisRunner::Run(profile_, app_id_);
  
    OnLaunchSuccess();
  }
  
  void StartupAppLauncher::OnLaunchSuccess() {
    delegate_->OnLaunchSucceeded();
  }
  
  void StartupAppLauncher::OnLaunchFailure(KioskAppLaunchError::Error error) {
    SYSLOG(ERROR) << "App launch failed, error: " << error;
    DCHECK_NE(KioskAppLaunchError::NONE, error);
  
    delegate_->OnLaunchFailed(error);
  }
  
  void StartupAppLauncher::BeginInstall() {
    DCHECK(!ready_to_launch_);
  
    SYSLOG(INFO) << "BeginInstall";
    extensions::file_util::SetUseSafeInstallation(true);
    KioskAppManager::Get()->UpdatePrimaryAppLoaderPrefs(app_id_);
    if (extensions::ExtensionSystem::Get(profile_)
            ->extension_service()
            ->pending_extension_manager()
            ->IsIdPending(app_id_)) {
      delegate_->OnInstallingApp();
      // Observe the crx installation events.
      install_observer_.Add(
          extensions::InstallTrackerFactory::GetForBrowserContext(profile_));
      return;
    }
  
    const extensions::Extension* primary_app = GetPrimaryAppExtension();
    if (!primary_app) {
      // The extension is skipped for installation due to some error.
      OnLaunchFailure(KioskAppLaunchError::UNABLE_TO_INSTALL);
      return;
    }
  
    if (!extensions::KioskModeInfo::IsKioskEnabled(primary_app)) {
      // The installed primary app is not kiosk enabled.
      OnLaunchFailure(KioskAppLaunchError::NOT_KIOSK_ENABLED);
      return;
    }
  
    // Install secondary apps.
    MaybeInstallSecondaryApps();
  }
  
  void StartupAppLauncher::MaybeInstallSecondaryApps() {
    if (ready_to_launch_)
      return;
  
    if (!AreSecondaryAppsInstalled() && !delegate_->IsNetworkReady()) {
      DelayNetworkCall(
          base::TimeDelta::FromMilliseconds(kDefaultNetworkRetryDelayMS),
          base::Bind(&StartupAppLauncher::MaybeInstallSecondaryApps,
                     weak_ptr_factory_.GetWeakPtr()));
      return;
    }
  
    secondary_apps_installed_ = 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);
  
    KioskAppManager::Get()->UpdateSecondaryAppsLoaderPrefs(secondary_app_ids);
    if (IsAnySecondaryAppPending()) {
      delegate_->OnInstallingApp();
      // Observe the crx installation events.
      install_observer_.Add(
          extensions::InstallTrackerFactory::GetForBrowserContext(profile_));
      return;
    }
  
    if (AreSecondaryAppsInstalled()) {
      // Check extension update before launching the primary kiosk app.
      MaybeCheckExtensionUpdate();
    } else {
      OnLaunchFailure(KioskAppLaunchError::UNABLE_TO_INSTALL);
    }
  }
  
  void StartupAppLauncher::OnReadyToLaunch() {
    DCHECK(ready_to_launch_);
    SYSLOG(INFO) << "Kiosk app is ready to launch.";
    MaybeUpdateAppData();
    delegate_->OnReadyToLaunch();
  }
  
  void StartupAppLauncher::MaybeUpdateAppData() {
    // Skip copying meta data from the current installed primary app when
    // there is a pending update.
    if (PrimaryAppHasPendingUpdate())
      return;
  
    KioskAppManager::Get()->ClearAppData(app_id_);
    KioskAppManager::Get()->UpdateAppDataFromProfile(app_id_, profile_, NULL);
  }
  
  }   // namespace chromeos
  