// 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/apps/app_service/publishers/web_apps_crosapi.h"
#include <memory>
#include <utility>
#include <vector>
#include "ash/constants/ash_features.h"
#include "ash/public/cpp/app_menu_constants.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/location.h"
#include "base/logging.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/app_service_proxy_factory.h"
#include "chrome/browser/apps/app_service/intent_util.h"
#include "chrome/browser/apps/app_service/launch_utils.h"
#include "chrome/browser/apps/app_service/menu_util.h"
#include "chrome/browser/apps/app_service/promise_apps/promise_app_web_apps_utils.h"
#include "chrome/browser/apps/browser_instance/browser_app_instance_registry.h"
#include "chrome/browser/ash/mall/mall_url.h"
#include "chrome/browser/web_applications/web_app_id_constants.h"
#include "chrome/browser/web_applications/web_app_utils.h"
#include "chrome/grit/generated_resources.h"
#include "chromeos/constants/chromeos_features.h"
#include "components/services/app_service/public/cpp/crosapi_utils.h"
#include "components/services/app_service/public/cpp/features.h"
#include "components/services/app_service/public/cpp/instance_registry.h"
#include "components/services/app_service/public/cpp/intent_util.h"
#include "extensions/common/constants.h"
namespace apps {
WebAppsCrosapi::WebAppsCrosapi(AppServiceProxy* proxy)
: apps::AppPublisher(proxy),
proxy_(proxy),
device_info_manager_(proxy->profile()) {}
WebAppsCrosapi::~WebAppsCrosapi() = default;
void WebAppsCrosapi::RegisterWebAppsCrosapiHost(
mojo::PendingReceiver<crosapi::mojom::AppPublisher> receiver) {
// At the moment the app service publisher will only accept one client
// publishing apps to ash chrome. Any extra clients will be ignored.
// TODO(crbug.com/40167449): Support SxS lacros.
if (receiver_.is_bound()) {
return;
}
receiver_.Bind(std::move(receiver));
receiver_.set_disconnect_handler(base::BindOnce(
&WebAppsCrosapi::OnCrosapiDisconnected, base::Unretained(this)));
}
void WebAppsCrosapi::GetCompressedIconData(const std::string& app_id,
int32_t size_in_dip,
ui::ResourceScaleFactor scale_factor,
LoadIconCallback callback) {
if (!LogIfNotConnected(FROM_HERE)) {
std::move(callback).Run(std::make_unique<IconValue>());
return;
}
controller_->GetCompressedIcon(app_id, size_in_dip, scale_factor,
std::move(callback));
}
void WebAppsCrosapi::Launch(const std::string& app_id,
int32_t event_flags,
LaunchSource launch_source,
WindowInfoPtr window_info) {
if (!LogIfNotConnected(FROM_HERE)) {
return;
}
// Redirect launches of the Mall app so that we can add additional context to
// the URL. Loading the context will cause a slight delay on first launch, but
// it is then cached in the DeviceInfoManager for subsequent launches.
// TODO(b/331702863): Remove this custom integration.
if (chromeos::features::IsCrosMallWebAppEnabled() &&
app_id == web_app::kMallAppId) {
device_info_manager_.GetDeviceInfo(base::BindOnce(
&WebAppsCrosapi::LaunchMallWithContext, weak_factory_.GetWeakPtr(),
event_flags, launch_source, std::move(window_info)));
return;
}
controller_->Launch(
CreateCrosapiLaunchParamsWithEventFlags(
proxy_, app_id, event_flags, launch_source,
window_info ? window_info->display_id : display::kInvalidDisplayId),
base::DoNothing());
}
void WebAppsCrosapi::LaunchAppWithFiles(
const std::string& app_id,
int32_t event_flags,
LaunchSource launch_source,
std::vector<base::FilePath> file_paths) {
if (!LogIfNotConnected(FROM_HERE)) {
return;
}
auto params = CreateCrosapiLaunchParamsWithEventFlags(
proxy_, app_id, event_flags, launch_source, display::kInvalidDisplayId);
params->intent =
apps_util::CreateCrosapiIntentForViewFiles(std::move(file_paths));
controller_->Launch(std::move(params), base::DoNothing());
}
void WebAppsCrosapi::LaunchAppWithIntent(const std::string& app_id,
int32_t event_flags,
IntentPtr intent,
LaunchSource launch_source,
WindowInfoPtr window_info,
LaunchCallback callback) {
if (!LogIfNotConnected(FROM_HERE)) {
std::move(callback).Run(LaunchResult(State::kFailed));
return;
}
auto params = CreateCrosapiLaunchParamsWithEventFlags(
proxy_, app_id, event_flags, launch_source,
window_info ? window_info->display_id : display::kInvalidDisplayId);
params->intent =
apps_util::ConvertAppServiceToCrosapiIntent(intent, proxy_->profile());
controller_->Launch(std::move(params), base::DoNothing());
// TODO(crbug.com/40202131): handle the case where launch fails.
std::move(callback).Run(LaunchResult(State::kSuccess));
}
void WebAppsCrosapi::LaunchAppWithParams(AppLaunchParams&& params,
LaunchCallback callback) {
if (!LogIfNotConnected(FROM_HERE)) {
std::move(callback).Run(LaunchResult());
return;
}
controller_->Launch(
apps::ConvertLaunchParamsToCrosapi(params, proxy_->profile()),
apps::LaunchResultToMojomLaunchResultCallback(std::move(callback)));
}
void WebAppsCrosapi::LaunchShortcut(const std::string& app_id,
const std::string& shortcut_id,
int64_t display_id) {
if (!LogIfNotConnected(FROM_HERE)) {
return;
}
controller_->ExecuteContextMenuCommand(app_id, shortcut_id,
base::DoNothing());
}
void WebAppsCrosapi::SetPermission(const std::string& app_id,
PermissionPtr permission) {
if (!LogIfNotConnected(FROM_HERE)) {
return;
}
controller_->SetPermission(app_id, std::move(permission));
}
void WebAppsCrosapi::Uninstall(const std::string& app_id,
UninstallSource uninstall_source,
bool clear_site_data,
bool report_abuse) {
if (!LogIfNotConnected(FROM_HERE)) {
return;
}
controller_->Uninstall(app_id, uninstall_source, clear_site_data,
report_abuse);
}
void WebAppsCrosapi::GetMenuModel(
const std::string& app_id,
MenuType menu_type,
int64_t display_id,
base::OnceCallback<void(MenuItems)> callback) {
bool is_system_web_app = false;
bool can_use_uninstall = false;
bool can_close = true;
bool allow_window_mode_selection = true;
WindowMode display_mode = WindowMode::kUnknown;
proxy_->AppRegistryCache().ForOneApp(
app_id,
[&is_system_web_app, &allow_window_mode_selection, &can_use_uninstall,
&can_close, &display_mode](const AppUpdate& update) {
is_system_web_app = update.InstallReason() == InstallReason::kSystem;
allow_window_mode_selection =
update.AllowWindowModeSelection().value_or(true);
can_use_uninstall = update.AllowUninstall().value_or(false);
can_close = update.AllowClose().value_or(true);
display_mode = update.WindowMode();
});
MenuItems menu_items;
if (display_mode != WindowMode::kUnknown && !is_system_web_app && can_close) {
if (!allow_window_mode_selection) {
apps::AddCommandItem(ash::LAUNCH_NEW,
IDS_APP_LIST_CONTEXT_MENU_NEW_WINDOW, menu_items);
} else {
CreateOpenNewSubmenu(display_mode == WindowMode::kBrowser
? IDS_APP_LIST_CONTEXT_MENU_NEW_TAB
: IDS_APP_LIST_CONTEXT_MENU_NEW_WINDOW,
menu_items);
}
}
if (ShouldAddCloseItem(app_id, menu_type, proxy_->profile())) {
AddCommandItem(ash::MENU_CLOSE, IDS_SHELF_CONTEXT_MENU_CLOSE, menu_items);
}
if (can_use_uninstall) {
AddCommandItem(ash::UNINSTALL, IDS_APP_LIST_UNINSTALL_ITEM, menu_items);
}
if (!is_system_web_app) {
AddCommandItem(ash::SHOW_APP_INFO, IDS_APP_CONTEXT_MENU_SHOW_INFO,
menu_items);
}
if (!LogIfNotConnected(FROM_HERE)) {
std::move(callback).Run(std::move(menu_items));
return;
}
controller_->GetMenuModel(
app_id, base::BindOnce(&WebAppsCrosapi::OnGetMenuModelFromCrosapi,
weak_factory_.GetWeakPtr(), app_id, menu_type,
std::move(menu_items), std::move(callback)));
}
void WebAppsCrosapi::UpdateAppSize(const std::string& app_id) {
if (!LogIfNotConnected(FROM_HERE)) {
return;
}
controller_->UpdateAppSize(app_id);
}
void WebAppsCrosapi::SetWindowMode(const std::string& app_id,
WindowMode window_mode) {
if (!LogIfNotConnected(FROM_HERE)) {
return;
}
controller_->SetWindowMode(app_id, window_mode);
}
void WebAppsCrosapi::OnGetMenuModelFromCrosapi(
const std::string& app_id,
MenuType menu_type,
MenuItems menu_items,
base::OnceCallback<void(MenuItems)> callback,
crosapi::mojom::MenuItemsPtr crosapi_menu_items) {
if (crosapi_menu_items->items.empty()) {
std::move(callback).Run(std::move(menu_items));
return;
}
auto separator_type = ui::DOUBLE_SEPARATOR;
const int crosapi_menu_items_size = crosapi_menu_items->items.size();
for (int item_index = 0; item_index < crosapi_menu_items_size; item_index++) {
const auto& crosapi_menu_item = crosapi_menu_items->items[item_index];
AddSeparator(std::exchange(separator_type, ui::PADDED_SEPARATOR),
menu_items);
// Uses integer |command_id| to store menu item index.
const int command_id = ash::LAUNCH_APP_SHORTCUT_FIRST + item_index;
auto& icon_image = crosapi_menu_item->image;
icon_image = ApplyBackgroundAndMask(icon_image);
AddShortcutCommandItem(command_id, crosapi_menu_item->id.value_or(""),
crosapi_menu_item->label, icon_image, menu_items);
}
std::move(callback).Run(std::move(menu_items));
}
void WebAppsCrosapi::PauseApp(const std::string& app_id) {
if (!LogIfNotConnected(FROM_HERE)) {
return;
}
controller_->PauseApp(app_id);
}
void WebAppsCrosapi::UnpauseApp(const std::string& app_id) {
if (!LogIfNotConnected(FROM_HERE)) {
return;
}
controller_->UnpauseApp(app_id);
}
void WebAppsCrosapi::StopApp(const std::string& app_id) {
if (!LogIfNotConnected(FROM_HERE)) {
return;
}
controller_->StopApp(app_id);
}
void WebAppsCrosapi::OpenNativeSettings(const std::string& app_id) {
if (!LogIfNotConnected(FROM_HERE)) {
return;
}
controller_->OpenNativeSettings(app_id);
}
void WebAppsCrosapi::ExecuteContextMenuCommand(const std::string& app_id,
int command_id,
const std::string& shortcut_id,
int64_t display_id) {
if (!LogIfNotConnected(FROM_HERE)) {
return;
}
controller_->ExecuteContextMenuCommand(app_id, shortcut_id,
base::DoNothing());
}
void WebAppsCrosapi::OnApps(std::vector<AppPtr> deltas) {
if (!web_app::IsWebAppsCrosapiEnabled()) {
return;
}
on_initial_apps_received_ = true;
if (!controller_.is_bound()) {
// If `controller_` is not bound, add `deltas` to `delta_app_cache_` to wait
// for registering the crosapi controller to publish all deltas saved in
// `delta_app_cache_`.
for (auto& delta : deltas) {
delta_app_cache_.push_back(std::move(delta));
}
return;
}
PublishImpl(std::move(deltas));
}
void WebAppsCrosapi::RegisterAppController(
mojo::PendingRemote<crosapi::mojom::AppController> controller) {
DCHECK(web_app::IsWebAppsCrosapiEnabled());
if (controller_.is_bound()) {
return;
}
controller_.Bind(std::move(controller));
controller_.set_disconnect_handler(base::BindOnce(
&WebAppsCrosapi::OnControllerDisconnected, base::Unretained(this)));
RegisterPublisher(AppType::kWeb);
if (on_initial_apps_received_) {
PublishImpl(std::move(delta_app_cache_));
delta_app_cache_.clear();
}
if (!delta_capability_access_cache_.empty()) {
PublishCapabilityAccessesImpl(std::move(delta_capability_access_cache_));
delta_capability_access_cache_.clear();
}
}
void WebAppsCrosapi::OnCapabilityAccesses(
std::vector<CapabilityAccessPtr> deltas) {
if (!web_app::IsWebAppsCrosapiEnabled()) {
return;
}
if (!controller_.is_bound()) {
// If `controller_` is not bound, add `deltas` to
// `delta_capability_access_cache_` to wait for registering the crosapi
// controller to publish all deltas saved in
// `delta_capability_access_cache_`.
for (auto& delta : deltas) {
delta_capability_access_cache_.push_back(std::move(delta));
}
return;
}
PublishCapabilityAccessesImpl(std::move(deltas));
}
bool WebAppsCrosapi::LogIfNotConnected(const base::Location& from_here) {
// It is possible that Lacros is briefly unavailable, for example if it shuts
// down for an update.
if (controller_.is_bound()) {
return true;
}
LOG(WARNING) << "Controller not connected: " << from_here.ToString();
return false;
}
void WebAppsCrosapi::OnCrosapiDisconnected() {
receiver_.reset();
controller_.reset();
}
void WebAppsCrosapi::OnControllerDisconnected() {
controller_.reset();
// If Lacros stops running (e.g. due to a crash/update), all apps will no
// longer be accessing capabilities.
ResetCapabilityAccess(AppType::kWeb);
}
void WebAppsCrosapi::PublishImpl(std::vector<AppPtr> deltas) {
// This is for prototyping and testing only. It is to provide an easy way to
// simulate web app promise icon behaviour for the UI/ client development of
// web app promise icons.
// TODO(b/261907269): Remove this code snippet and use real listeners for web
// app installation events.
if (ash::features::ArePromiseIconsForWebAppsEnabled()) {
for (auto& delta : deltas) {
apps::MaybeSimulatePromiseAppInstallationEvents(proxy(), delta.get());
}
}
apps::AppPublisher::Publish(std::move(deltas), AppType::kWeb,
should_notify_initialized_);
should_notify_initialized_ = false;
}
void WebAppsCrosapi::PublishCapabilityAccessesImpl(
std::vector<CapabilityAccessPtr> deltas) {
proxy()->OnCapabilityAccesses(std::move(deltas));
}
void WebAppsCrosapi::LaunchMallWithContext(int32_t event_flags,
apps::LaunchSource launch_source,
apps::WindowInfoPtr window_info,
apps::DeviceInfo device_info) {
LaunchAppWithIntent(
web_app::kMallAppId, event_flags,
std::make_unique<apps::Intent>(apps_util::kIntentActionView,
ash::GetMallLaunchUrl(device_info)),
launch_source, std::move(window_info), base::DoNothing());
}
} // namespace apps