blob: 47832b59f5e64a20a307a8772bb48ea635bcfbab [file] [log] [blame]
// Copyright 2019 The Chromium Authors. All rights reserved.
// 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/crostini_apps.h"
#include <utility>
#include "ash/public/cpp/app_menu_constants.h"
#include "chrome/browser/apps/app_service/dip_px_util.h"
#include "chrome/browser/apps/app_service/menu_util.h"
#include "chrome/browser/chromeos/crostini/crostini_features.h"
#include "chrome/browser/chromeos/crostini/crostini_package_service.h"
#include "chrome/browser/chromeos/crostini/crostini_pref_names.h"
#include "chrome/browser/chromeos/crostini/crostini_util.h"
#include "chrome/browser/chromeos/guest_os/guest_os_registry_service_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/common/chrome_features.h"
#include "chrome/grit/chrome_unscaled_resources.h"
#include "chrome/grit/generated_resources.h"
#include "components/prefs/pref_change_registrar.h"
#include "ui/display/display.h"
#include "ui/display/screen.h"
#include "ui/strings/grit/ui_strings.h"
// TODO(crbug.com/826982): the equivalent of
// CrostiniAppModelBuilder::MaybeCreateRootFolder. Does some sort of "root
// folder" abstraction belong here (on the publisher side of the App Service)
// or should we hard-code that in one particular subscriber (the App List UI)?
namespace {
bool ShouldShowDisplayDensityMenuItem(const std::string& app_id,
apps::mojom::MenuType menu_type,
int64_t display_id) {
// The default terminal app is crosh in a Chrome window and it doesn't run in
// the Crostini container so it doesn't support display density the same way.
if (menu_type != apps::mojom::MenuType::kShelf ||
app_id == crostini::GetTerminalId()) {
return false;
}
display::Display d;
if (!display::Screen::GetScreen()->GetDisplayWithDisplayId(display_id, &d)) {
return true;
}
return d.device_scale_factor() != 1.0;
}
} // namespace
namespace apps {
CrostiniApps::CrostiniApps(
const mojo::Remote<apps::mojom::AppService>& app_service,
Profile* profile)
: profile_(profile), registry_(nullptr), crostini_enabled_(false) {
Initialize(app_service);
}
CrostiniApps::~CrostiniApps() {
if (registry_) {
registry_->RemoveObserver(this);
}
}
void CrostiniApps::ReInitializeForTesting(
const mojo::Remote<apps::mojom::AppService>& app_service,
Profile* profile) {
// Some test code creates a profile (and therefore profile-linked services
// like the App Service) before it creates the fake user that lets
// IsCrostiniUIAllowedForProfile return true. To work around that, we issue a
// second Initialize call.
receiver().reset();
profile_ = profile;
registry_ = nullptr;
crostini_enabled_ = false;
Initialize(app_service);
}
void CrostiniApps::Initialize(
const mojo::Remote<apps::mojom::AppService>& app_service) {
DCHECK(profile_);
if (!crostini::CrostiniFeatures::Get()->IsUIAllowed(profile_)) {
return;
}
registry_ = guest_os::GuestOsRegistryServiceFactory::GetForProfile(profile_);
if (!registry_) {
return;
}
crostini_enabled_ = crostini::CrostiniFeatures::Get()->IsEnabled(profile_);
registry_->AddObserver(this);
pref_change_registrar_ = std::make_unique<PrefChangeRegistrar>();
pref_change_registrar_->Init(profile_->GetPrefs());
pref_change_registrar_->Add(
crostini::prefs::kCrostiniEnabled,
base::BindRepeating(&CrostiniApps::OnCrostiniEnabledChanged,
base::Unretained(this)));
PublisherBase::Initialize(app_service, apps::mojom::AppType::kCrostini);
}
void CrostiniApps::Connect(
mojo::PendingRemote<apps::mojom::Subscriber> subscriber_remote,
apps::mojom::ConnectOptionsPtr opts) {
std::vector<apps::mojom::AppPtr> apps;
for (const auto& pair :
registry_->GetRegisteredApps(guest_os::GuestOsRegistryService::VmType::
ApplicationList_VmType_TERMINA)) {
const std::string& app_id = pair.first;
const guest_os::GuestOsRegistryService::Registration& registration =
pair.second;
apps.push_back(Convert(app_id, registration, true));
}
mojo::Remote<apps::mojom::Subscriber> subscriber(
std::move(subscriber_remote));
subscriber->OnApps(std::move(apps));
subscribers_.Add(std::move(subscriber));
}
void CrostiniApps::LoadIcon(const std::string& app_id,
apps::mojom::IconKeyPtr icon_key,
apps::mojom::IconCompression icon_compression,
int32_t size_hint_in_dip,
bool allow_placeholder_icon,
LoadIconCallback callback) {
if (icon_key) {
if (icon_key->resource_id != apps::mojom::IconKey::kInvalidResourceId) {
// The icon is a resource built into the Chrome OS binary.
constexpr bool is_placeholder_icon = false;
LoadIconFromResource(icon_compression, size_hint_in_dip,
icon_key->resource_id, is_placeholder_icon,
static_cast<IconEffects>(icon_key->icon_effects),
std::move(callback));
return;
} else {
auto scale_factor = apps_util::GetPrimaryDisplayUIScaleFactor();
// Try loading the icon from an on-disk cache. If that fails, fall back
// to LoadIconFromVM.
LoadIconFromFileWithFallback(
icon_compression, size_hint_in_dip,
registry_->GetIconPath(app_id, scale_factor),
static_cast<IconEffects>(icon_key->icon_effects), std::move(callback),
base::BindOnce(&CrostiniApps::LoadIconFromVM,
weak_ptr_factory_.GetWeakPtr(), app_id,
icon_compression, size_hint_in_dip,
allow_placeholder_icon, scale_factor,
static_cast<IconEffects>(icon_key->icon_effects)));
return;
}
}
// On failure, we still run the callback, with the zero IconValue.
std::move(callback).Run(apps::mojom::IconValue::New());
}
void CrostiniApps::Launch(const std::string& app_id,
int32_t event_flags,
apps::mojom::LaunchSource launch_source,
int64_t display_id) {
crostini::LaunchCrostiniApp(profile_, app_id, display_id);
}
void CrostiniApps::Uninstall(const std::string& app_id,
bool clear_site_data,
bool report_abuse) {
crostini::CrostiniPackageService::GetForProfile(profile_)
->QueueUninstallApplication(app_id);
}
void CrostiniApps::GetMenuModel(const std::string& app_id,
apps::mojom::MenuType menu_type,
int64_t display_id,
GetMenuModelCallback callback) {
apps::mojom::MenuItemsPtr menu_items = apps::mojom::MenuItems::New();
if (menu_type == apps::mojom::MenuType::kShelf) {
AddCommandItem(ash::MENU_NEW_WINDOW, IDS_APP_LIST_NEW_WINDOW, &menu_items);
}
if (crostini::IsUninstallable(profile_, app_id)) {
AddCommandItem(ash::UNINSTALL, IDS_APP_LIST_UNINSTALL_ITEM, &menu_items);
}
if (app_id == crostini::GetTerminalId()) {
if (base::FeatureList::IsEnabled(features::kTerminalSystemApp)) {
AddCommandItem(ash::SETTINGS, IDS_INTERNAL_APP_SETTINGS, &menu_items);
}
if (crostini::IsCrostiniRunning(profile_)) {
AddCommandItem(ash::SHUTDOWN_GUEST_OS,
IDS_CROSTINI_SHUT_DOWN_LINUX_MENU_ITEM, &menu_items);
}
}
if (ShouldAddOpenItem(app_id, menu_type, profile_)) {
AddCommandItem(ash::MENU_OPEN_NEW, IDS_APP_CONTEXT_MENU_ACTIVATE_ARC,
&menu_items);
}
if (ShouldAddCloseItem(app_id, menu_type, profile_)) {
AddCommandItem(ash::MENU_CLOSE, IDS_SHELF_CONTEXT_MENU_CLOSE, &menu_items);
}
// Offer users the ability to toggle per-application UI scaling.
// Some apps have high-density display support and do not require scaling
// to match the system display density, but others are density-unaware and
// look better when scaled to match the display density.
if (ShouldShowDisplayDensityMenuItem(app_id, menu_type, display_id)) {
base::Optional<guest_os::GuestOsRegistryService::Registration>
registration = registry_->GetRegistration(app_id);
if (registration) {
if (registration->IsScaled()) {
AddCommandItem(ash::CROSTINI_USE_HIGH_DENSITY,
IDS_CROSTINI_USE_HIGH_DENSITY, &menu_items);
} else {
AddCommandItem(ash::CROSTINI_USE_LOW_DENSITY,
IDS_CROSTINI_USE_LOW_DENSITY, &menu_items);
}
}
}
std::move(callback).Run(std::move(menu_items));
}
void CrostiniApps::OnRegistryUpdated(
guest_os::GuestOsRegistryService* registry_service,
const std::vector<std::string>& updated_apps,
const std::vector<std::string>& removed_apps,
const std::vector<std::string>& inserted_apps) {
for (const std::string& app_id : updated_apps) {
PublishAppID(app_id, PublishAppIDType::kUpdate);
}
for (const std::string& app_id : removed_apps) {
PublishAppID(app_id, PublishAppIDType::kUninstall);
}
for (const std::string& app_id : inserted_apps) {
PublishAppID(app_id, PublishAppIDType::kInstall);
}
}
void CrostiniApps::OnAppIconUpdated(const std::string& app_id,
ui::ScaleFactor scale_factor) {
apps::mojom::AppPtr app = apps::mojom::App::New();
app->app_type = apps::mojom::AppType::kCrostini;
app->app_id = app_id;
app->icon_key = NewIconKey(app_id);
Publish(std::move(app), subscribers_);
}
void CrostiniApps::OnCrostiniEnabledChanged() {
crostini_enabled_ =
profile_ && crostini::CrostiniFeatures::Get()->IsEnabled(profile_);
auto show = crostini_enabled_ ? apps::mojom::OptionalBool::kTrue
: apps::mojom::OptionalBool::kFalse;
// The Crostini Terminal app is a hard-coded special case. It is the entry
// point to installing other Crostini apps.
apps::mojom::AppPtr app = apps::mojom::App::New();
app->app_type = apps::mojom::AppType::kCrostini;
app->app_id = crostini::GetTerminalId();
app->show_in_launcher = show;
app->show_in_shelf = show;
app->show_in_search = show;
Publish(std::move(app), subscribers_);
}
void CrostiniApps::LoadIconFromVM(const std::string app_id,
apps::mojom::IconCompression icon_compression,
int32_t size_hint_in_dip,
bool allow_placeholder_icon,
ui::ScaleFactor scale_factor,
IconEffects icon_effects,
LoadIconCallback callback) {
if (!allow_placeholder_icon) {
// Treat this as failure. We still run the callback, with a nullptr to
// indicate failure.
std::move(callback).Run(nullptr);
return;
}
// Provide a placeholder icon.
constexpr bool is_placeholder_icon = true;
LoadIconFromResource(icon_compression, size_hint_in_dip,
IDR_LOGO_CROSTINI_DEFAULT_192, is_placeholder_icon,
icon_effects, std::move(callback));
// Ask the VM to load the icon (and write a cached copy to the file system).
// The "Maybe" is because multiple requests for the same icon will be merged,
// calling OnAppIconUpdated only once. In OnAppIconUpdated, we'll publish a
// new IconKey, and subscribers can re-schedule new LoadIcon calls, with new
// LoadIconCallback's, that will pick up that cached copy.
//
// TODO(crbug.com/826982): add a safeguard to prevent an infinite loop where
// OnAppIconUpdated somehow doesn't write the cached icon file where we
// expect, leading to another MaybeRequestIcon call, leading to another
// OnAppIconUpdated call, leading to another MaybeRequestIcon call, etc.
registry_->MaybeRequestIcon(app_id, scale_factor);
}
apps::mojom::AppPtr CrostiniApps::Convert(
const std::string& app_id,
const guest_os::GuestOsRegistryService::Registration& registration,
bool new_icon_key) {
apps::mojom::AppPtr app = PublisherBase::MakeApp(
apps::mojom::AppType::kCrostini, app_id, apps::mojom::Readiness::kReady,
registration.Name(), apps::mojom::InstallSource::kUser);
const std::string& executable_file_name = registration.ExecutableFileName();
if (!executable_file_name.empty()) {
app->additional_search_terms.push_back(executable_file_name);
}
for (const std::string& keyword : registration.Keywords()) {
app->additional_search_terms.push_back(keyword);
}
if (new_icon_key) {
app->icon_key = NewIconKey(app_id);
}
app->last_launch_time = registration.LastLaunchTime();
app->install_time = registration.InstallTime();
auto show = !registration.NoDisplay() ? apps::mojom::OptionalBool::kTrue
: apps::mojom::OptionalBool::kFalse;
auto show_in_search = show;
if (registration.is_terminal_app()) {
show = crostini_enabled_ ? apps::mojom::OptionalBool::kTrue
: apps::mojom::OptionalBool::kFalse;
// The Crostini Terminal should appear in the app search, even when
// Crostini is not installed.
show_in_search = apps::mojom::OptionalBool::kTrue;
}
app->show_in_launcher = show;
app->show_in_shelf = app->show_in_search = show_in_search;
// TODO(crbug.com/955937): Enable once Crostini apps are managed inside App
// Management.
app->show_in_management = apps::mojom::OptionalBool::kFalse;
return app;
}
apps::mojom::IconKeyPtr CrostiniApps::NewIconKey(const std::string& app_id) {
DCHECK(!app_id.empty());
// Treat the Crostini Terminal as a special case, loading an icon defined by
// a resource instead of asking the Crostini VM (or the cache of previous
// responses from the Crostini VM). Presumably this is for bootstrapping: the
// Crostini Terminal icon (the UI for enabling and installing Crostini apps)
// should be showable even before the user has installed their first Crostini
// app and before bringing up an Crostini VM for the first time.
if (app_id == crostini::GetTerminalId()) {
return apps::mojom::IconKey::New(
apps::mojom::IconKey::kDoesNotChangeOverTime,
IDR_LOGO_CROSTINI_TERMINAL, apps::IconEffects::kNone);
}
return icon_key_factory_.MakeIconKey(apps::IconEffects::kNone);
}
void CrostiniApps::PublishAppID(const std::string& app_id,
PublishAppIDType type) {
if (type == PublishAppIDType::kUninstall) {
apps::mojom::AppPtr app = apps::mojom::App::New();
app->app_type = apps::mojom::AppType::kCrostini;
app->app_id = app_id;
app->readiness = apps::mojom::Readiness::kUninstalledByUser;
Publish(std::move(app), subscribers_);
return;
}
base::Optional<guest_os::GuestOsRegistryService::Registration> registration =
registry_->GetRegistration(app_id);
if (registration.has_value()) {
Publish(Convert(app_id, *registration, type == PublishAppIDType::kInstall),
subscribers_);
}
}
} // namespace apps