chromium/ash/wallpaper/sea_pen_wallpaper_manager.cc

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

#include "ash/wallpaper/sea_pen_wallpaper_manager.h"

#include <iterator>
#include <memory>
#include <string>
#include <utility>

#include "ash/constants/ash_pref_names.h"
#include "ash/public/cpp/image_util.h"
#include "ash/public/cpp/wallpaper/sea_pen_image.h"
#include "ash/public/cpp/wallpaper/wallpaper_types.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shell.h"
#include "ash/wallpaper/sea_pen_wallpaper_manager_session_delegate_impl.h"
#include "ash/wallpaper/wallpaper_constants.h"
#include "ash/wallpaper/wallpaper_utils/sea_pen_metadata_utils.h"
#include "ash/wallpaper/wallpaper_utils/wallpaper_file_utils.h"
#include "ash/webui/common/mojom/sea_pen.mojom.h"
#include "base/check.h"
#include "base/check_op.h"
#include "base/files/file.h"
#include "base/files/file_enumerator.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/functional/callback_forward.h"
#include "base/logging.h"
#include "base/metrics/histogram_functions.h"
#include "base/ranges/algorithm.h"
#include "base/sequence_checker.h"
#include "base/strings/string_number_conversions.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "base/threading/thread_restrictions.h"
#include "base/types/cxx23_to_underlying.h"
#include "components/account_id/account_id.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"
#include "services/data_decoder/public/mojom/image_decoder.mojom-shared.h"
#include "ui/gfx/image/image_skia.h"

