chromium/chrome/browser/ui/webui/ash/settings/calculator/size_calculator.cc

// Copyright 2020 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/ui/webui/ash/settings/calculator/size_calculator.h"

#include <cstdint>
#include <numeric>
#include <type_traits>

#include "ash/components/arc/disk_space/arc_disk_space_bridge.h"
#include "ash/components/arc/session/arc_bridge_service.h"
#include "ash/components/arc/session/arc_service_manager.h"
#include "ash/constants/ash_features.h"
#include "base/containers/contains.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/scoped_refptr.h"
#include "base/system/sys_info.h"
#include "base/task/thread_pool.h"
#include "base/values.h"
#include "chrome/browser/ash/borealis/borealis_features.h"
#include "chrome/browser/ash/borealis/borealis_service.h"
#include "chrome/browser/ash/crostini/crostini_features.h"
#include "chrome/browser/ash/crostini/crostini_pref_names.h"
#include "chrome/browser/ash/drive/drive_integration_service.h"
#include "chrome/browser/ash/drive/file_system_util.h"
#include "chrome/browser/ash/file_manager/path_util.h"
#include "chrome/browser/ash/profiles/profile_helper.h"
#include "chrome/browser/browsing_data/browsing_data_file_system_util.h"
#include "chrome/browser/profiles/profile.h"
#include "chromeos/ash/components/cryptohome/userdataauth_util.h"
#include "chromeos/ash/components/dbus/concierge/concierge_client.h"
#include "chromeos/ash/components/dbus/spaced/spaced_client.h"
#include "chromeos/ash/components/dbus/userdataauth/userdataauth_client.h"
#include "chromeos/ash/components/dbus/vm_concierge/concierge_service.pb.h"
#include "components/browsing_data/content/browsing_data_quota_helper.h"
#include "components/browsing_data/content/conditional_cache_counting_helper.h"
#include "components/browsing_data/content/cookie_helper.h"
#include "components/browsing_data/content/local_storage_helper.h"
#include "components/prefs/pref_service.h"
#include "components/user_manager/user_manager.h"
#include "content/public/browser/storage_partition.h"

