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

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: Addressing nits Created 4 years, 4 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..a8138087523353c03a02b6e047723d9ff1653b68
--- /dev/null
+++ b/chrome/browser/installable/installable_manager.cc
@@ -0,0 +1,442 @@
+// 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;
+}
+
+} // anonymous 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;
+ } else if (manifest_url.is_empty()) {
Lei Zhang 2016/08/03 06:49:51 no need for else after a return.
dominickn 2016/08/03 07:01:43 Done.
dominickn 2016/08/03 07:01:43 Done.
+ 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;
+ } else if (bitmap.drawsNothing()) {
Lei Zhang 2016/08/03 06:49:51 Ditto
dominickn 2016/08/03 07:01:43 Done.
+ 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;
+}

Powered by Google App Engine
This is Rietveld 408576698