// Copyright 2020 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/publishers/borealis_apps.h"
#include <optional>
#include "ash/public/cpp/app_menu_constants.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/strings/string_number_conversions.h"
#include "chrome/browser/apps/app_service/app_icon/app_icon_factory.h"
#include "chrome/browser/apps/app_service/app_launch_params.h"
#include "chrome/browser/apps/app_service/app_service_proxy.h"
#include "chrome/browser/apps/app_service/menu_util.h"
#include "chrome/browser/apps/app_service/publishers/guest_os_apps.h"
#include "chrome/browser/ash/borealis/borealis_app_launcher.h"
#include "chrome/browser/ash/borealis/borealis_app_uninstaller.h"
#include "chrome/browser/ash/borealis/borealis_context_manager.h"
#include "chrome/browser/ash/borealis/borealis_features.h"
#include "chrome/browser/ash/borealis/borealis_metrics.h"
#include "chrome/browser/ash/borealis/borealis_prefs.h"
#include "chrome/browser/ash/borealis/borealis_service.h"
#include "chrome/browser/ash/borealis/borealis_util.h"
#include "chrome/browser/ash/guest_os/guest_os_registry_service.h"
#include "chrome/browser/ash/guest_os/guest_os_registry_service_factory.h"
#include "chrome/browser/ash/guest_os/public/types.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/grit/chrome_unscaled_resources.h"
#include "chrome/grit/generated_resources.h"
#include "components/prefs/pref_service.h"
#include "ui/base/l10n/l10n_util.h"
namespace {
struct PermissionInfo {
apps::PermissionType permission;
const char* pref_name;
};
constexpr PermissionInfo permission_infos[] = {
{apps::PermissionType::kMicrophone, borealis::prefs::kBorealisMicAllowed},
};
const char* PermissionToPrefName(apps::PermissionType permission) {
for (const PermissionInfo& info : permission_infos) {
if (info.permission == permission) {
return info.pref_name;
}
}
return nullptr;
}
// Helper method to set up an apps visibility in all the major UI surfaces.
void SetAppVisibility(apps::App& app, bool visible) {
app.recommendable = visible;
app.searchable = visible;
app.show_in_launcher = visible;
app.show_in_shelf = visible;
app.show_in_search = visible;
app.show_in_management = visible;
app.handles_intents = visible;
}
apps::Permissions CreatePermissions(Profile* profile) {
apps::Permissions permissions;
for (const PermissionInfo& info : permission_infos) {
permissions.push_back(std::make_unique<apps::Permission>(
info.permission, profile->GetPrefs()->GetBoolean(info.pref_name),
/*is_managed=*/false));
}
return permissions;
}
} // namespace
namespace apps {
BorealisApps::BorealisApps(AppServiceProxy* proxy) : GuestOSApps(proxy) {
anonymous_app_observation_.Observe(
&borealis::BorealisService::GetForProfile(profile())->WindowManager());
pref_registrar_.Init(profile()->GetPrefs());
for (const PermissionInfo& info : permission_infos) {
pref_registrar_.Add(
info.pref_name,
base::BindRepeating(&apps::BorealisApps::OnPermissionChanged,
weak_factory_.GetWeakPtr()));
}
pref_registrar_.Add(borealis::prefs::kBorealisInstalledOnDevice,
base::BindRepeating(&BorealisApps::RefreshSpecialApps,
weak_factory_.GetWeakPtr()));
// TODO(b/170264723): When uninstalling borealis is completed, ensure that we
// remove the apps from the apps service.
}
BorealisApps::~BorealisApps() {
pref_registrar_.RemoveAll();
}
void BorealisApps::CallWithBorealisAllowed(
base::OnceCallback<void(bool)> callback) {
borealis::BorealisService::GetForProfile(profile())->Features().IsAllowed(
base::BindOnce(
[](base::OnceCallback<void(bool)> callback,
borealis::BorealisFeatures::AllowStatus allow_status) {
std::move(callback).Run(
allow_status ==
borealis::BorealisFeatures::AllowStatus::kAllowed);
},
std::move(callback)));
}
void BorealisApps::SetUpSpecialApps(bool allowed) {
// The special apps are only shown if borealis isn't installed and it can be.
bool installed = borealis::BorealisService::GetForProfile(profile())
->Features()
.IsEnabled();
bool shown = allowed && !installed;
// An app for borealis' installer. This app is not shown to users via
// launcher/management, and is only visible on the shelf.
auto installer_app = apps::AppPublisher::MakeApp(
apps::AppType::kBorealis, borealis::kInstallerAppId,
shown ? apps::Readiness::kReady : apps::Readiness::kDisabledByPolicy,
l10n_util::GetStringUTF8(IDS_BOREALIS_INSTALLER_APP_NAME),
apps::InstallReason::kDefault, apps::InstallSource::kSystem);
SetAppVisibility(*installer_app, shown);
installer_app->icon_key = apps::IconKey(IDR_LOGO_BOREALIS_STEAM_PENDING_192,
apps::IconEffects::kNone);
installer_app->show_in_launcher = false;
installer_app->show_in_management = false;
installer_app->show_in_search = false;
installer_app->allow_uninstall = false;
installer_app->allow_close = true;
AppPublisher::Publish(std::move(installer_app));
// A "steam" app, which is shown in launcher searches. This app is essentially
// a front for the installer, but we need it for two reasons:
// - The "real" steam app comes with the VM and so we won't have it before
// installation.
// - We want users to be able to search for steam and see the correct icon,
// but we want to visually distinguish that from the installer app above.
auto initial_steam_app = apps::AppPublisher::MakeApp(
apps::AppType::kBorealis, borealis::kLauncherSearchAppId,
shown ? apps::Readiness::kReady : apps::Readiness::kDisabledByPolicy,
l10n_util::GetStringUTF8(IDS_BOREALIS_INSTALLER_APP_NAME),
apps::InstallReason::kDefault, apps::InstallSource::kSystem);
SetAppVisibility(*initial_steam_app, shown);
initial_steam_app->icon_key =
apps::IconKey(IDR_LOGO_BOREALIS_STEAM_192, apps::IconEffects::kNone);
initial_steam_app->show_in_launcher = false;
initial_steam_app->show_in_management = false;
initial_steam_app->allow_uninstall = false;
initial_steam_app->allow_close = true;
AppPublisher::Publish(std::move(initial_steam_app));
}
bool BorealisApps::CouldBeAllowed() const {
// Borealis's permission check is actually dynamic, so instead of performing
// it here we just pretend that borealis is always allowed.
//
// For Borealis this is useful, we still want to allow uninstall when
// disallowed (to clean up the disk) but that requires us to provide an app
// which the user can right click on and uninstall.
return true;
}
apps::AppType BorealisApps::AppType() const {
return apps::AppType::kBorealis;
}
guest_os::VmType BorealisApps::VmType() const {
return guest_os::VmType::BOREALIS;
}
void BorealisApps::Initialize() {
GuestOSApps::Initialize();
CallWithBorealisAllowed(base::BindOnce(&BorealisApps::SetUpSpecialApps,
weak_factory_.GetWeakPtr()));
}
void BorealisApps::CreateAppOverrides(
const guest_os::GuestOsRegistryService::Registration& registration,
App* app) {
// The special apps are not GuestOs apps, they don't have a registration and
// can't be converted.
DCHECK_NE(registration.app_id(), borealis::kInstallerAppId);
DCHECK_NE(registration.app_id(), borealis::kLauncherSearchAppId);
// Borealis apps don't handle intents (like "open with").
app->handles_intents = false;
// Borealis apps are normal apps per apps-management.
app->show_in_management = true;
// Borealis supports uninstall per-app
app->allow_uninstall = true;
// Hide some known spurious "apps" from the user.
if (borealis::ShouldHideIrrelevantApp(registration)) {
SetAppVisibility(*app, false);
}
// Special handling for the steam client itself.
if (registration.app_id() == borealis::kClientAppId) {
app->permissions = CreatePermissions(profile());
} else {
// Identify games to App Service by PackageId.
// Steam games have PackageIds like "steam:123", where 123 is the Steam Game
// ID.
std::optional<int> app_id = borealis::ParseSteamGameId(registration.Exec());
if (app_id) {
app->installer_package_id = PackageId(
PackageType::kBorealis, base::NumberToString(app_id.value()));
}
}
}
int BorealisApps::DefaultIconResourceId() const {
return IDR_LOGO_BOREALIS_DEFAULT_192;
}
void BorealisApps::Launch(const std::string& app_id,
int32_t event_flags,
LaunchSource launch_source,
WindowInfoPtr window_info) {
LaunchAppWithIntent(app_id, event_flags, /*intent=*/nullptr, launch_source,
std::move(window_info), base::DoNothing());
}
void BorealisApps::LaunchAppWithIntent(const std::string& app_id,
int32_t event_flags,
IntentPtr intent,
LaunchSource launch_source,
WindowInfoPtr window_info,
LaunchCallback callback) {
borealis::BorealisService::GetForProfile(profile())->AppLauncher().Launch(
app_id, borealis::BorealisLaunchSource::kSteamInstallerApp,
base::DoNothing());
}
void BorealisApps::SetPermission(const std::string& app_id,
PermissionPtr permission_ptr) {
auto permission = permission_ptr->permission_type;
const char* pref_name = PermissionToPrefName(permission);
if (!pref_name) {
return;
}
profile()->GetPrefs()->SetBoolean(pref_name,
permission_ptr->IsPermissionEnabled());
}
void BorealisApps::Uninstall(const std::string& app_id,
UninstallSource uninstall_source,
bool clear_site_data,
bool report_abuse) {
borealis::BorealisService::GetForProfile(profile())
->AppUninstaller()
.Uninstall(app_id, base::DoNothing());
}
void BorealisApps::GetMenuModel(const std::string& app_id,
MenuType menu_type,
int64_t display_id,
base::OnceCallback<void(MenuItems)> callback) {
MenuItems menu_items;
// Apps should only be uninstallable if we can run the VM, but the vm itself
// should always be uninstallable.
if (borealis::BorealisService::GetForProfile(profile())
->Features()
.IsEnabled() ||
app_id == borealis::kClientAppId) {
AddCommandItem(ash::UNINSTALL, IDS_APP_LIST_UNINSTALL_ITEM, menu_items);
}
if (ShouldAddCloseItem(app_id, menu_type, profile())) {
AddCommandItem(ash::MENU_CLOSE, IDS_SHELF_CONTEXT_MENU_CLOSE, menu_items);
}
// TODO(b/162562622): Menu models for borealis apps.
std::move(callback).Run(std::move(menu_items));
}
void BorealisApps::OnPermissionChanged() {
auto app = std::make_unique<App>(AppType::kBorealis, borealis::kClientAppId);
app->permissions = CreatePermissions(profile());
AppPublisher::Publish(std::move(app));
}
void BorealisApps::RefreshSpecialApps() {
CallWithBorealisAllowed(base::BindOnce(&BorealisApps::SetUpSpecialApps,
weak_factory_.GetWeakPtr()));
}
void BorealisApps::OnAnonymousAppAdded(const std::string& shelf_app_id,
const std::string& shelf_app_name) {
auto app = AppPublisher::MakeApp(
AppType::kBorealis, shelf_app_id, Readiness::kReady, shelf_app_name,
InstallReason::kUser, InstallSource::kUnknown);
SetAppVisibility(*app, /*visible=*/false);
app->icon_key = IconKey(IDR_LOGO_BOREALIS_DEFAULT_192, IconEffects::kNone);
AppPublisher::Publish(std::move(app));
}
void BorealisApps::OnAnonymousAppRemoved(const std::string& shelf_app_id) {
// First uninstall the anonymous app, then remove it.
for (auto readiness : {Readiness::kUninstalledByUser, Readiness::kRemoved}) {
auto app = std::make_unique<App>(AppType::kBorealis, shelf_app_id);
app->readiness = readiness;
AppPublisher::Publish(std::move(app));
}
}
void BorealisApps::OnWindowManagerDeleted(
borealis::BorealisWindowManager* window_manager) {
anonymous_app_observation_.Reset();
}
} // namespace apps