| // Copyright 2019 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 <utility> |
| |
| #include "chrome/browser/web_applications/web_app_install_task.h" |
| |
| #include "base/bind.h" |
| #include "base/callback.h" |
| #include "base/logging.h" |
| #include "chrome/browser/installable/installable_metrics.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/web_applications/components/app_shortcut_manager.h" |
| #include "chrome/browser/web_applications/components/install_bounce_metric.h" |
| #include "chrome/browser/web_applications/components/install_finalizer.h" |
| #include "chrome/browser/web_applications/components/web_app_constants.h" |
| #include "chrome/browser/web_applications/components/web_app_data_retriever.h" |
| #include "chrome/browser/web_applications/components/web_app_icon_generator.h" |
| #include "chrome/browser/web_applications/components/web_app_install_utils.h" |
| #include "chrome/browser/web_applications/components/web_app_utils.h" |
| #include "chrome/common/web_application_info.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "content/public/browser/web_contents.h" |
| #include "third_party/blink/public/common/manifest/manifest.h" |
| #include "third_party/blink/public/mojom/manifest/display_mode.mojom.h" |
| #include "url/gurl.h" |
| |
| #if defined(OS_CHROMEOS) |
| #include "base/feature_list.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "chrome/common/chrome_features.h" |
| #include "components/arc/arc_service_manager.h" |
| #include "components/arc/mojom/app.mojom.h" |
| #include "components/arc/mojom/intent_helper.mojom.h" |
| #include "components/arc/session/arc_bridge_service.h" |
| #include "net/base/url_util.h" |
| #endif |
| |
| namespace web_app { |
| |
| namespace { |
| |
| #if defined(OS_CHROMEOS) |
| const char kChromeOsPlayPlatform[] = "chromeos_play"; |
| const char kPlayIntentPrefix[] = |
| "https://play.google.com/store/apps/details?id="; |
| const char kPlayStorePackage[] = "com.android.vending"; |
| |
| std::string ExtractQueryValueForName(const GURL& url, const std::string& name) { |
| for (net::QueryIterator it(url); !it.IsAtEnd(); it.Advance()) { |
| if (it.GetKey() == name) |
| return it.GetValue(); |
| } |
| return std::string(); |
| } |
| #endif // defined(OS_CHROMEOS) |
| |
| } // namespace |
| |
| WebAppInstallTask::WebAppInstallTask( |
| Profile* profile, |
| AppShortcutManager* shortcut_manager, |
| InstallFinalizer* install_finalizer, |
| std::unique_ptr<WebAppDataRetriever> data_retriever) |
| : data_retriever_(std::move(data_retriever)), |
| shortcut_manager_(shortcut_manager), |
| install_finalizer_(install_finalizer), |
| profile_(profile) {} |
| |
| WebAppInstallTask::~WebAppInstallTask() = default; |
| |
| void WebAppInstallTask::ExpectAppId(const AppId& expected_app_id) { |
| expected_app_id_ = expected_app_id; |
| } |
| |
| void WebAppInstallTask::InstallWebAppFromManifest( |
| content::WebContents* contents, |
| WebappInstallSource install_source, |
| InstallManager::WebAppInstallDialogCallback dialog_callback, |
| InstallManager::OnceInstallCallback install_callback) { |
| DCHECK(AreWebAppsUserInstallable(profile_)); |
| CheckInstallPreconditions(); |
| |
| Observe(contents); |
| dialog_callback_ = std::move(dialog_callback); |
| install_callback_ = std::move(install_callback); |
| install_source_ = install_source; |
| |
| auto web_app_info = std::make_unique<WebApplicationInfo>(); |
| |
| data_retriever_->CheckInstallabilityAndRetrieveManifest( |
| web_contents(), /*bypass_service_worker_check=*/false, |
| base::BindOnce(&WebAppInstallTask::OnDidPerformInstallableCheck, |
| base::Unretained(this), std::move(web_app_info), |
| /*force_shortcut_app=*/false)); |
| } |
| |
| void WebAppInstallTask::InstallWebAppFromManifestWithFallback( |
| content::WebContents* contents, |
| bool force_shortcut_app, |
| WebappInstallSource install_source, |
| InstallManager::WebAppInstallDialogCallback dialog_callback, |
| InstallManager::OnceInstallCallback install_callback) { |
| DCHECK(AreWebAppsUserInstallable(profile_)); |
| CheckInstallPreconditions(); |
| |
| Observe(contents); |
| dialog_callback_ = std::move(dialog_callback); |
| install_callback_ = std::move(install_callback); |
| install_source_ = install_source; |
| |
| data_retriever_->GetWebApplicationInfo( |
| web_contents(), |
| base::BindOnce(&WebAppInstallTask::OnGetWebApplicationInfo, |
| base::Unretained(this), force_shortcut_app)); |
| } |
| |
| void WebAppInstallTask::InstallWebAppFromInfo( |
| std::unique_ptr<WebApplicationInfo> web_application_info, |
| ForInstallableSite for_installable_site, |
| WebappInstallSource install_source, |
| InstallManager::OnceInstallCallback callback) { |
| DCHECK(AreWebAppsUserInstallable(profile_)); |
| CheckInstallPreconditions(); |
| |
| FilterAndResizeIconsGenerateMissing(web_application_info.get(), |
| /*icons_map*/ nullptr, |
| /*is_for_sync*/ false); |
| |
| install_source_ = install_source; |
| background_installation_ = true; |
| |
| RecordInstallEvent(for_installable_site); |
| |
| InstallFinalizer::FinalizeOptions options; |
| options.install_source = install_source; |
| |
| install_finalizer_->FinalizeInstall(*web_application_info, options, |
| std::move(callback)); |
| } |
| |
| void WebAppInstallTask::InstallWebAppWithParams( |
| content::WebContents* contents, |
| const InstallManager::InstallParams& install_params, |
| WebappInstallSource install_source, |
| InstallManager::OnceInstallCallback install_callback) { |
| CheckInstallPreconditions(); |
| |
| Observe(contents); |
| install_callback_ = std::move(install_callback); |
| install_source_ = install_source; |
| install_params_ = install_params; |
| background_installation_ = true; |
| |
| data_retriever_->GetWebApplicationInfo( |
| web_contents(), |
| base::BindOnce(&WebAppInstallTask::OnGetWebApplicationInfo, |
| base::Unretained(this), /*force_shortcut_app=*/false)); |
| } |
| |
| void WebAppInstallTask::InstallWebAppFromInfoRetrieveIcons( |
| content::WebContents* web_contents, |
| std::unique_ptr<WebApplicationInfo> web_application_info, |
| bool is_locally_installed, |
| WebappInstallSource install_source, |
| InstallManager::OnceInstallCallback callback) { |
| CheckInstallPreconditions(); |
| |
| Observe(web_contents); |
| install_callback_ = std::move(callback); |
| install_source_ = install_source; |
| background_installation_ = true; |
| |
| std::vector<GURL> icon_urls = |
| GetValidIconUrlsToDownload(*web_application_info, /*data=*/nullptr); |
| |
| // Skip downloading the page favicons as everything in is the URL list. |
| data_retriever_->GetIcons( |
| web_contents, icon_urls, /*skip_page_fav_icons*/ true, |
| install_source_ == WebappInstallSource::SYNC |
| ? WebAppIconDownloader::Histogram::kForSync |
| : WebAppIconDownloader::Histogram::kForCreate, |
| base::BindOnce(&WebAppInstallTask::OnIconsRetrieved, |
| base::Unretained(this), std::move(web_application_info), |
| is_locally_installed)); |
| } |
| |
| void WebAppInstallTask::UpdateWebAppFromInfo( |
| content::WebContents* web_contents, |
| const AppId& app_id, |
| std::unique_ptr<WebApplicationInfo> web_application_info, |
| InstallManager::OnceInstallCallback callback) { |
| Observe(web_contents); |
| install_callback_ = std::move(callback); |
| background_installation_ = true; |
| |
| std::vector<GURL> icon_urls = |
| GetValidIconUrlsToDownload(*web_application_info, /*data=*/nullptr); |
| |
| data_retriever_->GetIcons( |
| web_contents, std::move(icon_urls), |
| /*skip_page_fav_icons=*/true, WebAppIconDownloader::Histogram::kForUpdate, |
| base::BindOnce(&WebAppInstallTask::OnIconsRetrievedFinalizeUpdate, |
| base::Unretained(this), std::move(web_application_info))); |
| } |
| |
| void WebAppInstallTask::WebContentsDestroyed() { |
| CallInstallCallback(AppId(), InstallResultCode::kWebContentsDestroyed); |
| } |
| |
| void WebAppInstallTask::SetInstallFinalizerForTesting( |
| InstallFinalizer* install_finalizer) { |
| install_finalizer_ = install_finalizer; |
| } |
| |
| void WebAppInstallTask::CheckInstallPreconditions() { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| |
| // Concurrent calls are not allowed. |
| DCHECK(!web_contents()); |
| CHECK(!install_callback_); |
| } |
| |
| void WebAppInstallTask::RecordInstallEvent( |
| ForInstallableSite for_installable_site) { |
| DCHECK(install_source_ != kNoInstallSource); |
| |
| if (InstallableMetrics::IsReportableInstallSource(install_source_) && |
| for_installable_site == ForInstallableSite::kYes) { |
| InstallableMetrics::TrackInstallEvent(install_source_); |
| } |
| } |
| |
| void WebAppInstallTask::CallInstallCallback(const AppId& app_id, |
| InstallResultCode code) { |
| Observe(nullptr); |
| dialog_callback_.Reset(); |
| |
| install_source_ = kNoInstallSource; |
| |
| DCHECK(install_callback_); |
| std::move(install_callback_).Run(app_id, code); |
| } |
| |
| bool WebAppInstallTask::ShouldStopInstall() const { |
| // Install should stop early if WebContents is being destroyed. |
| // WebAppInstallTask::WebContentsDestroyed will get called eventually and the |
| // callback will be invoked at that point. |
| return !web_contents() || web_contents()->IsBeingDestroyed(); |
| } |
| |
| void WebAppInstallTask::OnGetWebApplicationInfo( |
| bool force_shortcut_app, |
| std::unique_ptr<WebApplicationInfo> web_app_info) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| if (ShouldStopInstall()) |
| return; |
| |
| if (!web_app_info) { |
| CallInstallCallback(AppId(), |
| InstallResultCode::kGetWebApplicationInfoFailed); |
| return; |
| } |
| |
| bool bypass_service_worker_check = false; |
| if (install_params_) |
| bypass_service_worker_check = install_params_->bypass_service_worker_check; |
| |
| data_retriever_->CheckInstallabilityAndRetrieveManifest( |
| web_contents(), bypass_service_worker_check, |
| base::BindOnce(&WebAppInstallTask::OnDidPerformInstallableCheck, |
| base::Unretained(this), std::move(web_app_info), |
| force_shortcut_app)); |
| } |
| |
| void WebAppInstallTask::OnDidPerformInstallableCheck( |
| std::unique_ptr<WebApplicationInfo> web_app_info, |
| bool force_shortcut_app, |
| const blink::Manifest& manifest, |
| bool valid_manifest_for_web_app, |
| bool is_installable) { |
| if (ShouldStopInstall()) |
| return; |
| |
| DCHECK(web_app_info); |
| |
| if (install_params_ && install_params_->require_manifest && |
| !valid_manifest_for_web_app) { |
| LOG(WARNING) << "Did not install " << web_app_info->app_url.spec() |
| << " because it didn't have a manifest for web app"; |
| CallInstallCallback(AppId(), InstallResultCode::kNotValidManifestForWebApp); |
| return; |
| } |
| |
| const auto for_installable_site = is_installable && !force_shortcut_app |
| ? ForInstallableSite::kYes |
| : ForInstallableSite::kNo; |
| |
| UpdateWebAppInfoFromManifest(manifest, web_app_info.get(), |
| for_installable_site); |
| |
| AppId app_id = GenerateAppIdFromURL(web_app_info->app_url); |
| |
| // Do the app_id expectation check if requested. |
| if (expected_app_id_.has_value() && *expected_app_id_ != app_id) { |
| CallInstallCallback(std::move(app_id), |
| InstallResultCode::kExpectedAppIdCheckFailed); |
| return; |
| } |
| |
| std::vector<GURL> icon_urls = |
| GetValidIconUrlsToDownload(*web_app_info, /*data=*/nullptr); |
| |
| // A system app should always have a manifest icon. |
| if (install_source_ == WebappInstallSource::SYSTEM_DEFAULT) { |
| DCHECK(!manifest.icons.empty()); |
| } |
| |
| // If the manifest specified icons, don't use the page icons. |
| const bool skip_page_favicons = !manifest.icons.empty(); |
| |
| CheckForPlayStoreIntentOrGetIcons(manifest, std::move(web_app_info), |
| std::move(icon_urls), for_installable_site, |
| skip_page_favicons); |
| } |
| |
| void WebAppInstallTask::CheckForPlayStoreIntentOrGetIcons( |
| const blink::Manifest& manifest, |
| std::unique_ptr<WebApplicationInfo> web_app_info, |
| std::vector<GURL> icon_urls, |
| ForInstallableSite for_installable_site, |
| bool skip_page_favicons) { |
| #if defined(OS_CHROMEOS) |
| // Background installations are not a user-triggered installs, and thus cannot |
| // be sent to the store. |
| if (base::FeatureList::IsEnabled(features::kApkWebAppInstalls) && |
| for_installable_site == ForInstallableSite::kYes && |
| !background_installation_) { |
| for (const auto& application : manifest.related_applications) { |
| std::string id = base::UTF16ToUTF8(application.id.string()); |
| if (!base::EqualsASCII(application.platform.string(), |
| kChromeOsPlayPlatform)) { |
| continue; |
| } |
| |
| std::string id_from_app_url = |
| ExtractQueryValueForName(application.url, "id"); |
| |
| if (id.empty()) { |
| if (id_from_app_url.empty()) |
| continue; |
| id = id_from_app_url; |
| } |
| |
| auto* arc_service_manager = arc::ArcServiceManager::Get(); |
| if (arc_service_manager) { |
| auto* instance = ARC_GET_INSTANCE_FOR_METHOD( |
| arc_service_manager->arc_bridge_service()->app(), IsInstallable); |
| if (instance) { |
| // Attach the referrer value. |
| std::string referrer = |
| ExtractQueryValueForName(application.url, "referrer"); |
| if (!referrer.empty()) |
| referrer = "&referrer=" + referrer; |
| |
| std::string intent = kPlayIntentPrefix + id + referrer; |
| instance->IsInstallable( |
| id, |
| base::BindOnce(&WebAppInstallTask::OnDidCheckForIntentToPlayStore, |
| weak_ptr_factory_.GetWeakPtr(), |
| std::move(web_app_info), std::move(icon_urls), |
| for_installable_site, skip_page_favicons, intent)); |
| return; |
| } |
| } |
| } |
| } |
| |
| #endif // defined(OS_CHROMEOS) |
| OnDidCheckForIntentToPlayStore(std::move(web_app_info), std::move(icon_urls), |
| for_installable_site, skip_page_favicons, |
| /*intent=*/"", |
| /*should_intent_to_store=*/false); |
| } |
| |
| void WebAppInstallTask::OnDidCheckForIntentToPlayStore( |
| std::unique_ptr<WebApplicationInfo> web_app_info, |
| std::vector<GURL> icon_urls, |
| ForInstallableSite for_installable_site, |
| bool skip_page_favicons, |
| const std::string& intent, |
| bool should_intent_to_store) { |
| #if defined(OS_CHROMEOS) |
| if (should_intent_to_store && !intent.empty()) { |
| auto* arc_service_manager = arc::ArcServiceManager::Get(); |
| if (arc_service_manager) { |
| auto* instance = ARC_GET_INSTANCE_FOR_METHOD( |
| arc_service_manager->arc_bridge_service()->intent_helper(), |
| HandleUrl); |
| if (instance) { |
| instance->HandleUrl(intent, kPlayStorePackage); |
| CallInstallCallback(AppId(), InstallResultCode::kIntentToPlayStore); |
| return; |
| } |
| } |
| } |
| #endif // defined(OS_CHROMEOS) |
| |
| data_retriever_->GetIcons( |
| web_contents(), icon_urls, skip_page_favicons, |
| install_source_ == WebappInstallSource::SYNC |
| ? WebAppIconDownloader::Histogram::kForSync |
| : WebAppIconDownloader::Histogram::kForCreate, |
| base::BindOnce(&WebAppInstallTask::OnIconsRetrievedShowDialog, |
| base::Unretained(this), std::move(web_app_info), |
| for_installable_site)); |
| } |
| |
| void WebAppInstallTask::OnIconsRetrieved( |
| std::unique_ptr<WebApplicationInfo> web_app_info, |
| bool is_locally_installed, |
| IconsMap icons_map) { |
| if (ShouldStopInstall()) |
| return; |
| |
| DCHECK(web_app_info); |
| |
| // Installing from sync should not change icon links. |
| FilterAndResizeIconsGenerateMissing( |
| web_app_info.get(), &icons_map, |
| /*is_for_sync=*/install_source_ == WebappInstallSource::SYNC); |
| |
| InstallFinalizer::FinalizeOptions options; |
| options.install_source = install_source_; |
| options.locally_installed = is_locally_installed; |
| |
| install_finalizer_->FinalizeInstall( |
| *web_app_info, options, |
| base::BindOnce(&WebAppInstallTask::OnInstallFinalized, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| void WebAppInstallTask::OnIconsRetrievedShowDialog( |
| std::unique_ptr<WebApplicationInfo> web_app_info, |
| ForInstallableSite for_installable_site, |
| IconsMap icons_map) { |
| if (ShouldStopInstall()) |
| return; |
| |
| DCHECK(web_app_info); |
| |
| FilterAndResizeIconsGenerateMissing( |
| web_app_info.get(), &icons_map, |
| /*is_for_sync=*/install_source_ == WebappInstallSource::SYNC); |
| |
| if (background_installation_) { |
| DCHECK(!dialog_callback_); |
| OnDialogCompleted(for_installable_site, /*user_accepted=*/true, |
| std::move(web_app_info)); |
| } else { |
| DCHECK(dialog_callback_); |
| std::move(dialog_callback_) |
| .Run(web_contents(), std::move(web_app_info), for_installable_site, |
| base::BindOnce(&WebAppInstallTask::OnDialogCompleted, |
| weak_ptr_factory_.GetWeakPtr(), |
| for_installable_site)); |
| } |
| } |
| |
| void WebAppInstallTask::OnIconsRetrievedFinalizeUpdate( |
| std::unique_ptr<WebApplicationInfo> web_app_info, |
| IconsMap icons_map) { |
| if (ShouldStopInstall()) |
| return; |
| |
| DCHECK(web_app_info); |
| |
| // TODO(crbug.com/926083): Abort update if icons fail to download. |
| FilterAndResizeIconsGenerateMissing(web_app_info.get(), &icons_map, |
| /*is_for_sync=*/false); |
| |
| install_finalizer_->FinalizeUpdate( |
| *web_app_info, base::BindOnce(&WebAppInstallTask::OnInstallFinalized, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| void WebAppInstallTask::OnDialogCompleted( |
| ForInstallableSite for_installable_site, |
| bool user_accepted, |
| std::unique_ptr<WebApplicationInfo> web_app_info) { |
| if (ShouldStopInstall()) |
| return; |
| |
| if (!user_accepted) { |
| CallInstallCallback(AppId(), InstallResultCode::kUserInstallDeclined); |
| return; |
| } |
| |
| WebApplicationInfo web_app_info_copy = *web_app_info; |
| |
| // This metric is recorded regardless of the installation result. |
| RecordInstallEvent(for_installable_site); |
| |
| InstallFinalizer::FinalizeOptions finalize_options; |
| finalize_options.install_source = install_source_; |
| if (install_params_ && |
| install_params_->display_mode != blink::mojom::DisplayMode::kUndefined) { |
| web_app_info_copy.open_as_window = |
| install_params_->display_mode != blink::mojom::DisplayMode::kBrowser; |
| } |
| |
| install_finalizer_->FinalizeInstall( |
| web_app_info_copy, finalize_options, |
| base::BindOnce(&WebAppInstallTask::OnInstallFinalizedCreateShortcuts, |
| weak_ptr_factory_.GetWeakPtr(), std::move(web_app_info))); |
| |
| // Check that the finalizer hasn't called OnInstallFinalizedCreateShortcuts |
| // synchronously: |
| DCHECK(install_callback_); |
| } |
| |
| void WebAppInstallTask::OnInstallFinalized(const AppId& app_id, |
| InstallResultCode code) { |
| if (ShouldStopInstall()) |
| return; |
| |
| CallInstallCallback(app_id, code); |
| } |
| |
| void WebAppInstallTask::OnInstallFinalizedCreateShortcuts( |
| std::unique_ptr<WebApplicationInfo> web_app_info, |
| const AppId& app_id, |
| InstallResultCode code) { |
| if (ShouldStopInstall()) |
| return; |
| |
| if (code != InstallResultCode::kSuccessNewInstall) { |
| CallInstallCallback(app_id, code); |
| return; |
| } |
| |
| RecordAppBanner(web_contents(), web_app_info->app_url); |
| RecordWebAppInstallationTimestamp(profile_->GetPrefs(), app_id, |
| install_source_); |
| |
| bool add_to_applications_menu = true; |
| bool add_to_desktop = true; |
| |
| if (install_params_) { |
| add_to_applications_menu = install_params_->add_to_applications_menu; |
| add_to_desktop = install_params_->add_to_desktop; |
| } |
| |
| auto create_shortcuts_callback = base::BindOnce( |
| &WebAppInstallTask::OnShortcutsCreated, weak_ptr_factory_.GetWeakPtr(), |
| std::move(web_app_info), app_id); |
| |
| if (add_to_applications_menu && shortcut_manager_->CanCreateShortcuts()) { |
| // TODO(ortuno): Make adding a shortcut to the applications menu independent |
| // from adding a shortcut to desktop. |
| shortcut_manager_->CreateShortcuts(app_id, add_to_desktop, |
| std::move(create_shortcuts_callback)); |
| } else { |
| std::move(create_shortcuts_callback).Run(false /* created_shortcuts */); |
| } |
| } |
| |
| void WebAppInstallTask::OnShortcutsCreated( |
| std::unique_ptr<WebApplicationInfo> web_app_info, |
| const AppId& app_id, |
| bool shortcut_created) { |
| if (ShouldStopInstall()) |
| return; |
| |
| bool add_to_quick_launch_bar = true; |
| if (install_params_) |
| add_to_quick_launch_bar = install_params_->add_to_quick_launch_bar; |
| |
| if (add_to_quick_launch_bar && |
| install_finalizer_->CanAddAppToQuickLaunchBar()) { |
| install_finalizer_->AddAppToQuickLaunchBar(app_id); |
| } |
| |
| if (!background_installation_) { |
| const bool can_reparent_tab = |
| install_finalizer_->CanReparentTab(app_id, shortcut_created); |
| |
| if (can_reparent_tab && web_app_info->open_as_window) |
| install_finalizer_->ReparentTab(app_id, shortcut_created, web_contents()); |
| |
| // TODO(loyso): Make revealing app shim independent from CanReparentTab. |
| if (can_reparent_tab && install_finalizer_->CanRevealAppShim()) |
| install_finalizer_->RevealAppShim(app_id); |
| } |
| |
| CallInstallCallback(app_id, InstallResultCode::kSuccessNewInstall); |
| } |
| |
| } // namespace web_app |