blob: ee60ae65ee8bdd2d4ff71430ba9207e3ab8e78c2 [file] [log] [blame]
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/installable/installable_manager.h"
#include <utility>
#include "base/bind.h"
#include "base/bind_helpers.h"
#include "base/callback.h"
#include "base/stl_util.h"
#include "base/strings/string_util.h"
#include "base/threading/sequenced_task_runner_handle.h"
#include "build/build_config.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ssl/security_state_tab_helper.h"
#include "components/security_state/core/security_state.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/manifest_icon_downloader.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/storage_partition.h"
#include "content/public/common/origin_util.h"
#include "content/public/common/url_constants.h"
#include "net/base/url_util.h"
#include "services/network/public/cpp/is_potentially_trustworthy.h"
#include "third_party/blink/public/common/manifest/manifest_icon_selector.h"
#include "third_party/blink/public/mojom/manifest/display_mode.mojom.h"
#include "url/origin.h"
#if defined(OS_ANDROID)
#include "chrome/browser/android/shortcut_helper.h"
#endif
namespace {
// This constant is the icon size on Android (48dp) multiplied by the scale
// factor of a Nexus 5 device (3x). It is the currently advertised minimum icon
// size for triggering banners.
const int kMinimumPrimaryIconSizeInPx = 144;
// 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;
int GetIdealPrimaryIconSizeInPx() {
#if defined(OS_ANDROID)
return ShortcutHelper::GetIdealHomescreenIconSizeInPx();
#else
return kMinimumPrimaryIconSizeInPx;
#endif
}
int GetMinimumPrimaryIconSizeInPx() {
#if defined(OS_ANDROID)
return ShortcutHelper::GetMinimumHomescreenIconSizeInPx();
#else
return kMinimumPrimaryIconSizeInPx;
#endif
}
int GetIdealPrimaryAdaptiveLauncherIconSizeInPx() {
#if defined(OS_ANDROID)
return ShortcutHelper::GetIdealAdaptiveLauncherIconSizeInPx();
#else
return kMinimumPrimaryAdaptiveLauncherIconSizeInPx;
#endif
}
int GetIdealSplashIconSizeInPx() {
#if defined(OS_ANDROID)
return ShortcutHelper::GetIdealSplashImageSizeInPx();
#else
return kMinimumPrimaryIconSizeInPx;
#endif
}
int GetMinimumSplashIconSizeInPx() {
#if defined(OS_ANDROID)
return ShortcutHelper::GetMinimumSplashImageSizeInPx();
#else
return kMinimumPrimaryIconSizeInPx;
#endif
}
using IconPurpose = blink::Manifest::ImageResource::Purpose;
struct ImageTypeDetails {
const char* extension;
const char* mimetype;
};
constexpr ImageTypeDetails kSupportedImageTypes[] = {
{".png", "image/png"},
// TODO(https://crbug.com/578122): Add SVG support for Android.
// TODO(https://crbug.com/466958): Add WebP support for Android.
#if !defined(OS_ANDROID)
{".svg", "image/svg+xml"},
{".webp", "image/webp"},
#endif
};
bool IsIconTypeSupported(const blink::Manifest::ImageResource& icon) {
// The type field is optional. If it isn't present, fall back on checking
// the src extension.
if (icon.type.empty()) {
std::string filename = icon.src.ExtractFileName();
for (const ImageTypeDetails& details : kSupportedImageTypes) {
if (base::EndsWith(filename, details.extension,
base::CompareCase::INSENSITIVE_ASCII)) {
return true;
}
}
return false;
}
for (const ImageTypeDetails& details : kSupportedImageTypes) {
if (base::EqualsASCII(icon.type, details.mimetype))
return true;
}
return false;
}
// Returns true if |manifest| specifies an SVG or PNG icon that either
// 1. has IconPurpose::ANY, with height and width >= kMinimumPrimaryIconSizeInPx
// (or size "any")
// 2. if maskable icon is preferred, has IconPurpose::MASKABLE with height and
// width >= kMinimumPrimaryAdaptiveLauncherIconSizeInPx (or size "any")
bool DoesManifestContainRequiredIcon(const blink::Manifest& manifest,
bool prefer_maskable_icon) {
for (const auto& icon : manifest.icons) {
if (!IsIconTypeSupported(icon))
continue;
if (!(base::Contains(icon.purpose,
blink::Manifest::ImageResource::Purpose::ANY) ||
(prefer_maskable_icon &&
base::Contains(
icon.purpose,
blink::Manifest::ImageResource::Purpose::MASKABLE)))) {
continue;
}
for (const auto& size : icon.sizes) {
if (size.IsEmpty()) // "any"
return true;
if (base::Contains(icon.purpose,
blink::Manifest::ImageResource::Purpose::ANY) &&
size.width() >= kMinimumPrimaryIconSizeInPx &&
size.height() >= kMinimumPrimaryIconSizeInPx) {
return true;
}
if (prefer_maskable_icon &&
base::Contains(icon.purpose,
blink::Manifest::ImageResource::Purpose::MASKABLE) &&
size.height() >= kMinimumPrimaryAdaptiveLauncherIconSizeInPx &&
size.width() >= kMinimumPrimaryAdaptiveLauncherIconSizeInPx) {
return true;
}
}
}
return false;
}
// Returns true if |params| specifies a full PWA check.
bool IsParamsForPwaCheck(const InstallableParams& params) {
return params.valid_manifest && params.has_worker &&
params.valid_primary_icon;
}
void OnDidCompleteGetAllErrors(
base::OnceCallback<void(std::vector<content::InstallabilityError>
installability_errors)> callback,
const InstallableData& data) {
std::vector<content::InstallabilityError> installability_errors;
for (auto error : data.errors) {
content::InstallabilityError installability_error =
GetInstallabilityError(error);
if (!installability_error.error_id.empty())
installability_errors.push_back(installability_error);
}
std::move(callback).Run(std::move(installability_errors));
}
void OnDidCompleteGetPrimaryIcon(
base::OnceCallback<void(const SkBitmap*)> callback,
const InstallableData& data) {
std::move(callback).Run(data.primary_icon);
}
} // namespace
InstallableManager::EligiblityProperty::EligiblityProperty() = default;
InstallableManager::EligiblityProperty::~EligiblityProperty() = default;
InstallableManager::ValidManifestProperty::ValidManifestProperty() = default;
InstallableManager::ValidManifestProperty::~ValidManifestProperty() = default;
InstallableManager::IconProperty::IconProperty()
: error(NO_ERROR_DETECTED),
purpose(IconPurpose::ANY),
icon(),
fetched(false) {}
InstallableManager::IconProperty::IconProperty(IconProperty&& other) = default;
InstallableManager::IconProperty::~IconProperty() {}
InstallableManager::IconProperty& InstallableManager::IconProperty::operator=(
InstallableManager::IconProperty&& other) = default;
InstallableManager::InstallableManager(content::WebContents* web_contents)
: content::WebContentsObserver(web_contents),
eligibility_(std::make_unique<EligiblityProperty>()),
manifest_(std::make_unique<ManifestProperty>()),
valid_manifest_(std::make_unique<ValidManifestProperty>()),
worker_(std::make_unique<ServiceWorkerProperty>()),
service_worker_context_(nullptr),
has_pwa_check_(false) {
// This is null in unit tests.
if (web_contents) {
content::StoragePartition* storage_partition =
content::BrowserContext::GetStoragePartition(
Profile::FromBrowserContext(web_contents->GetBrowserContext()),
web_contents->GetSiteInstance());
DCHECK(storage_partition);
service_worker_context_ = storage_partition->GetServiceWorkerContext();
service_worker_context_->AddObserver(this);
}
}
InstallableManager::~InstallableManager() {
if (service_worker_context_)
service_worker_context_->RemoveObserver(this);
}
// static
int InstallableManager::GetMinimumIconSizeInPx() {
return kMinimumPrimaryIconSizeInPx;
}
// static
bool InstallableManager::IsContentSecure(content::WebContents* web_contents) {
if (!web_contents)
return false;
// chrome:// URLs are considered secure.
const GURL& url = web_contents->GetLastCommittedURL();
if (url.scheme() == content::kChromeUIScheme)
return true;
// chrome-untrusted:// URLs are shipped with Chrome, so they are considered
// secure in this context.
if (url.scheme() == content::kChromeUIUntrustedScheme)
return true;
if (IsOriginConsideredSecure(url))
return true;
return security_state::IsSslCertificateValid(
SecurityStateTabHelper::FromWebContents(web_contents)
->GetSecurityLevel());
}
// static
bool InstallableManager::IsOriginConsideredSecure(const GURL& url) {
return net::IsLocalhost(url) ||
network::SecureOriginAllowlist::GetInstance().IsOriginAllowlisted(
url::Origin::Create(url));
}
void InstallableManager::GetData(const InstallableParams& params,
InstallableCallback callback) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
if (IsParamsForPwaCheck(params))
has_pwa_check_ = true;
// Return immediately if we're already working on a task. The new task will be
// looked at once the current task is finished.
bool was_active = task_queue_.HasCurrent();
task_queue_.Add({params, std::move(callback)});
if (was_active)
return;
WorkOnTask();
}
void InstallableManager::GetAllErrors(
base::OnceCallback<void(std::vector<content::InstallabilityError>
installability_errors)> callback) {
InstallableParams params;
params.check_eligibility = true;
params.valid_manifest = true;
params.check_webapp_manifest_display = true;
params.has_worker = true;
params.valid_primary_icon = true;
params.wait_for_worker = false;
params.is_debug_mode = true;
GetData(params,
base::BindOnce(OnDidCompleteGetAllErrors, std::move(callback)));
}
void InstallableManager::GetPrimaryIcon(
base::OnceCallback<void(const SkBitmap*)> callback) {
InstallableParams params;
params.valid_primary_icon = true;
GetData(params,
base::BindOnce(OnDidCompleteGetPrimaryIcon, std::move(callback)));
}
bool InstallableManager::IsIconFetchComplete(const IconUsage usage) const {
const auto it = icons_.find(usage);
if (it == icons_.end() || !it->second.fetched)
return false;
// If we fetched maskable icon, but fetching was not success, do not consider
// it's completed since we want to fallback to fetch ANY icon.
if (it->second.purpose == IconPurpose::MASKABLE &&
it->second.error != NO_ERROR_DETECTED) {
return false;
}
return true;
}
bool InstallableManager::IsMaskableIconFetched(const IconUsage usage) const {
const auto it = icons_.find(usage);
if (it == icons_.end() || !it->second.fetched)
return false;
// if we fetched MASKABLE icon, or fetched ANY icon for fallback, consider
// maskable icon is fetched.
return it->second.purpose == IconPurpose::MASKABLE ||
it->second.purpose == IconPurpose::ANY;
}
void InstallableManager::SetIconFetched(const IconUsage usage) {
icons_[usage].fetched = true;
}
std::vector<InstallableStatusCode> InstallableManager::GetErrors(
const InstallableParams& params) {
std::vector<InstallableStatusCode> errors;
if (params.check_eligibility && !eligibility_->errors.empty()) {
errors.insert(errors.end(), eligibility_->errors.begin(),
eligibility_->errors.end());
}
if (manifest_->error != NO_ERROR_DETECTED)
errors.push_back(manifest_->error);
if (params.valid_manifest && !valid_manifest_->errors.empty()) {
errors.insert(errors.end(), valid_manifest_->errors.begin(),
valid_manifest_->errors.end());
}
if (params.has_worker && worker_->error != NO_ERROR_DETECTED)
errors.push_back(worker_->error);
if (params.valid_primary_icon) {
IconProperty& icon = icons_[IconUsage::kPrimary];
// If the icon is MASKABLE, ignore any error since we want to fallback to
// fetch IconPurpose::ANY.
if (icon.error != NO_ERROR_DETECTED &&
icon.purpose != IconPurpose::MASKABLE)
errors.push_back(icon.error);
}
if (params.valid_splash_icon) {
IconProperty& icon = icons_[IconUsage::kSplash];
// If the error is NO_ACCEPTABLE_ICON, there is no icon suitable as a splash
// icon in the manifest. Ignore this case since we only want to fail the
// check if there was a suitable splash icon specified and we couldn't fetch
// it.
if (icon.error != NO_ERROR_DETECTED && icon.error != NO_ACCEPTABLE_ICON)
errors.push_back(icon.error);
}
return errors;
}
InstallableStatusCode InstallableManager::eligibility_error() const {
return eligibility_->errors.empty() ? NO_ERROR_DETECTED
: eligibility_->errors[0];
}
InstallableStatusCode InstallableManager::manifest_error() const {
return manifest_->error;
}
InstallableStatusCode InstallableManager::valid_manifest_error() const {
return valid_manifest_->errors.empty() ? NO_ERROR_DETECTED
: valid_manifest_->errors[0];
}
void InstallableManager::set_valid_manifest_error(
InstallableStatusCode error_code) {
valid_manifest_->errors.clear();
if (error_code != NO_ERROR_DETECTED)
valid_manifest_->errors.push_back(error_code);
}
InstallableStatusCode InstallableManager::worker_error() const {
return worker_->error;
}
InstallableStatusCode InstallableManager::icon_error(const IconUsage usage) {
return icons_[usage].error;
}
GURL& InstallableManager::icon_url(const IconUsage usage) {
return icons_[usage].url;
}
const SkBitmap* InstallableManager::icon(const IconUsage usage) {
return icons_[usage].icon.get();
}
content::WebContents* InstallableManager::GetWebContents() {
content::WebContents* contents = web_contents();
if (!contents || contents->IsBeingDestroyed())
return nullptr;
return contents;
}
bool InstallableManager::IsComplete(const InstallableParams& params) const {
// Returns true if for all resources:
// a. the params did not request it, OR
// b. the resource has been fetched/checked.
return (!params.check_eligibility || eligibility_->fetched) &&
manifest_->fetched &&
(!params.valid_manifest || valid_manifest_->fetched) &&
(!params.has_worker || worker_->fetched) &&
(!params.valid_primary_icon ||
IsIconFetchComplete(IconUsage::kPrimary)) &&
(!params.valid_splash_icon || IsIconFetchComplete(IconUsage::kSplash));
}
void InstallableManager::Reset() {
// Prevent any outstanding callbacks to or from this object from being called.
weak_factory_.InvalidateWeakPtrs();
icons_.clear();
// If we have paused tasks, we are waiting for a service worker.
task_queue_.Reset();
has_pwa_check_ = false;
eligibility_ = std::make_unique<EligiblityProperty>();
manifest_ = std::make_unique<ManifestProperty>();
valid_manifest_ = std::make_unique<ValidManifestProperty>();
worker_ = std::make_unique<ServiceWorkerProperty>();
OnResetData();
}
void InstallableManager::SetManifestDependentTasksComplete() {
valid_manifest_->fetched = true;
worker_->fetched = true;
SetIconFetched(IconUsage::kPrimary);
SetIconFetched(IconUsage::kSplash);
}
void InstallableManager::CleanupAndStartNextTask() {
// Sites can always register a service worker after we finish checking, so
// don't cache a missing service worker error to ensure we always check
// again.
if (worker_error() == NO_MATCHING_SERVICE_WORKER)
worker_ = std::make_unique<ServiceWorkerProperty>();
task_queue_.Next();
WorkOnTask();
}
void InstallableManager::RunCallback(
InstallableTask task,
std::vector<InstallableStatusCode> errors) {
const InstallableParams& params = task.params;
IconProperty null_icon;
IconProperty* primary_icon = &null_icon;
bool has_maskable_primary_icon = false;
IconProperty* splash_icon = &null_icon;
if (params.valid_primary_icon && IsIconFetchComplete(IconUsage::kPrimary)) {
primary_icon = &icons_[IconUsage::kPrimary];
has_maskable_primary_icon =
(primary_icon->purpose == IconPurpose::MASKABLE);
}
if (params.valid_splash_icon && IsIconFetchComplete(IconUsage::kSplash))
splash_icon = &icons_[IconUsage::kSplash];
InstallableData data = {
std::move(errors), manifest_url(), &manifest(),
primary_icon->url, primary_icon->icon.get(), has_maskable_primary_icon,
splash_icon->url, splash_icon->icon.get(), valid_manifest_->is_valid,
worker_->has_worker,
};
std::move(task.callback).Run(data);
}
void InstallableManager::WorkOnTask() {
if (!task_queue_.HasCurrent())
return;
const InstallableParams& params = task_queue_.Current().params;
auto errors = GetErrors(params);
bool check_passed = errors.empty();
if ((!check_passed && !params.is_debug_mode) || IsComplete(params)) {
// Yield the UI thread before processing the next task. If this object is
// deleted in the meantime, the next task naturally won't run.
base::SequencedTaskRunnerHandle::Get()->PostTask(
FROM_HERE, base::BindOnce(&InstallableManager::CleanupAndStartNextTask,
weak_factory_.GetWeakPtr()));
auto task = std::move(task_queue_.Current());
RunCallback(std::move(task), std::move(errors));
return;
}
if (params.check_eligibility && !eligibility_->fetched) {
CheckEligiblity();
} else if (!manifest_->fetched) {
FetchManifest();
} else if (params.valid_primary_icon && params.prefer_maskable_icon &&
!IsMaskableIconFetched(IconUsage::kPrimary)) {
CheckAndFetchBestIcon(GetIdealPrimaryAdaptiveLauncherIconSizeInPx(),
kMinimumPrimaryAdaptiveLauncherIconSizeInPx,
IconPurpose::MASKABLE, IconUsage::kPrimary);
} else if (params.valid_primary_icon &&
!IsIconFetchComplete(IconUsage::kPrimary)) {
CheckAndFetchBestIcon(GetIdealPrimaryIconSizeInPx(),
GetMinimumPrimaryIconSizeInPx(), IconPurpose::ANY,
IconUsage::kPrimary);
} else if (params.valid_manifest && !valid_manifest_->fetched) {
CheckManifestValid(params.check_webapp_manifest_display,
params.prefer_maskable_icon);
} else if (params.has_worker && !worker_->fetched) {
CheckServiceWorker();
} else if (params.valid_splash_icon &&
!IsIconFetchComplete(IconUsage::kSplash)) {
CheckAndFetchBestIcon(GetIdealSplashIconSizeInPx(),
GetMinimumSplashIconSizeInPx(), IconPurpose::ANY,
IconUsage::kSplash);
} else {
NOTREACHED();
}
}
void InstallableManager::CheckEligiblity() {
// Fail if this is an incognito window, non-main frame, or insecure context.
content::WebContents* web_contents = GetWebContents();
if (Profile::FromBrowserContext(web_contents->GetBrowserContext())
->IsOffTheRecord()) {
eligibility_->errors.push_back(IN_INCOGNITO);
}
if (web_contents->GetMainFrame()->GetParent()) {
eligibility_->errors.push_back(NOT_IN_MAIN_FRAME);
}
if (!IsContentSecure(web_contents)) {
eligibility_->errors.push_back(NOT_FROM_SECURE_ORIGIN);
}
eligibility_->fetched = true;
WorkOnTask();
}
void InstallableManager::FetchManifest() {
DCHECK(!manifest_->fetched);
content::WebContents* web_contents = GetWebContents();
DCHECK(web_contents);
web_contents->GetManifest(base::BindOnce(
&InstallableManager::OnDidGetManifest, weak_factory_.GetWeakPtr()));
}
void InstallableManager::OnDidGetManifest(const GURL& manifest_url,
const blink::Manifest& manifest) {
if (!GetWebContents())
return;
if (manifest_url.is_empty()) {
manifest_->error = NO_MANIFEST;
SetManifestDependentTasksComplete();
} else if (manifest.IsEmpty()) {
manifest_->error = MANIFEST_EMPTY;
SetManifestDependentTasksComplete();
}
manifest_->url = manifest_url;
manifest_->manifest = manifest;
manifest_->fetched = true;
WorkOnTask();
}
void InstallableManager::CheckManifestValid(bool check_webapp_manifest_display,
bool prefer_maskable_icon) {
DCHECK(!valid_manifest_->fetched);
DCHECK(!manifest().IsEmpty());
valid_manifest_->is_valid = IsManifestValidForWebApp(
manifest(), check_webapp_manifest_display, prefer_maskable_icon);
valid_manifest_->fetched = true;
WorkOnTask();
}
bool InstallableManager::IsManifestValidForWebApp(
const blink::Manifest& manifest,
bool check_webapp_manifest_display,
bool prefer_maskable_icon) {
bool is_valid = true;
if (manifest.IsEmpty()) {
valid_manifest_->errors.push_back(MANIFEST_EMPTY);
return false;
}
if (!manifest.start_url.is_valid()) {
valid_manifest_->errors.push_back(START_URL_NOT_VALID);
is_valid = false;
}
if ((manifest.name.is_null() || manifest.name.string().empty()) &&
(manifest.short_name.is_null() || manifest.short_name.string().empty())) {
valid_manifest_->errors.push_back(MANIFEST_MISSING_NAME_OR_SHORT_NAME);
is_valid = false;
}
if (check_webapp_manifest_display &&
manifest.display != blink::mojom::DisplayMode::kStandalone &&
manifest.display != blink::mojom::DisplayMode::kFullscreen &&
manifest.display != blink::mojom::DisplayMode::kMinimalUi) {
valid_manifest_->errors.push_back(MANIFEST_DISPLAY_NOT_SUPPORTED);
is_valid = false;
}
if (!DoesManifestContainRequiredIcon(manifest, prefer_maskable_icon)) {
valid_manifest_->errors.push_back(MANIFEST_MISSING_SUITABLE_ICON);
is_valid = false;
}
return is_valid;
}
void InstallableManager::CheckServiceWorker() {
DCHECK(!worker_->fetched);
DCHECK(!manifest().IsEmpty());
if (!service_worker_context_)
return;
// Check to see if there is a service worker for the manifest's scope.
service_worker_context_->CheckHasServiceWorker(
manifest().scope,
base::BindOnce(&InstallableManager::OnDidCheckHasServiceWorker,
weak_factory_.GetWeakPtr()));
}
void InstallableManager::OnDidCheckHasServiceWorker(
content::ServiceWorkerCapability capability) {
if (!GetWebContents())
return;
switch (capability) {
case content::ServiceWorkerCapability::SERVICE_WORKER_WITH_FETCH_HANDLER:
worker_->has_worker = true;
break;
case content::ServiceWorkerCapability::SERVICE_WORKER_NO_FETCH_HANDLER:
worker_->has_worker = false;
worker_->error = NOT_OFFLINE_CAPABLE;
break;
case content::ServiceWorkerCapability::NO_SERVICE_WORKER:
InstallableTask& task = task_queue_.Current();
if (task.params.wait_for_worker) {
// Wait for ServiceWorkerContextObserver::OnRegistrationCompleted. Set
// the param |wait_for_worker| to false so we only wait once per task.
task.params.wait_for_worker = false;
OnWaitingForServiceWorker();
task_queue_.PauseCurrent();
WorkOnTask();
return;
}
worker_->has_worker = false;
worker_->error = NO_MATCHING_SERVICE_WORKER;
break;
}
worker_->fetched = true;
WorkOnTask();
}
void InstallableManager::CheckAndFetchBestIcon(int ideal_icon_size_in_px,
int minimum_icon_size_in_px,
const IconPurpose purpose,
const IconUsage usage) {
DCHECK(!manifest().IsEmpty());
IconProperty& icon = icons_[usage];
icon.fetched = true;
icon.purpose = purpose;
icon.error = NO_ERROR_DETECTED;
GURL icon_url = blink::ManifestIconSelector::FindBestMatchingSquareIcon(
manifest().icons, ideal_icon_size_in_px, minimum_icon_size_in_px,
purpose);
if (icon_url.is_empty()) {
icon.error = NO_ACCEPTABLE_ICON;
} else {
bool can_download_icon = content::ManifestIconDownloader::Download(
GetWebContents(), icon_url, ideal_icon_size_in_px,
minimum_icon_size_in_px,
base::BindOnce(&InstallableManager::OnIconFetched,
weak_factory_.GetWeakPtr(), icon_url, usage));
if (can_download_icon)
return;
icon.error = CANNOT_DOWNLOAD_ICON;
}
WorkOnTask();
}
void InstallableManager::OnIconFetched(const GURL icon_url,
const IconUsage usage,
const SkBitmap& bitmap) {
if (!GetWebContents())
return;
IconProperty& icon = icons_[usage];
if (bitmap.drawsNothing()) {
icon.error = NO_ICON_AVAILABLE;
} else {
icon.url = icon_url;
icon.icon.reset(new SkBitmap(bitmap));
}
WorkOnTask();
}
void InstallableManager::OnRegistrationCompleted(const GURL& pattern) {
// If the scope doesn't match we keep waiting.
if (!content::ServiceWorkerContext::ScopeMatches(pattern, manifest().scope))
return;
bool was_active = task_queue_.HasCurrent();
// The existence of paused tasks implies that we are waiting for a service
// worker. We move any paused tasks back into the main queue so that the
// pipeline will call CheckHasServiceWorker again, in order to find out if
// the SW has a fetch handler.
// NOTE: If there are no paused tasks, that means:
// a) we've already failed the check, or
// b) we haven't yet called CheckHasServiceWorker.
task_queue_.UnpauseAll();
if (was_active)
return; // If the pipeline was already running, we don't restart it.
WorkOnTask();
}
void InstallableManager::OnDestruct(content::ServiceWorkerContext* context) {
service_worker_context_->RemoveObserver(this);
service_worker_context_ = nullptr;
}
void InstallableManager::DidFinishNavigation(
content::NavigationHandle* handle) {
if (handle->IsInMainFrame() && handle->HasCommitted() &&
!handle->IsSameDocument()) {
Reset();
}
}
void InstallableManager::DidUpdateWebManifestURL(
const base::Optional<GURL>& manifest_url) {
// A change in the manifest URL invalidates our entire internal state.
Reset();
}
void InstallableManager::WebContentsDestroyed() {
Reset();
Observe(nullptr);
}
const GURL& InstallableManager::manifest_url() const {
return manifest_->url;
}
const blink::Manifest& InstallableManager::manifest() const {
return manifest_->manifest;
}
bool InstallableManager::valid_manifest() {
return valid_manifest_->is_valid;
}
bool InstallableManager::has_worker() {
return worker_->has_worker;
}
WEB_CONTENTS_USER_DATA_KEY_IMPL(InstallableManager)