// Copyright 2023 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/app_install_service_ash.h"
#include "ash/constants/ash_features.h"
#include "base/debug/stack_trace.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/functional/callback_helpers.h"
#include "base/logging.h"
#include "base/metrics/histogram_functions.h"
#include "base/ranges/algorithm.h"
#include "chrome/browser/apps/app_service/app_install/app_install.pb.h"
#include "chrome/browser/apps/app_service/app_install/app_install_discovery_metrics.h"
#include "chrome/browser/apps/app_service/app_install/app_install_types.h"
#include "chrome/browser/apps/app_service/launch_utils.h"
#include "chrome/browser/ash/borealis/borealis_game_install_flow.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/webui/ash/app_install/app_install.mojom.h"
// TODO(crbug.com/40283709): Remove circular dependency.
#include "chrome/browser/ui/webui/ash/app_install/app_install_dialog.h" // nogncheck
#include "chrome/browser/ui/webui/ash/app_install/app_install_page_handler.h" // nogncheck
#include "chromeos/ash/components/browser_context_helper/browser_context_types.h"
#include "chromeos/constants/chromeos_features.h"
#include "components/services/app_service/public/cpp/app_registry_cache.h"
#include "components/services/app_service/public/cpp/app_types.h"
#include "components/services/app_service/public/cpp/package_id.h"
#include "components/services/app_service/public/cpp/types_util.h"
#include "components/user_manager/user_manager.h"
#include "net/base/url_util.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
#include "third_party/abseil-cpp/absl/types/variant.h"
#include "ui/gfx/native_widget_types.h"
namespace apps {
namespace {
std::optional<QueryError::Type> VerifyAppInstallData(
const base::expected<AppInstallData, QueryError>& data,
const PackageId& expected_package_id) {
if (data.has_value()) {
if (data->package_id != expected_package_id) {
return QueryError::kBadResponse;
}
if (!data->IsValidForInstallation()) {
return QueryError::kBadResponse;
}
return std::nullopt;
}
return data.error().type;
}
AppInstallResult AppInstallResultFromQueryError(
QueryError::Type query_error_type) {
switch (query_error_type) {
case QueryError::kConnectionError:
return AppInstallResult::kAlmanacFetchFailed;
case QueryError::kBadRequest:
return AppInstallResult::kBadAppRequest;
case QueryError::kBadResponse:
return AppInstallResult::kAppDataCorrupted;
}
}
void RecordInstallResult(base::OnceClosure callback,
AppInstallSurface surface,
AppInstallResult result) {
base::UmaHistogramEnumeration("Apps.AppInstallService.AppInstallResult",
result);
base::UmaHistogramEnumeration(
base::StrCat({"Apps.AppInstallService.AppInstallResult.",
base::ToString(surface)}),
result);
std::move(callback).Run();
}
} // namespace
base::OnceCallback<void(PackageId)>&
AppInstallServiceAsh::InstallAppCallbackForTesting() {
static base::NoDestructor<base::OnceCallback<void(PackageId)>> callback;
return *callback;
}
AppInstallServiceAsh::AppInstallServiceAsh(Profile& profile)
: profile_(profile),
device_info_manager_(&*profile_),
arc_app_installer_(&*profile_),
web_app_installer_(&*profile_) {}
AppInstallServiceAsh::~AppInstallServiceAsh() = default;
void AppInstallServiceAsh::InstallAppWithFallback(
AppInstallSurface surface,
std::string serialized_package_id,
std::optional<WindowIdentifier> anchor_window,
base::OnceClosure callback) {
if (std::optional<PackageId> package_id =
PackageId::FromString(serialized_package_id)) {
InstallApp(surface, std::move(package_id).value(), anchor_window,
std::move(callback));
return;
}
base::OnceCallback<void(AppInstallResult)> result_callback =
base::BindOnce(&RecordInstallResult, std::move(callback), surface);
FetchAppInstallUrl(
std::move(serialized_package_id),
base::BindOnce(&AppInstallServiceAsh::MaybeLaunchAppInstallUrl,
weak_ptr_factory_.GetWeakPtr(),
std::move(result_callback)));
}
void AppInstallServiceAsh::InstallApp(
AppInstallSurface surface,
PackageId package_id,
std::optional<gfx::NativeWindow> anchor_window,
base::OnceClosure callback) {
if (InstallAppCallbackForTesting()) {
std::move(InstallAppCallbackForTesting()).Run(package_id);
}
RecordAppDiscoveryMetricForInstallRequest(&profile_.get(), surface,
package_id);
base::OnceCallback<void(AppInstallResult)> result_callback =
base::BindOnce(&RecordInstallResult, std::move(callback), surface);
if (!CanUserInstall()) {
std::move(result_callback).Run(AppInstallResult::kUserTypeNotPermitted);
return;
}
switch (package_id.package_type()) {
case PackageType::kArc:
case PackageType::kGeForceNow:
case PackageType::kWeb:
case PackageType::kWebsite: {
// Observe for `anchor_window` being destroyed during async work.
std::unique_ptr<views::NativeWindowTracker> anchor_window_tracker;
if (anchor_window) {
anchor_window_tracker =
views::NativeWindowTracker::Create(*anchor_window);
}
FetchAppInstallData(
package_id,
base::BindOnce(&AppInstallServiceAsh::ShowDialogAndInstall,
weak_ptr_factory_.GetWeakPtr(), surface, package_id,
anchor_window, std::move(anchor_window_tracker),
std::move(result_callback)));
return;
}
case PackageType::kBorealis: {
if (!base::FeatureList::IsEnabled(
ash::features::kAppInstallServiceUriBorealis)) {
std::move(result_callback)
.Run(AppInstallResult::kAppProviderNotAvailable);
return;
}
// Parse the Steam Game ID from the PackageId.
uint64_t steam_game_id;
if (!base::StringToUint64(package_id.identifier(), &steam_game_id)) {
std::move(result_callback).Run(AppInstallResult::kAppDataCorrupted);
return;
}
borealis::UserRequestedSteamGameInstall(&*profile_, steam_game_id);
// We've now launched the Borealis installer or the Steam Store
// website. We don't yet know whether that flow will result in a
// successfully installed game.
std::move(result_callback).Run(AppInstallResult::kUnknown);
return;
}
case PackageType::kChromeApp:
case PackageType::kSystem:
case PackageType::kUnknown:
// TODO(b/303350800): Generalize to work with all app types.
std::move(result_callback).Run(AppInstallResult::kAppTypeNotSupported);
return;
}
}
void AppInstallServiceAsh::InstallAppHeadless(
AppInstallSurface surface,
PackageId package_id,
base::OnceCallback<void(bool success)> callback) {
FetchAppInstallData(
package_id, base::BindOnce(&AppInstallServiceAsh::PerformInstallHeadless,
weak_ptr_factory_.GetWeakPtr(), surface,
package_id, std::move(callback)));
}
void AppInstallServiceAsh::InstallAppHeadless(
AppInstallSurface surface,
AppInstallData data,
base::OnceCallback<void(bool success)> callback) {
PerformInstallHeadless(surface, data.package_id, std::move(callback), data);
}
bool AppInstallServiceAsh::CanUserInstall() const {
if (profile_->IsSystemProfile()) {
return false;
}
if (!ash::IsUserBrowserContext(&*profile_)) {
return false;
}
auto* user_manager = user_manager::UserManager::Get();
if (!user_manager) {
return false;
}
if (user_manager->IsLoggedInAsManagedGuestSession() ||
user_manager->IsLoggedInAsGuest() ||
user_manager->IsLoggedInAsAnyKioskApp()) {
return false;
}
return true;
}
void AppInstallServiceAsh::FetchAppInstallData(
PackageId package_id,
app_install_almanac_endpoint::GetAppInstallInfoCallback data_callback) {
device_info_manager_.GetDeviceInfo(
base::BindOnce(&AppInstallServiceAsh::FetchAppInstallDataWithDeviceInfo,
weak_ptr_factory_.GetWeakPtr(), std::move(package_id),
std::move(data_callback)));
}
void AppInstallServiceAsh::FetchAppInstallDataWithDeviceInfo(
PackageId package_id,
app_install_almanac_endpoint::GetAppInstallInfoCallback data_callback,
DeviceInfo device_info) {
app_install_almanac_endpoint::GetAppInstallInfo(
package_id, std::move(device_info), *profile_->GetURLLoaderFactory(),
std::move(data_callback));
}
void AppInstallServiceAsh::PerformInstallHeadless(
AppInstallSurface surface,
PackageId expected_package_id,
base::OnceCallback<void(bool success)> callback,
base::expected<AppInstallData, QueryError> data) {
// TODO(b/327535848): Record metrics for headless installs.
if (!data.has_value()) {
std::move(callback).Run(false);
return;
}
RecordAppDiscoveryMetricForInstallRequest(&profile_.get(), surface,
expected_package_id);
PerformInstall(surface, *data, std::move(callback));
}
void AppInstallServiceAsh::ShowDialogAndInstall(
AppInstallSurface surface,
PackageId expected_package_id,
std::optional<gfx::NativeWindow> anchor_window,
std::unique_ptr<views::NativeWindowTracker> anchor_window_tracker,
base::OnceCallback<void(AppInstallResult)> callback,
base::expected<AppInstallData, QueryError> data) {
gfx::NativeWindow parent =
anchor_window.has_value() &&
!anchor_window_tracker->WasNativeWindowDestroyed()
? anchor_window.value()
: nullptr;
if (std::optional<QueryError::Type> query_error =
VerifyAppInstallData(data, expected_package_id)) {
base::WeakPtr<ash::app_install::AppInstallDialog> dialog =
ash::app_install::AppInstallDialog::CreateDialog();
switch (query_error.value()) {
case QueryError::kConnectionError:
dialog->ShowConnectionError(
parent, base::BindOnce(&AppInstallServiceAsh::InstallApp,
weak_ptr_factory_.GetWeakPtr(), surface,
expected_package_id, anchor_window,
base::DoNothing()));
break;
case QueryError::kBadRequest:
case QueryError::kBadResponse:
dialog->ShowNoAppError(parent);
break;
}
std::move(callback).Run(
AppInstallResultFromQueryError(query_error.value()));
return;
}
bool show_install_dialog =
expected_package_id.package_type() == PackageType::kWeb ||
expected_package_id.package_type() == PackageType::kWebsite;
// If we can't show the install dialog, we must have an install URL to open.
if (!show_install_dialog) {
// This is checked by VerifyAppInstallData:
CHECK(data->install_url.is_valid());
LaunchUrlInInstalledAppOrBrowser(&*profile_, data->install_url,
LaunchSource::kFromInstaller);
std::move(callback).Run(AppInstallResult::kUnknown);
return;
}
// The install dialog is only used for web apps currently.
CHECK(absl::holds_alternative<WebAppInstallData>(data->app_type_data));
const WebAppInstallData& web_app_data =
absl::get<WebAppInstallData>(data->app_type_data);
if (expected_package_id.package_type() == PackageType::kWebsite) {
// kWebsite packages will end up installed as a regular kWeb app. Pass a
// kWeb package ID to the Install Dialog so that it can look for the correct
// installed app.
// An alternative would be to set the installer_package_id for shortcut web
// apps as kWebsite in App Service. However, this is difficult to manage
// correctly, as the user could already have a non-shortcut web app
// installed with the same identifier.
expected_package_id =
PackageId(PackageType::kWeb, expected_package_id.identifier());
}
std::vector<ash::app_install::mojom::ScreenshotPtr> screenshots;
for (auto& screenshot : data->screenshots) {
auto dialog_screenshot = ash::app_install::mojom::Screenshot::New();
dialog_screenshot->url = screenshot.url;
dialog_screenshot->size =
gfx::Size(screenshot.width_in_pixels, screenshot.height_in_pixels);
screenshots.push_back(std::move(dialog_screenshot));
}
base::WeakPtr<ash::app_install::AppInstallDialog> dialog =
ash::app_install::AppInstallDialog::CreateDialog();
dialog->ShowApp(&*profile_, parent, expected_package_id, data->name,
web_app_data.document_url, data->description, data->icon,
std::move(screenshots),
base::BindOnce(&AppInstallServiceAsh::InstallIfDialogAccepted,
weak_ptr_factory_.GetWeakPtr(), surface,
data.value(), dialog, std::move(callback)));
}
void AppInstallServiceAsh::InstallIfDialogAccepted(
AppInstallSurface surface,
AppInstallData data,
base::WeakPtr<ash::app_install::AppInstallDialog> dialog,
base::OnceCallback<void(AppInstallResult)> callback,
bool dialog_accepted) {
if (!dialog_accepted) {
std::move(callback).Run(AppInstallResult::kInstallDialogNotAccepted);
return;
}
PerformInstall(surface, data,
base::BindOnce(&AppInstallServiceAsh::ProcessInstallResult,
weak_ptr_factory_.GetWeakPtr(), surface, data,
dialog, std::move(callback)));
}
void AppInstallServiceAsh::ProcessInstallResult(
AppInstallSurface surface,
AppInstallData data,
base::WeakPtr<ash::app_install::AppInstallDialog> dialog,
base::OnceCallback<void(AppInstallResult)> callback,
bool install_success) {
if (!dialog) {
std::move(callback).Run(install_success
? AppInstallResult::kSuccess
: AppInstallResult::kAppTypeInstallFailed);
return;
}
if (install_success) {
dialog->SetInstallSucceeded();
std::move(callback).Run(AppInstallResult::kSuccess);
return;
}
dialog->SetInstallFailed(
base::BindOnce(&AppInstallServiceAsh::InstallIfDialogAccepted,
weak_ptr_factory_.GetWeakPtr(), surface, std::move(data),
dialog, std::move(callback)));
}
void AppInstallServiceAsh::PerformInstall(
AppInstallSurface surface,
AppInstallData data,
base::OnceCallback<void(bool)> install_callback) {
if (absl::holds_alternative<AndroidAppInstallData>(data.app_type_data)) {
arc_app_installer_.InstallApp(surface, std::move(data),
std::move(install_callback));
} else if (absl::holds_alternative<WebAppInstallData>(data.app_type_data)) {
web_app_installer_.InstallApp(surface, std::move(data),
std::move(install_callback));
} else {
LOG(ERROR) << "Unsupported AppInstallData type";
std::move(install_callback).Run(false);
}
}
void AppInstallServiceAsh::FetchAppInstallUrl(
std::string serialized_package_id,
base::OnceCallback<void(base::expected<GURL, QueryError>)> callback) {
device_info_manager_.GetDeviceInfo(
base::BindOnce(&AppInstallServiceAsh::FetchAppInstallUrlWithDeviceInfo,
weak_ptr_factory_.GetWeakPtr(),
std::move(serialized_package_id), std::move(callback)));
}
void AppInstallServiceAsh::FetchAppInstallUrlWithDeviceInfo(
std::string serialized_package_id,
base::OnceCallback<void(base::expected<GURL, QueryError>)> callback,
DeviceInfo device_info) {
app_install_almanac_endpoint::GetAppInstallUrl(
serialized_package_id, std::move(device_info),
*profile_->GetURLLoaderFactory(), std::move(callback));
}
void AppInstallServiceAsh::MaybeLaunchAppInstallUrl(
base::OnceCallback<void(AppInstallResult)> callback,
base::expected<GURL, QueryError> install_url) {
if (install_url.has_value()) {
LaunchUrlInInstalledAppOrBrowser(&*profile_, install_url.value(),
LaunchSource::kFromInstaller);
std::move(callback).Run(AppInstallResult::kInstallUrlFallback);
return;
}
base::WeakPtr<ash::app_install::AppInstallDialog> dialog =
ash::app_install::AppInstallDialog::CreateDialog();
switch (install_url.error().type) {
case QueryError::kConnectionError:
// TODO(b/339548810): Show connection error dialog instead, this needs
// the parameters necessary for a retry_callback to be plumbed through
// to here.
case QueryError::kBadRequest:
case QueryError::kBadResponse:
// TODO(b/339548810): Plumb the parent window through to here.
dialog->ShowNoAppError(/*parent=*/nullptr);
break;
}
std::move(callback).Run(
AppInstallResultFromQueryError(install_url.error().type));
}
} // namespace apps