namespace ash {

namespace {

constexpr char kMigrationHistogramName[] = "Ash.SeaPen.MigrationStatus";

// The max number of Sea Pen image files to keep in Sea Pen directory before
// adding a new file.
constexpr int kMaxSeaPenFiles = 11;

SeaPenWallpaperManager* g_instance = nullptr;

bool MigrateFiles(const base::FilePath& source, const base::FilePath& target) {
  DCHECK(!source.empty());
  DCHECK(!target.empty());
  DCHECK_EQ(target.GetComponents().back(),
            wallpaper_constants::kSeaPenWallpaperDirName);

  if (!base::PathExists(source) || base::IsDirectoryEmpty(source)) {
    VLOG(0) << "Skip migration as there are no files to move";
    return true;
  }

  if (base::PathExists(target) && !base::DeletePathRecursively(target)) {
    LOG(ERROR) << "Failed to delete existing storage directory";
    return false;
  }

  // Create the parents of `target` so that the future mv command will create
  // the actual `target`. Still succeeds if the directory already exists.
  base::File::Error error;
  if (!base::CreateDirectoryAndGetError(target.DirName(), &error)) {
    LOG(ERROR) << "Failed to stage new directory with error: " << error;
    return false;
  }

  return base::Move(source, target);
}

std::vector<uint32_t> GetImageIdsImpl(const base::FilePath& directory) {
  std::vector<std::pair<base::FilePath, base::Time>> jpg_paths_with_timestamp;

  base::FileEnumerator jpg_enumerator(directory,
                                      /*recursive=*/false,
                                      base::FileEnumerator::FILES, "*.jpg");
  for (base::FilePath jpg_path = jpg_enumerator.Next(); !jpg_path.empty();
       jpg_path = jpg_enumerator.Next()) {
    const base::FileEnumerator::FileInfo info = jpg_enumerator.GetInfo();
    jpg_paths_with_timestamp.emplace_back(jpg_path, info.GetLastModifiedTime());
  }

  base::ranges::sort(jpg_paths_with_timestamp, base::ranges::greater(),
                     &std::pair<base::FilePath, base::Time>::second);

  std::vector<base::FilePath> jpg_paths;
  jpg_paths.reserve(jpg_paths_with_timestamp.size());

  base::ranges::transform(jpg_paths_with_timestamp,
                          std::back_inserter(jpg_paths),
                          &std::pair<base::FilePath, base::Time>::first);

  return GetIdsFromFilePaths(jpg_paths);
}

void TouchFileImpl(const base::FilePath& file_path) {
  if (!base::TouchFile(file_path, /*last_accessed=*/base::Time::Now(),
                       /*last_modified=*/base::Time::Now())) {
    LOG(WARNING) << "Failed to update accessed and modified time";
  }
}

// Reads the image from the given `file_path` and returns data as string.
std::string GetStringContent(const base::FilePath& file_path) {
  DVLOG(3) << "SeaPen reading " << file_path.BaseName();
  if (file_path.empty() || !base::PathExists(file_path)) {
    LOG(WARNING) << "File path is empty or does not exist";
    return std::string();
  }

  std::string result;
  if (!base::ReadFileToString(file_path, &result)) {
    LOG(WARNING) << "Failed reading file";
    result.clear();
  }

  return result;
}

gfx::ImageSkia DropImageInfo(
    const gfx::ImageSkia& image,
    personalization_app::mojom::RecentSeaPenImageInfoPtr info) {
  return image;
}

// Deletes `file_path` from disk. Returns true if the file did exist and was
// deleted. Must only be called with paths ending in ".jpg".
bool DeleteJpgFromDisk(const base::FilePath& file_path) {
  DCHECK_EQ(".jpg", file_path.Extension());
  if (base::PathExists(file_path)) {
    return base::DeleteFile(file_path);
  }
  return false;
}

// Scans through all the images in Sea Pen wallpaper directory. Keeps only 9
// latest sea pen images based on the last modified time, the older files are
// removed.
void MaybeDeleteOldSeaPenImages(const base::FilePath& wallpaper_dir) {
  std::vector<std::pair<base::FilePath, base::Time>> sea_pen_files;

  // Enumerate normal files only; directories and symlinks are skipped.
  base::FileEnumerator enumerator(wallpaper_dir, true,
                                  base::FileEnumerator::FILES);
  for (base::FilePath file_path = enumerator.Next(); !file_path.empty();
       file_path = enumerator.Next()) {
    const base::FileEnumerator::FileInfo info = enumerator.GetInfo();
    sea_pen_files.emplace_back(file_path, info.GetLastModifiedTime());
  }

  if (sea_pen_files.size() <= kMaxSeaPenFiles) {
    return;
  }

  // Finds the n oldest files (n = total files - kMaxSeaPenFiles) then resizes
  // sea_pen_files to store only the old files.
  std::nth_element(sea_pen_files.begin(), sea_pen_files.end() - kMaxSeaPenFiles,
                   sea_pen_files.end(),
                   [](const auto& left, const auto& right) {
                     return left.second < right.second;
                   });
  sea_pen_files.resize(sea_pen_files.size() - kMaxSeaPenFiles);

  // Removes all the old images.
  for (const auto& [file_path, _] : sea_pen_files) {
    if (!DeleteJpgFromDisk(file_path)) {
      LOG(ERROR) << __func__ << " failed to remove old Sea Pen file";
      return;
    }
  }
}

void OnDecodeImageData(
    SeaPenWallpaperManager::GetImageAndMetadataCallback callback,
    const std::string& json,
    const gfx::ImageSkia& image) {
  if (image.isNull()) {
    // Do not bother decoding image metadata if we were unable to decode the
    // image.
    LOG(WARNING) << "Unable to decode image";
    std::move(callback).Run(gfx::ImageSkia(), nullptr);
    return;
  }
  DecodeJsonMetadata(json, base::BindOnce(std::move(callback), image));
}

}  // namespace

SeaPenWallpaperManager::SeaPenWallpaperManager()
    : session_delegate_(
          std::make_unique<SeaPenWallpaperManagerSessionDelegateImpl>()),
      blocking_task_runner_(base::ThreadPool::CreateSequencedTaskRunner(
          {base::MayBlock(), base::TaskPriority::USER_BLOCKING,
           base::TaskShutdownBehavior::BLOCK_SHUTDOWN})) {
  DCHECK_EQ(nullptr, g_instance);
  g_instance = this;
}

SeaPenWallpaperManager::~SeaPenWallpaperManager() {
  DCHECK_EQ(g_instance, this);
  g_instance = nullptr;
}

// static
SeaPenWallpaperManager* SeaPenWallpaperManager::GetInstance() {
  return g_instance;
}

// static
void SeaPenWallpaperManager::RegisterProfilePrefs(
    PrefRegistrySimple* pref_registry_simple) {
  pref_registry_simple->RegisterIntegerPref(
      ::ash::prefs::kWallpaperSeaPenMigrationStatus,
      base::to_underlying(
          SeaPenWallpaperManager::MigrationStatus::kNotStarted));
}

void SeaPenWallpaperManager::SaveSeaPenImage(
    const AccountId& account_id,
    const SeaPenImage& sea_pen_image,
    const personalization_app::mojom::SeaPenQueryPtr& query,
    SaveSeaPenImageCallback callback) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  image_util::DecodeImageData(
      base::BindOnce(&SeaPenWallpaperManager::OnSeaPenImageDecoded,
                     weak_factory_.GetWeakPtr(), account_id, sea_pen_image.id,
                     query.Clone(), std::move(callback)),
      data_decoder::mojom::ImageCodec::kDefault, sea_pen_image.jpg_bytes);
}

