chromium/chrome/browser/ui/webui/ash/settings/pages/storage/device_storage_handler.cc

// Copyright 2016 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
#pragma allow_unsafe_buffers
#endif

#include "chrome/browser/ui/webui/ash/settings/pages/storage/device_storage_handler.h"

#include <algorithm>
#include <limits>
#include <memory>
#include <optional>
#include <string>
#include <utility>

#include "ash/components/arc/arc_features.h"
#include "ash/public/cpp/new_window_delegate.h"
#include "base/check_op.h"
#include "base/debug/dump_without_crashing.h"
#include "base/notreached.h"
#include "base/values.h"
#include "chrome/browser/ash/arc/arc_util.h"
#include "chrome/browser/ash/file_manager/path_util.h"
#include "chrome/browser/platform_util.h"
#include "chrome/browser/ui/webui/ash/settings/os_settings_features_util.h"
#include "chrome/browser/ui/webui/ash/settings/pages/storage/device_storage_util.h"
#include "chrome/common/webui_url_constants.h"
#include "chrome/grit/generated_resources.h"
#include "chromeos/ash/components/cryptohome/cryptohome_parameters.h"
#include "chromeos/ash/components/dbus/cryptohome/UserDataAuth.pb.h"
#include "chromeos/ash/components/dbus/userdataauth/userdataauth_client.h"
#include "chromeos/ash/components/disks/disk.h"
#include "components/user_manager/user_names.h"
#include "content/public/browser/web_ui_data_source.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/text/bytes_formatting.h"

