| Index: chrome/browser/installable/installable_manager.cc
|
| diff --git a/chrome/browser/installable/installable_manager.cc b/chrome/browser/installable/installable_manager.cc
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..4ed52ce000bcaca08804e2f17a13be1504ba522e
|
| --- /dev/null
|
| +++ b/chrome/browser/installable/installable_manager.cc
|
| @@ -0,0 +1,444 @@
|
| +// 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 "base/bind.h"
|
| +#include "base/strings/string_util.h"
|
| +#include "chrome/browser/manifest/manifest_icon_downloader.h"
|
| +#include "chrome/browser/manifest/manifest_icon_selector.h"
|
| +#include "chrome/browser/profiles/profile.h"
|
| +#include "content/public/browser/browser_context.h"
|
| +#include "content/public/browser/browser_thread.h"
|
| +#include "content/public/browser/navigation_handle.h"
|
| +#include "content/public/browser/service_worker_context.h"
|
| +#include "content/public/browser/storage_partition.h"
|
| +#include "third_party/WebKit/public/platform/WebDisplayMode.h"
|
| +
|
| +namespace {
|
| +
|
| +const char kPngExtension[] = ".png";
|
| +
|
| +// This constant is the icon size on Android (48dp) multiplied by the scale
|
| +// factor of a Nexus 5 device (3x). For mobile and desktop platforms, a 144px
|
| +// icon is an approximate, appropriate lower bound.
|
| +// TODO(dominickn): consolidate with minimum_icon_size_in_dp across platforms.
|
| +const int kIconMinimumSizeInPx = 144;
|
| +
|
| +// Returns true if |manifest| specifies a PNG icon >= 144x144px (or size "any").
|
| +bool DoesManifestContainRequiredIcon(const content::Manifest& manifest) {
|
| + for (const auto& icon : manifest.icons) {
|
| + // The type field is optional. If it isn't present, fall back on checking
|
| + // the src extension, and allow the icon if the extension ends with png.
|
| + if (!base::EqualsASCII(icon.type.string(), "image/png") &&
|
| + !(icon.type.is_null() &&
|
| + base::EndsWith(icon.src.ExtractFileName(), kPngExtension,
|
| + base::CompareCase::INSENSITIVE_ASCII)))
|
| + continue;
|
| +
|
| + for (const auto& size : icon.sizes) {
|
| + if (size.IsEmpty()) // "any"
|
| + return true;
|
| + if (size.width() >= kIconMinimumSizeInPx &&
|
| + size.height() >= kIconMinimumSizeInPx) {
|
| + return true;
|
| + }
|
| + }
|
| + }
|
| +
|
| + return false;
|
| +}
|
| +
|
| +} // namespace
|
| +
|
| +DEFINE_WEB_CONTENTS_USER_DATA_KEY(InstallableManager);
|
| +
|
| +struct InstallableManager::ManifestProperty {
|
| + InstallableErrorCode error = NO_ERROR_DETECTED;
|
| + GURL url;
|
| + content::Manifest manifest;
|
| + bool fetched = false;
|
| +};
|
| +
|
| +struct InstallableManager::InstallableProperty {
|
| + InstallableErrorCode error = NO_ERROR_DETECTED;
|
| + bool installable = false;
|
| + bool fetched = false;
|
| +};
|
| +
|
| +struct InstallableManager::IconProperty {
|
| + IconProperty() :
|
| + error(NO_ERROR_DETECTED), url(), icon(), fetched(false) { }
|
| + IconProperty(IconProperty&& other) = default;
|
| + IconProperty& operator=(IconProperty&& other) = default;
|
| +
|
| + InstallableErrorCode error = NO_ERROR_DETECTED;
|
| + GURL url;
|
| + std::unique_ptr<SkBitmap> icon;
|
| + bool fetched;
|
| +
|
| + private:
|
| + // This class contains a std::unique_ptr and therefore must be move-only.
|
| + DISALLOW_COPY_AND_ASSIGN(IconProperty);
|
| +};
|
| +
|
| +
|
| +InstallableManager::InstallableManager(content::WebContents* web_contents)
|
| + : content::WebContentsObserver(web_contents),
|
| + manifest_(new ManifestProperty()),
|
| + installable_(new InstallableProperty()),
|
| + is_active_(false),
|
| + weak_factory_(this) { }
|
| +
|
| +InstallableManager::~InstallableManager() = default;
|
| +
|
| +bool InstallableManager::IsManifestValidForWebApp(
|
| + const content::Manifest& manifest) {
|
| + if (manifest.IsEmpty()) {
|
| + installable_->error = MANIFEST_EMPTY;
|
| + return false;
|
| + }
|
| +
|
| + if (!manifest.start_url.is_valid()) {
|
| + installable_->error = START_URL_NOT_VALID;
|
| + return false;
|
| + }
|
| +
|
| + if ((manifest.name.is_null() || manifest.name.string().empty()) &&
|
| + (manifest.short_name.is_null() || manifest.short_name.string().empty())) {
|
| + installable_->error = MANIFEST_MISSING_NAME_OR_SHORT_NAME;
|
| + return false;
|
| + }
|
| +
|
| + // TODO(dominickn,mlamouri): when Chrome supports "minimal-ui", it should be
|
| + // accepted. If we accept it today, it would fallback to "browser" and make
|
| + // this check moot. See https://crbug.com/604390.
|
| + if (manifest.display != blink::WebDisplayModeStandalone &&
|
| + manifest.display != blink::WebDisplayModeFullscreen) {
|
| + installable_->error = MANIFEST_DISPLAY_NOT_SUPPORTED;
|
| + return false;
|
| + }
|
| +
|
| + if (!DoesManifestContainRequiredIcon(manifest)) {
|
| + installable_->error = MANIFEST_MISSING_SUITABLE_ICON;
|
| + return false;
|
| + }
|
| +
|
| + return true;
|
| +}
|
| +
|
| +void InstallableManager::GetData(const InstallableParams& params,
|
| + const InstallableCallback& callback) {
|
| + DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
|
| +
|
| + // Return immediately if we've already working on a task. The new task will be
|
| + // looked at once the current task is finished.
|
| + tasks_.push_back({params, callback});
|
| + if (is_active_)
|
| + return;
|
| +
|
| + is_active_ = true;
|
| + StartNextTask();
|
| +}
|
| +
|
| +bool InstallableManager::IsIconFetched(const InstallableParams& params) const {
|
| + const auto it = icons_.find(
|
| + {params.ideal_icon_size_in_dp, params.minimum_icon_size_in_dp});
|
| + return it != icons_.end() && it->second.fetched;
|
| +}
|
| +
|
| +void InstallableManager::SetIconFetched(const InstallableParams& params) {
|
| + GetIcon(params).fetched = true;
|
| +}
|
| +
|
| +InstallableManager::IconProperty& InstallableManager::GetIcon(
|
| + const InstallableParams& params) {
|
| + return icons_[{params.ideal_icon_size_in_dp, params.minimum_icon_size_in_dp}];
|
| +}
|
| +
|
| +InstallableErrorCode InstallableManager::GetErrorCode(
|
| + const InstallableParams& params) {
|
| + if (manifest_->error != NO_ERROR_DETECTED)
|
| + return manifest_->error;
|
| +
|
| + if (params.check_installable && installable_->error != NO_ERROR_DETECTED)
|
| + return installable_->error;
|
| +
|
| + if (params.fetch_valid_icon) {
|
| + IconProperty& icon = GetIcon(params);
|
| + if (icon.error != NO_ERROR_DETECTED)
|
| + return icon.error;
|
| + }
|
| +
|
| + return NO_ERROR_DETECTED;
|
| +}
|
| +
|
| +InstallableErrorCode InstallableManager::manifest_error() const {
|
| + return manifest_->error;
|
| +}
|
| +
|
| +InstallableErrorCode InstallableManager::installable_error() const {
|
| + return installable_->error;
|
| +}
|
| +
|
| +void InstallableManager::set_installable_error(
|
| + InstallableErrorCode error_code) {
|
| + installable_->error = error_code;
|
| +}
|
| +
|
| +InstallableErrorCode InstallableManager::icon_error(
|
| + const InstallableManager::IconParams& icon_params) {
|
| + return icons_[icon_params].error;
|
| +}
|
| +
|
| +GURL& InstallableManager::icon_url(
|
| + const InstallableManager::IconParams& icon_params) {
|
| + return icons_[icon_params].url;
|
| +}
|
| +
|
| +const SkBitmap* InstallableManager::icon(
|
| + const InstallableManager::IconParams& icon_params) {
|
| + return icons_[icon_params].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 manifest_->fetched &&
|
| + (!params.check_installable || installable_->fetched) &&
|
| + (!params.fetch_valid_icon || IsIconFetched(params));
|
| +}
|
| +
|
| +void InstallableManager::Reset() {
|
| + // Prevent any outstanding callbacks to or from this object from being called.
|
| + weak_factory_.InvalidateWeakPtrs();
|
| + tasks_.clear();
|
| + icons_.clear();
|
| +
|
| + manifest_.reset(new ManifestProperty());
|
| + installable_.reset(new InstallableProperty());
|
| +
|
| + is_active_ = false;
|
| +}
|
| +
|
| +void InstallableManager::SetManifestDependentTasksComplete() {
|
| + DCHECK(!tasks_.empty());
|
| + const InstallableParams& params = tasks_[0].first;
|
| +
|
| + installable_->fetched = true;
|
| + SetIconFetched(params);
|
| +}
|
| +
|
| +void InstallableManager::StartNextTask() {
|
| + // If there's nothing to do, exit. Resources remain cached so any future calls
|
| + // won't re-fetch anything that has already been retrieved.
|
| + if (tasks_.empty()) {
|
| + is_active_ = false;
|
| + return;
|
| + }
|
| +
|
| + DCHECK(is_active_);
|
| + WorkOnTask();
|
| +}
|
| +
|
| +void InstallableManager::RunCallback(const Task& task,
|
| + InstallableErrorCode code) {
|
| + const InstallableParams& params = task.first;
|
| + IconProperty& icon = GetIcon(params);
|
| + InstallableData data = {
|
| + code,
|
| + manifest_url(),
|
| + manifest(),
|
| + params.fetch_valid_icon ? icon.url : GURL::EmptyGURL(),
|
| + params.fetch_valid_icon ? icon.icon.get() : nullptr,
|
| + params.check_installable ? is_installable() : false};
|
| +
|
| + task.second.Run(data);
|
| +}
|
| +
|
| +void InstallableManager::WorkOnTask() {
|
| + DCHECK(!tasks_.empty());
|
| + const Task& task = tasks_[0];
|
| + const InstallableParams& params = task.first;
|
| +
|
| + InstallableErrorCode code = GetErrorCode(params);
|
| + if (code != NO_ERROR_DETECTED || IsComplete(params)) {
|
| + RunCallback(task, code);
|
| + tasks_.erase(tasks_.begin());
|
| + StartNextTask();
|
| + return;
|
| + }
|
| +
|
| + if (!manifest_->fetched)
|
| + FetchManifest();
|
| + else if (params.check_installable && !installable_->fetched)
|
| + CheckInstallable();
|
| + else if (params.fetch_valid_icon && !IsIconFetched(params))
|
| + CheckAndFetchBestIcon();
|
| + else
|
| + NOTREACHED();
|
| +}
|
| +
|
| +void InstallableManager::FetchManifest() {
|
| + DCHECK(!manifest_->fetched);
|
| +
|
| + content::WebContents* web_contents = GetWebContents();
|
| + DCHECK(web_contents);
|
| +
|
| + web_contents->GetManifest(base::Bind(&InstallableManager::OnDidGetManifest,
|
| + weak_factory_.GetWeakPtr()));
|
| +}
|
| +
|
| +void InstallableManager::OnDidGetManifest(const GURL& manifest_url,
|
| + const content::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::CheckInstallable() {
|
| + DCHECK(!installable_->fetched);
|
| + DCHECK(!manifest().IsEmpty());
|
| +
|
| + if (IsManifestValidForWebApp(manifest())) {
|
| + CheckServiceWorker();
|
| + } else {
|
| + installable_->installable = false;
|
| + installable_->fetched = true;
|
| + WorkOnTask();
|
| + }
|
| +}
|
| +
|
| +void InstallableManager::CheckServiceWorker() {
|
| + DCHECK(!installable_->fetched);
|
| + DCHECK(!manifest().IsEmpty());
|
| + DCHECK(manifest().start_url.is_valid());
|
| +
|
| + content::WebContents* web_contents = GetWebContents();
|
| +
|
| + // Check to see if there is a single service worker controlling this page
|
| + // and the manifest's start url.
|
| + content::StoragePartition* storage_partition =
|
| + content::BrowserContext::GetStoragePartition(
|
| + Profile::FromBrowserContext(web_contents->GetBrowserContext()),
|
| + web_contents->GetSiteInstance());
|
| + DCHECK(storage_partition);
|
| +
|
| + storage_partition->GetServiceWorkerContext()->CheckHasServiceWorker(
|
| + web_contents->GetLastCommittedURL(), manifest().start_url,
|
| + base::Bind(&InstallableManager::OnDidCheckHasServiceWorker,
|
| + weak_factory_.GetWeakPtr()));
|
| +}
|
| +
|
| +void InstallableManager::OnDidCheckHasServiceWorker(bool has_service_worker) {
|
| + if (!GetWebContents())
|
| + return;
|
| +
|
| + if (has_service_worker) {
|
| + installable_->installable = true;
|
| + } else {
|
| + installable_->installable = false;
|
| + installable_->error = NO_MATCHING_SERVICE_WORKER;
|
| + }
|
| +
|
| + installable_->fetched = true;
|
| + WorkOnTask();
|
| +}
|
| +
|
| +void InstallableManager::CheckAndFetchBestIcon() {
|
| + DCHECK(!manifest().IsEmpty());
|
| + DCHECK(!tasks_.empty());
|
| +
|
| + const InstallableParams& params = tasks_[0].first;
|
| + IconProperty& icon = GetIcon(params);
|
| + icon.fetched = true;
|
| +
|
| + GURL icon_url = ManifestIconSelector::FindBestMatchingIcon(
|
| + manifest().icons, params.ideal_icon_size_in_dp,
|
| + params.minimum_icon_size_in_dp);
|
| +
|
| + if (icon_url.is_empty()) {
|
| + icon.error = NO_ACCEPTABLE_ICON;
|
| + } else {
|
| + bool can_download_icon = ManifestIconDownloader::Download(
|
| + GetWebContents(), icon_url, params.ideal_icon_size_in_dp,
|
| + params.minimum_icon_size_in_dp,
|
| + base::Bind(&InstallableManager::OnAppIconFetched,
|
| + weak_factory_.GetWeakPtr(), icon_url));
|
| + if (can_download_icon)
|
| + return;
|
| + icon.error = CANNOT_DOWNLOAD_ICON;
|
| + }
|
| +
|
| + WorkOnTask();
|
| +}
|
| +
|
| +void InstallableManager::OnAppIconFetched(const GURL icon_url,
|
| + const SkBitmap& bitmap) {
|
| + DCHECK(!tasks_.empty());
|
| + const InstallableParams& params = tasks_[0].first;
|
| + IconProperty& icon = GetIcon(params);
|
| +
|
| + if (!GetWebContents())
|
| + return;
|
| +
|
| + if (bitmap.drawsNothing()) {
|
| + icon.error = NO_ICON_AVAILABLE;
|
| + } else {
|
| + icon.url = icon_url;
|
| + icon.icon.reset(new SkBitmap(bitmap));
|
| + }
|
| +
|
| + WorkOnTask();
|
| +}
|
| +
|
| +void InstallableManager::DidFinishNavigation(
|
| + content::NavigationHandle* handle) {
|
| + if (handle->IsInMainFrame() && handle->HasCommitted() &&
|
| + !handle->IsSamePage()) {
|
| + Reset();
|
| + }
|
| +}
|
| +
|
| +void InstallableManager::WebContentsDestroyed() {
|
| + Reset();
|
| + Observe(nullptr);
|
| +}
|
| +
|
| +const GURL& InstallableManager::manifest_url() const {
|
| + return manifest_->url;
|
| +}
|
| +
|
| +const content::Manifest& InstallableManager::manifest() const {
|
| + return manifest_->manifest;
|
| +}
|
| +
|
| +bool InstallableManager::is_installable() const {
|
| + return installable_->installable;
|
| +}
|
| +
|
| +// static
|
| +int InstallableManager::GetMinimumIconSizeInPx() {
|
| + return kIconMinimumSizeInPx;
|
| +}
|
|
|