blob: 372d33181d27841e651a25fa5772da5248a1e636 [file] [log] [blame]
// 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/devtools/protocol/pwa_handler.h"
#include <vector>
#include "base/check.h"
#include "base/command_line.h"
#include "base/feature_list.h"
#include "base/files/file_path.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/functional/concurrent_callbacks.h"
#include "base/numerics/clamped_math.h"
#include "base/strings/strcat.h"
#include "base/strings/to_string.h"
#include "base/types/expected.h"
#include "build/build_config.h"
#include "chrome/browser/badging/badge_manager.h"
#include "chrome/browser/badging/badge_manager_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/web_applications/locks/app_lock.h"
#include "chrome/browser/web_applications/os_integration/os_integration_manager.h"
#include "chrome/browser/web_applications/os_integration/web_app_file_handler_manager.h"
#include "chrome/browser/web_applications/proto/web_app_install_state.pb.h"
#include "chrome/browser/web_applications/proto/web_app_os_integration_state.pb.h"
#include "chrome/browser/web_applications/web_app.h"
#include "chrome/browser/web_applications/web_app_command_scheduler.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_registrar.h"
#include "chrome/browser/web_applications/web_app_ui_manager.h"
#include "chrome/common/url_constants.h"
#include "components/webapps/browser/install_result_code.h"
#include "components/webapps/browser/installable/installable_metrics.h"
#include "components/webapps/common/web_app_id.h"
#include "content/public/browser/devtools_agent_host.h"
#include "content/public/browser/web_contents.h"
#include "third_party/blink/public/common/features.h"
#include "url/gurl.h"
namespace {
namespace errors {
// Returns a common error when WebApp component is unavailable in the scenario,
// e.g. in incognito mode.
protocol::Response WebAppUnavailable() {
return protocol::Response::ServerError(
"Webapps are not available in current profile.");
}
// Returns a common error when the to-be-installed webapp has a different
// manifest id than the required.
protocol::Response InconsistentManifestId(const std::string& in_manifest_id,
const std::string& url_or_appid) {
return protocol::Response::InvalidRequest(
base::StrCat({"Expected manifest id ", in_manifest_id,
" does not match input url or app id ", url_or_appid}));
}
} // namespace errors
using FileHandlers =
std::unique_ptr<protocol::Array<protocol::PWA::FileHandler>>;
base::expected<FileHandlers, protocol::Response> GetFileHandlersFromApp(
const webapps::AppId app_id,
const std::string in_manifest_id,
web_app::AppLock& app_lock,
base::Value::Dict& debug_value) {
const web_app::WebApp* web_app = app_lock.registrar().GetAppById(app_id);
if (web_app == nullptr) {
return base::unexpected(protocol::Response::InvalidParams(
base::StrCat({"Unknown web-app manifest id ", in_manifest_id})));
}
using protocol::PWA::FileHandler;
using protocol::PWA::FileHandlerAccept;
auto file_handlers = std::make_unique<protocol::Array<FileHandler>>();
for (const auto& input_handler : web_app->current_os_integration_states()
.file_handling()
.file_handlers()) {
auto accepts = std::make_unique<protocol::Array<FileHandlerAccept>>();
for (const auto& input_accept : input_handler.accept()) {
auto file_extensions = std::make_unique<protocol::Array<std::string>>();
for (const auto& input_file_extension : input_accept.file_extensions()) {
file_extensions->push_back(input_file_extension);
}
accepts->push_back(FileHandlerAccept::Create()
.SetMediaType(input_accept.mimetype())
.SetFileExtensions(std::move(file_extensions))
.Build());
}
file_handlers->push_back(FileHandler::Create()
.SetAction(input_handler.action())
.SetAccepts(std::move(accepts))
.SetDisplayName(input_handler.display_name())
.Build());
}
return base::ok(std::move(file_handlers));
}
// A shared way to handle the callback of WebAppUiManager::LaunchApp.
base::expected<std::string, protocol::Response> GetTargetIdFromLaunch(
const std::string& in_manifest_id,
const std::optional<GURL>& url,
base::WeakPtr<Browser> browser,
base::WeakPtr<content::WebContents> web_contents,
apps::LaunchContainer container) {
// The callback will always be provided with a valid Browser
// instance, but the web_contents is associated with the newly
// opened web app, and it can be used to indicate the success of the
// launch operation.
// See web_app::WebAppLaunchUtils::LaunchWebApp() for more
// information.
if (web_contents) {
return content::DevToolsAgentHost::GetOrCreateForTab(web_contents.get())
->GetId();
}
std::string msg = "Failed to launch " + in_manifest_id;
if (url) {
msg += " from url " + url->spec();
}
return base::unexpected(protocol::Response::InvalidRequest(msg));
}
} // namespace
PWAHandler::PWAHandler(protocol::UberDispatcher* dispatcher,
const std::string& target_id)
: target_id_(target_id) {
protocol::PWA::Dispatcher::wire(dispatcher, this);
}
PWAHandler::~PWAHandler() = default;
// TODO(crbug.com/331214986): Consider if the API should allow setting a browser
// context id as the profile id to override the default behavior.
Profile* PWAHandler::GetProfile() const {
auto host = content::DevToolsAgentHost::GetForId(target_id_);
if (host && host->GetBrowserContext()) {
return Profile::FromBrowserContext(host->GetBrowserContext());
}
return ProfileManager::GetLastUsedProfile();
}
web_app::WebAppCommandScheduler* PWAHandler::GetScheduler() const {
web_app::WebAppProvider* provider =
web_app::WebAppProvider::GetForWebApps(GetProfile());
if (provider) {
return &provider->scheduler();
}
return nullptr;
}
content::WebContents* PWAHandler::GetWebContents() const {
auto host = content::DevToolsAgentHost::GetForId(target_id_);
if (!host) {
return nullptr;
}
return host->GetWebContents();
}
void PWAHandler::GetOsAppState(
const std::string& in_manifest_id,
std::unique_ptr<GetOsAppStateCallback> callback) {
Profile* profile = GetProfile();
const webapps::AppId app_id =
web_app::GenerateAppIdFromManifestId(GURL{in_manifest_id});
int badge_count = 0;
{
badging::BadgeManager* badge_manager =
badging::BadgeManagerFactory::GetForProfile(profile);
CHECK(badge_manager);
std::optional<badging::BadgeManager::BadgeValue> badge =
badge_manager->GetBadgeValue(app_id);
if (badge && *badge) {
badge_count = base::ClampedNumeric<int32_t>(**badge);
}
}
web_app::WebAppProvider* provider =
web_app::WebAppProvider::GetForWebApps(profile);
if (!provider) {
std::move(callback)->sendFailure(errors::WebAppUnavailable());
return;
}
provider->scheduler().ScheduleCallbackWithResult(
"PWAHandler::GetOsAppState", web_app::AppLockDescription(app_id),
base::BindOnce(&GetFileHandlersFromApp, app_id, in_manifest_id),
base::BindOnce(
[](int badge_count, std::unique_ptr<GetOsAppStateCallback> callback,
base::expected<FileHandlers, protocol::Response>&& file_handlers) {
if (file_handlers.has_value()) {
std::move(callback)->sendSuccess(
badge_count, std::move(file_handlers).value());
} else {
std::move(callback)->sendFailure(file_handlers.error());
}
},
badge_count, std::move(callback)),
base::expected<FileHandlers, protocol::Response>(
base::unexpected(protocol::Response::ServerError(
base::StrCat({"web-app is shutting down when querying manifest ",
in_manifest_id})))));
}
// Install from the manifest_id only. Require a WebContents.
void PWAHandler::InstallFromManifestId(
const std::string& in_manifest_id,
std::unique_ptr<InstallCallback> callback) {
content::WebContents* contents = GetWebContents();
if (contents == nullptr) {
std::move(callback)->sendFailure(protocol::Response::InvalidRequest(
base::StrCat({"The devtools session has no associated web page when "
"installing ",
in_manifest_id})));
return;
}
auto* scheduler = GetScheduler();
if (!scheduler) {
std::move(callback)->sendFailure(errors::WebAppUnavailable());
return;
}
scheduler->FetchManifestAndInstall(
webapps::WebappInstallSource::DEVTOOLS, contents->GetWeakPtr(),
base::BindOnce(
[](const std::string& in_manifest_id,
base::WeakPtr<web_app::WebAppScreenshotFetcher>,
content::WebContents* initiator_web_contents,
std::unique_ptr<web_app::WebAppInstallInfo> web_app_info,
web_app::WebAppInstallationAcceptanceCallback
acceptance_callback) {
// Returning false here ended up causing the kUserInstallDeclined,
// so the error message needs to be updated accordingly.
// TODO(crbug.com/331214986): Modify the DialogCallback to allow
// clients providing their own error code rather than always
// returning kUserInstallDeclined. And maybe change it to a more
// neutral name other than "Dialog" to avoid implying the use of UI.
const bool manifest_match =
(web_app_info->manifest_id().spec() == in_manifest_id);
std::move(acceptance_callback)
.Run(manifest_match, std::move(web_app_info));
},
in_manifest_id),
base::BindOnce(
[](const std::string& in_manifest_id,
std::unique_ptr<InstallCallback> callback,
const webapps::AppId& app_id, webapps::InstallResultCode code) {
if (webapps::IsSuccess(code)) {
std::move(callback)->sendSuccess();
} else {
// See the comment above.
if (code == webapps::InstallResultCode::kUserInstallDeclined) {
return std::move(callback)->sendFailure(
errors::InconsistentManifestId(in_manifest_id, app_id));
}
std::move(callback)->sendFailure(
protocol::Response::InvalidRequest(
base::StrCat({"Failed to install ", in_manifest_id, ": ",
base::ToString(code)})));
}
},
in_manifest_id, std::move(callback)),
web_app::FallbackBehavior::kCraftedManifestOnly);
}
void PWAHandler::InstallFromUrl(const std::string& in_manifest_id,
const std::string& in_install_url_or_bundle_url,
std::unique_ptr<InstallCallback> callback) {
GURL url{in_install_url_or_bundle_url};
// Technically unnecessary, but let's check it anyway to avoid leaking
// unexpected schemes.
if (!url.is_valid()) {
std::move(callback)->sendFailure(
protocol::Response::InvalidParams(base::StrCat(
{"Invalid installUrlOrBundleUrl ", in_install_url_or_bundle_url})));
return;
}
// TODO(crbug.com/337872319): Support installing isolated apps on chrome-os.
if (!url.SchemeIsHTTPOrHTTPS()) {
std::move(callback)->sendFailure(
protocol::Response::MethodNotFound(base::StrCat(
{"Installing webapp from url ", in_install_url_or_bundle_url,
" with scheme [", url.scheme(), "] is not supported yet."})));
return;
}
auto* scheduler = GetScheduler();
if (!scheduler) {
std::move(callback)->sendFailure(errors::WebAppUnavailable());
return;
}
scheduler->FetchInstallInfoFromInstallUrl(
GURL{in_manifest_id}, url,
base::BindOnce(&PWAHandler::InstallFromInstallInfo,
weak_ptr_factory_.GetWeakPtr(), in_manifest_id,
in_install_url_or_bundle_url, std::move(callback)));
}
void PWAHandler::InstallFromInstallInfo(
const std::string& in_manifest_id,
const std::string& in_install_url_or_bundle_url,
std::unique_ptr<InstallCallback> callback,
std::unique_ptr<web_app::WebAppInstallInfo> web_app_info) {
if (!web_app_info) {
std::move(callback)->sendFailure(protocol::Response::InvalidRequest(
base::StrCat({"Couldn't fetch install info for ", in_manifest_id,
" from ", in_install_url_or_bundle_url})));
return;
}
if (web_app_info->manifest_id().spec() != in_manifest_id) {
std::move(callback)->sendFailure(errors::InconsistentManifestId(
in_manifest_id, in_install_url_or_bundle_url));
return;
}
auto* scheduler = GetScheduler();
if (!scheduler) {
std::move(callback)->sendFailure(errors::WebAppUnavailable());
return;
}
scheduler->InstallFromInfoWithParams(
std::move(web_app_info), false, webapps::WebappInstallSource::DEVTOOLS,
base::BindOnce(
[](const std::string& in_manifest_id,
const std::string& in_install_url_or_bundle_url,
std::unique_ptr<InstallCallback> callback,
const webapps::AppId& app_id, webapps::InstallResultCode code) {
if (webapps::IsSuccess(code)) {
std::move(callback)->sendSuccess();
} else {
std::move(callback)->sendFailure(
protocol::Response::InvalidRequest(
base::StrCat({"Failed to install ", in_manifest_id,
" from ", in_install_url_or_bundle_url,
": ", base::ToString(code)})));
}
},
in_manifest_id, in_install_url_or_bundle_url, std::move(callback)),
// TODO(crbug.com/331214986): Create command-line flag to fake all os
// integration for Chrome.
web_app::WebAppInstallParams{});
}
void PWAHandler::Install(
const std::string& in_manifest_id,
std::optional<std::string> in_install_url_or_bundle_url,
std::unique_ptr<InstallCallback> callback) {
if (in_install_url_or_bundle_url) {
InstallFromUrl(in_manifest_id,
std::move(in_install_url_or_bundle_url).value(),
std::move(callback));
} else {
InstallFromManifestId(in_manifest_id, std::move(callback));
}
}
void PWAHandler::Uninstall(const std::string& in_manifest_id,
std::unique_ptr<UninstallCallback> callback) {
const webapps::AppId app_id =
web_app::GenerateAppIdFromManifestId(GURL{in_manifest_id});
auto* scheduler = GetScheduler();
if (!scheduler) {
std::move(callback)->sendFailure(errors::WebAppUnavailable());
return;
}
scheduler->RemoveUserUninstallableManagements(
app_id, webapps::WebappUninstallSource::kDevtools,
base::BindOnce(
[](const std::string& in_manifest_id,
std::unique_ptr<UninstallCallback> callback,
webapps::UninstallResultCode result) {
if (webapps::UninstallSucceeded(result)) {
std::move(callback)->sendSuccess();
} else {
std::move(callback)->sendFailure(
protocol::Response::InvalidRequest(
base::StrCat({"Failed to uninstall ", in_manifest_id,
": ", base::ToString(result)})));
}
},
in_manifest_id, std::move(callback)));
}
void PWAHandler::Launch(const std::string& in_manifest_id,
std::optional<std::string> in_url,
std::unique_ptr<LaunchCallback> callback) {
const webapps::AppId app_id =
web_app::GenerateAppIdFromManifestId(GURL{in_manifest_id});
const auto url =
(in_url ? std::optional<GURL>{in_url.value()} : std::nullopt);
web_app::WebAppProvider* provider =
web_app::WebAppProvider::GetForWebApps(GetProfile());
if (!provider) {
std::move(callback)->sendFailure(errors::WebAppUnavailable());
return;
}
if (url) {
if (!url->is_valid()) {
std::move(callback)->sendFailure(protocol::Response::InvalidParams(
base::StrCat({"Invalid input url ", in_url.value()})));
return;
}
// TODO(crbug.com/338406726): Remove after launches correctly fail when url
// is out of scope.
bool is_in_scope;
if (base::FeatureList::IsEnabled(
blink::features::kWebAppEnableScopeExtensions)) {
is_in_scope =
provider->registrar_unsafe().IsUrlInAppExtendedScope(*url, app_id);
} else {
is_in_scope = provider->registrar_unsafe().IsUrlInAppScope(*url, app_id);
}
if (!is_in_scope) {
std::move(callback)->sendFailure(
protocol::Response::InvalidParams(base::StrCat(
{"Requested url ", url->spec(),
" is not in the scope of the web app ", in_manifest_id})));
return;
}
}
provider->scheduler().LaunchApp(
app_id, url,
base::BindOnce(
[](const std::string& in_manifest_id, const std::optional<GURL>& url,
std::unique_ptr<LaunchCallback> callback,
base::WeakPtr<Browser> browser,
base::WeakPtr<content::WebContents> web_contents,
apps::LaunchContainer container) {
auto result = GetTargetIdFromLaunch(in_manifest_id, url, browser,
web_contents, container);
if (result.has_value()) {
std::move(callback)->sendSuccess(std::move(result).value());
} else {
std::move(callback)->sendFailure(std::move(result).error());
}
},
in_manifest_id, url, std::move(callback)));
}
void PWAHandler::LaunchFilesInApp(
const std::string& in_manifest_id,
std::unique_ptr<protocol::Array<std::string>> in_files,
std::unique_ptr<LaunchFilesInAppCallback> callback) {
const GURL manifest_id = GURL{in_manifest_id};
const webapps::AppId app_id =
web_app::GenerateAppIdFromManifestId(manifest_id);
web_app::WebAppProvider* provider =
web_app::WebAppProvider::GetForWebApps(GetProfile());
if (!provider) {
std::move(callback)->sendFailure(errors::WebAppUnavailable());
return;
}
if (!in_files || in_files->empty()) {
std::move(callback)->sendFailure(
protocol::Response::InvalidParams(base::StrCat(
{"PWA.launchFilesInApp needs input files to be opened by web app ",
in_manifest_id})));
return;
}
std::vector<base::FilePath> files{in_files->size()};
for (const auto& file : *in_files) {
// TODO(b/331214986): Support platform-dependent file path encoding.
// TODO(b/331214986): May want to fail if the files do not exist.
files.push_back(base::FilePath::FromUTF8Unsafe(file));
}
const web_app::WebAppFileHandlerManager::LaunchInfos launch_infos =
provider->os_integration_manager()
.file_handler_manager()
.GetMatchingFileHandlerUrls(app_id, files);
if (launch_infos.empty()) {
std::move(callback)->sendFailure(protocol::Response::InvalidParams(
base::StrCat({"Files are not supported by web app ", in_manifest_id})));
return;
}
base::ConcurrentCallbacks<base::expected<std::string, protocol::Response>>
concurrent;
for (const auto& [url, paths] : launch_infos) {
provider->scheduler().LaunchApp(
app_id, *base::CommandLine::ForCurrentProcess(),
/* current_directory */ base::FilePath{},
/* url_handler_launch_url */ std::nullopt,
/* protocol_handler_launch_url */ std::nullopt,
/* file_launch_url */ url, paths,
base::BindOnce(
[](const std::string& in_manifest_id,
base::OnceCallback<void(
base::expected<std::string, protocol::Response>)> callback,
base::WeakPtr<Browser> browser,
base::WeakPtr<content::WebContents> web_contents,
apps::LaunchContainer container) {
std::move(callback).Run(
GetTargetIdFromLaunch(in_manifest_id, std::optional<GURL>{},
browser, web_contents, container));
},
in_manifest_id, concurrent.CreateCallback()));
}
std::move(concurrent)
.Done(base::BindOnce(
[](std::unique_ptr<LaunchFilesInAppCallback> callback,
std::vector<base::expected<std::string, protocol::Response>>
results) {
auto ids = std::make_unique<protocol::Array<std::string>>();
std::string errors;
for (auto& result : results) {
if (result.has_value()) {
ids->push_back(std::move(result).value());
} else {
errors += std::move(result).error().Message();
errors += "; ";
}
}
if (errors.empty()) {
std::move(callback)->sendSuccess(std::move(ids));
} else {
std::move(callback)->sendFailure(
protocol::Response::InvalidRequest(std::move(errors)));
}
},
std::move(callback)));
}
protocol::Response PWAHandler::OpenCurrentPageInApp(
const std::string& in_manifest_id) {
content::WebContents* contents = GetWebContents();
if (contents == nullptr) {
return protocol::Response::InvalidRequest(
base::StrCat({"The devtools session has no associated web page when "
"opening ",
in_manifest_id, " in its app."}));
}
web_app::WebAppProvider* provider =
web_app::WebAppProvider::GetForWebApps(GetProfile());
if (!provider) {
return errors::WebAppUnavailable();
}
const webapps::AppId app_id =
web_app::GenerateAppIdFromManifestId(GURL{in_manifest_id});
// Since this logic is only needed on MacOS, for the sake of simplicity the
// unsafe access of the registrar is fine at the moment instead of wrapping it
// in a command.
const bool shortcut_created = [provider, &app_id]() {
auto state =
provider->registrar_unsafe().GetAppCurrentOsIntegrationState(app_id);
if (!state.has_value()) {
return false;
}
return state->has_shortcut();
}();
if (!provider->ui_manager().CanReparentAppTabToWindow(
app_id, shortcut_created, contents)) {
return protocol::Response::InvalidParams(
base::StrCat({"The web app ", in_manifest_id,
" cannot be opened in its app. Check if the app is "
"correctly installed."}));
}
Browser* browser = provider->ui_manager().ReparentAppTabToWindow(
contents, app_id, shortcut_created);
if (browser == nullptr) {
return protocol::Response::InvalidRequest(base::StrCat(
{"The current page ", contents->GetLastCommittedURL().spec(),
" cannot be opened in the web app ", in_manifest_id}));
}
return protocol::Response::Success();
}
void PWAHandler::ChangeAppUserSettings(
const std::string& in_manifest_id,
std::optional<bool> in_link_capturing,
std::optional<protocol::PWA::DisplayMode> in_display_mode,
std::unique_ptr<ChangeAppUserSettingsCallback> callback) {
const webapps::AppId app_id =
web_app::GenerateAppIdFromManifestId(GURL{in_manifest_id});
// Always checks the availability of web app system to ensure the consistency
// of the API behavior.
auto* scheduler = GetScheduler();
if (!scheduler) {
std::move(callback)->sendFailure(errors::WebAppUnavailable());
return;
}
std::optional<web_app::mojom::UserDisplayMode> user_display_mode{};
if (in_display_mode) {
if (in_display_mode.value() == protocol::PWA::DisplayModeEnum::Standalone) {
user_display_mode = web_app::mojom::UserDisplayMode::kStandalone;
} else if (in_display_mode.value() ==
protocol::PWA::DisplayModeEnum::Browser) {
user_display_mode = web_app::mojom::UserDisplayMode::kBrowser;
} else {
std::move(callback)->sendFailure(
protocol::Response::InvalidParams(base::StrCat(
{"Unrecognized displayMode ", in_display_mode.value(),
" when changing user settings of web app ", in_manifest_id})));
return;
}
}
#if BUILDFLAG(IS_CHROMEOS)
// TODO(crbug.com/339453269): Implement changeUserAppSettings/LinkCapturing on
// ChromeOS.
// TL:DR; the ChromeOS uses apps::AppServiceProxyFactory instead, and the
// SetSupportedLinksPreference would associate all the supported links to the
// app.
if (in_link_capturing) {
std::move(callback)->sendFailure(protocol::Response::InvalidRequest(
"Changing AppUserSettings/LinkCapturing on ChromeOS is not supported "
"yet."));
return;
}
#endif
base::ConcurrentCallbacks<std::optional<std::string>> concurrent;
scheduler->ScheduleCallbackWithResult(
// TODO(crbug.com/339453269): Find a way to forward the error of the set
// operation back here.
"PWAHandler::ChangeAppUserSettings", web_app::AppLockDescription(app_id),
base::BindOnce(
[](const webapps::AppId& app_id, web_app::AppLock& app_lock,
base::Value::Dict& debug_value) -> std::optional<std::string> {
// Only consider apps that are installed with or without OS
// integration. Apps coming via sync should not be considered.
if (app_lock.registrar().IsInstallState(
app_id, {web_app::proto::InstallState::
INSTALLED_WITH_OS_INTEGRATION,
web_app::proto::InstallState::
INSTALLED_WITHOUT_OS_INTEGRATION})) {
return std::nullopt;
}
return "WebApp is not installed";
},
app_id),
concurrent.CreateCallback(),
/* result_on_shutdown= */
std::optional<std::string>{std::in_place,
"WebApp system is shuting down."});
if (in_link_capturing) {
scheduler->SetAppCapturesSupportedLinksDisableOverlapping(
app_id, in_link_capturing.value(),
base::BindOnce(concurrent.CreateCallback(),
std::optional<std::string>{}));
}
if (user_display_mode) {
// TODO(crbug.com/331214986): Create command-line flag to fake all os
// integration for Chrome.
scheduler->SetUserDisplayMode(app_id, user_display_mode.value(),
base::BindOnce(concurrent.CreateCallback(),
std::optional<std::string>{}));
}
std::move(concurrent)
.Done(base::BindOnce(
[](const std::string& in_manifest_id,
std::unique_ptr<ChangeAppUserSettingsCallback> callback,
std::vector<std::optional<std::string>> results) {
std::string errors;
for (const auto& result : results) {
if (result) {
errors.append(result.value()).append(";");
}
}
if (errors.empty()) {
std::move(callback)->sendSuccess();
} else {
std::move(callback)->sendFailure(
protocol::Response::InvalidRequest(base::StrCat(
{"Failed to change the user settings of web app ",
in_manifest_id, ". ", errors})));
}
},
in_manifest_id, std::move(callback)));
}