// Copyright 2024 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/apps/app_service/app_install/web_app_installer.h"
#include <memory>
#include "base/barrier_callback.h"
#include "base/metrics/histogram_functions.h"
#include "chrome/browser/ash/crosapi/crosapi_ash.h"
#include "chrome/browser/ash/crosapi/crosapi_manager.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/web_applications/commands/install_app_from_verified_manifest_command.h"
#include "chrome/browser/web_applications/mojom/user_display_mode.mojom-shared.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_params.h"
#include "chrome/browser/web_applications/web_app_provider.h"
#include "chrome/browser/web_applications/web_app_utils.h"
#include "chromeos/crosapi/mojom/web_app_types.mojom.h"
#include "components/services/app_service/public/cpp/app_types.h"
#include "components/webapps/browser/install_result_code.h"
#include "components/webapps/browser/installable/installable_metrics.h"
#include "net/base/net_errors.h"
#include "net/traffic_annotation/network_traffic_annotation.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
#include "services/network/public/cpp/simple_url_loader.h"
#include "services/network/public/mojom/url_response_head.mojom.h"
namespace {
// Maximum size of the manifest file. 1MB.
constexpr int kMaxManifestSizeInBytes = 1024 * 1024;
constexpr net::NetworkTrafficAnnotationTag kTrafficAnnotation =
net::DefineNetworkTrafficAnnotation("app_install_service_web_app_installer",
R"(
semantics {
sender: "App Install Service"
description:
"Sends a request to a Google server to retrieve web app installation"
"data."
trigger:
"Requests are sent as part of App Install Service triggered installs "
"for web apps."
internal: {
contacts {
email: "[email protected]"
}
}
user_data: {
type: NONE
}
data: "None"
destination: GOOGLE_OWNED_SERVICE
last_reviewed: "2024-01-02"
}
policy {
cookies_allowed: NO
setting: "This feature cannot be disabled by settings."
policy_exception_justification:
"This feature is required to deliver core user experiences and "
"cannot be disabled by policy."
}
)");
int GetResponseCode(network::SimpleURLLoader* simple_loader) {
if (simple_loader->ResponseInfo() && simple_loader->ResponseInfo()->headers) {
return simple_loader->ResponseInfo()->headers->response_code();
} else {
return -1;
}
}
void RecordInstallResultMetric(apps::AppInstallSurface surface,
apps::WebAppInstallResult result) {
base::UmaHistogramEnumeration(
"Apps.AppInstallService.WebAppInstaller.InstallResult", result);
base::UmaHistogramEnumeration(
base::StrCat({"Apps.AppInstallService.WebAppInstaller.InstallResult.",
base::ToString(surface)}),
result);
}
void RecordCommandResultMetric(apps::AppInstallSurface surface,
webapps::InstallResultCode code) {
base::UmaHistogramEnumeration(
"Apps.AppInstallService.WebAppInstaller.CommandResultCode", code);
base::UmaHistogramEnumeration(
base::StrCat({"Apps.AppInstallService.WebAppInstaller.CommandResultCode.",
base::ToString(surface)}),
code);
}
} // namespace
namespace apps {
WebAppInstaller::WebAppInstaller(Profile* profile) : profile_(profile) {
// Check CrosapiManager::IsInitialized as it is not initialized in some unit
// tests. This should never fail in production code.
if (web_app::IsWebAppsCrosapiEnabled() &&
crosapi::CrosapiManager::IsInitialized()) {
// Add an observer to observe when the lacros bridge connects.
crosapi::WebAppServiceAsh* web_app_service_ash =
crosapi::CrosapiManager::Get()->crosapi_ash()->web_app_service_ash();
web_app_service_observer_.Observe(web_app_service_ash);
}
}
WebAppInstaller::~WebAppInstaller() = default;
void WebAppInstaller::InstallApp(AppInstallSurface surface,
AppInstallData data,
WebAppInstalledCallback callback) {
CHECK(absl::holds_alternative<WebAppInstallData>(data.app_type_data));
// Retrieve web manifest
auto resource_request = std::make_unique<network::ResourceRequest>();
resource_request->url =
absl::get<WebAppInstallData>(data.app_type_data).proxied_manifest_url;
if (!resource_request->url.is_valid()) {
LOG(ERROR) << "Manifest URL for " << data.name
<< "is invalid: " << resource_request->url;
RecordInstallResultMetric(surface,
WebAppInstallResult::kInvalidManifestUrl);
std::move(callback).Run(/*success=*/false);
return;
}
resource_request->method = "GET";
resource_request->credentials_mode = network::mojom::CredentialsMode::kOmit;
std::unique_ptr<network::SimpleURLLoader> simple_loader =
network::SimpleURLLoader::Create(std::move(resource_request),
kTrafficAnnotation);
auto* loader_ptr = simple_loader.get();
loader_ptr->DownloadToString(
profile_->GetURLLoaderFactory().get(),
base::BindOnce(&WebAppInstaller::OnManifestRetrieved,
weak_ptr_factory_.GetWeakPtr(), std::move(surface),
std::move(data), std::move(callback),
std::move(simple_loader)),
kMaxManifestSizeInBytes);
}
void WebAppInstaller::OnWebAppProviderBridgeConnected() {
MaybeSendPendingCrosapiRequests();
}
void WebAppInstaller::OnWebAppServiceAshDestroyed() {
web_app_service_observer_.Reset();
}
void WebAppInstaller::OnManifestRetrieved(
AppInstallSurface surface,
AppInstallData data,
WebAppInstalledCallback callback,
std::unique_ptr<network::SimpleURLLoader> url_loader,
std::unique_ptr<std::string> response) {
if (url_loader->NetError() != net::OK) {
LOG(ERROR) << "Downloading manifest failed for " << data.name
<< " with error code: " << GetResponseCode(url_loader.get());
RecordInstallResultMetric(
surface, url_loader->NetError() == net::ERR_HTTP_RESPONSE_CODE_FAILURE
? WebAppInstallResult::kManifestResponseError
: WebAppInstallResult::kManifestNetworkError);
std::move(callback).Run(/*success=*/false);
return;
}
if (response->empty()) {
RecordInstallResultMetric(surface,
WebAppInstallResult::kManifestResponseEmpty);
std::move(callback).Run(/*success=*/false);
return;
}
webapps::AppId expected_app_id =
web_app::GenerateAppIdFromManifestId(GURL(data.package_id.identifier()));
auto& web_app_data = absl::get<WebAppInstallData>(data.app_type_data);
auto* provider = web_app::WebAppProvider::GetForWebApps(profile_);
if (web_app::IsWebAppsCrosapiEnabled()) {
auto web_app_install_info =
crosapi::mojom::WebAppVerifiedManifestInstallInfo::New();
web_app_install_info->document_url = web_app_data.document_url;
web_app_install_info->verified_manifest_url =
web_app_data.original_manifest_url;
web_app_install_info->expected_app_id = expected_app_id;
web_app_install_info->verified_manifest_contents = std::move(*response);
web_app_install_info->install_source = [&] {
switch (surface) {
case AppInstallSurface::kAppInstallUriUnknown:
case AppInstallSurface::kAppInstallUriShowoff:
case AppInstallSurface::kAppInstallUriMall:
case AppInstallSurface::kAppInstallUriGetit:
case AppInstallSurface::kAppInstallUriLauncher:
case AppInstallSurface::kAppInstallUriPeripherals:
return crosapi::mojom::WebAppInstallSource::kAlmanacInstallAppUri;
case AppInstallSurface::kAppPreloadServiceOem:
return crosapi::mojom::WebAppInstallSource::kOemPreload;
case AppInstallSurface::kAppPreloadServiceDefault:
return crosapi::mojom::WebAppInstallSource::kDefaultPreload;
case AppInstallSurface::kOobeAppRecommendations:
return crosapi::mojom::WebAppInstallSource::kOobeAppRecommendations;
}
}();
pending_crosapi_requests_.emplace_back(
std::move(web_app_install_info),
base::BindOnce(&WebAppInstaller::OnAppInstalled,
weak_ptr_factory_.GetWeakPtr(), surface,
std::move(callback)));
MaybeSendPendingCrosapiRequests();
return;
} else {
webapps::WebappInstallSource install_source = [&] {
switch (surface) {
case AppInstallSurface::kAppInstallUriUnknown:
case AppInstallSurface::kAppInstallUriShowoff:
case AppInstallSurface::kAppInstallUriMall:
case AppInstallSurface::kAppInstallUriGetit:
case AppInstallSurface::kAppInstallUriLauncher:
case AppInstallSurface::kAppInstallUriPeripherals:
return webapps::WebappInstallSource::ALMANAC_INSTALL_APP_URI;
case AppInstallSurface::kAppPreloadServiceOem:
return webapps::WebappInstallSource::PRELOADED_OEM;
case AppInstallSurface::kAppPreloadServiceDefault:
return webapps::WebappInstallSource::PRELOADED_DEFAULT;
case AppInstallSurface::kOobeAppRecommendations:
return webapps::WebappInstallSource::OOBE_APP_RECOMMENDATIONS;
}
}();
bool is_website = data.package_id.package_type() == PackageType::kWebsite;
web_app::WebAppInstallParams install_params;
if (is_website) {
install_params.user_display_mode =
web_app_data.open_as_window
? web_app::mojom::UserDisplayMode::kStandalone
: web_app::mojom::UserDisplayMode::kBrowser;
}
provider->command_manager().ScheduleCommand(
std::make_unique<web_app::InstallAppFromVerifiedManifestCommand>(
install_source,
/*document_url=*/web_app_data.document_url,
/*verified_manifest_url=*/web_app_data.original_manifest_url,
/*verified_manifest_contents=*/std::move(*response),
expected_app_id, /*is_diy_app=*/is_website, install_params,
base::BindOnce(&WebAppInstaller::OnAppInstalled,
weak_ptr_factory_.GetWeakPtr(), surface,
std::move(callback))));
}
}
void WebAppInstaller::MaybeSendPendingCrosapiRequests() {
CHECK(web_app::IsWebAppsCrosapiEnabled());
crosapi::mojom::WebAppProviderBridge* web_app_provider_bridge =
crosapi::CrosapiManager::Get()
->crosapi_ash()
->web_app_service_ash()
->GetWebAppProviderBridge();
if (!web_app_provider_bridge) {
return;
}
for (PendingCrosapiRequest& request :
std::exchange(pending_crosapi_requests_, {})) {
web_app_provider_bridge->InstallWebAppFromVerifiedManifest(
std::move(request.info), std::move(request.callback));
}
CHECK(pending_crosapi_requests_.empty());
}
void WebAppInstaller::OnAppInstalled(AppInstallSurface surface,
WebAppInstalledCallback callback,
const webapps::AppId& app_id,
webapps::InstallResultCode code) {
bool success = webapps::IsSuccess(code);
RecordInstallResultMetric(surface,
success ? WebAppInstallResult::kSuccess
: WebAppInstallResult::kWebAppInstallError);
RecordCommandResultMetric(surface, code);
std::move(callback).Run(success);
}
WebAppInstaller::PendingCrosapiRequest::PendingCrosapiRequest(
crosapi::mojom::WebAppVerifiedManifestInstallInfoPtr info,
crosapi::mojom::WebAppProviderBridge::
InstallWebAppFromVerifiedManifestCallback callback)
: info(std::move(info)), callback(std::move(callback)) {}
WebAppInstaller::PendingCrosapiRequest::PendingCrosapiRequest(
PendingCrosapiRequest&&) = default;
WebAppInstaller::PendingCrosapiRequest::~PendingCrosapiRequest() = default;
} // namespace apps