chromium/chrome/browser/ash/plugin_vm/plugin_vm_installer.cc

// Copyright 2019 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/plugin_vm/plugin_vm_installer.h"

#include <memory>
#include <string>

#include "base/files/file.h"
#include "base/files/file_util.h"
#include "base/files/scoped_file.h"
#include "base/functional/bind.h"
#include "base/strings/string_util.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "base/uuid.h"
#include "chrome/browser/ash/guest_os/guest_os_dlc_helper.h"
#include "chrome/browser/ash/plugin_vm/plugin_vm_drive_image_download_service.h"
#include "chrome/browser/ash/plugin_vm/plugin_vm_features.h"
#include "chrome/browser/ash/plugin_vm/plugin_vm_license_checker.h"
#include "chrome/browser/ash/plugin_vm/plugin_vm_manager.h"
#include "chrome/browser/ash/plugin_vm/plugin_vm_manager_factory.h"
#include "chrome/browser/ash/plugin_vm/plugin_vm_metrics_util.h"
#include "chrome/browser/ash/plugin_vm/plugin_vm_pref_names.h"
#include "chrome/browser/ash/plugin_vm/plugin_vm_util.h"
#include "chrome/browser/ash/profiles/profile_helper.h"
#include "chrome/browser/download/background_download_service_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_key.h"
#include "chromeos/ash/components/dbus/debug_daemon/debug_daemon_client.h"
#include "chromeos/ash/components/dbus/spaced/spaced_client.h"
#include "components/download/public/background_service/background_download_service.h"
#include "components/download/public/background_service/download_metadata.h"
#include "components/prefs/pref_service.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/device_service.h"
#include "content/public/browser/network_service_instance.h"
#include "net/traffic_annotation/network_traffic_annotation.h"
#include "services/device/public/mojom/wake_lock_provider.mojom.h"

// This file contains VLOG logging to aid debugging tast tests.
#define LOG_FUNCTION_CALL() \
  VLOG(2) << "PluginVmInstaller::" << __func__ << " called"

