chromium/ash/public/cpp/holding_space/holding_space_image.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 "ash/public/cpp/holding_space/holding_space_image.h"

#include <algorithm>
#include <map>
#include <set>

#include "ash/public/cpp/holding_space/holding_space_constants.h"
#include "ash/public/cpp/image_util.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/location.h"
#include "chromeos/ui/base/file_icon_util.h"
#include "ui/gfx/image/image_skia_operations.h"
#include "ui/gfx/image/image_skia_rep.h"
#include "ui/gfx/image/image_skia_source.h"
#include "ui/gfx/paint_vector_icon.h"
#include "ui/gfx/skia_util.h"

namespace ash {
namespace {

// Appearance.
constexpr int kFileTypeIconSize = 20;

// Whether image invalidation should be done without delay.
bool g_use_zero_invalidation_delay_for_testing = false;

}  // namespace

// HoldingSpaceImage::ImageSkiaSource ------------------------------------------

class HoldingSpaceImage::ImageSkiaSource : public gfx::ImageSkiaSource {
 public:
  ImageSkiaSource(const base::WeakPtr<HoldingSpaceImage>& host,
                  const gfx::ImageSkia& placeholder)
      : host_(host), placeholder_(placeholder) {}

  ImageSkiaSource(const ImageSkiaSource&) = delete;
  ImageSkiaSource& operator=(const ImageSkiaSource&) = delete;
  ~ImageSkiaSource() override = default;

 private:
  // gfx::ImageSkiaSource:
  gfx::ImageSkiaRep GetImageForScale(float scale) override {
    if (host_)
      host_->LoadBitmap(scale);

    // Use `placeholder_` while we wait for the async bitmap to resolve.
    return placeholder_.GetRepresentation(scale);
  }