namespace ash::settings {
namespace {

// Computes the size of MyFiles and Play files.
int64_t ComputeLocalFilesSize(const base::FilePath& my_files,
                              const base::FilePath& play_files) {
  // Compute size of MyFiles.
  int64_t size = base::ComputeDirectorySize(my_files);

  // Compute size of Play Files.
  if (int64_t play_size = base::ComputeDirectorySize(play_files);
      play_size > 0) {
    // Remove size of the Download folder, because it is already counted as part
    // of MyFiles.
    play_size -= base::ComputeDirectorySize(play_files.AppendASCII("Download"));
    if (play_size > 0) {
      size += play_size;
    }
  }

  return size;
}

}  // namespace

std::ostream& operator<<(std::ostream& out, SizeCalculator::CalculationType t) {
  switch (t) {
#define PRINT(s)                              \
  case SizeCalculator::CalculationType::k##s: \
    return out << #s;
    PRINT(Total)
    PRINT(Available)
    PRINT(MyFiles)
    PRINT(BrowsingData)
    PRINT(AppsExtensions)
    PRINT(DriveOfflineFiles)
    PRINT(Crostini)
    PRINT(OtherUsers)
    PRINT(System)
#undef PRINT
  }

  return out << "CalculationType("
             << static_cast<
                    std::underlying_type_t<SizeCalculator::CalculationType>>(t)
             << ")";
}

SizeCalculator::SizeCalculator(CalculationType calculation_type)
    : calculation_type_(calculation_type) {}

SizeCalculator::~SizeCalculator() {}

void SizeCalculator::StartCalculation() {
  if (calculating_) {
    return;
  }
  calculating_ = true;
  PerformCalculation();
}

void SizeCalculator::AddObserver(Observer* observer) {
  observers_.AddObserver(observer);
}

void SizeCalculator::RemoveObserver(Observer* observer) {
  observers_.RemoveObserver(observer);
}

void SizeCalculator::NotifySizeCalculated(const int64_t size) {
  calculating_ = false;

  LOG_IF(ERROR, size < 0) << "Got negative size " << size
                          << " while calculating " << calculation_type_;

  for (Observer& observer : observers_) {
    observer.OnSizeCalculated(calculation_type_, size);
  }
}

TotalDiskSpaceCalculator::TotalDiskSpaceCalculator(Profile* profile)
    : SizeCalculator(CalculationType::kTotal), profile_(profile) {}

TotalDiskSpaceCalculator::~TotalDiskSpaceCalculator() = default;

void TotalDiskSpaceCalculator::PerformCalculation() {
  if (user_manager::UserManager::Get()
          ->IsCurrentUserCryptohomeDataEphemeral()) {
    GetTotalDiskSpace();
    return;
  }
  GetRootDeviceSize();
}

void TotalDiskSpaceCalculator::GetRootDeviceSize() {
  SpacedClient::Get()->GetRootDeviceSize(
      base::BindOnce(&TotalDiskSpaceCalculator::OnGetRootDeviceSize,
                     weak_ptr_factory_.GetWeakPtr()));
}

void TotalDiskSpaceCalculator::OnGetRootDeviceSize(
    std::optional<int64_t> reply) {
  if (reply.has_value()) {
    NotifySizeCalculated(reply.value());
    return;
  }

  // FakeSpacedClient does not have a proper implementation of
  // GetRootDeviceSize. If SpacedClient::GetRootDeviceSize does not return a
  // value, use GetTotalDiskSpace as a fallback.
  VLOG(1) << "SpacedClient::OnGetRootDeviceSize gave an empty reply. "
             "Using GetTotalDiskSpace as fallback.";
  GetTotalDiskSpace();
}

void TotalDiskSpaceCalculator::GetTotalDiskSpace() {
  base::ThreadPool::PostTaskAndReplyWithResult(
      FROM_HERE, {base::MayBlock(), base::TaskPriority::USER_VISIBLE},
      base::BindOnce(&base::SysInfo::AmountOfTotalDiskSpace,
                     file_manager::util::GetMyFilesFolderForProfile(profile_)),
      base::BindOnce(&TotalDiskSpaceCalculator::NotifySizeCalculated,
                     weak_ptr_factory_.GetWeakPtr()));
}

FreeDiskSpaceCalculator::FreeDiskSpaceCalculator(Profile* profile)
    : SizeCalculator(CalculationType::kAvailable), profile_(profile) {}

FreeDiskSpaceCalculator::~FreeDiskSpaceCalculator() = default;

void FreeDiskSpaceCalculator::PerformCalculation() {
  if (user_manager::UserManager::Get()
          ->IsCurrentUserCryptohomeDataEphemeral()) {
    GetFreeDiskSpace();
    return;
  }
  GetUserFreeDiskSpace();
}

void FreeDiskSpaceCalculator::GetUserFreeDiskSpace() {
  SpacedClient::Get()->GetFreeDiskSpace(
      file_manager::util::GetMyFilesFolderForProfile(profile_).value(),
      base::BindOnce(&FreeDiskSpaceCalculator::OnGetUserFreeDiskSpace,
                     weak_ptr_factory_.GetWeakPtr()));
}

void FreeDiskSpaceCalculator::OnGetUserFreeDiskSpace(
    std::optional<int64_t> reply) {
  if (reply.has_value()) {
    NotifySizeCalculated(reply.value());
    return;
  }

  // FakeSpacedClient does not have a proper implementation of
  // GetFreeDiskSpace. If SpacedClient::GetFreeDiskSpace does not return a
  // value, use GetFreeDiskSpace as a fallback.
  VLOG(1) << "SpacedClient::GetFreeDiskSpace gave an empty reply. "
             "Using GetFreeDiskSpace as fallback.";
  GetFreeDiskSpace();
}

void FreeDiskSpaceCalculator::GetFreeDiskSpace() {
  base::ThreadPool::PostTaskAndReplyWithResult(
      FROM_HERE, {base::MayBlock(), base::TaskPriority::USER_VISIBLE},
      base::BindOnce(&base::SysInfo::AmountOfFreeDiskSpace,
                     file_manager::util::GetMyFilesFolderForProfile(profile_)),
      base::BindOnce(&FreeDiskSpaceCalculator::NotifySizeCalculated,
                     weak_ptr_factory_.GetWeakPtr()));
}

DriveOfflineSizeCalculator::DriveOfflineSizeCalculator(Profile* profile)
    : SizeCalculator(CalculationType::kDriveOfflineFiles), profile_(profile) {}

DriveOfflineSizeCalculator::~DriveOfflineSizeCalculator() = default;

void DriveOfflineSizeCalculator::PerformCalculation() {
  drive::DriveIntegrationService* const service =
      drive::util::GetIntegrationServiceByProfile(profile_);
  if (!service) {
    NotifySizeCalculated(0);
    return;
  }

  base::ThreadPool::PostTaskAndReplyWithResult(
      FROM_HERE, {base::MayBlock(), base::TaskPriority::USER_VISIBLE},
      base::BindOnce(&drive::util::ComputeDriveFsContentCacheSize,
                     service->GetDriveFsContentCachePath()),
      base::BindOnce(&DriveOfflineSizeCalculator::NotifySizeCalculated,
                     weak_ptr_factory_.GetWeakPtr()));
}

MyFilesSizeCalculator::MyFilesSizeCalculator(Profile* profile)
    : SizeCalculator(CalculationType::kMyFiles), profile_(profile) {}

MyFilesSizeCalculator::~MyFilesSizeCalculator() = default;

void MyFilesSizeCalculator::PerformCalculation() {
  base::ThreadPool::PostTaskAndReplyWithResult(
      FROM_HERE, {base::MayBlock(), base::TaskPriority::BEST_EFFORT},
      base::BindOnce(&ComputeLocalFilesSize,
                     file_manager::util::GetMyFilesFolderForProfile(profile_),
                     file_manager::util::GetAndroidFilesPath()),
      base::BindOnce(&MyFilesSizeCalculator::NotifySizeCalculated,
                     weak_ptr_factory_.GetWeakPtr()));
}

BrowsingDataSizeCalculator::BrowsingDataSizeCalculator(Profile* profile)
    : SizeCalculator(CalculationType::kBrowsingData), profile_(profile) {}

BrowsingDataSizeCalculator::~BrowsingDataSizeCalculator() = default;

void BrowsingDataSizeCalculator::PerformCalculation() {
  has_browser_cache_size_ = false;
  has_browser_site_data_size_ = false;

  // Fetch the size of http cache in browsing data.
  browsing_data::ConditionalCacheCountingHelper::Count(
      profile_->GetDefaultStoragePartition(), base::Time(), base::Time::Max(),
      base::BindOnce(&BrowsingDataSizeCalculator::OnGetCacheSize,
                     weak_ptr_factory_.GetWeakPtr()));

  // Fetch the size of site data in browsing data.
  if (!site_data_size_collector_.get()) {
    content::StoragePartition* storage_partition =
        profile_->GetDefaultStoragePartition();
    site_data_size_collector_ = std::make_unique<SiteDataSizeCollector>(
        storage_partition->GetPath(),
        new browsing_data::CookieHelper(storage_partition,
                                        base::NullCallback()),
        new browsing_data::LocalStorageHelper(storage_partition),
        BrowsingDataQuotaHelper::Create(storage_partition));
  }
  site_data_size_collector_->Fetch(
      base::BindOnce(&BrowsingDataSizeCalculator::OnGetBrowsingDataSize,
                     weak_ptr_factory_.GetWeakPtr(), /*is_site_data=*/true));
}

void BrowsingDataSizeCalculator::OnGetCacheSize(bool is_upper_limit,
                                                int64_t size) {
  DCHECK(!is_upper_limit);
  OnGetBrowsingDataSize(/*is_site_data=*/false, size);
}

void BrowsingDataSizeCalculator::OnGetBrowsingDataSize(bool is_site_data,
                                                       int64_t size) {
  if (is_site_data) {
    has_browser_site_data_size_ = true;
    browser_site_data_size_ = size;
  } else {
    has_browser_cache_size_ = true;
    browser_cache_size_ = size;
  }
  if (has_browser_cache_size_ && has_browser_site_data_size_) {
    int64_t browsing_data_size;
    if (browser_cache_size_ >= 0 && browser_site_data_size_ >= 0) {
      browsing_data_size = browser_site_data_size_ + browser_cache_size_;
    } else {
      browsing_data_size = -1;
    }

    NotifySizeCalculated(browsing_data_size);
  }
}

AppsSizeCalculator::AppsSizeCalculator(Profile* profile)
    : SizeCalculator(CalculationType::kAppsExtensions), profile_(profile) {}

AppsSizeCalculator::~AppsSizeCalculator() {
  arc::ArcServiceManager::Get()
      ->arc_bridge_service()
      ->disk_space()
      ->RemoveObserver(this);
}

void AppsSizeCalculator::OnConnectionReady() {
  is_android_running_ = true;
  StartCalculation();
}

void AppsSizeCalculator::OnConnectionClosed() {
  is_android_running_ = false;
}

void AppsSizeCalculator::AddObserver(Observer* observer) {
  // Start observing arc mojo connection when the first observer is added, to
  // allow the calculation of android apps.
  if (observers_.empty()) {
    arc::ArcServiceManager::Get()
        ->arc_bridge_service()
        ->disk_space()
        ->AddObserver(this);
  }
  SizeCalculator::AddObserver(observer);
}

void AppsSizeCalculator::RemoveObserver(Observer* observer) {
  SizeCalculator::RemoveObserver(observer);
  // Stop observing arc connection if all observers have been removed.
  if (observers_.empty()) {
    arc::ArcServiceManager::Get()
        ->arc_bridge_service()
        ->disk_space()
        ->RemoveObserver(this);
  }
}

void AppsSizeCalculator::PerformCalculation() {
  apps_extensions_size_ = 0;
  has_apps_extensions_size_ = false;
  android_apps_size_ = 0;
  has_android_apps_size_ = false;
  borealis_apps_size_ = 0;
  has_borealis_apps_size_ = false;

  UpdateAppsSize();
  UpdateAndroidAppsSize();
  UpdateBorealisAppsSize();
}

void AppsSizeCalculator::UpdateAppsSize() {
  // Apps and extensions installed from the web store located in
  // [user-hash]/Extensions.
  const base::FilePath extensions_path =
      profile_->GetPath().AppendASCII("Extensions");

  base::ThreadPool::PostTaskAndReplyWithResult(
      FROM_HERE, {base::MayBlock(), base::TaskPriority::BEST_EFFORT},
      base::BindOnce(&base::ComputeDirectorySize, extensions_path),
      base::BindOnce(&AppsSizeCalculator::OnGetAppsSize,
                     weak_ptr_factory_.GetWeakPtr()));
}

void AppsSizeCalculator::OnGetAppsSize(int64_t total_bytes) {
  apps_extensions_size_ = total_bytes;
  has_apps_extensions_size_ = true;
  UpdateAppsAndExtensionsSize();
}

void AppsSizeCalculator::UpdateAndroidAppsSize() {
  if (!is_android_running_) {
    has_android_apps_size_ = true;
    UpdateAppsAndExtensionsSize();
    return;
  }

  bool success = false;
  auto* arc_disk_space_bridge =
      arc::ArcDiskSpaceBridge::GetForBrowserContext(profile_);
  if (arc_disk_space_bridge) {
    success = arc_disk_space_bridge->GetApplicationsSize(
        base::BindOnce(&AppsSizeCalculator::OnGetAndroidAppsSize,
                       weak_ptr_factory_.GetWeakPtr()));
  }
  if (!success) {
    has_android_apps_size_ = true;
    UpdateAppsAndExtensionsSize();
  }
}

void AppsSizeCalculator::OnGetAndroidAppsSize(
    bool succeeded,
    arc::mojom::ApplicationsSizePtr size) {
  has_android_apps_size_ = true;
  if (succeeded) {
    android_apps_size_ = size->total_code_bytes + size->total_data_bytes +
                         size->total_cache_bytes;
  }
  UpdateAppsAndExtensionsSize();
}

void AppsSizeCalculator::UpdateBorealisAppsSize() {
  borealis::BorealisService* borealis_service =
      borealis::BorealisService::GetForProfile(profile_);
  if (!borealis_service || !borealis_service->Features().IsEnabled()) {
    has_borealis_apps_size_ = true;
    return;
  }
  vm_tools::concierge::ListVmDisksRequest request;
  request.set_cryptohome_id(
      ash::ProfileHelper::GetUserIdHashFromProfile(profile_));
  request.set_storage_location(vm_tools::concierge::STORAGE_CRYPTOHOME_ROOT);
  request.set_vm_name("borealis");
  ash::ConciergeClient::Get()->ListVmDisks(
      std::move(request),
      base::BindOnce(&AppsSizeCalculator::OnGetBorealisAppsSize,
                     weak_ptr_factory_.GetWeakPtr()));
}

void AppsSizeCalculator::OnGetBorealisAppsSize(
    std::optional<vm_tools::concierge::ListVmDisksResponse> response) {
  if (!response) {
    LOG(ERROR) << "Failed to get response from concierge";
    has_borealis_apps_size_ = true;
    UpdateAppsAndExtensionsSize();
    return;
  }
  if (!response->success()) {
    LOG(ERROR) << "concierge failed to list vm disks, returned error: " +
                      response->failure_reason();
    has_borealis_apps_size_ = true;
    UpdateAppsAndExtensionsSize();
    return;
  }
  auto image = base::ranges::find(response->images(), "borealis",
                                  &vm_tools::concierge::VmDiskInfo::name);
  if (image == response->images().end()) {
    LOG(ERROR) << "Couldn't find Borealis VM";
    has_borealis_apps_size_ = true;
    UpdateAppsAndExtensionsSize();
    return;
  }
  borealis_apps_size_ = image->size();
  has_borealis_apps_size_ = true;
  UpdateAppsAndExtensionsSize();
}

void AppsSizeCalculator::UpdateAppsAndExtensionsSize() {
  if (has_apps_extensions_size_ && has_android_apps_size_ &&
      has_borealis_apps_size_) {
    NotifySizeCalculated(apps_extensions_size_ + android_apps_size_ +
                         borealis_apps_size_);
  }
}

CrostiniSizeCalculator::CrostiniSizeCalculator(Profile* profile)
    : SizeCalculator(CalculationType::kCrostini), profile_(profile) {}

CrostiniSizeCalculator::~CrostiniSizeCalculator() = default;

void CrostiniSizeCalculator::PerformCalculation() {
  if (!crostini::CrostiniFeatures::Get()->IsEnabled(profile_)) {
    NotifySizeCalculated(
        profile_->GetPrefs()->GetInt64(crostini::prefs::kCrostiniLastDiskSize));
    return;
  }

  vm_tools::concierge::ListVmDisksRequest request;
  request.set_cryptohome_id(
      ash::ProfileHelper::GetUserIdHashFromProfile(profile_));
  request.set_storage_location(vm_tools::concierge::STORAGE_CRYPTOHOME_ROOT);
  ash::ConciergeClient::Get()->ListVmDisks(
      std::move(request),
      base::BindOnce(&CrostiniSizeCalculator::OnGetCrostiniSize,
                     weak_ptr_factory_.GetWeakPtr()));
}

void CrostiniSizeCalculator::OnGetCrostiniSize(
    std::optional<vm_tools::concierge::ListVmDisksResponse> response) {
  if (!response) {
    LOG(ERROR) << "Failed to get list of VM disks. Empty response.";
    NotifySizeCalculated(
        profile_->GetPrefs()->GetInt64(crostini::prefs::kCrostiniLastDiskSize));
    return;
  }

  if (!response->success()) {
    LOG(ERROR) << "Failed to list VM disks: " << response->failure_reason();
    NotifySizeCalculated(
        profile_->GetPrefs()->GetInt64(crostini::prefs::kCrostiniLastDiskSize));
    return;
  }

  int64_t vm_disk_usage = response->total_size();

  // If Borealis is installed then we need to subtract its size from Crostini
  // in order for it to not be double counted.
  if (borealis::BorealisService::GetForProfile(profile_)
          ->Features()
          .IsEnabled()) {
    auto image = base::ranges::find(response->images(), "borealis",
                                    &vm_tools::concierge::VmDiskInfo::name);
    if (image == response->images().end()) {
      LOG(ERROR) << "Couldn't find Borealis VM";
    } else {
      vm_disk_usage -= image->size();
    }
  }
  profile_->GetPrefs()->SetInt64(crostini::prefs::kCrostiniLastDiskSize,
                                 vm_disk_usage);
  NotifySizeCalculated(vm_disk_usage);
}

OtherUsersSizeCalculator::OtherUsersSizeCalculator()
    : SizeCalculator(CalculationType::kOtherUsers) {}

OtherUsersSizeCalculator::~OtherUsersSizeCalculator() = default;

void OtherUsersSizeCalculator::PerformCalculation() {
  other_users_.clear();
  user_sizes_.clear();
  const user_manager::UserList& users =
      user_manager::UserManager::Get()->GetUsers();
  for (user_manager::User* user : users) {
    if (user->is_active()) {
      continue;
    }
    other_users_.push_back(user);
    user_data_auth::GetAccountDiskUsageRequest request;
    *request.mutable_identifier() =
        cryptohome::CreateAccountIdentifierFromAccountId(user->GetAccountId());
    UserDataAuthClient::Get()->GetAccountDiskUsage(
        request, base::BindOnce(&OtherUsersSizeCalculator::OnGetOtherUserSize,
                                weak_ptr_factory_.GetWeakPtr()));
  }
  // We should show "0 B" if there is no other user.
  if (other_users_.empty()) {
    NotifySizeCalculated(0);
  }
}

void OtherUsersSizeCalculator::OnGetOtherUserSize(
    std::optional<user_data_auth::GetAccountDiskUsageReply> reply) {
  user_sizes_.push_back(
      user_data_auth::AccountDiskUsageReplyToUsageSize(reply));
  if (user_sizes_.size() != other_users_.size()) {
    return;
  }

  // If all the requests succeed, shows the total bytes in the UI.
  const int64_t other_users_total_bytes =
      base::Contains(user_sizes_, -1)
          ? -1
          : std::accumulate(user_sizes_.begin(), user_sizes_.end(), 0LL);
  NotifySizeCalculated(other_users_total_bytes);
}

}  // namespace ash::settings