chromium/chrome/browser/ash/policy/skyvault/local_files_migration_manager.cc

// Copyright 2024 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/policy/skyvault/local_files_migration_manager.h"

#include <memory>
#include <string>

#include "base/check_is_test.h"
#include "base/feature_list.h"
#include "base/files/file_enumerator.h"
#include "base/files/file_path.h"
#include "base/functional/bind.h"
#include "base/location.h"
#include "base/logging.h"
#include "base/no_destructor.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "base/time/time.h"
#include "base/timer/wall_clock_timer.h"
#include "chrome/browser/ash/file_manager/path_util.h"
#include "chrome/browser/ash/policy/skyvault/local_files_migration_constants.h"
#include "chrome/browser/ash/policy/skyvault/migration_coordinator.h"
#include "chrome/browser/ash/policy/skyvault/migration_notification_manager.h"
#include "chrome/browser/ash/policy/skyvault/policy_utils.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_selections.h"
#include "chrome/common/chrome_features.h"
#include "chrome/common/pref_names.h"
#include "chromeos/ash/components/browser_context_helper/browser_context_helper.h"
#include "chromeos/ash/components/cryptohome/cryptohome_parameters.h"
#include "chromeos/ash/components/cryptohome/error_util.h"
#include "chromeos/ash/components/cryptohome/userdataauth_util.h"
#include "chromeos/ash/components/dbus/cryptohome/UserDataAuth.pb.h"
#include "chromeos/ash/components/dbus/userdataauth/userdataauth_client.h"
#include "components/prefs/pref_service.h"
#include "components/user_manager/user.h"
#include "content/public/browser/browser_context.h"