void SeaPenWallpaperManager::DeleteSeaPenImage(
    const AccountId& account_id,
    const uint32_t image_id,
    DeleteRecentSeaPenImageCallback callback) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  blocking_task_runner_->PostTaskAndReplyWithResult(
      FROM_HERE,
      base::BindOnce(&DeleteJpgFromDisk,
                     GetFilePathForImageId(account_id, image_id)),
      std::move(callback));
}

void SeaPenWallpaperManager::GetImageIds(const AccountId& account_id,
                                         GetImageIdsCallback callback) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  blocking_task_runner_->PostTaskAndReplyWithResult(
      FROM_HERE,
      base::BindOnce(&GetImageIdsImpl,
                     session_delegate_->GetStorageDirectory(account_id)),
      std::move(callback));
}

void SeaPenWallpaperManager::TouchFile(const AccountId& account_id,
                                       const uint32_t image_id) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  blocking_task_runner_->PostTask(
      FROM_HERE, base::BindOnce(&TouchFileImpl,
                                GetFilePathForImageId(account_id, image_id)));
}

void SeaPenWallpaperManager::GetTemplateIdFromFile(
    const AccountId& account_id,
    const uint32_t image_id,
    GetTemplateIdFromFileCallback callback) {
  blocking_task_runner_->PostTaskAndReplyWithResult(
      FROM_HERE,
      base::BindOnce(&GetStringContent,
                     GetFilePathForImageId(account_id, image_id)),
      base::BindOnce(&SeaPenWallpaperManager::OnFileReadGetTemplateId,
                     weak_factory_.GetWeakPtr(), std::move(callback)));
}

void SeaPenWallpaperManager::GetImageAndMetadata(
    const AccountId& account_id,
    const uint32_t image_id,
    GetImageAndMetadataCallback callback) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  blocking_task_runner_->PostTaskAndReplyWithResult(
      FROM_HERE,
      base::BindOnce(&GetStringContent,
                     GetFilePathForImageId(account_id, image_id)),
      base::BindOnce(&SeaPenWallpaperManager::OnFileRead,
                     weak_factory_.GetWeakPtr(), std::move(callback)));
}

void SeaPenWallpaperManager::GetImage(const AccountId& account_id,
                                      const uint32_t image_id,
                                      GetImageCallback callback) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  GetImageAndMetadata(account_id, image_id,
                      base::BindOnce(&DropImageInfo).Then(std::move(callback)));
}

bool SeaPenWallpaperManager::ShouldMigrate(const AccountId& account_id) {
  // Fake accounts used in test may not have a valid account id key and will
  // fail.
  if (!account_id.HasAccountIdKey()) {
    return false;
  }
  auto* pref_service = session_delegate_->GetPrefService(account_id);
  const auto migration_status = static_cast<MigrationStatus>(
      pref_service->GetInteger(prefs::kWallpaperSeaPenMigrationStatus));
  base::UmaHistogramEnumeration(kMigrationHistogramName, migration_status);
  switch (migration_status) {
    case MigrationStatus::kNotStarted:
    case MigrationStatus::kFailed:
      return true;
    case MigrationStatus::kCrashed:
    case MigrationStatus::kSuccess:
      return false;
  }
  return true;
}

void SeaPenWallpaperManager::Migrate(
    const AccountId& account_id,
    const base::FilePath& source,
    MigrateSeaPenFilesIfNecessaryCallback callback) {
  DVLOG(0) << "Begin SeaPen migration";
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  DCHECK(ShouldMigrate(account_id));
  DCHECK(!source.empty());
  DCHECK_EQ(source.BaseName().value(), account_id.GetAccountIdKey());

  auto* pref_service = session_delegate_->GetPrefService(account_id);

  const base::FilePath target =
      session_delegate_->GetStorageDirectory(account_id);

  if (target.empty()) {
    LOG(ERROR) << "Storage directory path is empty, try migration later";
    pref_service->SetInteger(::ash::prefs::kWallpaperSeaPenMigrationStatus,
                             base::to_underlying(MigrationStatus::kFailed));
    base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
        FROM_HERE, base::BindOnce(std::move(callback), false));
    return;
  }

  auto migration_task = base::BindOnce(&MigrateFiles, source, target);

  auto on_migration_complete = base::BindOnce(
      &SeaPenWallpaperManager::OnMigrationComplete, weak_factory_.GetWeakPtr(),
      account_id, std::move(callback));

  // Preemptively write kCrashed status. Success or failure will overwrite this
  // value. If the user begins migration again and still has `kCrashed` status,
  // something went wrong.
  pref_service->SetInteger(::ash::prefs::kWallpaperSeaPenMigrationStatus,
                           base::to_underlying(MigrationStatus::kCrashed));

  pref_service->CommitPendingWrite(base::BindOnce(
      &SeaPenWallpaperManager::BeginMigration, weak_factory_.GetWeakPtr(),
      std::move(migration_task), std::move(on_migration_complete)));
}

