Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(2664)

Unified Diff: chrome/browser/installable/installable_manager.cc

Issue 2160513002: Extract AppBannerDataFetcher into an InstallableManager. (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: s/checker/manager; collapse valid manifest + SW into one Created 4 years, 5 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
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;
+}

Powered by Google App Engine
This is Rietveld 408576698