chromium/chrome/browser/screen_ai/screen_ai_dlc_installer.cc

// Copyright 2022 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/screen_ai/screen_ai_dlc_installer.h"

#include <string_view>

#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/logging.h"
#include "base/metrics/histogram_functions.h"
#include "base/task/thread_pool.h"
#include "chrome/browser/screen_ai/screen_ai_install_state.h"
#include "chromeos/ash/components/dbus/dlcservice/dlcservice.pb.h"
#include "chromeos/ash/components/dbus/dlcservice/dlcservice_client.h"
#include "services/screen_ai/public/cpp/utilities.h"

namespace {

constexpr char kScreenAIDlcName[] = "screen-ai";

// Retry delay will exponentially increase.
constexpr int kBaseRetryDelayInSeconds = 3;
constexpr int kMaxRetryDelayInSeconds = 180;
constexpr int kMaxInstallRetries = 5;
constexpr int kUninstallDelayInSeconds = 300;

struct InstallMetadata {
  bool dlc_available_from_before_this_session = false;
  int install_retries = 0;
  int retry_delay_in_seconds = kBaseRetryDelayInSeconds;
};

// These values are persisted to logs. Entries should not be renumbered and
// numeric values should never be reused.
// If any value is added, please update `DlcInstallResult` in `enums.xml`.
enum class DlcInstallResult {
  kSuccess = 0,
  kErrorInternal = 1,
  kErrorBusy = 2,
  kErrorNeedReboot = 3,
  kErrorInvalidDlc = 4,
  kErrorAllocation = 5,
  kErrorNoImageFound = 6,

  kMaxValue = kErrorNoImageFound,
};

void RecordDlcInstallResult(std::string_view result_string) {
  DlcInstallResult result_enum = DlcInstallResult::kSuccess;

  if (result_string == dlcservice::kErrorNone) {
    result_enum = DlcInstallResult::kSuccess;
  } else if (result_string == dlcservice::kErrorInternal) {
    result_enum = DlcInstallResult::kErrorInternal;
  } else if (result_string == dlcservice::kErrorBusy) {
    result_enum = DlcInstallResult::kErrorBusy;
  } else if (result_string == dlcservice::kErrorNeedReboot) {
    result_enum = DlcInstallResult::kErrorNeedReboot;
  } else if (result_string == dlcservice::kErrorInvalidDlc) {
    result_enum = DlcInstallResult::kErrorInvalidDlc;
  } else if (result_string == dlcservice::kErrorAllocation) {
    result_enum = DlcInstallResult::kErrorAllocation;
  } else if (result_string == dlcservice::kErrorNoImageFound) {
    result_enum = DlcInstallResult::kErrorNoImageFound;
  } else {
    NOTREACHED() << "Unexpected error: " << result_string;
  }

  base::UmaHistogramEnumeration("Accessibility.ScreenAI.DlcInstallResult",
                                result_enum);
}

void InstallInternal(InstallMetadata metadata);

int CalculateNextDelayInSeconds(int delay_in_seconds) {
  return std::min(delay_in_seconds * delay_in_seconds, kMaxRetryDelayInSeconds);
}

void OnInstallCompleted(
    InstallMetadata metadata,
    const ash::DlcserviceClient::InstallResult& install_result) {
  if (install_result.error == dlcservice::kErrorBusy &&
      metadata.install_retries < kMaxInstallRetries) {
    VLOG(1) << "ScreenAI installation failed as DLC service is busy, retrying.";
    base::TimeDelta retry_delay =
        base::Seconds(metadata.retry_delay_in_seconds);

    metadata.retry_delay_in_seconds =
        CalculateNextDelayInSeconds(metadata.retry_delay_in_seconds);
    metadata.install_retries++;

    base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
        FROM_HERE, base::BindOnce(&InstallInternal, metadata), retry_delay);
    return;
  }

  // Record metric only for new installs.
  if (!metadata.dlc_available_from_before_this_session) {
    screen_ai::ScreenAIInstallState::RecordComponentInstallationResult(
        /*install=*/true,
        /*successful=*/install_result.error == dlcservice::kErrorNone);
  }

  RecordDlcInstallResult(install_result.error);

  if (install_result.error != dlcservice::kErrorNone) {
    VLOG(0) << "ScreenAI installation failed: " << install_result.error;
    screen_ai::ScreenAIInstallState::GetInstance()->SetState(
        screen_ai::ScreenAIInstallState::State::kDownloadFailed);
    return;
  }

