blob: 03ff000e9801876b7630433cdefc969722699cce [file] [log] [blame]
// Copyright 2021 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/ui/web_applications/sub_apps_service_impl.h"
#include <string>
#include <utility>
#include <vector>
#include "base/check.h"
#include "base/functional/bind.h"
#include "base/functional/concurrent_callbacks.h"
#include "base/i18n/message_formatter.h"
#include "base/strings/utf_string_conversions.h"
#include "base/types/expected.h"
#include "base/types/expected_macros.h"
#include "chrome/browser/notifications/notification_display_service.h"
#include "chrome/browser/notifications/notification_display_service_factory.h"
#include "chrome/browser/notifications/notification_handler.h"
#include "chrome/browser/permissions/permission_decision_auto_blocker_factory.h"
#include "chrome/browser/policy/policy_util.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/web_applications/sub_apps_install_dialog_controller.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_finalizer.h"
#include "chrome/browser/web_applications/web_app_install_params.h"
#include "chrome/browser/web_applications/web_app_management_type.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_tab_helper.h"
#include "chrome/browser/web_applications/web_app_utils.h"
#include "chrome/common/pref_names.h"
#include "chrome/grit/generated_resources.h"
#include "components/content_settings/core/common/content_settings_types.h"
#include "components/permissions/permission_decision_auto_blocker.h"
#include "components/webapps/browser/installable/installable_metrics.h"
#include "components/webapps/browser/uninstall_result_code.h"
#include "components/webapps/common/web_app_id.h"
#include "content/public/browser/isolated_context_util.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/web_contents.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/message_center/public/cpp/notification.h"
#include "url/gurl.h"
#include "url/origin.h"
#if BUILDFLAG(IS_CHROMEOS)
#include "ash/constants/notifier_catalogs.h"
#endif
using blink::mojom::SubAppsService;
using blink::mojom::SubAppsServiceAddParametersPtr;
using blink::mojom::SubAppsServiceAddResult;
using blink::mojom::SubAppsServiceAddResultPtr;
using blink::mojom::SubAppsServiceListResult;
using blink::mojom::SubAppsServiceListResultEntry;
using blink::mojom::SubAppsServiceListResultEntryPtr;
using blink::mojom::SubAppsServiceRemoveResult;
using blink::mojom::SubAppsServiceRemoveResultPtr;
using blink::mojom::SubAppsServiceResultCode;
namespace web_app {
// TODO(crbug.com/40924576): Replace registrar_unsafe with Locks
// TODO (crbug.com/1467862): Move from //c/b/ui/web_applications to
// //c/b/web_applications
namespace {
constexpr char kSubAppsUninstallNotifierId[] = "sub_apps_service";
// Resolve string `path` with `origin`, and if the resulting GURL isn't same
// origin with `origin` then return an error (for which the caller needs to
// raise a `ReportBadMessageAndDeleteThis`).
base::expected<GURL, std::string> ConvertPathToUrl(const std::string& path,
const url::Origin& origin) {
GURL resolved = origin.GetURL().Resolve(path);
if (!origin.IsSameOriginWith(resolved)) {
return base::unexpected(
"SubAppsServiceImpl: Different origin arg to that of the calling app.");
}
if (resolved.is_empty()) {
return base::unexpected("SubAppsServiceImpl: Empty url.");
}
if (!resolved.is_valid()) {
return base::unexpected("SubAppsServiceImpl: Invalid url.");
}
return base::ok(resolved);
}
std::string ConvertUrlToPath(const webapps::ManifestId& manifest_id) {
return manifest_id.PathForRequest();
}
base::expected<std::vector<SubAppInstallParams>, std::string>
AddOptionsFromMojo(
const url::Origin& origin,
const std::vector<SubAppsServiceAddParametersPtr>& sub_apps_to_add_mojo) {
std::vector<SubAppInstallParams> sub_apps;
for (const auto& sub_app : sub_apps_to_add_mojo) {
ASSIGN_OR_RETURN(webapps::ManifestId manifest_id,
ConvertPathToUrl(sub_app->manifest_id_path, origin));
ASSIGN_OR_RETURN(GURL install_url,
ConvertPathToUrl(sub_app->install_url_path, origin));
sub_apps.emplace_back(std::move(manifest_id), std::move(install_url));
}
return sub_apps;
}
Profile* GetProfile(content::RenderFrameHost& render_frame_host) {
return Profile::FromBrowserContext(render_frame_host.GetBrowserContext());
}
WebAppProvider* GetWebAppProvider(content::RenderFrameHost& render_frame_host) {
auto* const initiator_web_contents =
content::WebContents::FromRenderFrameHost(&render_frame_host);
auto* provider = WebAppProvider::GetForWebContents(initiator_web_contents);
DCHECK(provider);
return provider;
}
const webapps::AppId* GetAppId(content::RenderFrameHost& render_frame_host) {
auto* const initiator_web_contents =
content::WebContents::FromRenderFrameHost(&render_frame_host);
return WebAppTabHelper::GetAppId(initiator_web_contents);
}
blink::mojom::SubAppsServiceResultCode InstallResultCodeToMojo(
webapps::InstallResultCode install_result_code) {
return webapps::IsSuccess(install_result_code)
? blink::mojom::SubAppsServiceResultCode::kSuccess
: blink::mojom::SubAppsServiceResultCode::kFailure;
}
void ReturnAllAddsAsFailed(
const std::vector<SubAppsServiceAddParametersPtr>& sub_apps,
SubAppsServiceImpl::AddCallback result_callback) {
std::vector<SubAppsServiceAddResultPtr> result;
for (const auto& sub_app : sub_apps) {
result.emplace_back(SubAppsServiceAddResult::New(
sub_app->manifest_id_path, SubAppsServiceResultCode::kFailure));
}
std::move(result_callback).Run(std::move(result));
}
bool IsInstalledNonChildApp(content::RenderFrameHost& render_frame_host) {
auto* app_id = GetAppId(render_frame_host);
if (!app_id) {
return false;
}
auto* provider = GetWebAppProvider(render_frame_host);
auto* web_app = provider->registrar_unsafe().GetAppById(*app_id);
return (web_app && !web_app->IsSubAppInstalledApp());
}
// Verify that the calling app has the SubApps permissions policy set and that
// it is an installed IWA that is not a sub app itself. This check is called
// from `CreateIfAllowed` and from each of the APIs entry points to avoid a
// potential race between the parent app calling an API while being uninstalled.
bool CanAccessSubAppsApi(content::RenderFrameHost& render_frame_host) {
return render_frame_host.IsFeatureEnabled(
network::mojom::PermissionsPolicyFeature::kSubApps) &&
content::HasIsolatedContextCapability(&render_frame_host) &&
IsInstalledNonChildApp(render_frame_host);
}
bool ShouldSkipUserConfirmation(content::RenderFrameHost& frame) {
#if BUILDFLAG(IS_CHROMEOS)
auto const* profile = Profile::FromBrowserContext(frame.GetBrowserContext());
if (!profile) {
return false;
}
auto const* prefs = profile->GetPrefs();
if (!prefs) {
return false;
}
return policy::IsOriginInAllowlist(
frame.GetLastCommittedURL(), prefs,
prefs::kSubAppsAPIsAllowedWithoutGestureAndAuthorizationForOrigins);
#else // BUILDFLAG(IS_CHROMEOS)
return false;
#endif // BUILDFLAG(IS_CHROMEOS)
}
} // namespace
SubAppsServiceImpl::SubAppsServiceImpl(
content::RenderFrameHost& render_frame_host,
mojo::PendingReceiver<SubAppsService> receiver)
: DocumentService(render_frame_host, std::move(receiver)) {}
SubAppsServiceImpl::~SubAppsServiceImpl() = default;
SubAppsServiceImpl::AddCallInfo::AddCallInfo() = default;
SubAppsServiceImpl::AddCallInfo::~AddCallInfo() = default;
// static
void SubAppsServiceImpl::CreateIfAllowed(
content::RenderFrameHost* render_frame_host,
mojo::PendingReceiver<SubAppsService> receiver) {
CHECK(render_frame_host);
// This class is created only on the primary main frame.
if (!render_frame_host->IsInPrimaryMainFrame()) {
receiver.reset();
return;
}
// Bail if Web Apps aren't enabled on current profile.
if (!AreWebAppsEnabled(Profile::FromBrowserContext(
content::WebContents::FromRenderFrameHost(render_frame_host)
->GetBrowserContext()))) {
receiver.reset();
return;
}
// Bail if the calling app is not an Isolated Web App or is not installed or
// is a sub-app itself.
if (!CanAccessSubAppsApi(*render_frame_host)) {
receiver.reset();
return;
}
// The object is bound to the lifetime of `render_frame_host` and the mojo
// connection. See DocumentService for details.
new SubAppsServiceImpl(*render_frame_host, std::move(receiver));
}
void SubAppsServiceImpl::Add(
std::vector<SubAppsServiceAddParametersPtr> sub_apps_to_add,
AddCallback result_callback) {
WebAppProvider* provider = GetWebAppProvider(render_frame_host());
if (!provider->on_registry_ready().is_signaled()) {
provider->on_registry_ready().Post(
FROM_HERE,
base::BindOnce(&SubAppsServiceImpl::Add, weak_ptr_factory_.GetWeakPtr(),
std::move(sub_apps_to_add), std::move(result_callback)));
return;
}
if (!CanAccessSubAppsApi(render_frame_host())) {
ReturnAllAddsAsFailed(sub_apps_to_add, std::move(result_callback));
return;
}
if (sub_apps_to_add.empty()) {
std::move(result_callback).Run({});
return;
}
// Check if origin is embargoed because of too many dismissals.
if (PermissionDecisionAutoBlockerFactory::GetForProfile(
Profile::FromBrowserContext(render_frame_host().GetBrowserContext()))
->IsEmbargoed(render_frame_host().GetLastCommittedOrigin().GetURL(),
ContentSettingsType::SUB_APP_INSTALLATION_PROMPTS)) {
ReturnAllAddsAsFailed(sub_apps_to_add, std::move(result_callback));
return;
}
ASSIGN_OR_RETURN(
(std::vector<SubAppInstallParams> add_options),
AddOptionsFromMojo(render_frame_host().GetLastCommittedOrigin(),
sub_apps_to_add),
// Compromised renderer, bail immediately (this call deletes *this).
&SubAppsServiceImpl::ReportBadMessageAndDeleteThis, this);
CHECK(AreWebAppsUserInstallable(
Profile::FromBrowserContext(render_frame_host().GetBrowserContext())));
// Assign id to this add call
int add_call_id = next_add_call_id_++;
AddCallInfo& add_call_info = add_call_info_[add_call_id];
add_call_info.mojo_callback = std::move(result_callback);
auto parent_manifest_id = provider->registrar_unsafe()
.GetAppById(*GetAppId(render_frame_host()))
->manifest_id();
CollectInstallData(add_call_id, std::move(add_options), parent_manifest_id);
}
void SubAppsServiceImpl::CollectInstallData(
int add_call_id,
std::vector<SubAppInstallParams> requested_installs,
webapps::ManifestId parent_manifest_id) {
WebAppProvider* provider = GetWebAppProvider(render_frame_host());
base::ConcurrentCallbacks<
std::pair<webapps::ManifestId, std::unique_ptr<WebAppInstallInfo>>>
concurrent;
// Schedule data collection for each requested install
for (const auto& [manifest_id, url_to_load] : requested_installs) {
// Check if app is the parent app itself
if (manifest_id == parent_manifest_id) {
add_call_info_.at(add_call_id)
.results.emplace_back(SubAppsServiceAddResult::New(
ConvertUrlToPath(manifest_id),
blink::mojom::SubAppsServiceResultCode::kFailure));
continue;
}
// Check if app is already installed as a sub app
if (provider->registrar_unsafe().WasInstalledBySubApp(
GenerateAppIdFromManifestId(manifest_id, parent_manifest_id))) {
add_call_info_.at(add_call_id)
.results.emplace_back(SubAppsServiceAddResult::New(
ConvertUrlToPath(manifest_id),
blink::mojom::SubAppsServiceResultCode::kSuccess));
continue;
}
provider->scheduler().FetchInstallInfoFromInstallUrl(
manifest_id, url_to_load, parent_manifest_id,
base::BindOnce(
[](webapps::ManifestId manifest_app_id,
std::unique_ptr<WebAppInstallInfo> install_info) {
return std::pair(manifest_app_id, std::move(install_info));
},
manifest_id)
.Then(concurrent.CreateCallback()));
}
std::move(concurrent)
.Done(base::BindOnce(&SubAppsServiceImpl::ProcessInstallData,
weak_ptr_factory_.GetWeakPtr(), add_call_id));
}
void SubAppsServiceImpl::ProcessInstallData(
int add_call_id,
std::vector<std::pair<webapps::ManifestId,
std::unique_ptr<WebAppInstallInfo>>> install_data) {
AddCallInfo& add_call_info = add_call_info_.at(add_call_id);
const webapps::AppId* parent_app_id = GetAppId(render_frame_host());
for (auto& [manifest_id, install_info] : install_data) {
// If manifest_id is empty, the app was already installed and no install
// info was collected. If it is invalid, do not to trigger an installation
// since the command will run into problems.
if (!manifest_id.is_valid()) {
continue;
}
if (install_info) {
install_info->parent_app_id = *parent_app_id;
install_info->user_display_mode = mojom::UserDisplayMode::kStandalone;
add_call_info.install_infos.emplace_back(std::move(install_info));
} else {
// Log error if install info could not be loaded
add_call_info.results.emplace_back(SubAppsServiceAddResult::New(
ConvertUrlToPath(manifest_id),
blink::mojom::SubAppsServiceResultCode::kFailure));
}
}
FinishAddCallOrShowInstallDialog(add_call_id);
}
void SubAppsServiceImpl::FinishAddCallOrShowInstallDialog(int add_call_id) {
AddCallInfo& add_call_info = add_call_info_.at(add_call_id);
if (add_call_info.install_infos.empty()) {
FinishAddCall(add_call_id, {});
return;
}
if (ShouldSkipUserConfirmation(render_frame_host())) {
ProcessDialogResponse(add_call_id, true);
return;
}
WebAppRegistrar& registrar =
GetWebAppProvider(render_frame_host())->registrar_unsafe();
const webapps::AppId* parent_app_id = GetAppId(render_frame_host());
add_call_info.install_dialog =
std::make_unique<SubAppsInstallDialogController>();
add_call_info.install_dialog->Init(
base::BindOnce(&SubAppsServiceImpl::ProcessDialogResponse,
weak_ptr_factory_.GetWeakPtr(), add_call_id),
add_call_info.install_infos,
/*parent_app_name=*/registrar.GetAppShortName(*parent_app_id),
*parent_app_id, GetProfile(render_frame_host()),
/*window=*/
content::WebContents::FromRenderFrameHost(&render_frame_host())
->GetTopLevelNativeWindow());
}
void SubAppsServiceImpl::ProcessDialogResponse(int add_call_id,
bool dialog_accepted) {
if (dialog_accepted) {
PermissionDecisionAutoBlockerFactory::GetForProfile(
Profile::FromBrowserContext(render_frame_host().GetBrowserContext()))
->RemoveEmbargoAndResetCounts(
render_frame_host().GetLastCommittedOrigin().GetURL(),
ContentSettingsType::SUB_APP_INSTALLATION_PROMPTS);
ScheduleSubAppInstalls(add_call_id);
return;
}
// Dialog was declined.
PermissionDecisionAutoBlockerFactory::GetForProfile(
Profile::FromBrowserContext(render_frame_host().GetBrowserContext()))
->RecordDismissAndEmbargo(
render_frame_host().GetLastCommittedOrigin().GetURL(),
ContentSettingsType::SUB_APP_INSTALLATION_PROMPTS,
/*dismissed_prompt_was_quiet=*/false);
AddCallInfo& add_call_info = add_call_info_.at(add_call_id);
for (const std::unique_ptr<web_app::WebAppInstallInfo>& install_info :
add_call_info.install_infos) {
add_call_info.results.emplace_back(SubAppsServiceAddResult::New(
ConvertUrlToPath(install_info->manifest_id()),
blink::mojom::SubAppsServiceResultCode::kFailure));
}
FinishAddCall(add_call_id, {});
}
void SubAppsServiceImpl::ScheduleSubAppInstalls(int add_call_id) {
AddCallInfo& add_call_info = add_call_info_.at(add_call_id);
// Schedule install for each install_info that was collected
WebAppProvider* provider = GetWebAppProvider(render_frame_host());
base::ConcurrentCallbacks<SubAppInstallResult> concurrent;
for (auto& install_info : add_call_info.install_infos) {
webapps::ManifestId manifest_id = install_info->manifest_id();
provider->scheduler().InstallFromInfoWithParams(
std::move(install_info), /*overwrite_existing_manifest_fields=*/false,
webapps::WebappInstallSource::SUB_APP,
base::BindOnce(
[](webapps::ManifestId manifest_id, const webapps::AppId& app_id,
webapps::InstallResultCode result_code) {
return SubAppInstallResult(manifest_id, app_id, result_code);
},
manifest_id)
.Then(concurrent.CreateCallback()),
WebAppInstallParams());
}
std::move(concurrent)
.Done(base::BindOnce(&SubAppsServiceImpl::FinishAddCall,
weak_ptr_factory_.GetWeakPtr(), add_call_id));
}
void SubAppsServiceImpl::FinishAddCall(
int add_call_id,
std::vector<SubAppInstallResult> install_results) {
AddCallInfo& add_call_info = add_call_info_.at(add_call_id);
for (const auto& [manifest_id, app_id, result_code] : install_results) {
add_call_info.results.emplace_back(SubAppsServiceAddResult::New(
ConvertUrlToPath(manifest_id), InstallResultCodeToMojo(result_code)));
}
std::move(add_call_info.mojo_callback).Run(std::move(add_call_info.results));
add_call_info_.erase(add_call_id);
}
void SubAppsServiceImpl::List(ListCallback result_callback) {
WebAppProvider* provider = GetWebAppProvider(render_frame_host());
if (!provider->on_registry_ready().is_signaled()) {
provider->on_registry_ready().Post(
FROM_HERE, base::BindOnce(&SubAppsServiceImpl::List,
weak_ptr_factory_.GetWeakPtr(),
std::move(result_callback)));
return;
}
if (!CanAccessSubAppsApi(render_frame_host())) {
return std::move(result_callback)
.Run(SubAppsServiceListResult::New(
SubAppsServiceResultCode::kFailure,
std::vector<SubAppsServiceListResultEntryPtr>()));
}
const WebAppRegistrar& registrar = provider->registrar_unsafe();
std::vector<SubAppsServiceListResultEntryPtr> sub_apps_list;
for (const webapps::AppId& sub_app_id :
registrar.GetAllSubAppIds(*GetAppId(render_frame_host()))) {
webapps::ManifestId manifest_id = registrar.GetAppManifestId(sub_app_id);
CHECK(manifest_id.is_valid());
sub_apps_list.push_back(SubAppsServiceListResultEntry::New(
ConvertUrlToPath(manifest_id), registrar.GetAppShortName(sub_app_id)));
}
std::move(result_callback)
.Run(SubAppsServiceListResult::New(SubAppsServiceResultCode::kSuccess,
std::move(sub_apps_list)));
}
void SubAppsServiceImpl::Remove(
const std::vector<std::string>& manifest_id_paths,
RemoveCallback result_callback) {
WebAppProvider* provider = GetWebAppProvider(render_frame_host());
if (!provider->on_registry_ready().is_signaled()) {
provider->on_registry_ready().Post(
FROM_HERE,
base::BindOnce(&SubAppsServiceImpl::Remove,
weak_ptr_factory_.GetWeakPtr(), manifest_id_paths,
std::move(result_callback)));
return;
}
if (!CanAccessSubAppsApi(render_frame_host())) {
std::vector<SubAppsServiceRemoveResultPtr> result;
for (const std::string& manifest_id_path : manifest_id_paths) {
result.emplace_back(SubAppsServiceRemoveResult::New(
manifest_id_path, SubAppsServiceResultCode::kFailure));
}
return std::move(result_callback).Run(std::move(result));
}
// Take weak pointer early as this may get deleted by RemoveSubApp().
base::WeakPtr<SubAppsServiceImpl> weak_ptr = weak_ptr_factory_.GetWeakPtr();
base::ConcurrentCallbacks<SubAppsServiceRemoveResultPtr> concurrent;
for (const std::string& manifest_id_path : manifest_id_paths) {
RemoveSubApp(manifest_id_path, concurrent.CreateCallback(),
GetAppId(render_frame_host()));
}
std::move(concurrent)
.Done(base::BindOnce(&SubAppsServiceImpl::NotifyUninstall, weak_ptr,
std::move(result_callback)));
}
void SubAppsServiceImpl::RemoveSubApp(
const std::string& manifest_id_path,
base::OnceCallback<void(SubAppsServiceRemoveResultPtr)> callback,
const webapps::AppId* calling_app_id) {
// Convert `manifest_id_path` from path form to full URL form.
ASSIGN_OR_RETURN(
const webapps::ManifestId manifest_id,
ConvertPathToUrl(manifest_id_path,
render_frame_host().GetLastCommittedOrigin()),
// Compromised renderer, bail immediately (this call deletes *this).
&SubAppsServiceImpl::ReportBadMessageAndDeleteThis, this);
WebAppProvider* provider = GetWebAppProvider(render_frame_host());
const webapps::AppId* parent_app_id = GetAppId(render_frame_host());
if (!parent_app_id) {
return ReportBadMessageAndDeleteThis("Parent app id is null");
}
webapps::ManifestId parent_manifest_id =
provider->registrar_unsafe().GetAppManifestId(*parent_app_id);
if (!parent_manifest_id.is_valid()) {
return ReportBadMessageAndDeleteThis("Parent manifest is null");
}
webapps::AppId sub_app_id =
GenerateAppIdFromManifestId(manifest_id, parent_manifest_id);
const WebApp* app = provider->registrar_unsafe().GetAppById(sub_app_id);
// Verify that the app we're trying to remove exists, is installed and that
// its parent_app is the one doing the current call.
if (!app || !app->parent_app_id() ||
*calling_app_id != *app->parent_app_id() ||
!provider->registrar_unsafe().IsInRegistrar(sub_app_id)) {
return std::move(callback).Run(SubAppsServiceRemoveResult::New(
manifest_id_path, SubAppsServiceResultCode::kFailure));
}
provider->scheduler().RemoveInstallManagementMaybeUninstall(
sub_app_id, WebAppManagement::Type::kSubApp,
webapps::WebappUninstallSource::kSubApp,
base::BindOnce(
[](std::string manifest_id_path,
webapps::UninstallResultCode result_code) {
SubAppsServiceResultCode result =
webapps::UninstallSucceeded(result_code)
? SubAppsServiceResultCode::kSuccess
: SubAppsServiceResultCode::kFailure;
return SubAppsServiceRemoveResult::New(manifest_id_path, result);
},
manifest_id_path)
.Then(std::move(callback)));
}
void SubAppsServiceImpl::NotifyUninstall(
RemoveCallback result_callback,
std::vector<SubAppsServiceRemoveResultPtr> remove_results) {
int num_successful_uninstalls = std::ranges::count(
remove_results, SubAppsServiceResultCode::kSuccess,
[](const auto& result) { return result->result_code; });
// If any apps were uninstalled, notify the user.
if (num_successful_uninstalls > 0) {
WebAppRegistrar& registrar =
GetWebAppProvider(render_frame_host())->registrar_unsafe();
const webapps::AppId* parent_app_id = GetAppId(render_frame_host());
const std::u16string parent_app_name =
base::UTF8ToUTF16(registrar.GetAppShortName(*parent_app_id));
const GURL start_url = registrar.GetAppStartUrl(*parent_app_id);
const std::u16string title =
base::i18n::MessageFormatter::FormatWithNamedArgs(
l10n_util::GetStringUTF16(
IDS_SUB_APPS_UNINSTALL_NOTIFICATION_TITLE),
/*name0=*/"NUM_SUB_APPS", num_successful_uninstalls,
/*name1=*/"APP_NAME", parent_app_name);
const std::u16string message =
base::i18n::MessageFormatter::FormatWithNamedArgs(
l10n_util::GetStringUTF16(
IDS_SUB_APPS_UNINSTALL_NOTIFICATION_DESCRIPTION),
/*name0=*/"APP_NAME", parent_app_name);
message_center::Notification notification(
message_center::NOTIFICATION_TYPE_SIMPLE,
kSubAppsUninstallNotificationId, title, message, ui::ImageModel(),
/*display_source=*/std::u16string(),
/*origin_url=*/start_url,
#if BUILDFLAG(IS_CHROMEOS)
message_center::NotifierId(
message_center::NotifierType::SYSTEM_COMPONENT,
kSubAppsUninstallNotifierId,
ash::NotificationCatalogName::kSubAppsUninstall),
#else
message_center::NotifierId(
message_center::NotifierType::SYSTEM_COMPONENT,
kSubAppsUninstallNotifierId),
#endif
message_center::RichNotificationData(),
/*delegate=*/nullptr);
notification.SetSystemPriority();
NotificationDisplayServiceFactory::GetForProfile(
GetProfile(render_frame_host()))
->Display(NotificationHandler::Type::WEB_PERSISTENT, notification,
/*metadata=*/nullptr);
}
std::move(result_callback).Run(std::move(remove_results));
}
} // namespace web_app