blob: 0108f6efc5520152b31e41255f21d68229cdf77b [file] [log] [blame]
// Copyright 2022 The Chromium Authors
// 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/containers/flat_set.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/histogram_macros.h"
#include "base/strings/to_string.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/generated_icon_fix_util.h"
#include "chrome/browser/web_applications/install_bounce_metric.h"
#include "chrome/browser/web_applications/jobs/manifest_to_web_app_install_info_job.h"
#include "chrome/browser/web_applications/locks/shared_web_contents_with_app_lock.h"
#include "chrome/browser/web_applications/proto/web_app_install_state.pb.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_icon_operations.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_registry_update.h"
#include "chrome/browser/web_applications/web_app_utils.h"
#include "chrome/browser/web_applications/web_contents/web_app_data_retriever.h"
#include "chrome/browser/web_applications/web_contents/web_contents_manager.h"
#include "components/webapps/browser/install_result_code.h"
#include "components/webapps/browser/installable/installable_logging.h"
#include "components/webapps/browser/installable/installable_metrics.h"
#include "components/webapps/browser/web_contents/web_app_url_loader.h"
#include "content/public/browser/web_contents.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.install_state =
AreAppsLocallyInstalledBySync()
? proto::InstallState::INSTALLED_WITH_OS_INTEGRATION
: proto::InstallState::SUGGESTED_FROM_ANOTHER_DEVICE;
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(
const webapps::AppId& app_id,
const webapps::ManifestId& manifest_id,
const GURL& start_url,
const std::string& title,
const GURL& scope,
const std::optional<SkColor>& theme_color,
const std::optional<mojom::UserDisplayMode>& user_display_mode,
const std::vector<apps::IconInfo>& manifest_icons,
const std::vector<apps::IconInfo>& trusted_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),
manifest_icons(manifest_icons),
trusted_icons(trusted_icons) {
CHECK(!app_id.empty());
CHECK(manifest_id.is_valid());
CHECK(!manifest_id.is_empty());
}
InstallFromSyncCommand::Params::Params(const Params&) = default;
InstallFromSyncCommand::InstallFromSyncCommand(
Profile* profile,
const Params& params,
OnceInstallCallback install_callback)
: WebAppCommand<SharedWebContentsWithAppLock,
const webapps::AppId&,
webapps::InstallResultCode>(
"InstallFromSyncCommand",
SharedWebContentsWithAppLockDescription({params.app_id}),
std::move(install_callback),
/*args_for_shutdown=*/
std::make_tuple(params.app_id,
webapps::InstallResultCode::
kCancelledOnWebAppProviderShuttingDown)),
profile_(profile),
params_(params),
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>(
params_.manifest_id, 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_.manifest_icons;
fallback_install_info_->trusted_icons = params_.trusted_icons;
GetMutableDebugValue().Set("app_id", params_.app_id);
GetMutableDebugValue().Set("manifest_id", params_.manifest_id.spec());
GetMutableDebugValue().Set("title", params_.title);
GetMutableDebugValue().Set(
"user_display_mode",
params_.user_display_mode
? base::ToString(params_.user_display_mode.value())
: "<unset>");
GetMutableDebugValue().Set("scope", params_.scope.spec());
GetMutableDebugValue().Set("start_url", params_.start_url.spec());
GetMutableDebugValue().Set("fallback_install", false);
}
InstallFromSyncCommand::~InstallFromSyncCommand() = default;
void InstallFromSyncCommand::StartWithLock(
std::unique_ptr<SharedWebContentsWithAppLock> lock) {
lock_ = std::move(lock);
url_loader_ = lock_->web_contents_manager().CreateUrlLoader();
data_retriever_ = lock_->web_contents_manager().CreateDataRetriever();
url_loader_->LoadUrl(
params_.start_url, &lock_->shared_web_contents(),
webapps::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(
webapps::WebAppUrlLoaderResult result) {
GetMutableDebugValue().Set("WebAppUrlLoader::Result",
ConvertUrlLoaderResultToString(result));
if (result != webapps::WebAppUrlLoaderResult::kUrlLoaded) {
install_error_log_entry_.LogUrlLoaderError(
"OnWebAppUrlLoaded", params_.start_url.spec(), result);
}
if (result == webapps::WebAppUrlLoaderResult::kRedirectedUrlLoaded) {
InstallFallback(webapps::InstallResultCode::kInstallURLRedirected);
return;
}
if (result == webapps::WebAppUrlLoaderResult::kFailedPageTookTooLong) {
InstallFallback(webapps::InstallResultCode::kInstallURLLoadTimeOut);
return;
}
if (result != webapps::WebAppUrlLoaderResult::kUrlLoaded) {
InstallFallback(webapps::InstallResultCode::kInstallURLLoadFailed);
return;
}
data_retriever_->GetWebAppInstallInfo(
&lock_->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;
}
// Populate fallback info with the data retrieved from `GetWebAppInstallInfo`.
// `trusted_icons` do not need to be updated here because the
// `WebAppInstallInfo` obtained here is from the page metadata.
fallback_install_info_->description = web_app_info->description;
if (!web_app_info->manifest_icons.empty()) {
fallback_install_info_->manifest_icons = web_app_info->manifest_icons;
}
fallback_install_info_->mobile_capable = web_app_info->mobile_capable;
data_retriever_->CheckInstallabilityAndRetrieveManifest(
&lock_->shared_web_contents(),
base::BindOnce(&InstallFromSyncCommand::OnDidPerformInstallableCheck,
weak_ptr_factory_.GetWeakPtr(), std::move(web_app_info)));
}
void InstallFromSyncCommand::OnDidPerformInstallableCheck(
std::unique_ptr<WebAppInstallInfo> page_info,
blink::mojom::ManifestPtr opt_manifest,
bool valid_manifest_for_web_app,
webapps::InstallableStatusCode error_code) {
if (!opt_manifest) {
InstallFallback(webapps::InstallResultCode::kExpectedAppIdCheckFailed);
return;
}
page_info->title = base::UTF8ToUTF16(params_.title);
// If the page doesn't have a favicon, then the icon fetcher will hang
// forever.
// TODO(crbug.com/40226606): Allow favicons without waiting for them to
// be updated on the page.
manifest_to_install_info_job_ =
ManifestToWebAppInstallInfoJob::CreateAndStart(
*opt_manifest, *data_retriever_.get(),
/*background_installation=*/true, webapps::WebappInstallSource::SYNC,
lock_->shared_web_contents().GetWeakPtr(), [](IconUrlSizeSet&) {},
GetMutableDebugValue(),
base::BindOnce(
&InstallFromSyncCommand::
OnWebAppInstallInfoRetrievedMergeAndFinalizeInstall,
weak_ptr_factory_.GetWeakPtr()),
WebAppInstallInfoConstructOptions{}, page_info->Clone());
}
void InstallFromSyncCommand::
OnWebAppInstallInfoRetrievedMergeAndFinalizeInstall(
std::unique_ptr<WebAppInstallInfo> install_info) {
// Ensure that the manifest linked is the right one.
webapps::AppId generated_app_id =
GenerateAppIdFromManifestId(install_info->manifest_id());
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);
GetMutableDebugValue().Set("app_id_error", 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;
}
CHECK(!install_info_);
install_info_ = std::move(install_info);
install_info_->user_display_mode = params_.user_display_mode;
FinalizeInstall(FinalizeMode::kNormalWebAppInfo);
}
void InstallFromSyncCommand::OnIconsRetrievedForFallbackInfo(
IconsDownloadedResult result,
IconsMap icons_map,
DownloadedIconsHttpResults icons_http_results) {
PopulateProductIcons(fallback_install_info_.get(), &icons_map);
PopulateOtherIcons(fallback_install_info_.get(), 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(
*fallback_install_info_.get(), result, icons_map, icons_http_results);
FinalizeInstall(FinalizeMode::kFallbackWebAppInfo);
}
void InstallFromSyncCommand::FinalizeInstall(FinalizeMode mode) {
WebAppInstallInfo* current_info = mode == FinalizeMode::kNormalWebAppInfo
? install_info_.get()
: fallback_install_info_.get();
current_info->generated_icon_fix =
generated_icon_fix_util::CreateInitialTimeWindow(
proto::GENERATED_ICON_FIX_SOURCE_SYNC_INSTALL);
lock_->install_finalizer().FinalizeInstall(
*current_info, GetFinalizerOptionForSyncInstall(),
base::BindOnce(&InstallFromSyncCommand::OnInstallFinalized,
weak_ptr_factory_.GetWeakPtr(), mode));
}
void InstallFromSyncCommand::OnInstallFinalized(
FinalizeMode mode,
const webapps::AppId& app_id,
webapps::InstallResultCode code) {
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);
GetMutableDebugValue().Set("fallback_install", true);
GetMutableDebugValue().Set("fallback_install_reason", base::ToString(code));
IconUrlSizeSet 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(
&lock_->shared_web_contents(), std::move(icon_urls),
/*download_page_favicons=*/false,
/*fail_all_if_any_fail=*/false,
base::BindOnce(&InstallFromSyncCommand::OnIconsRetrievedForFallbackInfo,
weak_ptr_factory_.GetWeakPtr()));
}
void InstallFromSyncCommand::ReportResultAndDestroy(
const webapps::AppId& app_id,
webapps::InstallResultCode code) {
bool success = IsSuccess(code);
GetMutableDebugValue().Set("result_code", base::ToString(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));
// TODO(crbug.com/40826246): migrate LogToInstallManager to take a
// base::Value::Dict
if (install_error_log_entry_.HasErrorDict()) {
command_manager()->LogToInstallManager(
install_error_log_entry_.TakeErrorDict());
}
if (manifest_to_install_info_job_) {
base::Value::Dict install_info_job_error_dict =
manifest_to_install_info_job_
->GetManifestToWebAppInfoGenerationErrors();
if (!install_info_job_error_dict.empty()) {
command_manager()->LogToInstallManager(
std::move(install_info_job_error_dict));
}
}
base::UmaHistogramEnumeration("Webapp.InstallResult.Sync", code);
CompleteAndSelfDestruct(
success ? CommandResult::kSuccess : CommandResult::kFailure, app_id,
code);
}
} // namespace web_app