chromium/chrome/browser/apps/almanac_api_client/almanac_app_icon_loader.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/apps/almanac_api_client/almanac_app_icon_loader.h"

#include "base/functional/bind.h"
#include "base/one_shot_event.h"
#include "base/strings/string_util.h"
#include "chrome/browser/apps/almanac_api_client/almanac_icon_cache.h"
#include "chrome/browser/apps/app_service/app_icon/app_icon_factory.h"
#include "chrome/browser/image_fetcher/image_fetcher_service_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_key.h"
#include "components/services/app_service/public/cpp/icon_effects.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_contents_observer.h"
#include "net/traffic_annotation/network_traffic_annotation.h"
#include "ui/gfx/image/image.h"
#include "url/gurl.h"

namespace apps {

namespace {

constexpr int kSvgRasterSize = 192;

bool IsSvgExtension(const GURL& icon_url, std::string_view icon_mime_type) {
  std::string url_string = icon_url.spec();
  return icon_mime_type.empty()
             ? base::EndsWith(url_string, ".svg", base::CompareCase::SENSITIVE)
             : icon_mime_type == "image/svg+xml";
}

}  // namespace

// Creates a background WebContents for downloading and rendering an SVG image.
class AlmanacAppIconLoader::SvgLoader : public content::WebContentsObserver {
 public:
  explicit SvgLoader(Profile& profile) {
    // SVG icons require a full renderer to rasterize to a bitmap.
    web_contents_ = content::WebContents::Create(
        content::WebContents::CreateParams(&profile));

    // Load about:blank to initialise the WebContents before using it.
    Observe(web_contents_.get());
    content::NavigationController::LoadURLParams load_params{
        GURL(url::kAboutBlankURL)};
    load_params.transition_type = ui::PAGE_TRANSITION_GENERATED;
    web_contents_->GetController().LoadURLWithParams(load_params);
    // Logic continues in DidStopLoading().
  }

  ~SvgLoader() override {}

  // content::WebContentsObserver:
  void DidStopLoading() override {
    if (!web_contents_ready_.is_signaled()) {
      web_contents_ready_.Signal();
    }
  }

  void GetSvg(GURL svg_url,
              base::OnceCallback<void(std::optional<SkBitmap>)> callback) {
    web_contents_ready_.Post(
        FROM_HERE,
        base::BindOnce(base::IgnoreResult(&content::WebContents::DownloadImage),
                       web_contents_->GetWeakPtr(), svg_url,
                       /*is_favicon=*/false,
                       gfx::Size(kSvgRasterSize, kSvgRasterSize),
                       /*max_bitmap_size=*/0,  // No max size.
                       /*bypass_cache=*/false,
                       base::BindOnce(&SvgLoader::OnImageDownloaded,
                                      std::move(callback))));
  }

  void WebContentsDestroyed() override { Observe(nullptr); }

 private:
  static void OnImageDownloaded(
      base::OnceCallback<void(std::optional<SkBitmap>)> callback,
      int id,
      int http_status_code,
      const GURL& image_url,
      const std::vector<SkBitmap>& bitmaps,
      const std::vector<gfx::Size>& sizes) {
    if (!bitmaps.empty()) {
      std::move(callback).Run(bitmaps.front());
      return;
    }
    std::move(callback).Run(std::nullopt);
  }

  std::unique_ptr<content::WebContents> web_contents_;
  base::OneShotEvent web_contents_ready_;
};

AlmanacAppIconLoader::AlmanacAppIconLoader(Profile& profile)
    : profile_(profile.GetWeakPtr()) {}

AlmanacAppIconLoader::~AlmanacAppIconLoader() = default;

void AlmanacAppIconLoader::GetAppIcon(
    const GURL& icon_url,
    std::string_view icon_mime_type,
    bool icon_masking_allowed,
    base::OnceCallback<void(apps::IconValuePtr)> callback) {
  if (!profile_) {
    std::move(callback).Run(nullptr);
  }

  if (IsSvgExtension(icon_url, icon_mime_type)) {
    if (!svg_loader_) {
      svg_loader_ = std::make_unique<SvgLoader>(*profile_);
    }
    svg_loader_->GetSvg(
        icon_url, base::BindOnce(&AlmanacAppIconLoader::OnSvgLoaded,
                                 weak_ptr_factory_.GetWeakPtr(),
                                 icon_masking_allowed, std::move(callback)));
    return;
  }

  if (!icon_cache_) {
    icon_cache_ = std::make_unique<AlmanacIconCache>(profile_->GetProfileKey());
  }
  icon_cache_->GetIcon(
      icon_url, base::BindOnce(&AlmanacAppIconLoader::OnBitmapLoaded,
                               weak_ptr_factory_.GetWeakPtr(),
                               icon_masking_allowed, std::move(callback)));
}

void AlmanacAppIconLoader::OnSvgLoaded(
    bool icon_masking_allowed,
    base::OnceCallback<void(apps::IconValuePtr)> callback,
    std::optional<SkBitmap> bitmap) {
  if (!bitmap.has_value()) {
    std::move(callback).Run(nullptr);
    return;
  }

  ApplyIconEffects(icon_masking_allowed,
                   gfx::Image::CreateFrom1xBitmap(bitmap.value()),
                   std::move(callback));
}

void AlmanacAppIconLoader::OnBitmapLoaded(
    bool icon_masking_allowed,
    base::OnceCallback<void(apps::IconValuePtr)> callback,
    const gfx::Image& icon_bitmap) {
  ApplyIconEffects(icon_masking_allowed, icon_bitmap, std::move(callback));
}

void AlmanacAppIconLoader::ApplyIconEffects(
    bool icon_masking_allowed,
    gfx::Image icon_bitmap,
    base::OnceCallback<void(apps::IconValuePtr)> callback) {
  if (!profile_ || icon_bitmap.IsEmpty()) {
    std::move(callback).Run(nullptr);
    return;
  }

  apps::IconValuePtr icon_value = std::make_unique<apps::IconValue>();
  icon_value->icon_type = apps::IconType::kStandard;
  icon_value->is_placeholder_icon = false;
  icon_value->is_maskable_icon = icon_masking_allowed;
  icon_value->uncompressed = icon_bitmap.AsImageSkia();

  apps::ApplyIconEffects(
      profile_.get(), /*app_id=*/std::nullopt,
      icon_masking_allowed ? apps::IconEffects::kCrOsStandardMask
                           : apps::IconEffects::kCrOsStandardIcon,
      icon_bitmap.Width(), std::move(icon_value), std::move(callback));
}

}  // namespace apps