void SeaPenWallpaperManager::SetSessionDelegateForTesting(
    std::unique_ptr<SessionDelegate> session_delegate) {
  session_delegate_ = std::move(session_delegate);
}

base::FilePath SeaPenWallpaperManager::GetFilePathForImageId(
    const AccountId& account_id,
    const uint32_t image_id) const {
  return session_delegate_->GetStorageDirectory(account_id)
      .Append(base::NumberToString(image_id))
      .AddExtension(".jpg");
}

void SeaPenWallpaperManager::BeginMigration(
    base::OnceCallback<bool()> migration_task,
    base::OnceCallback<void(bool)> on_migration_complete) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  DVLOG(0) << __PRETTY_FUNCTION__;
  blocking_task_runner_->PostTaskAndReplyWithResult(
      FROM_HERE, std::move(migration_task), std::move(on_migration_complete));
}

void SeaPenWallpaperManager::OnMigrationComplete(
    const AccountId& account_id,
    MigrateSeaPenFilesIfNecessaryCallback callback,
    bool success) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  DVLOG(0) << __func__ << " " << (success ? "succeeded" : "failed");

  auto* pref_service = session_delegate_->GetPrefService(account_id);

  pref_service->SetInteger(
      ::ash::prefs::kWallpaperSeaPenMigrationStatus,
      base::to_underlying(success ? MigrationStatus::kSuccess
                                  : MigrationStatus::kFailed));

  std::move(callback).Run(success);
}

void SeaPenWallpaperManager::OnSeaPenImageDecoded(
    const AccountId& account_id,
    const uint32_t image_id,
    const personalization_app::mojom::SeaPenQueryPtr& query,
    SaveSeaPenImageCallback callback,
    const gfx::ImageSkia& image_skia) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (image_skia.isNull()) {
    LOG(ERROR) << __func__ << "Failed to decode Sea Pen image";
    std::move(callback).Run(/*success=*/false);
    return;
  }
  DVLOG(2) << __func__ << " image_skia.size()=" << image_skia.size().ToString();

  const base::FilePath image_path = GetFilePathForImageId(account_id, image_id);

  blocking_task_runner_->PostTaskAndReplyWithResult(
      FROM_HERE,
      base::BindOnce(&CreateDirectoryAndLogError, image_path.DirName())
          .Then(
              base::BindOnce(&MaybeDeleteOldSeaPenImages, image_path.DirName()))
          .Then(base::BindOnce(&ResizeAndSaveWallpaper, image_skia, image_path,
                               WallpaperLayout::WALLPAPER_LAYOUT_CENTER_CROPPED,
                               image_skia.size(),
                               QueryDictToXmpString(SeaPenQueryToDict(query)))),
      std::move(callback));
}

void SeaPenWallpaperManager::OnFileRead(GetImageAndMetadataCallback callback,
                                        const std::string data) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (data.empty()) {
    LOG(WARNING) << "Unable to read file";
    std::move(callback).Run(gfx::ImageSkia(), nullptr);
    return;
  }
  image_util::DecodeImageData(
      base::BindOnce(OnDecodeImageData, std::move(callback),
                     ExtractDcDescriptionContents(data)),
      data_decoder::mojom::ImageCodec::kDefault, data);
}

void SeaPenWallpaperManager::OnFileReadGetTemplateId(
    GetTemplateIdFromFileCallback callback,
    const std::string& data) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (data.empty()) {
    LOG(WARNING) << "Unable to read file";
    std::move(callback).Run(std::nullopt);
    return;
  }

  DecodeJsonMetadataGetTemplateId(ExtractDcDescriptionContents(data),
                                  std::move(callback));
}

}  // namespace ash