namespace policy::local_user_files {

namespace {

// Returns true if `cloud_provider` is set to Google Drive or OneDrive.
bool IsMigrationEnabled(CloudProvider cloud_provider) {
  return cloud_provider == CloudProvider::kGoogleDrive ||
         cloud_provider == CloudProvider::kOneDrive;
}

// Returns a list of files under MyFiles.
std::vector<base::FilePath> GetMyFilesContents(Profile* profile) {
  base::FilePath my_files_path = GetMyFilesPath(profile);
  std::vector<base::FilePath> files;

  base::FileEnumerator enumerator(my_files_path,
                                  /*recursive=*/true,
                                  /*file_type=*/base::FileEnumerator::FILES |
                                      base::FileEnumerator::DIRECTORIES);
  for (base::FilePath path = enumerator.Next(); !path.empty();
       path = enumerator.Next()) {
    if (enumerator.GetInfo().IsDirectory()) {
      // Do not move directories - this moves the contents too.
      continue;
    }
    // Ignore hidden files.
    // TODO(aidazolic): Also Play and Linux?
    if (base::StartsWith(path.BaseName().value(), ".")) {
      continue;
    }
    files.push_back(path);
  }
  return files;
}

}  // namespace

LocalFilesMigrationManager::LocalFilesMigrationManager(
    content::BrowserContext* context)
    : context_(context),
      coordinator_(std::make_unique<MigrationCoordinator>(
          Profile::FromBrowserContext(context))),
      scheduling_timer_(std::make_unique<base::WallClockTimer>()) {
  CHECK(base::FeatureList::IsEnabled(features::kSkyVaultV2));

  notification_manager_ =
      MigrationNotificationManagerFactory::GetForBrowserContext(context);
  CHECK(notification_manager_);
}

LocalFilesMigrationManager::~LocalFilesMigrationManager() = default;

void LocalFilesMigrationManager::Shutdown() {
  weak_factory_.InvalidateWeakPtrs();
}

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

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

void LocalFilesMigrationManager::SetNotificationManagerForTesting(
    MigrationNotificationManager* notification_manager) {
  CHECK_IS_TEST();
  notification_manager_ = notification_manager;
}

void LocalFilesMigrationManager::SetCoordinatorForTesting(
    std::unique_ptr<MigrationCoordinator> coordinator) {
  CHECK_IS_TEST();
  coordinator_ = std::move(coordinator);
}

void LocalFilesMigrationManager::OnLocalUserFilesPolicyChanged() {
  bool local_user_files_allowed_old = local_user_files_allowed_;
  local_user_files_allowed_ = LocalUserFilesAllowed();
  CloudProvider cloud_provider_old = cloud_provider_;
  cloud_provider_ = GetMigrationDestination();

  if (local_user_files_allowed_ == local_user_files_allowed_old &&
      cloud_provider_ == cloud_provider_old) {
    // No change.
    return;
  }

  // If local files are allowed or migration is turned off, just stop ongoing
  // migration or timers if any.
  if (local_user_files_allowed_ || !IsMigrationEnabled(cloud_provider_)) {
    MaybeStopMigration();
    if (local_user_files_allowed_) {
      SetLocalUserFilesWriteEnabled(/*enabled=*/true);
    }
    return;
  }

  // If the destination changed, stop ongoing migration or timers if any.
  if (IsMigrationEnabled(cloud_provider_) &&
      cloud_provider_ != cloud_provider_old) {
    MaybeStopMigration();
  }

  // TODO(b/354716629): Confirm under which conditions we fail here.
  Profile* profile = Profile::FromBrowserContext(context_);
  const bool google_drive_disabled =
      !drive::DriveIntegrationServiceFactory::FindForProfile(profile)
           ->is_enabled();
  // TODO(b/354716629): Confirm conditions. Add OneDrive.
  if ((cloud_provider_ == CloudProvider::kGoogleDrive &&
       google_drive_disabled)) {
    notification_manager_->ShowConfigurationErrorNotification(cloud_provider_);
    return;
  }

  // Local files are disabled and migration destination is set - initiate
  // migration.
  InformUser();
}

void LocalFilesMigrationManager::InformUser() {
  CHECK(!local_user_files_allowed_);
  CHECK(IsMigrationEnabled(cloud_provider_));

  migration_start_time_ = base::Time::Now() + kTotalMigrationTimeout;

  notification_manager_->ShowMigrationInfoDialog(
      cloud_provider_, migration_start_time_,
      base::BindOnce(&LocalFilesMigrationManager::SkipMigrationDelay,
                     weak_factory_.GetWeakPtr()));
  // Schedule another dialog closer to the migration.
  scheduling_timer_->Start(
      FROM_HERE, migration_start_time_ - kFinalMigrationTimeout,
      base::BindOnce(
          &LocalFilesMigrationManager::ScheduleMigrationAndInformUser,
          weak_factory_.GetWeakPtr()));
}

void LocalFilesMigrationManager::ScheduleMigrationAndInformUser() {
  if (local_user_files_allowed_ || !IsMigrationEnabled(cloud_provider_)) {
    return;
  }

  notification_manager_->ShowMigrationInfoDialog(
      cloud_provider_, migration_start_time_,
      base::BindOnce(&LocalFilesMigrationManager::SkipMigrationDelay,
                     weak_factory_.GetWeakPtr()));
  // Also schedule migration to automatically start after the timeout.
  scheduling_timer_->Start(
      FROM_HERE, migration_start_time_,
      base::BindOnce(&LocalFilesMigrationManager::OnTimeoutExpired,
                     weak_factory_.GetWeakPtr()));
}

void LocalFilesMigrationManager::SkipMigrationDelay() {
  scheduling_timer_->Stop();
  GetPathsToUpload();
}

void LocalFilesMigrationManager::OnTimeoutExpired() {
  // TODO(aidazolic): This could cause issues if the dialog doesn't close fast
  // enough, and the user clicks "Upload now" exactly then.
  notification_manager_->CloseDialog();
  GetPathsToUpload();
}

void LocalFilesMigrationManager::GetPathsToUpload() {
  CHECK(!coordinator_->IsRunning());
  // Check policies again.
  if (local_user_files_allowed_ || !IsMigrationEnabled(cloud_provider_)) {
    return;
  }

  Profile* profile = Profile::FromBrowserContext(context_);
  CHECK(profile);

  base::ThreadPool::PostTaskAndReplyWithResult(
      FROM_HERE, {base::MayBlock()},
      base::BindOnce(&GetMyFilesContents, profile),
      base::BindOnce(&LocalFilesMigrationManager::StartMigration,
                     weak_factory_.GetWeakPtr()));
  in_progress_ = true;
  notification_manager_->ShowMigrationProgressNotification(cloud_provider_);
}

void LocalFilesMigrationManager::StartMigration(
    std::vector<base::FilePath> files) {
  CHECK(!coordinator_->IsRunning());
  // Check policies again.
  if (local_user_files_allowed_ || !IsMigrationEnabled(cloud_provider_)) {
    return;
  }

  // TODO(aidazolic): Add unique ID of the device.
  coordinator_->Run(cloud_provider_, std::move(files), kDestinationDirName,
                    base::BindOnce(&LocalFilesMigrationManager::OnMigrationDone,
                                   weak_factory_.GetWeakPtr()));
}

void LocalFilesMigrationManager::OnMigrationDone(
    std::map<base::FilePath, MigrationUploadError> errors) {
  in_progress_ = false;
  // TODO(aidazolic): Get destination folder path in drive.
  const base::FilePath destination_path = base::FilePath();
  if (!errors.empty()) {
    // TODO(aidazolic): Use error message; add on-click action.
    notification_manager_->ShowMigrationErrorNotification(
        cloud_provider_, destination_path, std::move(errors));

    LOG(ERROR) << "Local files migration failed.";
  } else {
    for (auto& observer : observers_) {
      observer.OnMigrationSucceeded();
    }
    notification_manager_->ShowMigrationCompletedNotification(cloud_provider_,
                                                              destination_path);
    VLOG(1) << "Local files migration done";
  }
  if (cleanup_in_progress_) {
    LOG(ERROR) << "Local files cleanup is already running";
    return;
  }
  cleanup_in_progress_ = true;
  std::unique_ptr<chromeos::FilesCleanupHandler> cleanup_handler =
      std::make_unique<chromeos::FilesCleanupHandler>();
  chromeos::FilesCleanupHandler* cleanup_handler_ptr = cleanup_handler.get();
  cleanup_handler_ptr->Cleanup(
      base::BindOnce(&LocalFilesMigrationManager::OnCleanupDone,
                     weak_factory_.GetWeakPtr(), std::move(cleanup_handler)));
}

void LocalFilesMigrationManager::OnCleanupDone(
    std::unique_ptr<chromeos::FilesCleanupHandler> cleanup_handler,
    const std::optional<std::string>& error_message) {
  cleanup_in_progress_ = false;
  if (error_message.has_value()) {
    LOG(ERROR) << "Local files cleanup failed: " << error_message.value();
  } else {
    VLOG(1) << "Local files cleanup done";
  }
  SetLocalUserFilesWriteEnabled(/*enabled=*/false);
}

void LocalFilesMigrationManager::SetLocalUserFilesWriteEnabled(bool enabled) {
  const user_manager::User* user =
      ash::BrowserContextHelper::Get()->GetUserByBrowserContext(context_);
  user_data_auth::SetUserDataStorageWriteEnabledRequest request;
  *request.mutable_account_id() =
      cryptohome::CreateAccountIdentifierFromAccountId(user->GetAccountId());
  request.set_enabled(enabled);
  ash::UserDataAuthClient::Get()->SetUserDataStorageWriteEnabled(
      request,
      base::BindOnce(&LocalFilesMigrationManager::OnFilesWriteRestricted,
                     weak_factory_.GetWeakPtr()));
}

void LocalFilesMigrationManager::OnFilesWriteRestricted(
    std::optional<user_data_auth::SetUserDataStorageWriteEnabledReply> reply) {
  if (!reply.has_value() ||
      reply->error() != user_data_auth::CRYPTOHOME_ERROR_NOT_SET) {
    LOG(ERROR) << "Could not restrict write access";
  }
}

void LocalFilesMigrationManager::MaybeStopMigration() {
  // Stop the timer. No-op if not running.
  scheduling_timer_->Stop();

  if (coordinator_->IsRunning()) {
    coordinator_->Stop();
  }

  if (in_progress_) {
    in_progress_ = false;
  }
  notification_manager_->CloseAll();
}

// static
LocalFilesMigrationManagerFactory*
LocalFilesMigrationManagerFactory::GetInstance() {
  static base::NoDestructor<LocalFilesMigrationManagerFactory> factory;
  return factory.get();
}

LocalFilesMigrationManager*
LocalFilesMigrationManagerFactory::GetForBrowserContext(
    content::BrowserContext* context) {
  return static_cast<LocalFilesMigrationManager*>(
      GetInstance()->GetServiceForBrowserContext(context, /*create=*/true));
}

LocalFilesMigrationManagerFactory::LocalFilesMigrationManagerFactory()
    : ProfileKeyedServiceFactory(
          "LocalFilesMigrationManager",
          ProfileSelections::Builder()
              .WithRegular(ProfileSelection::kOriginalOnly)
              // TODO(crbug.com/41488885): Check if this service is needed for
              // Ash Internals.
              .WithAshInternals(ProfileSelection::kOriginalOnly)
              .Build()) {
  DependsOn(policy::local_user_files::MigrationNotificationManagerFactory::
                GetInstance());
}

LocalFilesMigrationManagerFactory::~LocalFilesMigrationManagerFactory() =
    default;

bool LocalFilesMigrationManagerFactory::ServiceIsNULLWhileTesting() const {
  return true;
}

std::unique_ptr<KeyedService>
LocalFilesMigrationManagerFactory::BuildServiceInstanceForBrowserContext(
    content::BrowserContext* context) const {
  return std::make_unique<LocalFilesMigrationManager>(context);
}

}  // namespace policy::local_user_files