// 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/ui/webui/app_management/app_management_page_handler_chromeos.h"
#include <set>
#include <string>
#include <vector>
#include "base/containers/flat_map.h"
#include "base/containers/flat_set.h"
#include "base/functional/bind.h"
#include "base/logging.h"
#include "chrome/browser/apps/app_service/app_service_proxy.h"
#include "chrome/browser/apps/app_service/app_service_proxy_factory.h"
#include "chrome/browser/ash/app_list/arc/arc_app_utils.h"
#include "chrome/browser/ash/apps/apk_web_app_service.h"
#include "chrome/browser/ash/crosapi/crosapi_ash.h"
#include "chrome/browser/ash/crosapi/crosapi_manager.h"
#include "chrome/browser/ash/crosapi/web_app_service_ash.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/webui/app_management/app_management_page_handler_base.h"
#include "chrome/browser/ui/webui/app_management/app_management_shelf_delegate_chromeos.h"
#include "chrome/browser/ui/webui/ash/settings/os_settings_features_util.h"
#include "chrome/browser/web_applications/web_app_command_scheduler.h"
#include "chrome/browser/web_applications/web_app_provider.h"
#include "chrome/browser/web_applications/web_app_registrar.h"
#include "chromeos/crosapi/mojom/web_app_service.mojom.h"
#include "components/services/app_service/public/cpp/intent_filter.h"
#include "components/services/app_service/public/cpp/intent_filter_util.h"
#include "components/services/app_service/public/cpp/preferred_apps_list_handle.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/common/constants.h"
#include "extensions/common/extension.h"
#include "extensions/common/permissions/permission_message.h"
#include "extensions/common/permissions/permissions_data.h"
#include "mojo/public/cpp/bindings/pending_receiver.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/text/bytes_formatting.h"
#include "ui/webui/resources/cr_components/app_management/app_management.mojom.h"
namespace {
// Returns a list of intent filters that support http/https given an app ID.
apps::IntentFilters GetSupportedLinkIntentFilters(Profile* profile,
const std::string& app_id) {
apps::IntentFilters intent_filters;
apps::AppServiceProxyFactory::GetForProfile(profile)
->AppRegistryCache()
.ForOneApp(app_id,
[&app_id, &intent_filters](const apps::AppUpdate& update) {
if (update.Readiness() == apps::Readiness::kReady) {
for (auto& filter : update.IntentFilters()) {
if (apps_util::IsSupportedLinkForApp(app_id, filter)) {
intent_filters.emplace_back(std::move(filter));
}
}
}
});
return intent_filters;
}
// Returns a list of URLs supported by an app given an app ID.
std::vector<std::string> GetSupportedLinks(Profile* profile,
const std::string& app_id) {
std::set<std::string> supported_links;
auto intent_filters = GetSupportedLinkIntentFilters(profile, app_id);
for (auto& filter : intent_filters) {
for (const auto& link :
apps_util::GetSupportedLinksForAppManagement(filter)) {
supported_links.insert(link);
}
}
return std::vector<std::string>(supported_links.begin(),
supported_links.end());
}
app_management::mojom::LocalePtr CreateLocaleForTag(
const std::string& locale_tag,
const std::string& system_locale) {
const std::string display_name =
base::UTF16ToUTF8(l10n_util::GetDisplayNameForLocale(
locale_tag, system_locale, /*is_for_ui=*/true));
const std::string native_display_name = base::UTF16ToUTF8(
l10n_util::GetDisplayNameForLocale(locale_tag, locale_tag,
/*is_for_ui=*/true));
// In ICU library, undefined locale is treated as unknown language
// (ICU-20273).
constexpr char kUndefinedTranslatedLocaleName[] = "und";
// In Android, it's possible for Apps to set custom locale tag, hence these
// locales might be untranslatable (based on ICU-20273).
// In this case, we'll pass empty string and let the UI decides what to
// display. For ARC, we'll display the `locale_tag` as is (this is safe
// within the limit specified by IETF BCP 47, as no malicious HTML tags
// could be formed).
return app_management::mojom::Locale::New(
locale_tag,
display_name == kUndefinedTranslatedLocaleName ? "" : display_name,
native_display_name == kUndefinedTranslatedLocaleName
? ""
: native_display_name);
}
app_management::mojom::ExtensionAppPermissionMessagePtr
CreateExtensionAppPermissionMessage(
const extensions::PermissionMessage& message) {
std::vector<std::string> submessages;
for (const auto& submessage : message.submessages()) {
submessages.push_back(base::UTF16ToUTF8(submessage));
}
return app_management::mojom::ExtensionAppPermissionMessage::New(
base::UTF16ToUTF8(message.message()), std::move(submessages));
}
std::optional<std::string> MaybeFormatBytes(std::optional<uint64_t> bytes) {
if (bytes.has_value()) {
// ui::FormatBytes requires a non-negative signed integer. In general, we
// expect that converting from unsigned to signed int here should always
// yield a positive value, since overflowing into negative would require an
// implausibly large app (2^63 bytes ~= 9 exabytes).
int64_t signed_bytes = static_cast<int64_t>(bytes.value());
if (signed_bytes < 0) {
// TODO(crbug.com/40063212): Investigate ARC apps which have negative data
// sizes.
LOG(ERROR) << "Invalid app size: " << signed_bytes;
base::debug::DumpWithoutCrashing();
return std::nullopt;
}
return base::UTF16ToUTF8(ui::FormatBytes(signed_bytes));
}
return std::nullopt;
}
} // namespace
AppManagementPageHandlerChromeOs::AppManagementPageHandlerChromeOs(
mojo::PendingReceiver<app_management::mojom::PageHandler> receiver,
mojo::PendingRemote<app_management::mojom::Page> page,
Profile* profile,
AppManagementPageHandlerBase::Delegate& delegate)
: AppManagementPageHandlerBase(std::move(receiver),
std::move(page),
profile),
shelf_delegate_(this, profile),
delegate_(delegate) {
apps::AppServiceProxy* proxy =
apps::AppServiceProxyFactory::GetForProfile(profile);
preferred_apps_list_handle_observer_.Observe(&proxy->PreferredAppsList());
}
AppManagementPageHandlerChromeOs::~AppManagementPageHandlerChromeOs() = default;
void AppManagementPageHandlerChromeOs::OnPinnedChanged(
const std::string& app_id,
bool pinned) {
NotifyAppChanged(app_id);
}
void AppManagementPageHandlerChromeOs::GetSubAppToParentMap(
GetSubAppToParentMapCallback callback) {
auto* provider = web_app::WebAppProvider::GetForWebApps(profile());
if (provider) {
// Web apps are managed in the current process (Ash or Lacros).
provider->scheduler().ScheduleCallbackWithResult(
"AppManagementPageHandlerBase::GetSubAppToParentMap",
web_app::AllAppsLockDescription(),
base::BindOnce(
[](web_app::AllAppsLock& lock, base::Value::Dict& debug_value) {
return lock.registrar().GetSubAppToParentMap();
}),
/*on_complete=*/std::move(callback),
/*arg_for_shutdown=*/base::flat_map<std::string, std::string>());
return;
}
// Web app data needs to be fetched from the Lacros process.
crosapi::mojom::WebAppProviderBridge* web_app_provider_bridge =
crosapi::CrosapiManager::Get()
->crosapi_ash()
->web_app_service_ash()
->GetWebAppProviderBridge();
if (web_app_provider_bridge) {
web_app_provider_bridge->GetSubAppToParentMap(std::move(callback));
return;
}
LOG(ERROR) << "Could not find WebAppProviderBridge.";
// Reaching here means that WebAppProviderBridge and WebAppProvider were both
// not found.
std::move(callback).Run(base::flat_map<std::string, std::string>());
}
void AppManagementPageHandlerChromeOs::GetExtensionAppPermissionMessages(
const std::string& app_id,
GetExtensionAppPermissionMessagesCallback callback) {
extensions::ExtensionRegistry* registry =
extensions::ExtensionRegistry::Get(profile());
const extensions::Extension* extension = registry->GetExtensionById(
app_id, extensions::ExtensionRegistry::ENABLED |
extensions::ExtensionRegistry::DISABLED |
extensions::ExtensionRegistry::BLOCKLISTED);
std::vector<app_management::mojom::ExtensionAppPermissionMessagePtr> messages;
if (extension) {
for (const auto& message :
extension->permissions_data()->GetPermissionMessages()) {
messages.push_back(CreateExtensionAppPermissionMessage(message));
}
}
std::move(callback).Run(std::move(messages));
}
void AppManagementPageHandlerChromeOs::SetPinned(const std::string& app_id,
bool pinned) {
shelf_delegate_.SetPinned(app_id, pinned);
}
void AppManagementPageHandlerChromeOs::SetResizeLocked(
const std::string& app_id,
bool locked) {
apps::AppServiceProxyFactory::GetForProfile(profile())->SetResizeLocked(
app_id, locked);
}
void AppManagementPageHandlerChromeOs::Uninstall(const std::string& app_id) {
auto* const proxy = apps::AppServiceProxyFactory::GetForProfile(profile());
std::optional<bool> allow_uninstall;
proxy->AppRegistryCache().ForOneApp(
app_id, [&](const apps::AppUpdate& update) {
allow_uninstall = update.AllowUninstall();
});
if (!allow_uninstall.value_or(false)) {
mojo::ReportBadMessage("Invalid attempt to uninstall app.");
return;
}
proxy->Uninstall(app_id, apps::UninstallSource::kAppManagement,
delegate_->GetUninstallAnchorWindow());
}
void AppManagementPageHandlerChromeOs::SetPreferredApp(
const std::string& app_id,
bool is_preferred_app) {
auto* proxy = apps::AppServiceProxyFactory::GetForProfile(profile());
bool is_preferred_app_for_supported_links =
proxy->PreferredAppsList().IsPreferredAppForSupportedLinks(app_id);
if (is_preferred_app && !is_preferred_app_for_supported_links) {
proxy->SetSupportedLinksPreference(app_id);
} else if (!is_preferred_app && is_preferred_app_for_supported_links) {
proxy->RemoveSupportedLinksPreference(app_id);
}
}
void AppManagementPageHandlerChromeOs::GetOverlappingPreferredApps(
const std::string& app_id,
GetOverlappingPreferredAppsCallback callback) {
auto intent_filters = GetSupportedLinkIntentFilters(profile(), app_id);
auto* proxy = apps::AppServiceProxyFactory::GetForProfile(profile());
base::flat_set<std::string> app_ids =
proxy->PreferredAppsList().FindPreferredAppsForFilters(intent_filters);
app_ids.erase(app_id);
// Erase all IDs that do not correspond to installed apps in App Service. Such
// IDs could be apps that have been uninstalled but did not have their
// preference updated correctly, or the legacy "use_browser" preference. This
// prevents attempting to show an overlapping app dialog for an app that
// doesn't currently exist.
base::EraseIf(app_ids, [proxy](const std::string& app_id) {
return !proxy->AppRegistryCache().IsAppInstalled(app_id);
});
std::move(callback).Run(std::move(app_ids).extract());
}
void AppManagementPageHandlerChromeOs::UpdateAppSize(
const std::string& app_id) {
auto* proxy = apps::AppServiceProxyFactory::GetForProfile(profile());
proxy->UpdateAppSize(app_id);
}
void AppManagementPageHandlerChromeOs::SetWindowMode(
const std::string& app_id,
apps::WindowMode window_mode) {
NOTIMPLEMENTED();
}
void AppManagementPageHandlerChromeOs::SetRunOnOsLoginMode(
const std::string& app_id,
apps::RunOnOsLoginMode run_on_os_login_mode) {
NOTIMPLEMENTED();
}
void AppManagementPageHandlerChromeOs::ShowDefaultAppAssociationsUi() {
NOTIMPLEMENTED();
}
void AppManagementPageHandlerChromeOs::OpenStorePage(
const std::string& app_id) {
auto* proxy = apps::AppServiceProxyFactory::GetForProfile(profile());
auto* apk_service = ash::ApkWebAppService::Get(profile());
proxy->AppRegistryCache().ForOneApp(
app_id, [&proxy, &apk_service](const apps::AppUpdate& update) {
if (update.InstallSource() == apps::InstallSource::kPlayStore) {
std::string package_name = update.PublisherId();
if (apk_service->IsWebAppInstalledFromArc(update.AppId())) {
package_name =
apk_service->GetPackageNameForWebApp(update.AppId()).value();
}
GURL url("https://play.google.com/store/apps/details?id=" +
package_name);
proxy->LaunchAppWithUrl(arc::kPlayStoreAppId, ui::EF_NONE, url,
apps::LaunchSource::kFromChromeInternal);
} else if (update.InstallSource() ==
apps::InstallSource::kChromeWebStore) {
GURL url("https://chrome.google.com/webstore/detail/" +
update.AppId());
proxy->LaunchAppWithUrl(extensions::kWebStoreAppId, ui::EF_NONE, url,
apps::LaunchSource::kFromChromeInternal);
}
});
}
void AppManagementPageHandlerChromeOs::SetAppLocale(
const std::string& app_id,
const std::string& locale_tag) {
apps::AppServiceProxyFactory::GetForProfile(profile())->SetAppLocale(
app_id, locale_tag);
}
void AppManagementPageHandlerChromeOs::OnPreferredAppChanged(
const std::string& app_id,
bool is_preferred_app) {
NotifyAppChanged(app_id);
}
void AppManagementPageHandlerChromeOs::OnPreferredAppsListWillBeDestroyed(
apps::PreferredAppsListHandle* handle) {
preferred_apps_list_handle_observer_.Reset();
}
app_management::mojom::AppPtr AppManagementPageHandlerChromeOs::CreateApp(
const std::string& app_id) {
app_management::mojom::AppPtr app =
AppManagementPageHandlerBase::CreateApp(app_id);
if (!app) {
return nullptr;
}
auto* proxy = apps::AppServiceProxyFactory::GetForProfile(profile());
app->is_preferred_app =
proxy->PreferredAppsList().IsPreferredAppForSupportedLinks(app_id);
app->supported_links = GetSupportedLinks(profile(), app->id);
app->is_pinned = shelf_delegate_.IsPinned(app_id);
app->is_policy_pinned = shelf_delegate_.IsPolicyPinned(app_id);
proxy->AppRegistryCache().ForOneApp(
app_id, [this, &app](const apps::AppUpdate& update) {
app->app_size = MaybeFormatBytes(update.AppSizeInBytes());
app->data_size = MaybeFormatBytes(update.DataSizeInBytes());
app->resize_locked = update.ResizeLocked().value_or(false);
app->hide_resize_locked = !update.ResizeLocked().has_value();
app->allow_uninstall = update.AllowUninstall().value_or(false);
if (ash::settings::IsPerAppLanguageEnabled(profile())) {
const std::string& system_locale =
g_browser_process->GetApplicationLocale();
// Translate supported locales.
for (const std::string& locale_tag : update.SupportedLocales()) {
app->supported_locales.push_back(
CreateLocaleForTag(locale_tag, system_locale));
}
// Translate selected locale.
std::optional<std::string> locale_tag = update.SelectedLocale();
if (locale_tag.has_value()) {
app->selected_locale =
CreateLocaleForTag(*locale_tag, system_locale);
}
}
});
return app;
}