  VLOG(2) << "ScreenAI installation completed in path: "
          << install_result.root_path;
  if (!install_result.root_path.empty()) {
    screen_ai::ScreenAIInstallState::GetInstance()->SetComponentFolder(
        base::FilePath(install_result.root_path));
  }

  base::UmaHistogramCounts100("Accessibility.ScreenAI.Component.InstallRetries",
                              metadata.install_retries);
}

void OnUninstallCompleted(std::string_view err) {
  screen_ai::ScreenAIInstallState::RecordComponentInstallationResult(
      /*install=*/false,
      /*successful=*/err == dlcservice::kErrorNone);

  if (err != dlcservice::kErrorNone) {
    VLOG(0) << "Unistall failed: " << err;
  }
}

void OnInstallProgress(double progress) {
  screen_ai::ScreenAIInstallState::GetInstance()->SetDownloadProgress(progress);
}

// This function can be called only on a thread that can be blocked.
bool CheckIfDlcExistsOnNonUIThread() {
  return base::PathExists(screen_ai::GetComponentDir());
}

void InstallInternal(InstallMetadata metadata) {
  dlcservice::InstallRequest install_request;
  install_request.set_id(kScreenAIDlcName);
  ash::DlcserviceClient::Get()->Install(
      install_request, base::BindOnce(&OnInstallCompleted, metadata),
      base::BindRepeating(&OnInstallProgress));
}

void UninstallIfNotUsedAndAvailableOnDisk() {
  // If ScreenAIInstallState is not `kNotDownloaded`, it means that a client has
  // asked for this DLC and it should not be uninstalled.
  // Note that "Not downloaded" does not necessarily mean that the DLC is not
  // on disk, but just states that from ScreenAI point of view, no client
  // requested downloading it.
  if (screen_ai::ScreenAIInstallState::GetInstance()->get_state() !=
      screen_ai::ScreenAIInstallState::State::kNotDownloaded) {
    return;
  }

  // Checking if DLC exists on disk should be done on a non-UI thread, but
  // actual uninstall should be done on the same thread that called this
  // function.
  base::ThreadPool::PostTaskAndReplyWithResult(
      FROM_HERE,
      {base::MayBlock(), base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN},
      base::BindOnce(&CheckIfDlcExistsOnNonUIThread),
      base::BindOnce([](bool dlc_exists) {
        if (dlc_exists) {
          screen_ai::dlc_installer::Uninstall();
        }
      }));
}

}  // namespace

namespace screen_ai::dlc_installer {

void Install() {
  screen_ai::ScreenAIInstallState::GetInstance()->SetState(
      screen_ai::ScreenAIInstallState::State::kDownloading);

  // Need to know installation state for metrics.
  base::ThreadPool::PostTaskAndReplyWithResult(
      FROM_HERE,
      {base::MayBlock(), base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN},
      base::BindOnce(&CheckIfDlcExistsOnNonUIThread),
      base::BindOnce([](bool dlc_exists) {
        InstallMetadata metadata;
        metadata.dlc_available_from_before_this_session = dlc_exists;
        InstallInternal(metadata);
      }));
}

void Uninstall() {
  ash::DlcserviceClient::Get()->Uninstall(
      kScreenAIDlcName, base::BindOnce(&OnUninstallCompleted));
}

void ManageInstallation(PrefService* local_state) {
  if (screen_ai::ScreenAIInstallState::ShouldInstall(local_state)) {
    Install();
    return;
  }

  // This function is run on browser startup. The DLC uninstallation will be
  // called after a delay, so that if a feature relies on it and has not yet
  // triggered it, it would have time to do it. This is specifically helpful for
  // tests. Note that uninstall should happen on the same thread that runs this
  // function.
  base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
      FROM_HERE, base::BindOnce(&UninstallIfNotUsedAndAvailableOnDisk),
      base::Seconds(kUninstallDelayInSeconds));
}

int CalculateNextDelayInSecondsForTesting(int delay_in_seconds) {
  return CalculateNextDelayInSeconds(delay_in_seconds);
}

int base_retry_delay_in_seconds_for_testing() {
  return kBaseRetryDelayInSeconds;
}

int max_install_retries_for_testing() {
  return kMaxInstallRetries;
}

}  // namespace screen_ai::dlc_installer