  const base::WeakPtr<HoldingSpaceImage> host_;
  const gfx::ImageSkia placeholder_;
};

// HoldingSpaceImage -----------------------------------------------------------

HoldingSpaceImage::HoldingSpaceImage(const gfx::Size& max_size,
                                     const base::FilePath& backing_file_path,
                                     AsyncBitmapResolver async_bitmap_resolver)
    : HoldingSpaceImage(max_size,
                        backing_file_path,
                        async_bitmap_resolver,
                        CreateDefaultPlaceholderImageSkiaResolver()) {}

HoldingSpaceImage::HoldingSpaceImage(
    const gfx::Size& max_size,
    const base::FilePath& backing_file_path,
    AsyncBitmapResolver async_bitmap_resolver,
    PlaceholderImageSkiaResolver placeholder_image_skia_resolver)
    : max_size_(max_size),
      backing_file_path_(backing_file_path),
      async_bitmap_resolver_(async_bitmap_resolver),
      placeholder_image_skia_resolver_(placeholder_image_skia_resolver) {
  placeholder_ = placeholder_image_skia_resolver_.Run(
      backing_file_path_, max_size_, /*dark_background=*/std::nullopt,
      /*is_folder=*/std::nullopt);
  CreateImageSkia();
}

HoldingSpaceImage::~HoldingSpaceImage() = default;

// static
HoldingSpaceImage::PlaceholderImageSkiaResolver
HoldingSpaceImage::CreateDefaultPlaceholderImageSkiaResolver(
    bool use_light_mode_as_default) {
  return base::BindRepeating(
      [](bool use_light_mode_as_default,
         const base::FilePath& backing_file_path, const gfx::Size& size,
         const std::optional<bool>& dark_background,
         const std::optional<bool>& is_folder) {
        // The requested image `size` should be >= `kFileTypeIconSize` to
        // give the `file_type_icon` generated below enough space to fully
        // paint.
        DCHECK_GE(size.height(), kFileTypeIconSize);
        DCHECK_GE(size.width(), kFileTypeIconSize);

        const gfx::ImageSkia file_type_icon =
            is_folder.value_or(false)
                ? chromeos::GetIconFromType(
                      chromeos::IconType::kFolder,
                      dark_background.value_or(!use_light_mode_as_default))
                : chromeos::GetIconForPath(
                      backing_file_path,
                      dark_background.value_or(!use_light_mode_as_default));

        return gfx::ImageSkiaOperations::CreateSuperimposedImage(
            image_util::CreateEmptyImage(size), file_type_icon);
      },
      use_light_mode_as_default);
}

// static
void HoldingSpaceImage::SetUseZeroInvalidationDelayForTesting(bool value) {
  g_use_zero_invalidation_delay_for_testing = value;
}

bool HoldingSpaceImage::operator==(const HoldingSpaceImage& rhs) const {
  return gfx::BitmapsAreEqual(*image_skia_.bitmap(), *rhs.image_skia_.bitmap());
}

base::CallbackListSubscription HoldingSpaceImage::AddImageSkiaChangedCallback(
    CallbackList::CallbackType callback) const {
  return callback_list_.Add(std::move(callback));
}

void HoldingSpaceImage::LoadBitmap(float scale) {
  async_bitmap_resolver_.Run(
      backing_file_path_, gfx::ScaleToCeiledSize(image_skia_.size(), scale),
      base::BindOnce(&HoldingSpaceImage::OnBitmapLoaded,
                     weak_factory_.GetWeakPtr(), backing_file_path_, scale));
}

void HoldingSpaceImage::OnBitmapLoaded(const base::FilePath& file_path,
                                       float scale,
                                       const SkBitmap* bitmap,
                                       base::File::Error error) {
  if (!bitmap) {
    DCHECK_NE(error, base::File::FILE_OK);
    if (backing_file_path_ != file_path) {
      // Retry load if the backing file path has changed while the image load
      // was in progress.
      LoadBitmap(scale);
    } else {
      // A new `error` may mean a better file type image can be displayed. The
      // `error`, for example, may indicate that the file is in fact a folder in
      // which case there is a more appropriate icon that can be shown. Notify
      // subscribers to invalidate themselves.
      async_bitmap_resolver_error_ = error;
      callback_list_.Notify();
    }
    return;
  }

  DCHECK_EQ(error, base::File::FILE_OK);
  async_bitmap_resolver_error_ = base::File::FILE_OK;

  // Force invalidate `image_skia_` for `scale` so that it will request the
  // updated `gfx::ImageSkiaRep` at next access.
  image_skia_.RemoveRepresentation(scale);
  image_skia_.AddRepresentation(gfx::ImageSkiaRep(*bitmap, scale));
  image_skia_.RemoveUnsupportedRepresentationsForScale(scale);

  // Update the placeholder image, so the newly loaded representation becomes
  // the default for any `ImageSkia` instances created when the holding space
  // image gets invalidated.
  placeholder_.RemoveRepresentation(scale);
  placeholder_.AddRepresentation(gfx::ImageSkiaRep(*bitmap, scale));
  placeholder_.RemoveUnsupportedRepresentationsForScale(scale);

  callback_list_.Notify();
}

gfx::ImageSkia HoldingSpaceImage::GetImageSkia(
    const std::optional<gfx::Size>& opt_size,
    bool dark_background) const {
  const gfx::Size size = opt_size.value_or(max_size_);

  // Requested `size` must be less than or equal to `max_size_` to avoid
  // pixelation that would otherwise occur due to upscaling.
  DCHECK_LE(size.height(), max_size_.height());
  DCHECK_LE(size.width(), max_size_.width());

  // When an error occurs, fallback to a resolved placeholder image.
  if (async_bitmap_resolver_error_ &&
      async_bitmap_resolver_error_ != base::File::FILE_OK) {
    return placeholder_image_skia_resolver_.Run(
        backing_file_path_, size, dark_background,
        /*is_folder=*/async_bitmap_resolver_error_ ==
            base::File::FILE_ERROR_NOT_A_FILE);
  }

  // Short-circuit resizing logic.
  if (image_skia_.size() == size)
    return image_skia_;

  return image_util::ResizeAndCropImage(image_skia_, size);
}

void HoldingSpaceImage::Invalidate() {
  if (invalidate_timer_.IsRunning())
    return;

  // Schedule an invalidation task with a delay to reduce number of image loads
  // when multiple image invalidations are requested in quick succession. The
  // delay is selected somewhat arbitrarily to be non trivial but still not
  // easily noticeable by the user.
  invalidate_timer_.Start(FROM_HERE,
                          g_use_zero_invalidation_delay_for_testing
                              ? base::TimeDelta()
                              : base::Milliseconds(250),
                          base::BindOnce(&HoldingSpaceImage::OnInvalidateTimer,
                                         base::Unretained(this)));
}

void HoldingSpaceImage::UpdateBackingFilePath(const base::FilePath& file_path) {
  backing_file_path_ = file_path;
}

bool HoldingSpaceImage::UsingPlaceholder() const {
  return !async_bitmap_resolver_error_ ||
         *async_bitmap_resolver_error_ != base::File::FILE_OK;
}

bool HoldingSpaceImage::FireInvalidateTimerForTesting() {
  if (!invalidate_timer_.IsRunning())
    return false;
  invalidate_timer_.FireNow();
  return true;
}

void HoldingSpaceImage::OnInvalidateTimer() {
  // Invalidate the existing pointers to:
  // *   Invalidate the previous `image_skia_`'s host pointer, and prevent it
  //     from requesting bitmap loads.
  // *   Prevent pending bitmap request callbacks from running.
  weak_factory_.InvalidateWeakPtrs();

  // Cache scales for which the previous `image_skia_` has reps.
  std::set<float> scales;
  for (const gfx::ImageSkiaRep& image_rep : image_skia_.image_reps())
    scales.insert(image_rep.scale());

  // Create a new `image_skia_`.
  CreateImageSkia();

  // If the `async_bitmap_resolver_` has not previously failed, notify
  // subscribers that the `image_skia_` has been invalidated. A new bitmap will
  // be asynchronously resolved at the next `GetImageSkia()` invocation.
  if (!async_bitmap_resolver_error_ ||
      async_bitmap_resolver_error_ == base::File::FILE_OK) {
    callback_list_.Notify();
    return;
  }

  // If the `async_bitmap_resolver_` failed previously, force asynchronous
  // resolution of new bitmaps. Failure to do so would cause `GetImageSkia()`
  // invocations to continue returning placeholders. Note that subscribers will
  // be notified of `image_skia_` invalidation when the `async_bitmap_resolver_`
  // returns.
  DCHECK(!scales.empty());
  for (const float& scale : scales)
    image_skia_.GetRepresentation(scale);
}

void HoldingSpaceImage::CreateImageSkia() {
  image_skia_ =
      gfx::ImageSkia(std::make_unique<ImageSkiaSource>(
                         /*host=*/weak_factory_.GetWeakPtr(), placeholder_),
                     max_size_);
}

}  // namespace ash