| // Copyright 2022 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/web_applications/commands/install_from_sync_command.h" |
| |
| #include <memory> |
| #include <utility> |
| |
| #include "base/callback_helpers.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/web_applications/commands/web_app_command.h" |
| #include "chrome/browser/web_applications/install_bounce_metric.h" |
| #include "chrome/browser/web_applications/web_app_command_manager.h" |
| #include "chrome/browser/web_applications/web_app_helpers.h" |
| #include "chrome/browser/web_applications/web_app_install_info.h" |
| #include "chrome/browser/web_applications/web_app_install_utils.h" |
| #include "chrome/browser/web_applications/web_app_uninstall_job.h" |
| #include "chrome/browser/web_applications/web_app_utils.h" |
| #include "components/webapps/browser/install_result_code.h" |
| #include "components/webapps/browser/installable/installable_metrics.h" |
| #include "content/public/browser/web_contents.h" |
| #include "net/http/http_status_code.h" |
| |
| namespace web_app { |
| |
| namespace { |
| |
| WebAppInstallFinalizer::FinalizeOptions GetFinalizerOptionForSyncInstall() { |
| WebAppInstallFinalizer::FinalizeOptions finalize_options( |
| webapps::WebappInstallSource::SYNC); |
| finalize_options.overwrite_existing_manifest_fields = true; |
| // If app is not locally installed then no OS integration like OS shortcuts. |
| finalize_options.locally_installed = AreAppsLocallyInstalledBySync(); |
| finalize_options.add_to_applications_menu = AreAppsLocallyInstalledBySync(); |
| finalize_options.add_to_desktop = AreAppsLocallyInstalledBySync(); |
| // Never add the app to the quick launch bar after sync. |
| finalize_options.add_to_quick_launch_bar = false; |
| if (IsChromeOsDataMandatory()) { |
| finalize_options.chromeos_data.emplace(); |
| finalize_options.chromeos_data->show_in_launcher = |
| AreAppsLocallyInstalledBySync(); |
| } |
| return finalize_options; |
| } |
| |
| } // namespace |
| |
| InstallFromSyncCommand::Params::~Params() = default; |
| |
| InstallFromSyncCommand::Params::Params( |
| AppId app_id, |
| const absl::optional<std::string>& manifest_id, |
| const GURL& start_url, |
| const std::string& title, |
| const GURL& scope, |
| const absl::optional<SkColor>& theme_color, |
| const absl::optional<UserDisplayMode>& user_display_mode, |
| const std::vector<apps::IconInfo>& icons) |
| : app_id(app_id), |
| manifest_id(manifest_id), |
| start_url(start_url), |
| title(title), |
| scope(scope), |
| theme_color(theme_color), |
| user_display_mode(user_display_mode), |
| icons(icons) {} |
| |
| InstallFromSyncCommand::Params::Params(const Params&) = default; |
| |
| InstallFromSyncCommand::InstallFromSyncCommand( |
| WebAppUrlLoader* url_loader, |
| Profile* profile, |
| WebAppInstallFinalizer* finalizer, |
| WebAppRegistrar* registrar, |
| std::unique_ptr<WebAppDataRetriever> data_retriever, |
| const Params& params, |
| OnceInstallCallback install_callback) |
| : WebAppCommand( |
| WebAppCommandLock::CreateForAppAndWebContentsLock({params.app_id})), |
| url_loader_(url_loader), |
| profile_(profile), |
| finalizer_(finalizer), |
| registrar_(registrar), |
| data_retriever_(std::move(data_retriever)), |
| params_(params), |
| install_callback_(std::move(install_callback)), |
| install_error_log_entry_(true, webapps::WebappInstallSource::SYNC) { |
| #if BUILDFLAG(IS_CHROMEOS) |
| DCHECK(AreAppsLocallyInstalledBySync()); |
| #endif |
| DCHECK(params_.start_url.is_valid()); |
| fallback_install_info_ = std::make_unique<WebAppInstallInfo>(); |
| fallback_install_info_->manifest_id = params_.manifest_id; |
| fallback_install_info_->start_url = params_.start_url; |
| fallback_install_info_->title = base::UTF8ToUTF16(params_.title); |
| fallback_install_info_->user_display_mode = params_.user_display_mode; |
| fallback_install_info_->scope = params_.scope; |
| fallback_install_info_->theme_color = params_.theme_color; |
| fallback_install_info_->manifest_icons = params_.icons; |
| } |
| |
| InstallFromSyncCommand::~InstallFromSyncCommand() = default; |
| |
| base::Value InstallFromSyncCommand::ToDebugValue() const { |
| base::Value::Dict debug_value; |
| debug_value.Set("name", "InstallFromSyncCommand"); |
| debug_value.Set("app_id", params_.app_id); |
| debug_value.Set("manifest_id", params_.manifest_id.value_or("<unset>")); |
| debug_value.Set("start_url", params_.start_url.spec()); |
| debug_value.Set("error_log", base::Value(error_log_.Clone())); |
| return base::Value(std::move(debug_value)); |
| } |
| |
| void InstallFromSyncCommand::OnShutdown() { |
| ReportResultAndDestroy( |
| params_.app_id, |
| webapps::InstallResultCode::kCancelledOnWebAppProviderShuttingDown); |
| } |
| |
| void InstallFromSyncCommand::OnSyncSourceRemoved() { |
| // Since this is a sync install command, if an uninstall is queued, just |
| // cancel this command. |
| ReportResultAndDestroy(params_.app_id, |
| webapps::InstallResultCode::kHaltedBySyncUninstall); |
| } |
| |
| void InstallFromSyncCommand::Start() { |
| url_loader_->LoadUrl( |
| params_.start_url, shared_web_contents(), |
| WebAppUrlLoader::UrlComparison::kIgnoreQueryParamsAndRef, |
| base::BindOnce( |
| &InstallFromSyncCommand::OnWebAppUrlLoadedGetWebAppInstallInfo, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| void InstallFromSyncCommand::SetFallbackTriggeredForTesting( |
| base::OnceCallback<void(webapps::InstallResultCode code)> callback) { |
| fallback_triggered_for_testing_ = std::move(callback); |
| } |
| |
| void InstallFromSyncCommand::OnWebAppUrlLoadedGetWebAppInstallInfo( |
| WebAppUrlLoader::Result result) { |
| if (result != WebAppUrlLoader::Result::kUrlLoaded) { |
| base::Value::Dict url_loader_error; |
| url_loader_error.Set("WebAppUrlLoader::Result", |
| ConvertUrlLoaderResultToString(result)); |
| error_log_.Append(std::move(url_loader_error)); |
| install_error_log_entry_.LogUrlLoaderError( |
| "OnWebAppUrlLoaded", params_.start_url.spec(), result); |
| } |
| |
| if (result == WebAppUrlLoader::Result::kRedirectedUrlLoaded) { |
| InstallFallback(webapps::InstallResultCode::kInstallURLRedirected); |
| return; |
| } |
| |
| if (result == WebAppUrlLoader::Result::kFailedPageTookTooLong) { |
| InstallFallback(webapps::InstallResultCode::kInstallURLLoadTimeOut); |
| return; |
| } |
| |
| if (result != WebAppUrlLoader::Result::kUrlLoaded) { |
| InstallFallback(webapps::InstallResultCode::kInstallURLLoadFailed); |
| return; |
| } |
| |
| data_retriever_->GetWebAppInstallInfo( |
| shared_web_contents(), |
| base::BindOnce(&InstallFromSyncCommand::OnGetWebAppInstallInfo, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| void InstallFromSyncCommand::OnGetWebAppInstallInfo( |
| std::unique_ptr<WebAppInstallInfo> web_app_info) { |
| if (!web_app_info) { |
| InstallFallback(webapps::InstallResultCode::kGetWebAppInstallInfoFailed); |
| return; |
| } |
| DCHECK(!install_info_); |
| install_info_ = std::move(web_app_info); |
| install_info_->user_display_mode = params_.user_display_mode; |
| // Prefer the synced title to the one from the page's metadata |
| install_info_->title = base::ASCIIToUTF16(params_.title); |
| |
| // Populate fallback info with the data retrieved from `GetWebAppInstallInfo` |
| fallback_install_info_->description = install_info_->description; |
| if (!install_info_->manifest_icons.empty()) |
| fallback_install_info_->manifest_icons = install_info_->manifest_icons; |
| fallback_install_info_->mobile_capable = install_info_->mobile_capable; |
| |
| data_retriever_->CheckInstallabilityAndRetrieveManifest( |
| shared_web_contents(), /*bypass_service_worker_check=*/true, |
| base::BindOnce(&InstallFromSyncCommand::OnDidPerformInstallableCheck, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| void InstallFromSyncCommand::OnDidPerformInstallableCheck( |
| blink::mojom::ManifestPtr opt_manifest, |
| const GURL& manifest_url, |
| bool valid_manifest_for_web_app, |
| bool is_installable) { |
| if (opt_manifest) { |
| UpdateWebAppInfoFromManifest(*opt_manifest, manifest_url, |
| install_info_.get()); |
| } else { |
| // If there is no manifest, set the manifest id from the parameters. |
| install_info_->manifest_id = params_.manifest_id; |
| } |
| |
| // Ensure that the manifest linked is the right one. |
| AppId generated_app_id = |
| GenerateAppId(install_info_->manifest_id, install_info_->start_url); |
| if (params_.app_id != generated_app_id) { |
| // Add the error to the log. |
| base::Value::Dict expected_id_error; |
| expected_id_error.Set("expected_app_id", params_.app_id); |
| expected_id_error.Set("app_id", generated_app_id); |
| error_log_.Append(std::move(expected_id_error)); |
| |
| install_error_log_entry_.LogExpectedAppIdError( |
| "OnDidPerformInstallableCheck", params_.start_url.spec(), |
| generated_app_id, params_.app_id); |
| |
| InstallFallback(webapps::InstallResultCode::kExpectedAppIdCheckFailed); |
| return; |
| } |
| |
| const bool manifest_has_icons = opt_manifest && !opt_manifest->icons.empty(); |
| |
| std::vector<GURL> icon_urls = GetValidIconUrlsToDownload(*install_info_); |
| data_retriever_->GetIcons( |
| shared_web_contents(), std::move(icon_urls), |
| /*skip_page_favicons=*/manifest_has_icons, |
| base::BindOnce(&InstallFromSyncCommand::OnIconsRetrievedFinalizeInstall, |
| weak_ptr_factory_.GetWeakPtr(), |
| FinalizeMode::kNormalWebAppInfo)); |
| } |
| |
| void InstallFromSyncCommand::OnIconsRetrievedFinalizeInstall( |
| FinalizeMode mode, |
| IconsDownloadedResult result, |
| IconsMap icons_map, |
| DownloadedIconsHttpResults icons_http_results) { |
| WebAppInstallInfo* current_info = mode == FinalizeMode::kNormalWebAppInfo |
| ? install_info_.get() |
| : fallback_install_info_.get(); |
| PopulateProductIcons(current_info, &icons_map); |
| PopulateOtherIcons(current_info, icons_map); |
| |
| RecordDownloadedIconsHttpResultsCodeClass( |
| "WebApp.Icon.HttpStatusCodeClassOnSync", result, icons_http_results); |
| UMA_HISTOGRAM_ENUMERATION("WebApp.Icon.DownloadedResultOnSync", result); |
| RecordDownloadedIconHttpStatusCodes( |
| "WebApp.Icon.DownloadedHttpStatusCodeOnSync", icons_http_results); |
| install_error_log_entry_.LogDownloadedIconsErrors( |
| *current_info, result, icons_map, icons_http_results); |
| |
| finalizer_->FinalizeInstall( |
| *current_info, GetFinalizerOptionForSyncInstall(), |
| base::BindOnce(&InstallFromSyncCommand::OnInstallFinalized, |
| weak_ptr_factory_.GetWeakPtr(), mode)); |
| } |
| |
| void InstallFromSyncCommand::OnInstallFinalized(FinalizeMode mode, |
| const AppId& app_id, |
| webapps::InstallResultCode code, |
| OsHooksErrors os_hooks_errors) { |
| if (mode == FinalizeMode::kNormalWebAppInfo && !IsSuccess(code)) { |
| InstallFallback(code); |
| return; |
| } |
| ReportResultAndDestroy(app_id, code); |
| } |
| |
| void InstallFromSyncCommand::InstallFallback(webapps::InstallResultCode code) { |
| DCHECK(!IsSuccess(code)); |
| DCHECK(code != webapps::InstallResultCode::kWebContentsDestroyed); |
| DCHECK(code != webapps::InstallResultCode::kInstallTaskDestroyed); |
| |
| std::vector<GURL> icon_urls = |
| GetValidIconUrlsToDownload(*fallback_install_info_); |
| |
| base::UmaHistogramEnumeration("WebApp.Install.SyncFallbackInstallInitiated", |
| code); |
| |
| if (fallback_triggered_for_testing_) |
| std::move(fallback_triggered_for_testing_).Run(code); |
| |
| // It is OK to skip downloading the page favicons as everything in is the URL |
| // list. |
| // TODO(dmurph): Also use favicons. https://crbug.com/1328977. |
| data_retriever_->GetIcons( |
| shared_web_contents(), std::move(icon_urls), |
| /*skip_page_favicons=*/true, |
| base::BindOnce(&InstallFromSyncCommand::OnIconsRetrievedFinalizeInstall, |
| weak_ptr_factory_.GetWeakPtr(), |
| FinalizeMode::kFallbackWebAppInfo)); |
| } |
| |
| void InstallFromSyncCommand::ReportResultAndDestroy( |
| const AppId& app_id, |
| webapps::InstallResultCode code) { |
| bool success = IsSuccess(code); |
| if (success) { |
| RecordWebAppInstallationTimestamp(profile_->GetPrefs(), app_id, |
| webapps::WebappInstallSource::SYNC); |
| } |
| // The install event is NOT reported on purpose (e.g. |
| // `webapps::InstallableMetrics::TrackInstallEvent(source)` is not called), as |
| // a sync install is not a recordable install source. |
| DCHECK(!webapps::InstallableMetrics::IsReportableInstallSource( |
| webapps::WebappInstallSource::SYNC)); |
| if (install_error_log_entry_.HasErrorDict()) { |
| command_manager()->LogToInstallManager( |
| install_error_log_entry_.TakeErrorDict()); |
| } |
| |
| base::UmaHistogramEnumeration("WebApp.InstallResult.Sync", code); |
| SignalCompletionAndSelfDestruct( |
| success ? CommandResult::kSuccess : CommandResult::kFailure, |
| base::BindOnce(std::move(install_callback_), app_id, code)); |
| } |
| |
| } // namespace web_app |