blob: 127f80a060932f73fab6a936e8f52500d958da47 [file] [log] [blame]
// 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 "components/webapps/browser/installable/installable_icon_fetcher.h"
#include "base/check_is_test.h"
#include "base/functional/callback.h"
#include "base/memory/scoped_refptr.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/single_thread_task_runner.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "base/threading/thread_restrictions.h"
#include "build/android_buildflags.h"
#include "components/favicon/content/large_icon_service_getter.h"
#include "components/favicon/core/large_icon_service.h"
#include "components/favicon_base/favicon_types.h"
#include "components/webapps/browser/features.h"
#include "components/webapps/browser/installable/installable_evaluator.h"
#include "content/public/browser/manifest_icon_downloader.h"
#include "content/public/browser/web_contents.h"
#include "third_party/blink/public/common/manifest/manifest_icon_selector.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "ui/gfx/codec/png_codec.h"
#include "url/gurl.h"
#if BUILDFLAG(IS_ANDROID)
#include "components/webapps/browser/android/webapps_icon_utils.h"
#endif
namespace webapps {
namespace {
// This constant is the smallest possible adaptive launcher icon size for any
// device density.
// The ideal icon size is 83dp (see documentation for
// R.dimen.webapk_adaptive_icon_size for discussion of maskable icon size). For
// a manifest to be valid, we do NOT need an maskable icon to be 83dp for the
// device's screen density. Instead, we only need the maskable icon be larger
// than (or equal to) 83dp in the smallest screen density (that is the mdpi
// screen density). For mdpi devices, 1dp is 1px. Therefore, we have 83px here.
// Requiring the minimum icon size (in pixel) independent of the device's screen
// density is because we use mipmap-anydpi-v26 to specify adaptive launcher
// icon, and it will make the icon adaptive as long as there is one usable
// maskable icon (if that icon is of wrong size, it'll be automatically
// resized).
const int kMinimumPrimaryAdaptiveLauncherIconSizeInPx = 83;
using IconPurpose = blink::mojom::ManifestImageResource_Purpose;
int GetIdealPrimaryIconSizeInPx(IconPurpose purpose) {
#if BUILDFLAG(IS_ANDROID)
if (purpose == IconPurpose::MASKABLE) {
return WebappsIconUtils::GetIdealAdaptiveLauncherIconSizeInPx();
} else {
return WebappsIconUtils::GetIdealHomescreenIconSizeInPx();
}
#else
if (purpose == IconPurpose::MASKABLE) {
return kMinimumPrimaryAdaptiveLauncherIconSizeInPx;
} else {
return InstallableEvaluator::GetMinimumIconSizeInPx();
}
#endif
}
int GetMinimumPrimaryIconSizeInPx(IconPurpose purpose) {
if (purpose == IconPurpose::MASKABLE) {
return kMinimumPrimaryAdaptiveLauncherIconSizeInPx;
} else {
#if BUILDFLAG(IS_ANDROID)
return WebappsIconUtils::GetMinimumHomescreenIconSizeInPx();
#else
return InstallableEvaluator::GetMinimumIconSizeInPx();
#endif
}
}
// On Android, |LargeIconWorker::GetLargeIconRawBitmap| will try to find the
// largest icon that is also larger than the minimum size from database, and
// scale to the ideal size. However it doesn't work on desktop as Chrome stores
// icons scaled to 16x16 and 32x32 in the database. We need to find other way to
// fetch favicon on desktop.
int GetMinimumFaviconForPrimaryIconSizeInPx() {
if (test::g_minimum_favicon_size_for_testing) {
CHECK_IS_TEST();
return test::g_minimum_favicon_size_for_testing;
} else {
#if BUILDFLAG(IS_ANDROID)
return features::kMinimumFaviconSize;
#else
NOTREACHED();
#endif
}
}
void ProcessFaviconInBackground(
const favicon_base::FaviconRawBitmapResult& bitmap_result,
scoped_refptr<base::SequencedTaskRunner> ui_thread_task_runner,
base::OnceCallback<void(const SkBitmap&)> success_callback,
base::OnceCallback<void(InstallableStatusCode)> failed_callback) {
SkBitmap decoded;
if (bitmap_result.is_valid()) {
base::AssertLongCPUWorkAllowed();
decoded = gfx::PNGCodec::Decode(*bitmap_result.bitmap_data);
}
int min_size = GetMinimumFaviconForPrimaryIconSizeInPx();
if (decoded.isNull() || decoded.width() < min_size ||
decoded.height() < min_size) {
ui_thread_task_runner->PostTask(
FROM_HERE, base::BindOnce(std::move(failed_callback),
InstallableStatusCode::NO_ACCEPTABLE_ICON));
return;
}
ui_thread_task_runner->PostTask(
FROM_HERE, base::BindOnce(std::move(success_callback), decoded));
}
#if BUILDFLAG(IS_DESKTOP_ANDROID)
// Generates a homescreen icon for `page_url` and posts a task to invoke
// `callback` on `ui_thread_task_runner.`
void GenerateHomeScreenIconInBackground(
const GURL& page_url,
scoped_refptr<base::SequencedTaskRunner> ui_thread_task_runner,
base::OnceCallback<void(const GURL& url, const SkBitmap&)> callback) {
SkBitmap bitmap =
WebappsIconUtils::GenerateHomeScreenIconInBackground(page_url);
ui_thread_task_runner->PostTask(
FROM_HERE, base::BindOnce(std::move(callback), page_url, bitmap));
}
#endif // BUILDFLAG(IS_DESKTOP_ANDROID)
} // namespace
namespace test {
int g_minimum_favicon_size_for_testing = 0;
}
InstallableIconFetcher::InstallableIconFetcher(
content::WebContents* web_contents,
InstallablePageData& data,
const std::vector<blink::Manifest::ImageResource>& manifest_icons,
bool prefer_maskable,
bool fetch_favicon,
base::OnceCallback<void(InstallableStatusCode)> finish_callback)
: web_contents_(web_contents->GetWeakPtr()),
page_data_(data),
manifest_icons_(manifest_icons),
prefer_maskable_(prefer_maskable),
fetch_favicon_(fetch_favicon),
finish_callback_(std::move(finish_callback)) {
downloading_icons_type_.push_back(IconPurpose::ANY);
if (prefer_maskable_) {
downloading_icons_type_.push_back(IconPurpose::MASKABLE);
}
TryFetchingNextIcon();
}
InstallableIconFetcher::~InstallableIconFetcher() = default;
void InstallableIconFetcher::TryFetchingNextIcon() {
while (!downloading_icons_type_.empty()) {
IconPurpose purpose = downloading_icons_type_.back();
downloading_icons_type_.pop_back();
GURL icon_url = blink::ManifestIconSelector::FindBestMatchingSquareIcon(
manifest_icons_.get(), GetIdealPrimaryIconSizeInPx(purpose),
GetMinimumPrimaryIconSizeInPx(purpose), purpose);
if (icon_url.is_empty()) {
continue;
}
bool can_download_icon = content::ManifestIconDownloader::Download(
web_contents_.get(), icon_url, GetIdealPrimaryIconSizeInPx(purpose),
GetMinimumPrimaryIconSizeInPx(purpose),
InstallableEvaluator::kMaximumIconSizeInPx,
base::BindOnce(&InstallableIconFetcher::OnManifestIconFetched,
weak_ptr_factory_.GetWeakPtr(), icon_url, purpose));
if (can_download_icon) {
// We have started to download the current icon, wait for it to complete.
return;
}
}
if (fetch_favicon_) {
FetchFavicon();
return;
}
MaybeEndWithError(InstallableStatusCode::NO_ACCEPTABLE_ICON);
}
void InstallableIconFetcher::OnManifestIconFetched(const GURL& icon_url,
const IconPurpose purpose,
const SkBitmap& bitmap) {
if (bitmap.drawsNothing()) {
TryFetchingNextIcon();
return;
}
OnIconFetched(icon_url, purpose, bitmap);
}
void InstallableIconFetcher::FetchFavicon() {
favicon::LargeIconService* favicon_service =
favicon::GetLargeIconService(web_contents_->GetBrowserContext());
if (!favicon_service) {
MaybeEndWithError(InstallableStatusCode::NO_ACCEPTABLE_ICON);
return;
}
favicon_service->GetLargeIconRawBitmapForPageUrl(
web_contents_->GetLastCommittedURL(),
GetIdealPrimaryIconSizeInPx(IconPurpose::ANY),
/*size_in_pixel_to_resize_to=*/std::nullopt,
favicon::LargeIconService::NoBigEnoughIconBehavior::kReturnBitmap,
base::BindOnce(&InstallableIconFetcher::OnFaviconFetched,
weak_ptr_factory_.GetWeakPtr()),
&favicon_task_tracker_);
}
void InstallableIconFetcher::OnFaviconFetched(
const favicon_base::LargeIconResult& result) {
if (!result.bitmap.is_valid()) {
MaybeEndWithError(InstallableStatusCode::NO_ACCEPTABLE_ICON);
return;
}
base::ThreadPool::PostTask(
FROM_HERE,
{base::MayBlock(), base::TaskPriority::USER_VISIBLE,
base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN},
base::BindOnce(&ProcessFaviconInBackground, result.bitmap,
base::SingleThreadTaskRunner::GetCurrentDefault(),
base::BindOnce(&InstallableIconFetcher::OnIconFetched,
weak_ptr_factory_.GetWeakPtr(),
result.bitmap.icon_url, IconPurpose::ANY),
base::BindOnce(&InstallableIconFetcher::MaybeEndWithError,
weak_ptr_factory_.GetWeakPtr())));
}
void InstallableIconFetcher::OnIconFetched(const GURL& icon_url,
const IconPurpose purpose,
const SkBitmap& bitmap) {
page_data_->OnPrimaryIconFetched(icon_url, purpose, bitmap);
std::move(finish_callback_).Run(InstallableStatusCode::NO_ERROR_DETECTED);
}
void InstallableIconFetcher::MaybeEndWithError(InstallableStatusCode code) {
#if BUILDFLAG(IS_DESKTOP_ANDROID)
// Desktop android will generate an icon if none is available.
base::ThreadPool::PostTask(
FROM_HERE,
{base::MayBlock(), base::TaskPriority::USER_VISIBLE,
base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN},
base::BindOnce(
&GenerateHomeScreenIconInBackground,
web_contents_->GetLastCommittedURL(),
base::SingleThreadTaskRunner::GetCurrentDefault(),
base::BindOnce(&InstallableIconFetcher::OnHomeScreenIconGenerated,
weak_ptr_factory_.GetWeakPtr())));
return;
#else
// Other platforms report an error if no icon is available.
EndWithError(code);
#endif // BUILDFLAG(IS_DESKTOP_ANDROID)
}
void InstallableIconFetcher::EndWithError(InstallableStatusCode code) {
page_data_->OnPrimaryIconFetchedError(code);
std::move(finish_callback_).Run(code);
}
#if BUILDFLAG(IS_DESKTOP_ANDROID)
void InstallableIconFetcher::OnHomeScreenIconGenerated(const GURL& page_url,
const SkBitmap& bitmap) {
if (bitmap.drawsNothing()) {
EndWithError(InstallableStatusCode::NO_ACCEPTABLE_ICON);
return;
}
OnIconFetched(page_url, IconPurpose::ANY, bitmap);
}
#endif
} // namespace webapps