namespace plugin_vm {

namespace {

constexpr int64_t kBytesPerGigabyte = 1024 * 1024 * 1024;
// Size to use for calculating progress when the actual size isn't available.
constexpr int64_t kDownloadSizeFallbackEstimate = 15LL * kBytesPerGigabyte;

constexpr char kFailureReasonHistogram[] = "PluginVm.SetupFailureReason";

constexpr char kHomeDirectory[] = "/home/chronos/user";

ash::ConciergeClient* GetConciergeClient() {
  return ash::ConciergeClient::Get();
}

constexpr char kIsoSignature[] = "CD001";
constexpr int64_t kIsoOffsets[] = {0x8001, 0x8801, 0x9001};

bool IsIsoImage(const base::FilePath& image) {
  base::File file(image, base::File::FLAG_OPEN | base::File::FLAG_READ);
  if (!file.IsValid()) {
    LOG(ERROR) << "Failed to open " << image.value();
    return false;
  }

  std::vector<uint8_t> data(strlen(kIsoSignature));
  for (auto offset : kIsoOffsets) {
    if (file.ReadAndCheck(offset, data) &&
        std::string(data.begin(), data.end()) == kIsoSignature) {
      return true;
    }
  }
  return false;
}

std::optional<base::ScopedFD> PrepareFD(const base::FilePath& image) {
  base::File file(image, base::File::FLAG_OPEN | base::File::FLAG_READ);
  if (!file.IsValid()) {
    LOG(ERROR) << "Failed to open " << image.value();
    return std::nullopt;
  }

  return base::ScopedFD(file.TakePlatformFile());
}

PluginVmSetupResult BucketForCancelledInstall(
    PluginVmInstaller::InstallingState installing_state) {
  switch (installing_state) {
    case PluginVmInstaller::InstallingState::kInactive:
      NOTREACHED_IN_MIGRATION();
      [[fallthrough]];
    case PluginVmInstaller::InstallingState::kCheckingLicense:
      return PluginVmSetupResult::kUserCancelledValidatingLicense;
    case PluginVmInstaller::InstallingState::kCheckingDiskSpace:
      return PluginVmSetupResult::kUserCancelledCheckingDiskSpace;
    case PluginVmInstaller::InstallingState::kDownloadingDlc:
      return PluginVmSetupResult::kUserCancelledDownloadingPluginVmDlc;
    case PluginVmInstaller::InstallingState::kStartingDispatcher:
      return PluginVmSetupResult::kUserCancelledStartingDispatcher;
    case PluginVmInstaller::InstallingState::kCheckingForExistingVm:
      return PluginVmSetupResult::kUserCancelledCheckingForExistingVm;
    case PluginVmInstaller::InstallingState::kDownloadingImage:
      return PluginVmSetupResult::kUserCancelledDownloadingPluginVmImage;
    case PluginVmInstaller::InstallingState::kImporting:
      return PluginVmSetupResult::kUserCancelledImportingPluginVmImage;
  }
}

}  // namespace

PluginVmInstaller::PluginVmInstaller(Profile* profile)
    : profile_(profile),
      download_service_(BackgroundDownloadServiceFactory::GetForKey(
          profile->GetProfileKey())) {}

std::optional<PluginVmInstaller::FailureReason> PluginVmInstaller::Start() {
  LOG_FUNCTION_CALL();
  if (IsProcessing()) {
    LOG(ERROR) << "Download of a PluginVm image couldn't be started as"
               << " another PluginVm image is currently being processed "
               << "in state " << GetStateName(state_) << ", "
               << GetInstallingStateName(installing_state_);
    return FailureReason::OPERATION_IN_PROGRESS;
  }

  // Defensive check preventing any download attempts when PluginVm is
  // not allowed to run (this might happen in rare cases if PluginVm has
  // been disabled but the installer icon is still visible).
  if (!PluginVmFeatures::Get()->IsAllowed(profile_)) {
    LOG(ERROR) << "Download of PluginVm image cannot be started because "
               << "the user is not allowed to run PluginVm";
    return FailureReason::NOT_ALLOWED;
  }

  if (content::GetNetworkConnectionTracker()->IsOffline()) {
    return FailureReason::OFFLINE;
  }

  // Reset camera/mic permissions, we don't want it to persist across
  // re-installation.
  profile_->GetPrefs()->SetBoolean(prefs::kPluginVmCameraAllowed, false);
  profile_->GetPrefs()->SetBoolean(prefs::kPluginVmMicAllowed, false);

  // Request wake lock when state_ goes to kInstalling, and cancel it when state
  // goes back to kIdle.
  GetWakeLock()->RequestWakeLock();
  state_ = State::kInstalling;
  progress_ = 0;

  // Perform the first step asynchronously to ensure OnError() isn't called
  // before Start() returns.
  content::GetUIThreadTaskRunner({})->PostTask(
      FROM_HERE, base::BindOnce(&PluginVmInstaller::CheckLicense,
                                weak_ptr_factory_.GetWeakPtr()));

  return std::nullopt;
}

void PluginVmInstaller::Cancel() {
  LOG_FUNCTION_CALL();
  if (state_ != State::kInstalling) {
    RecordPluginVmSetupResultHistogram(
        PluginVmSetupResult::kUserCancelledWithoutStarting);
    return;
  }

  RecordPluginVmSetupResultHistogram(
      BucketForCancelledInstall(installing_state_));

  state_ = State::kCancelling;
  switch (installing_state_) {
    case InstallingState::kCheckingLicense:
    case InstallingState::kCheckingDiskSpace:
    case InstallingState::kCheckingForExistingVm:
    case InstallingState::kStartingDispatcher:
      // These can't be cancelled, so we wait for completion.
      return;
    case InstallingState::kDownloadingDlc:
      //  For DLC, we also block progress callbacks.
      dlc_installation_.reset();
      return;
    case InstallingState::kDownloadingImage:
      CancelDownload();
      return;
    case InstallingState::kImporting:
      CancelImport();
      return;
    default:
      NOTREACHED_IN_MIGRATION();
  }
}

bool PluginVmInstaller::IsProcessing() {
  return state_ != State::kIdle;
}

void PluginVmInstaller::SetObserver(Observer* observer) {
  observer_ = observer;
}

void PluginVmInstaller::RemoveObserver() {
  observer_ = nullptr;
}

std::string PluginVmInstaller::GetCurrentDownloadGuid() {
  return current_download_guid_;
}

void PluginVmInstaller::OnDownloadStarted() {}

void PluginVmInstaller::OnDownloadProgressUpdated(uint64_t bytes_downloaded,
                                                  int64_t content_length) {
  DCHECK_EQ(installing_state_, InstallingState::kDownloadingImage);

  if (expected_image_size_ == kImageSizeUnknown) {
    if (content_length > 0) {
      expected_image_size_ = content_length;
    }
  } else if (expected_image_size_ > 0) {
    if (content_length != expected_image_size_) {
      expected_image_size_ = kImageSizeError;
    }
  }

  if (observer_) {
    observer_->OnDownloadProgressUpdated(bytes_downloaded, content_length);
  }

  if (content_length <= 0) {
    content_length = kDownloadSizeFallbackEstimate;
  }

  UpdateProgress(
      std::min(1., static_cast<double>(bytes_downloaded) / content_length));
}

void PluginVmInstaller::OnDownloadCompleted(
    const download::CompletionInfo& info) {
  downloaded_image_ = info.path;
  downloaded_image_size_ = info.bytes_downloaded;
  current_download_guid_.clear();

  if (downloaded_image_for_testing_) {
    downloaded_image_ = downloaded_image_for_testing_.value();
  }

  if (!VerifyDownload(info.hash256)) {
    LOG(ERROR) << "Expected image size: " << expected_image_size_
               << ", downloaded image size: " << downloaded_image_size_;
    if (expected_image_size_ == kImageSizeUnknown ||
        expected_image_size_ == downloaded_image_size_) {
      OnDownloadFailed(FailureReason::HASH_MISMATCH);
    } else {
      OnDownloadFailed(FailureReason::DOWNLOAD_SIZE_MISMATCH);
    }
    return;
  }

  StartImport();
}

void PluginVmInstaller::OnDownloadFailed(FailureReason reason) {
  RemoveTemporaryImageIfExists();
  current_download_guid_.clear();

  if (using_drive_download_service_) {
    drive_download_service_->ResetState();
    using_drive_download_service_ = false;
  }

  InstallFailed(reason);
}

void PluginVmInstaller::OnDiskImageProgress(
    const vm_tools::concierge::DiskImageStatusResponse& signal) {
  if (signal.command_uuid() != current_import_command_uuid_) {
    return;
  }

  const uint64_t percent_completed = signal.progress();
  const vm_tools::concierge::DiskImageStatus status = signal.status();

  switch (status) {
    case vm_tools::concierge::DiskImageStatus::DISK_STATUS_CREATED:
      VLOG(1) << "Disk image status indicates that importing is done.";
      RequestFinalStatus();
      return;
    case vm_tools::concierge::DiskImageStatus::DISK_STATUS_IN_PROGRESS:
      UpdateProgress(percent_completed / 100.);
      return;
    case vm_tools::concierge::DiskImageStatus::DISK_STATUS_NOT_ENOUGH_SPACE:
      LOG(ERROR) << "Disk image import signals out of space condition with "
                    "current progress: "
                 << percent_completed;
      OnImported(FailureReason::OUT_OF_DISK_SPACE);
      return;
    default:
      LOG(ERROR) << "Disk image status signal has status: " << status
                 << " with error message: " << signal.failure_reason()
                 << " and current progress: " << percent_completed;
      OnImported(FailureReason::UNEXPECTED_DISK_IMAGE_STATUS);
      return;
  }
}

bool PluginVmInstaller::VerifyDownload(
    const std::string& downloaded_archive_hash) {
  if (downloaded_archive_hash.empty()) {
    LOG(ERROR) << "No hash found for downloaded PluginVm image archive";
    return false;
  }
  const base::Value* plugin_vm_image_hash_ptr =
      profile_->GetPrefs()
          ->GetDict(prefs::kPluginVmImage)
          .Find(prefs::kPluginVmImageHashKeyName);
  if (!plugin_vm_image_hash_ptr) {
    LOG(ERROR) << "Hash of PluginVm image is not specified";
    return false;
  }
  std::string plugin_vm_image_hash = plugin_vm_image_hash_ptr->GetString();

  if (!base::EqualsCaseInsensitiveASCII(plugin_vm_image_hash,
                                        downloaded_archive_hash)) {
    LOG(ERROR) << "Downloaded PluginVm image archive hash ("
               << downloaded_archive_hash << ") doesn't match "
               << "hash specified by the PluginVmImage policy ("
               << plugin_vm_image_hash << ")";
    return false;
  }

  return true;
}

int64_t PluginVmInstaller::RequiredFreeDiskSpace() {
  return static_cast<int64_t>(profile_->GetPrefs()->GetInteger(
             prefs::kPluginVmRequiredFreeDiskSpaceGB)) *
         kBytesPerGigabyte;
}

void PluginVmInstaller::SetDownloadServiceForTesting(
    download::BackgroundDownloadService* download_service) {
  download_service_ = download_service;
}

void PluginVmInstaller::SetDownloadedImageForTesting(
    const base::FilePath& downloaded_image) {
  downloaded_image_for_testing_ = downloaded_image;
}

void PluginVmInstaller::SetDriveDownloadServiceForTesting(
    std::unique_ptr<PluginVmDriveImageDownloadService> drive_download_service) {
  drive_download_service_ = std::move(drive_download_service);
}

PluginVmInstaller::~PluginVmInstaller() = default;

void PluginVmInstaller::CheckLicense() {
  UpdateInstallingState(InstallingState::kCheckingLicense);

  if (skip_license_check_for_testing_) {
    OnLicenseChecked(true);
    return;
  }
  license_checker_ = std::make_unique<PluginVmLicenseChecker>(profile_);
  license_checker_->CheckLicense(base::BindOnce(
      &PluginVmInstaller::OnLicenseChecked, weak_ptr_factory_.GetWeakPtr()));
}

void PluginVmInstaller::OnLicenseChecked(bool license_is_valid) {
  if (state_ == State::kCancelling) {
    CancelFinished();
    return;
  }

  if (!license_is_valid) {
    LOG(ERROR) << "Install of a PluginVm image couldn't be started as"
               << " there is not a valid license associated with the user.";
    InstallFailed(FailureReason::INVALID_LICENSE);
    return;
  }

  CheckForExistingVm();
}

void PluginVmInstaller::CheckForExistingVm() {
  DCHECK_EQ(installing_state_, InstallingState::kCheckingLicense);
  UpdateInstallingState(InstallingState::kCheckingForExistingVm);

  GetConciergeClient()->WaitForServiceToBeAvailable(
      base::BindOnce(&PluginVmInstaller::OnConciergeAvailable,
                     weak_ptr_factory_.GetWeakPtr()));
}

void PluginVmInstaller::OnConciergeAvailable(bool success) {
  if (!success) {
    LOG(ERROR) << "Concierge did not become available";
    OnImported(FailureReason::CONCIERGE_NOT_AVAILABLE);
    return;
  }

  vm_tools::concierge::ListVmDisksRequest request;
  request.set_cryptohome_id(
      ash::ProfileHelper::GetUserIdHashFromProfile(profile_));
  request.set_all_locations(true);
  request.set_vm_name(kPluginVmName);

  GetConciergeClient()->ListVmDisks(
      std::move(request), base::BindOnce(&PluginVmInstaller::OnListVmDisks,
                                         weak_ptr_factory_.GetWeakPtr()));
}

void PluginVmInstaller::OnListVmDisks(
    std::optional<vm_tools::concierge::ListVmDisksResponse> response) {
  if (state_ == State::kCancelling) {
    CancelFinished();
    return;
  }

  if (!response || !response->success()) {
    LOG(ERROR) << "Failed to list VM disks: "
               << (response ? response->failure_reason() : "[Empty response]");
    InstallFailed(FailureReason::LIST_VM_DISKS_FAILED);
    return;
  }

  if (response->images_size() > 0) {
    auto& image = response->images(0);
    if (image.storage_location() ==
        vm_tools::concierge::STORAGE_CRYPTOHOME_PLUGINVM) {
      RecordPluginVmSetupResultHistogram(PluginVmSetupResult::kVmAlreadyExists);
      if (observer_) {
        observer_->OnVmExists();
      }
      profile_->GetPrefs()->SetBoolean(prefs::kPluginVmImageExists, true);
      InstallFinished();
    } else {
      LOG(ERROR) << "VM " << image.name() << " exists, but in wrong location";
      InstallFailed(FailureReason::EXISTING_IMAGE_INVALID);
    }
    return;
  }

  CheckDiskSpace();
}

void PluginVmInstaller::CheckDiskSpace() {
  DCHECK_EQ(installing_state_, InstallingState::kCheckingForExistingVm);
  UpdateInstallingState(InstallingState::kCheckingDiskSpace);

  ash::SpacedClient::Get()->GetFreeDiskSpace(
      kHomeDirectory, base::BindOnce(&PluginVmInstaller::OnAvailableDiskSpace,
                                     weak_ptr_factory_.GetWeakPtr()));
}

void PluginVmInstaller::OnAvailableDiskSpace(std::optional<int64_t> bytes) {
  if (state_ == State::kCancelling) {
    CancelFinished();
    return;
  }

  if (free_disk_space_for_testing_ != -1) {
    bytes = std::optional<int64_t>(free_disk_space_for_testing_);
  }

  if (!bytes.has_value() || bytes.value() < RequiredFreeDiskSpace()) {
    InstallFailed(FailureReason::INSUFFICIENT_DISK_SPACE);
    return;
  }

  StartDlcDownload();
}

void PluginVmInstaller::StartDlcDownload() {
  LOG_FUNCTION_CALL();
  DCHECK_EQ(installing_state_, InstallingState::kCheckingDiskSpace);
  UpdateInstallingState(InstallingState::kDownloadingDlc);

  if (!GetPluginVmImageDownloadUrl().is_valid()) {
    InstallFailed(FailureReason::INVALID_IMAGE_URL);
    return;
  }

  dlc_installation_ = std::make_unique<guest_os::GuestOsDlcInstallation>(
      kPitaDlc,
      base::BindOnce(&PluginVmInstaller::OnDlcDownloadCompleted,
                     weak_ptr_factory_.GetWeakPtr()),
      base::BindRepeating(&PluginVmInstaller::OnDlcDownloadProgressUpdated,
                          weak_ptr_factory_.GetWeakPtr()));
}

void PluginVmInstaller::OnDlcDownloadProgressUpdated(double progress) {
  DCHECK_EQ(installing_state_, InstallingState::kDownloadingDlc);
  if (state_ == State::kCancelling) {
    return;
  }

  UpdateProgress(progress);
}

void PluginVmInstaller::OnDlcDownloadCompleted(
    guest_os::GuestOsDlcInstallation::Result install_result) {
  DCHECK_EQ(installing_state_, InstallingState::kDownloadingDlc);
  dlc_installation_.reset();

  // If success, continue to the next state.
  if (install_result.has_value()) {
    RecordPluginVmDlcUseResultHistogram(PluginVmDlcUseResult::kDlcSuccess);
    StartDispatcher();
    return;
  }

  // At this point, PluginVM DLC download failed.
  PluginVmDlcUseResult result = PluginVmDlcUseResult::kInternalDlcError;
  FailureReason reason = FailureReason::DLC_INTERNAL;

  switch (install_result.error()) {
    case guest_os::GuestOsDlcInstallation::Error::Cancelled:
      DCHECK(state_ == State::kCancelling);
      CancelFinished();
      return;
    case guest_os::GuestOsDlcInstallation::Error::Invalid:
      LOG(ERROR)
          << "PluginVM DLC is not supported, need to enable PluginVM DLC.";
      result = PluginVmDlcUseResult::kInvalidDlcError;
      reason = FailureReason::DLC_UNSUPPORTED;
      break;
    case guest_os::GuestOsDlcInstallation::Error::Busy:
      LOG(ERROR)
          << "PluginVM DLC is not able to be downloaded as dlcservice is busy.";
      result = PluginVmDlcUseResult::kBusyDlcError;
      reason = FailureReason::DLC_BUSY;
      break;
    case guest_os::GuestOsDlcInstallation::Error::NeedReboot:
      LOG(ERROR) << "Device has pending update and needs a reboot to use "
                    "PluginVM DLC.";
      result = PluginVmDlcUseResult::kNeedRebootDlcError;
      reason = FailureReason::DLC_NEED_REBOOT;
      break;
    case guest_os::GuestOsDlcInstallation::Error::DiskFull:
      LOG(ERROR) << "Device needs to free space to use PluginVM DLC.";
      result = PluginVmDlcUseResult::kNeedSpaceDlcError;
      reason = FailureReason::DLC_NEED_SPACE;
      break;
    case guest_os::GuestOsDlcInstallation::Error::NeedUpdate:
      LOG(ERROR) << "The PluginVM DLC could not be found in the server."
                 << "The version the OS is on is probably not live.";
      result = PluginVmDlcUseResult::kNoImageFoundDlcError;
      // Keep using the reason `FailureReason::DLC_INTERNAL`, but distinguish so
      // developers can see why it wasn't updated as well as for metrics
      // reporting.
      break;
    case guest_os::GuestOsDlcInstallation::Error::Offline:
    case guest_os::GuestOsDlcInstallation::Error::Internal:
    case guest_os::GuestOsDlcInstallation::Error::UnknownFailure:
      LOG(ERROR) << "Failed to download PluginVM DLC: "
                 << install_result.error();
      break;
  }

  RecordPluginVmDlcUseResultHistogram(result);
  InstallFailed(reason);
}

void PluginVmInstaller::StartDispatcher() {
  LOG_FUNCTION_CALL();
  DCHECK_EQ(installing_state_, InstallingState::kDownloadingDlc);
  UpdateInstallingState(InstallingState::kStartingDispatcher);

  PluginVmManagerFactory::GetForProfile(profile_)->StartDispatcher(
      base::BindOnce(&PluginVmInstaller::OnDispatcherStarted,
                     weak_ptr_factory_.GetWeakPtr()));
}

void PluginVmInstaller::OnDispatcherStarted(bool success) {
  if (state_ == State::kCancelling) {
    CancelFinished();
    return;
  }

  if (!success) {
    InstallFailed(FailureReason::DISPATCHER_NOT_AVAILABLE);
    return;
  }

  StartDownload();
}

void PluginVmInstaller::StartDownload() {
  DCHECK_EQ(installing_state_, InstallingState::kStartingDispatcher);
  UpdateInstallingState(InstallingState::kDownloadingImage);
  UpdateProgress(/*state_progress=*/0);

  GURL url = GetPluginVmImageDownloadUrl();
  // This may have changed since running StartDlcDownload.
  if (!url.is_valid()) {
    InstallFailed(FailureReason::INVALID_IMAGE_URL);
    return;
  }

  expected_image_size_ = kImageSizeUnknown;
  downloaded_image_size_ = kImageSizeUnknown;
  std::optional<std::string> drive_id = GetIdFromDriveUrl(url);
  using_drive_download_service_ = drive_id.has_value();

  if (using_drive_download_service_) {
    if (!drive_download_service_) {
      drive_download_service_ =
          std::make_unique<PluginVmDriveImageDownloadService>(this, profile_);
    } else {
      drive_download_service_->ResetState();
    }

    drive_download_service_->StartDownload(drive_id.value());
  } else {
    download_service_->StartDownload(GetDownloadParams(url));
  }
}

void PluginVmInstaller::OnStartDownload(
    const std::string& download_guid,
    download::DownloadParams::StartResult start_result) {
  if (start_result == download::DownloadParams::ACCEPTED) {
    current_download_guid_ = download_guid;
  } else {
    OnDownloadFailed(FailureReason::DOWNLOAD_FAILED_UNKNOWN);
  }
}

void PluginVmInstaller::StartImport() {
  LOG_FUNCTION_CALL();
  DCHECK_EQ(installing_state_, InstallingState::kDownloadingImage);
  UpdateInstallingState(InstallingState::kImporting);
  UpdateProgress(/*state_progress=*/0);

  base::ThreadPool::PostTaskAndReplyWithResult(
      FROM_HERE, {base::TaskPriority::USER_VISIBLE, base::MayBlock()},
      base::BindOnce(&IsIsoImage, downloaded_image_),
      base::BindOnce(&PluginVmInstaller::OnImageTypeDetected,
                     weak_ptr_factory_.GetWeakPtr()));
}

void PluginVmInstaller::OnImageTypeDetected(bool is_iso_image) {
  creating_new_vm_ = is_iso_image;

  if (!GetConciergeClient()->IsDiskImageProgressSignalConnected()) {
    LOG(ERROR) << "Disk image progress signal is not connected";
    OnImported(FailureReason::SIGNAL_NOT_CONNECTED);
    return;
  }

  GetConciergeClient()->AddDiskImageObserver(this);

  base::ThreadPool::PostTaskAndReplyWithResult(
      FROM_HERE, {base::TaskPriority::USER_VISIBLE, base::MayBlock()},
      base::BindOnce(&PrepareFD, downloaded_image_),
      base::BindOnce(&PluginVmInstaller::OnFDPrepared,
                     weak_ptr_factory_.GetWeakPtr()));
}

void PluginVmInstaller::OnFDPrepared(std::optional<base::ScopedFD> maybeFd) {
  // In case import has been cancelled meantime.
  if (state_ != State::kInstalling) {
    return;
  }

  if (!maybeFd.has_value()) {
    LOG(ERROR) << "Could not open downloaded image";
    OnImported(FailureReason::COULD_NOT_OPEN_IMAGE);
    return;
  }

  base::ScopedFD fd(std::move(maybeFd.value()));

  if (creating_new_vm_) {
    vm_tools::concierge::CreateDiskImageRequest request;
    request.set_cryptohome_id(
        ash::ProfileHelper::GetUserIdHashFromProfile(profile_));
    request.set_vm_name(kPluginVmName);
    request.set_storage_location(
        vm_tools::concierge::STORAGE_CRYPTOHOME_PLUGINVM);
    request.set_source_size(downloaded_image_size_);

    VLOG(1) << "Making call to concierge to set up VM from an ISO";

    GetConciergeClient()->CreateDiskImageWithFd(
        std::move(fd), request,
        base::BindOnce(&PluginVmInstaller::OnImportDiskImage<
                           vm_tools::concierge::CreateDiskImageResponse>,
                       weak_ptr_factory_.GetWeakPtr()));
  } else {
    vm_tools::concierge::ImportDiskImageRequest request;
    request.set_cryptohome_id(
        ash::ProfileHelper::GetUserIdHashFromProfile(profile_));
    request.set_vm_name(kPluginVmName);
    request.set_storage_location(
        vm_tools::concierge::STORAGE_CRYPTOHOME_PLUGINVM);
    request.set_source_size(downloaded_image_size_);

    VLOG(1) << "Making call to concierge to import disk image";

    GetConciergeClient()->ImportDiskImage(
        std::move(fd), request,
        base::BindOnce(&PluginVmInstaller::OnImportDiskImage<
                           vm_tools::concierge::ImportDiskImageResponse>,
                       weak_ptr_factory_.GetWeakPtr()));
  }
}

template <typename ReplyType>
void PluginVmInstaller::OnImportDiskImage(std::optional<ReplyType> reply) {
  if (!reply.has_value()) {
    LOG(ERROR) << "Could not retrieve response from Create/ImportDiskImage "
               << "call to concierge";
    OnImported(FailureReason::INVALID_IMPORT_RESPONSE);
    return;
  }

  ReplyType response = reply.value();

  switch (response.status()) {
    case vm_tools::concierge::DiskImageStatus::DISK_STATUS_IN_PROGRESS:
      VLOG(1) << "Disk image creation/import is now in progress";
      current_import_command_uuid_ = response.command_uuid();
      // Image in progress. Waiting for progress signals...
      // TODO(crbug.com/41460680): think about adding a timeout here,
      //   i.e. what happens if concierge dies and does not report any signal
      //   back, not even an error signal. Right now, the user would see
      //   the "Configuring Plugin VM" screen forever. Maybe that's OK
      //   at this stage though.
      break;
    case vm_tools::concierge::DiskImageStatus::DISK_STATUS_NOT_ENOUGH_SPACE:
      LOG(ERROR) << "Disk image import operation ran out of disk space";
      OnImported(FailureReason::OUT_OF_DISK_SPACE);
      break;
    default:
      LOG(ERROR) << "Disk image is not in progress. Status: "
                 << response.status() << ", " << response.failure_reason();
      OnImported(FailureReason::UNEXPECTED_DISK_IMAGE_STATUS);
      break;
  }
}

void PluginVmInstaller::RequestFinalStatus() {
  vm_tools::concierge::DiskImageStatusRequest status_request;
  status_request.set_command_uuid(current_import_command_uuid_);
  GetConciergeClient()->DiskImageStatus(
      status_request, base::BindOnce(&PluginVmInstaller::OnFinalDiskImageStatus,
                                     weak_ptr_factory_.GetWeakPtr()));
}

void PluginVmInstaller::OnFinalDiskImageStatus(
    std::optional<vm_tools::concierge::DiskImageStatusResponse> reply) {
  if (!reply.has_value()) {
    LOG(ERROR) << "Could not retrieve response from DiskImageStatus call to "
               << "concierge";
    OnImported(FailureReason::INVALID_DISK_IMAGE_STATUS_RESPONSE);
    return;
  }

  vm_tools::concierge::DiskImageStatusResponse response = reply.value();
  DCHECK(response.command_uuid() == current_import_command_uuid_);
  switch (response.status()) {
    case vm_tools::concierge::DiskImageStatus::DISK_STATUS_CREATED:
      OnImported(std::nullopt);
      break;
    case vm_tools::concierge::DiskImageStatus::DISK_STATUS_NOT_ENOUGH_SPACE:
      LOG(ERROR) << "Disk image import operation ran out of disk space "
                 << "with current progress: " << response.progress();
      OnImported(FailureReason::OUT_OF_DISK_SPACE);
      break;
    default:
      LOG(ERROR) << "Disk image is not created. Status: " << response.status()
                 << ", " << response.failure_reason();
      OnImported(FailureReason::IMAGE_IMPORT_FAILED);
      break;
  }
}

void PluginVmInstaller::OnImported(
    std::optional<FailureReason> failure_reason) {
  LOG_FUNCTION_CALL();
  GetConciergeClient()->RemoveDiskImageObserver(this);
  RemoveTemporaryImageIfExists();
  current_import_command_uuid_.clear();

  if (failure_reason) {
    if (creating_new_vm_) {
      LOG(ERROR) << "New VM creation failed";
    } else {
      LOG(ERROR) << "Image import failed";
    }
    InstallFailed(*failure_reason);
    return;
  }

  profile_->GetPrefs()->SetBoolean(prefs::kPluginVmImageExists, true);
  RecordPluginVmSetupResultHistogram(PluginVmSetupResult::kSuccess);
  if (observer_) {
    if (creating_new_vm_) {
      observer_->OnCreated();
    } else {
      observer_->OnImported();
    }
  }
  InstallFinished();
}

void PluginVmInstaller::UpdateInstallingState(
    InstallingState installing_state) {
  LOG_FUNCTION_CALL() << " with state "
                      << GetInstallingStateName(installing_state);
  DCHECK_NE(installing_state, InstallingState::kInactive);
  installing_state_ = installing_state;
  observer_->OnStateUpdated(installing_state_);
}

void PluginVmInstaller::UpdateProgress(double state_progress) {
  DCHECK_EQ(state_, State::kInstalling);
  if (state_progress < 0 || state_progress > 1) {
    LOG(ERROR) << "Unexpected progress value " << state_progress
               << " in installing state "
               << GetInstallingStateName(installing_state_);
    return;
  }

  double start_range = 0;
  double end_range = 0;
  switch (installing_state_) {
    case InstallingState::kDownloadingDlc:
      start_range = 0;
      end_range = 0.01;
      break;
    case InstallingState::kDownloadingImage:
      start_range = 0.01;
      end_range = 0.45;
      break;
    case InstallingState::kImporting:
      start_range = 0.45;
      end_range = 1;
      break;
    default:
      // Other states take a negligible amount of time so we don't send progress
      // updates.
      NOTREACHED_IN_MIGRATION();
  }

  double new_progress =
      start_range + (end_range - start_range) * state_progress;
  if (new_progress < progress_) {
    LOG(ERROR) << "Progress went backwards from " << progress_ << " to "
               << new_progress;
    return;
  }

  progress_ = new_progress;
  if (observer_) {
    observer_->OnProgressUpdated(new_progress);
  }
}

void PluginVmInstaller::InstallFailed(FailureReason reason) {
  LOG_FUNCTION_CALL() << " with failure reason " << static_cast<int>(reason);
  state_ = State::kIdle;
  GetWakeLock()->CancelWakeLock();
  installing_state_ = InstallingState::kInactive;
  base::UmaHistogramEnumeration(kFailureReasonHistogram, reason);
  RecordPluginVmSetupResultHistogram(PluginVmSetupResult::kError);
  if (observer_) {
    observer_->OnError(reason);
  }
}

void PluginVmInstaller::InstallFinished() {
  LOG_FUNCTION_CALL();
  state_ = State::kIdle;
  GetWakeLock()->CancelWakeLock();
  installing_state_ = InstallingState::kInactive;
}

void PluginVmInstaller::CancelDownload() {
  if (using_drive_download_service_) {
    DCHECK(drive_download_service_);
    drive_download_service_->CancelDownload();
  } else {
    download_service_->CancelDownload(current_download_guid_);
    current_download_guid_.clear();
  }
  CancelFinished();
}

void PluginVmInstaller::CancelImport() {
  VLOG(1) << "Cancelling disk image import with command_uuid: "
          << current_import_command_uuid_;

  vm_tools::concierge::CancelDiskImageRequest request;
  request.set_command_uuid(current_import_command_uuid_);
  GetConciergeClient()->CancelDiskImageOperation(
      request, base::BindOnce(&PluginVmInstaller::OnImportDiskImageCancelled,
                              weak_ptr_factory_.GetWeakPtr()));
}

void PluginVmInstaller::OnImportDiskImageCancelled(
    std::optional<vm_tools::concierge::CancelDiskImageResponse> reply) {
  DCHECK_EQ(state_, State::kCancelling);
  DCHECK_EQ(installing_state_, InstallingState::kImporting);

  RemoveTemporaryImageIfExists();

  if (!reply.has_value()) {
    LOG(ERROR) << "Could not retrieve response from CancelDiskImageOperation "
               << "call to concierge";
    CancelFinished();
    return;
  }

  vm_tools::concierge::CancelDiskImageResponse response = reply.value();
  if (response.success()) {
    VLOG(1) << "Import disk image request has been cancelled successfully";
  } else {
    LOG(ERROR) << "Import disk image request failed to be cancelled, "
               << response.failure_reason();
  }

  CancelFinished();
}

void PluginVmInstaller::CancelFinished() {
  DCHECK_EQ(state_, State::kCancelling);
  state_ = State::kIdle;
  GetWakeLock()->CancelWakeLock();
  installing_state_ = InstallingState::kInactive;

  if (observer_) {
    observer_->OnCancelFinished();
  }
}

std::string PluginVmInstaller::GetStateName(State state) {
  switch (state) {
    case State::kIdle:
      return "kIdle";
    case State::kInstalling:
      return "kInstalling";
    case State::kCancelling:
      return "kCancelling";
  }
}

std::string PluginVmInstaller::GetInstallingStateName(InstallingState state) {
  switch (state) {
    case InstallingState::kInactive:
      return "kInactive";
    case InstallingState::kCheckingDiskSpace:
      return "kCheckingDiskSpace";
    case InstallingState::kCheckingForExistingVm:
      return "kCheckingForExistingVm";
    case InstallingState::kDownloadingDlc:
      return "kDownloadingDlc";
    case InstallingState::kStartingDispatcher:
      return "kStartingDispatcher";
    case InstallingState::kDownloadingImage:
      return "kDownloadingImage";
    case InstallingState::kImporting:
      return "kImporting";
    case InstallingState::kCheckingLicense:
      return "kCheckingLicense";
  }
}

GURL PluginVmInstaller::GetPluginVmImageDownloadUrl() {
  const base::Value* url_ptr = profile_->GetPrefs()
                                   ->GetDict(prefs::kPluginVmImage)
                                   .Find(prefs::kPluginVmImageUrlKeyName);
  if (!url_ptr) {
    LOG(ERROR) << "Url to PluginVm image is not specified";
    return GURL();
  }
  return GURL(url_ptr->GetString());
}

download::DownloadParams PluginVmInstaller::GetDownloadParams(const GURL& url) {
  download::DownloadParams params;

  // DownloadParams
  params.client = download::DownloadClient::PLUGIN_VM_IMAGE;
  params.guid = base::Uuid::GenerateRandomV4().AsLowercaseString();
  params.callback = base::BindRepeating(&PluginVmInstaller::OnStartDownload,
                                        weak_ptr_factory_.GetWeakPtr());

  params.traffic_annotation = net::MutableNetworkTrafficAnnotationTag(
      kPluginVmNetworkTrafficAnnotation);

  // RequestParams
  params.request_params.url = url;
  params.request_params.method = "GET";
  // Disable Safe Browsing/checks because the download is system-initiated,
  // the target is specified via enterprise policy, and contents will be
  // validated by comparing hashes.
  params.request_params.require_safety_checks = false;

  // SchedulingParams
  // User initiates download by clicking on PluginVm icon so priorities should
  // be the highest.
  params.scheduling_params.priority = download::SchedulingParams::Priority::UI;
  params.scheduling_params.battery_requirements =
      download::SchedulingParams::BatteryRequirements::BATTERY_INSENSITIVE;
  params.scheduling_params.network_requirements =
      download::SchedulingParams::NetworkRequirements::NONE;

  return params;
}

void PluginVmInstaller::RemoveTemporaryImageIfExists() {
  if (using_drive_download_service_) {
    drive_download_service_->RemoveTemporaryArchive(
        base::BindOnce(&PluginVmInstaller::OnTemporaryImageRemoved,
                       weak_ptr_factory_.GetWeakPtr()));
  } else if (!downloaded_image_.empty()) {
    base::ThreadPool::PostTaskAndReplyWithResult(
        FROM_HERE, {base::TaskPriority::USER_VISIBLE, base::MayBlock()},
        base::BindOnce(&base::DeleteFile, downloaded_image_),
        base::BindOnce(&PluginVmInstaller::OnTemporaryImageRemoved,
                       weak_ptr_factory_.GetWeakPtr()));
  }
}

void PluginVmInstaller::OnTemporaryImageRemoved(bool success) {
  if (!success) {
    LOG(ERROR) << "Downloaded PluginVm image located in "
               << downloaded_image_.value() << " failed to be deleted";
    return;
  }
  downloaded_image_.clear();
  creating_new_vm_ = false;
}

device::mojom::WakeLock* PluginVmInstaller::GetWakeLock() {
  if (!wake_lock_) {
    mojo::Remote<device::mojom::WakeLockProvider> wake_lock_provider;
    content::GetDeviceService().BindWakeLockProvider(
        wake_lock_provider.BindNewPipeAndPassReceiver());
    wake_lock_provider->GetWakeLockWithoutContext(
        device::mojom::WakeLockType::kPreventAppSuspension,
        device::mojom::WakeLockReason::kOther, "Plugin VM Installer",
        wake_lock_.BindNewPipeAndPassReceiver());
  }
  return wake_lock_.get();
}

}  // namespace plugin_vm

#undef LOG_FUNCTION_CALL