Chromium Code Reviews| 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..259f0e2196f060bec753f989135f823666bbc25e |
| --- /dev/null |
| +++ b/chrome/browser/installable/installable_manager.cc |
| @@ -0,0 +1,400 @@ |
| +// 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/strings/string_util.h" |
| +#include "chrome/browser/installable/installable_logging.h" |
| +#include "chrome/browser/installable/installable_property.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; |
| +} |
| + |
| +} // anonymous namespace |
| + |
| +DEFINE_WEB_CONTENTS_USER_DATA_KEY(InstallableManager); |
| + |
| +InstallableManager::InstallableManager(content::WebContents* web_contents) |
| + : content::WebContentsObserver(web_contents), |
| + is_active_(false), |
| + weak_factory_(this) { } |
| + |
| +InstallableManager::~InstallableManager() = default; |
| + |
| +bool InstallableManager::IsManifestValidForWebApp( |
| + const content::Manifest& manifest) { |
| + if (manifest.IsEmpty()) { |
| + installable_prop_.error = MANIFEST_EMPTY; |
| + return false; |
| + } |
| + |
| + if (!manifest.start_url.is_valid()) { |
| + installable_prop_.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_prop_.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_prop_.error = MANIFEST_DISPLAY_NOT_SUPPORTED; |
| + return false; |
| + } |
| + |
| + if (!DoesManifestContainRequiredIcon(manifest)) { |
| + installable_prop_.error = MANIFEST_MISSING_SUITABLE_ICON; |
| + return false; |
| + } |
| + |
| + return true; |
| +} |
| + |
| +void InstallableManager::GetData(const InstallableParams& params, |
| + const InstallableCallback& callback) { |
| + DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| + |
| + // If we've already working on a task, or running a callback, return straight |
| + // away. The new task will be dealt with once the current work completes. |
| + 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; |
| +} |
| + |
| +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_prop_.error != NO_ERROR_DETECTED) |
| + return manifest_prop_.error; |
| + if (params.check_installable && |
| + installable_prop_.error != NO_ERROR_DETECTED) { |
| + return installable_prop_.error; |
| + } |
| + if (params.fetch_valid_icon) { |
| + IconProperty& icon = GetIcon(params); |
| + if (icon.error != NO_ERROR_DETECTED) |
| + return icon.error; |
| + } |
| + |
| + return NO_ERROR_DETECTED; |
| +} |
| + |
| +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_prop_.fetched && |
| + (!params.check_installable || installable_prop_.fetched) && |
| + (!params.fetch_valid_icon || IsIconFetched(params)); |
| +} |
| + |
| +bool InstallableManager::IsContentsValid( |
| + content::WebContents* web_contents) const { |
| + if (!web_contents || !is_active_) |
|
benwells
2016/07/29 04:36:54
Why are the contents not valid if the manager isn'
dominickn
2016/07/31 23:32:04
Deleted the method.
|
| + return false; |
| + |
| + return true; |
| +} |
| + |
| +void InstallableManager::Reset() { |
| + // Prevent any outstanding callbacks to or from this object from being called. |
| + weak_factory_.InvalidateWeakPtrs(); |
| + tasks_.clear(); |
| + icons_.clear(); |
| + |
| + manifest_prop_.Reset(); |
| + installable_prop_.Reset(); |
| + |
| + is_active_ = false; |
| +} |
| + |
| +void InstallableManager::SetManifestDependentTasksComplete() { |
| + DCHECK(!tasks_.empty()); |
| + const InstallableParams& params = tasks_[0].first; |
| + |
| + installable_prop_.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; |
| + } |
| + |
| + is_active_ = true; |
|
benwells
2016/07/29 04:36:53
Nit: I think you can delete this line, maybe add D
dominickn
2016/07/31 23:32:04
Done.
|
| + 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_prop_.fetched) { |
| + FetchManifest(); |
| + } else if (params.check_installable && !installable_prop_.fetched) { |
| + CheckInstallable(); |
| + } else if (params.fetch_valid_icon && !IsIconFetched(params)) { |
| + CheckAndFetchBestIcon(); |
| + } else { |
| + NOTREACHED(); |
| + } |
| +} |
| + |
| +void InstallableManager::FetchManifest() { |
| + DCHECK(!manifest_prop_.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 (!IsContentsValid(GetWebContents())) { |
| + Reset(); |
|
benwells
2016/07/29 04:36:54
Should you reset here, or just return?
dominickn
2016/07/31 23:32:04
I'm not really sure about the guarantees of WebCon
|
| + } else if (manifest_url.is_empty()) { |
| + manifest_prop_.error = NO_MANIFEST; |
| + SetManifestDependentTasksComplete(); |
| + } else if (manifest.IsEmpty()) { |
| + manifest_prop_.error = MANIFEST_EMPTY; |
| + SetManifestDependentTasksComplete(); |
| + } |
| + |
| + manifest_prop_.url = manifest_url; |
| + manifest_prop_.manifest = manifest; |
| + manifest_prop_.fetched = true; |
| + WorkOnTask(); |
| +} |
| + |
| +void InstallableManager::CheckInstallable() { |
| + DCHECK(!installable_prop_.fetched); |
| + DCHECK(!manifest().IsEmpty()); |
| + |
| + if (IsManifestValidForWebApp(manifest())) { |
| + CheckServiceWorker(); |
| + } else { |
| + installable_prop_.value = false; |
| + installable_prop_.fetched = true; |
| + WorkOnTask(); |
| + } |
| +} |
| + |
| +void InstallableManager::CheckServiceWorker() { |
| + DCHECK(!installable_prop_.fetched); |
| + DCHECK(!manifest().IsEmpty()); |
| + |
| + if (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. |
| + Profile* profile = |
| + Profile::FromBrowserContext(web_contents->GetBrowserContext()); |
| + content::StoragePartition* storage_partition = |
| + content::BrowserContext::GetStoragePartition( |
| + profile, web_contents->GetSiteInstance()); |
| + DCHECK(storage_partition); |
| + |
| + storage_partition->GetServiceWorkerContext()->CheckHasServiceWorker( |
| + web_contents->GetLastCommittedURL(), manifest().start_url, |
| + base::Bind(&InstallableManager::OnDidCheckHasServiceWorker, |
| + weak_factory_.GetWeakPtr())); |
| + } else { |
| + installable_prop_.value = false; |
|
benwells
2016/07/29 04:36:53
Is there an error to be set in this case? Oh, hang
dominickn
2016/07/31 23:32:04
Ah yes, now that this method is only called from t
|
| + installable_prop_.fetched = true; |
| + WorkOnTask(); |
| + } |
| +} |
| + |
| +void InstallableManager::OnDidCheckHasServiceWorker(bool has_service_worker) { |
| + if (!IsContentsValid(GetWebContents())) |
| + Reset(); |
|
benwells
2016/07/29 04:36:54
Ditto about reset vs return.
dominickn
2016/07/31 23:32:04
Done.
|
| + |
| + if (has_service_worker) { |
| + installable_prop_.value = true; |
| + } else { |
| + installable_prop_.value = false; |
| + installable_prop_.error = NO_MATCHING_SERVICE_WORKER; |
| + } |
| + |
| + installable_prop_.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 (!IsContentsValid(GetWebContents())) { |
| + Reset(); |
| + } else 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_prop_.url; |
| +} |
| + |
| +const content::Manifest& InstallableManager::manifest() const { |
| + return manifest_prop_.manifest; |
| +} |
| + |
| +bool InstallableManager::is_installable() const { |
| + return installable_prop_.value; |
| +} |
| + |
| +// static |
| +int InstallableManager::GetMinimumIconSizeInPx() { |
| + return kIconMinimumSizeInPx; |
| +} |