namespace ash::settings {

namespace {

using disks::Disk;
using disks::DiskMountManager;

constexpr char kIsExternalStorageEnabled[] = "isExternalStorageEnabled";
// Dummy UUID for testing. The UUID is taken from
// ash/components/arc/volume_mounter/arc_volume_mounter_bridge.cc.
constexpr char kDummyUuid[] = "00000000000000000000000000000000DEADBEEF";

const char* CalculationTypeToEventName(SizeCalculator::CalculationType x) {
  switch (x) {
    case SizeCalculator::CalculationType::kTotal:
      return "storage-size-stat-changed";
    case SizeCalculator::CalculationType::kMyFiles:
      return "storage-my-files-size-changed";
    case SizeCalculator::CalculationType::kBrowsingData:
      return "storage-browsing-data-size-changed";
    case SizeCalculator::CalculationType::kAppsExtensions:
      return "storage-apps-size-changed";
    case SizeCalculator::CalculationType::kDriveOfflineFiles:
      return "storage-drive-offline-size-changed";
    case SizeCalculator::CalculationType::kCrostini:
      return "storage-crostini-size-changed";
    case SizeCalculator::CalculationType::kOtherUsers:
      return "storage-other-users-size-changed";
    case SizeCalculator::CalculationType::kSystem:
      return "storage-system-size-changed";
    default:
      NOTREACHED_IN_MIGRATION();
      return "";
  }
}

}  // namespace

StorageHandler::StorageHandler(Profile* profile,
                               content::WebUIDataSource* html_source)
    : total_disk_space_calculator_(profile),
      free_disk_space_calculator_(profile),
      drive_offline_size_calculator_(profile),
      my_files_size_calculator_(profile),
      browsing_data_size_calculator_(profile),
      apps_size_calculator_(profile),
      crostini_size_calculator_(profile),
      other_users_size_calculator_(),
      profile_(profile),
      source_name_(html_source->GetSource()),
      special_volume_path_pattern_("[a-z]+://.*") {}

StorageHandler::~StorageHandler() {
  StopObservingEvents();
}

void StorageHandler::RegisterMessages() {
  DCHECK(web_ui());

  web_ui()->RegisterMessageCallback(
      "updateAndroidEnabled",
      base::BindRepeating(&StorageHandler::HandleUpdateAndroidEnabled,
                          base::Unretained(this)));
  web_ui()->RegisterMessageCallback(
      "updateStorageInfo",
      base::BindRepeating(&StorageHandler::HandleUpdateStorageInfo,
                          base::Unretained(this)));
  web_ui()->RegisterMessageCallback(
      "openMyFiles", base::BindRepeating(&StorageHandler::HandleOpenMyFiles,
                                         base::Unretained(this)));
  web_ui()->RegisterMessageCallback(
      "updateExternalStorages",
      base::BindRepeating(&StorageHandler::HandleUpdateExternalStorages,
                          base::Unretained(this)));
  web_ui()->RegisterMessageCallback(
      "openBrowsingDataSettings",
      base::BindRepeating(&StorageHandler::HandleOpenBrowsingDataSettings,
                          base::Unretained(this)));
  web_ui()->RegisterMessageCallback(
      "getStorageEncryptionInfo",
      base::BindRepeating(&StorageHandler::HandleGetStorageEncryption,
                          base::Unretained(this)));
}

void StorageHandler::OnJavascriptAllowed() {
  if (base::FeatureList::IsEnabled(arc::kUsbStorageUIFeature)) {
    arc_observation_.Observe(arc::ArcSessionManager::Get());
  }

  // Start observing mount/unmount events to update the connected device list.
  DiskMountManager::GetInstance()->AddObserver(this);

  // Start observing calculators.
  total_disk_space_calculator_.AddObserver(this);
  free_disk_space_calculator_.AddObserver(this);
  drive_offline_size_calculator_.AddObserver(this);
  my_files_size_calculator_.AddObserver(this);
  browsing_data_size_calculator_.AddObserver(this);
  apps_size_calculator_.AddObserver(this);
  crostini_size_calculator_.AddObserver(this);
  other_users_size_calculator_.AddObserver(this);
}

void StorageHandler::OnJavascriptDisallowed() {
  // Ensure that pending callbacks do not complete and cause JS to be evaluated.
  weak_ptr_factory_.InvalidateWeakPtrs();

  if (base::FeatureList::IsEnabled(arc::kUsbStorageUIFeature)) {
    DCHECK(arc_observation_.IsObservingSource(arc::ArcSessionManager::Get()));
    arc_observation_.Reset();
  }

  StopObservingEvents();
}

void StorageHandler::HandleUpdateAndroidEnabled(
    const base::Value::List& unused_args) {
  // OnJavascriptAllowed() calls ArcSessionManager::AddObserver() later.
  AllowJavascript();
}

void StorageHandler::HandleUpdateStorageInfo(const base::Value::List& args) {
  AllowJavascript();
  total_disk_space_calculator_.StartCalculation();
  free_disk_space_calculator_.StartCalculation();
  drive_offline_size_calculator_.StartCalculation();
  my_files_size_calculator_.StartCalculation();
  browsing_data_size_calculator_.StartCalculation();
  apps_size_calculator_.StartCalculation();
  crostini_size_calculator_.StartCalculation();
  other_users_size_calculator_.StartCalculation();
}

void StorageHandler::HandleGetStorageEncryption(const base::Value::List& args) {
  AllowJavascript();
  CHECK_EQ(1U, args.size());
  std::string callback_id = args[0].GetString();
  ::user_data_auth::GetVaultPropertiesRequest request;
  request.set_username(
      user_manager::CanonicalizeUserID(profile_->GetProfileUserName()));
  UserDataAuthClient::Get()->GetVaultProperties(
      request,
      base::BindOnce(&StorageHandler::OnGetVaultProperties,
                     weak_ptr_factory_.GetWeakPtr(), std::move(callback_id)));
}

void StorageHandler::OnGetVaultProperties(
    const std::string& callback_id,
    std::optional<user_data_auth::GetVaultPropertiesReply> reply) {
  // Default is Unknown.
  std::u16string encryption_type =
      l10n_util::GetStringUTF16(IDS_SETTINGS_STORAGE_SIZE_UNKNOWN);
  if (reply.has_value()) {
    switch (reply.value().encryption_type()) {
      case user_data_auth::CRYPTOHOME_VAULT_ENCRYPTION_FSCRYPT:
      case user_data_auth::CRYPTOHOME_VAULT_ENCRYPTION_DMCRYPT:
        encryption_type = l10n_util::GetStringUTF16(
            IDS_SETTINGS_STORAGE_ITEM_ENCRYPTION_AES_256);
        break;
      case user_data_auth::CRYPTOHOME_VAULT_ENCRYPTION_ECRYPTFS:
        encryption_type = l10n_util::GetStringUTF16(
            IDS_SETTINGS_STORAGE_ITEM_ENCRYPTION_AES_128);
        break;
      default:
        // This is unexpected state and we should continue to default.
        break;
    }
  }

  ResolveJavascriptCallback(base::Value(std::move(callback_id)),
                            base::Value(encryption_type.c_str()));
}

void StorageHandler::HandleOpenMyFiles(const base::Value::List& unused_args) {
  const base::FilePath my_files_path =
      file_manager::util::GetMyFilesFolderForProfile(profile_);
  platform_util::OpenItem(profile_, my_files_path, platform_util::OPEN_FOLDER,
                          platform_util::OpenOperationCallback());
}

void StorageHandler::HandleOpenBrowsingDataSettings(
    const base::Value::List& unused_args) {
  ash::NewWindowDelegate::GetPrimary()->OpenUrl(
      GURL(chrome::kChromeUISettingsURL)
          .Resolve(chrome::kClearBrowserDataSubPage),
      ash::NewWindowDelegate::OpenUrlFrom::kUserInteraction,
      ash::NewWindowDelegate::Disposition::kSwitchToTab);
}

void StorageHandler::HandleUpdateExternalStorages(
    const base::Value::List& unused_args) {
  UpdateExternalStorages();
}

void StorageHandler::UpdateExternalStorages() {
  base::Value::List devices;
  for (const auto& mount_point :
       DiskMountManager::GetInstance()->mount_points()) {
    if (!IsEligibleForAndroidStorage(mount_point.source_path)) {
      continue;
    }

    const Disk* disk = DiskMountManager::GetInstance()->FindDiskBySourcePath(
        mount_point.source_path);

    // Assigning a dummy UUID for diskless volume for testing.
    const std::string uuid = disk ? disk->fs_uuid() : kDummyUuid;
    std::string label = disk ? disk->device_label() : std::string();
    if (label.empty()) {
      // To make volume labels consistent with Files app, we follow how Files
      // generates a volume label when the volume doesn't have specific label.
      // That is, we use the base name of mount path instead in such cases.
      // TODO(fukino): Share the implementation to compute the volume name with
      // Files app. crbug.com/1002535.
      label = base::FilePath(mount_point.mount_path).BaseName().AsUTF8Unsafe();
    }
    base::Value::Dict device;
    device.Set("uuid", uuid);
    device.Set("label", label);
    devices.Append(std::move(device));
  }
  FireWebUIListener("onExternalStoragesUpdated", devices);
}

void StorageHandler::OnArcPlayStoreEnabledChanged(bool enabled) {
  base::Value::Dict update;
  update.Set(kIsExternalStorageEnabled, IsExternalStorageEnabled(profile_));
  content::WebUIDataSource::Update(profile_, source_name_, std::move(update));
}

void StorageHandler::OnMountEvent(
    DiskMountManager::MountEvent event,
    MountError error_code,
    const DiskMountManager::MountPoint& mount_info) {
  if (error_code != MountError::kSuccess) {
    return;
  }

  if (!IsEligibleForAndroidStorage(mount_info.source_path)) {
    return;
  }

  UpdateExternalStorages();
}

void StorageHandler::OnSizeCalculated(
    const SizeCalculator::CalculationType& calculation_type,
    int64_t total_bytes) {
  // The total disk space is rounded to the next power of 2.
  if (calculation_type == SizeCalculator::CalculationType::kTotal) {
    total_bytes = RoundByteSize(total_bytes);
  }

  // Store calculated item's size.
  const int item_index = static_cast<int>(calculation_type);
  storage_items_total_bytes_[item_index] = total_bytes;

  // Mark item as calculated.
  calculation_state_.set(item_index);

  // Update proper UI item on the storage page.
  switch (calculation_type) {
    case SizeCalculator::CalculationType::kTotal:
    case SizeCalculator::CalculationType::kAvailable:
      UpdateOverallStatistics();
      break;
    case SizeCalculator::CalculationType::kMyFiles:
    case SizeCalculator::CalculationType::kBrowsingData:
    case SizeCalculator::CalculationType::kAppsExtensions:
    case SizeCalculator::CalculationType::kDriveOfflineFiles:
    case SizeCalculator::CalculationType::kCrostini:
    case SizeCalculator::CalculationType::kOtherUsers:
      UpdateStorageItem(calculation_type);
      break;
    default:
      NOTREACHED_IN_MIGRATION()
          << "Unexpected calculation type: " << item_index;
  }
  UpdateSystemSizeItem();
}

void StorageHandler::StopObservingEvents() {
  // Stop observing mount/unmount events to update the connected device list.
  DiskMountManager::GetInstance()->RemoveObserver(this);

  // Stop observing calculators.
  total_disk_space_calculator_.RemoveObserver(this);
  free_disk_space_calculator_.RemoveObserver(this);
  drive_offline_size_calculator_.RemoveObserver(this);
  my_files_size_calculator_.RemoveObserver(this);
  browsing_data_size_calculator_.RemoveObserver(this);
  apps_size_calculator_.RemoveObserver(this);
  crostini_size_calculator_.RemoveObserver(this);
  other_users_size_calculator_.RemoveObserver(this);
}

void StorageHandler::UpdateStorageItem(
    const SizeCalculator::CalculationType& calculation_type) {
  const int item_index = static_cast<int>(calculation_type);
  const int64_t total_bytes = storage_items_total_bytes_[item_index];

  std::u16string message;
  if (total_bytes < 0) {
    message = l10n_util::GetStringUTF16(IDS_SETTINGS_STORAGE_SIZE_UNKNOWN);
  } else {
    message = ui::FormatBytes(total_bytes);
  }

  if (calculation_type == SizeCalculator::CalculationType::kOtherUsers) {
    bool no_other_users = (total_bytes == 0);
    FireWebUIListener(CalculationTypeToEventName(calculation_type),
                      base::Value(message), base::Value(no_other_users));
  } else {
    FireWebUIListener(CalculationTypeToEventName(calculation_type),
                      base::Value(message));
  }
}

void StorageHandler::UpdateOverallStatistics() {
  const int total_space_index =
      static_cast<int>(SizeCalculator::CalculationType::kTotal);
  const int free_disk_space_index =
      static_cast<int>(SizeCalculator::CalculationType::kAvailable);

  if (!calculation_state_.test(total_space_index) ||
      !calculation_state_.test(free_disk_space_index)) {
    return;
  }

  // Update the total disk space by rounding it to the next power of 2.

  int64_t total_bytes = storage_items_total_bytes_[total_space_index];
  int64_t available_bytes = storage_items_total_bytes_[free_disk_space_index];
  int64_t in_use_bytes = total_bytes - available_bytes;

  if (total_bytes <= 0 || available_bytes < 0) {
    // We can't get useful information from the storage page if total_bytes <= 0
    // or available_bytes is less than 0. This is not expected to happen.
    DUMP_WILL_BE_NOTREACHED()
        << "Unable to retrieve total or available disk space";
    return;
  }

  if (in_use_bytes < 0) {
    // TODO(crbug.com/40889316): This shouldn't happen, but we still need to
    // clarify when and how often it does. To be replaced with
    // CHECK_GE(in_use_bytes, 0).
    LOG(WARNING) << "Calculated total space (" << total_bytes
                 << ") lower than available space (" << available_bytes << ")";
    base::debug::DumpWithoutCrashing();
    return;
  }

  base::Value::Dict size_stat;
  size_stat.Set("availableSize", ui::FormatBytes(available_bytes));
  size_stat.Set("usedSize", ui::FormatBytes(in_use_bytes));
  size_stat.Set("usedRatio", static_cast<double>(in_use_bytes) / total_bytes);
  int storage_space_state =
      static_cast<int>(StorageSpaceState::kStorageSpaceNormal);
  if (available_bytes < kSpaceCriticallyLowBytes) {
    storage_space_state =
        static_cast<int>(StorageSpaceState::kStorageSpaceCriticallyLow);
  } else if (available_bytes < kSpaceLowBytes) {
    storage_space_state = static_cast<int>(StorageSpaceState::kStorageSpaceLow);
  }
  size_stat.Set("spaceState", storage_space_state);

  FireWebUIListener(
      CalculationTypeToEventName(SizeCalculator::CalculationType::kTotal),
      size_stat);
}

void StorageHandler::UpdateSystemSizeItem() {
  // If some size calculations are pending, return early and wait for all
  // calculations to complete.
  if (!calculation_state_.all()) {
    return;
  }

  int64_t system_bytes = 0;
  for (int i = 0; i < SizeCalculator::kCalculationTypeCount; ++i) {
    int64_t total_bytes_for_current_item = storage_items_total_bytes_[i];
    // Handle errors.
    if (total_bytes_for_current_item < 0) {
      if (i == static_cast<int>(SizeCalculator::CalculationType::kTotal) ||
          i == static_cast<int>(SizeCalculator::CalculationType::kAvailable)) {
        // Abort the calculation and display an error under "System".
        system_bytes = -1;
        break;
      }
      // Skip this storage item, which effectively means that its actual size is
      // added under "System" instead of its actual storage row.
      continue;
    }
    // The total amount of disk space counts positively towards system's size.
    if (i == static_cast<int>(SizeCalculator::CalculationType::kTotal)) {
      system_bytes += total_bytes_for_current_item;
      continue;
    }
    // All other items are subtracted from the total amount of disk space.
    system_bytes -= total_bytes_for_current_item;
  }

  // Update UI.
  std::u16string message;
  if (system_bytes < 0) {
    message = l10n_util::GetStringUTF16(IDS_SETTINGS_STORAGE_SIZE_UNKNOWN);
  } else {
    message = ui::FormatBytes(system_bytes);
  }
  FireWebUIListener(
      CalculationTypeToEventName(SizeCalculator::CalculationType::kSystem),
      base::Value(message));
}

bool StorageHandler::IsEligibleForAndroidStorage(std::string source_path) {
  // Android's StorageManager volume concept relies on assumption that it is
  // local filesystem. Hence, special volumes like DriveFS should not be
  // listed on the Settings.
  return !RE2::FullMatch(source_path, special_volume_path_pattern_);
}

}  // namespace ash::settings