| // Copyright 2020 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/web_applications/app_service/web_apps.h" |
| |
| #include <utility> |
| |
| #include "base/callback.h" |
| #include "base/callback_helpers.h" |
| #include "base/feature_list.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/intent_util.h" |
| #include "chrome/browser/apps/app_service/launch_utils.h" |
| #include "chrome/browser/content_settings/host_content_settings_map_factory.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/web_applications/system_web_apps/system_web_app_manager.h" |
| #include "chrome/browser/web_applications/web_app.h" |
| #include "chrome/browser/web_applications/web_app_constants.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_provider.h" |
| #include "chrome/browser/web_applications/web_app_registrar.h" |
| #include "chrome/browser/web_applications/web_app_utils.h" |
| #include "components/content_settings/core/common/content_settings.h" |
| #include "components/content_settings/core/common/content_settings_pattern.h" |
| #include "components/content_settings/core/common/content_settings_types.h" |
| #include "components/webapps/browser/installable/installable_metrics.h" |
| |
| #if BUILDFLAG(IS_CHROMEOS_ASH) |
| #include "ash/constants/ash_features.h" |
| #include "ash/public/cpp/app_menu_constants.h" |
| #include "ash/webui/projector_app/public/cpp/projector_app_constants.h" // nogncheck |
| #include "base/bind.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "chrome/browser/apps/app_service/menu_item_constants.h" |
| #include "chrome/browser/apps/app_service/menu_util.h" |
| #include "chrome/browser/ash/crosapi/browser_util.h" |
| #include "chrome/browser/ash/crostini/crostini_terminal.h" |
| #include "chrome/browser/ash/crostini/crostini_util.h" |
| #include "chrome/browser/web_applications/web_app_icon_manager.h" |
| #include "chrome/grit/generated_resources.h" |
| #include "components/services/app_service/public/cpp/app_registry_cache.h" |
| #include "components/services/app_service/public/cpp/instance_registry.h" |
| #include "components/services/app_service/public/cpp/intent_filter_util.h" |
| #endif |
| |
| using apps::IconEffects; |
| |
| namespace web_app { |
| |
| namespace { |
| |
| apps::AppType GetWebAppType() { |
| // After moving the ordinary Web Apps to Lacros chrome, the remaining web |
| // apps in ash Chrome will be only System Web Apps. Change the app type |
| // to kSystemWeb for this case and the kWeb app type will be published from |
| // the publisher for Lacros web apps. |
| #if BUILDFLAG(IS_CHROMEOS_ASH) |
| if (crosapi::browser_util::IsLacrosEnabled() && IsWebAppsCrosapiEnabled()) { |
| return apps::AppType::kSystemWeb; |
| } |
| #endif |
| |
| return apps::AppType::kWeb; |
| } |
| |
| bool ShouldObserveMediaRequests() { |
| return true; |
| } |
| |
| #if BUILDFLAG(IS_CHROMEOS_ASH) |
| // Make |app_id| the preferred app for handling |url|, without needing the user |
| // to choose the app through an intent picker. This function must be called |
| // after the corresponding intent filter has already been registered. |
| void AddDefaultPreferredApp(const std::string& app_id, |
| const GURL& url, |
| apps::mojom::AppService* app_service) { |
| auto intent_filter = apps_util::CreateIntentFilterForUrlScope(url); |
| app_service->AddPreferredApp( |
| apps::ConvertAppTypeToMojomAppType(GetWebAppType()), app_id, |
| std::move(intent_filter), |
| /*intent=*/nullptr, /*from_publisher=*/true); |
| } |
| #endif // BUILDFLAG(IS_CHROMEOS_ASH) |
| |
| } // namespace |
| |
| WebApps::WebApps(apps::AppServiceProxy* proxy) |
| : apps::AppPublisher(proxy), |
| profile_(proxy->profile()), |
| provider_(WebAppProvider::GetForLocalAppsUnchecked(profile_)), |
| app_service_(proxy->AppService().get()), |
| app_type_(GetWebAppType()), |
| #if BUILDFLAG(IS_CHROMEOS_ASH) |
| instance_registry_(&proxy->InstanceRegistry()), |
| #endif |
| publisher_helper_(profile_, |
| provider_, |
| app_type_, |
| this, |
| ShouldObserveMediaRequests()) { |
| Initialize(proxy->AppService()); |
| } |
| |
| WebApps::~WebApps() = default; |
| |
| void WebApps::Shutdown() { |
| if (provider_) { |
| publisher_helper().Shutdown(); |
| } |
| } |
| |
| const WebApp* WebApps::GetWebApp(const AppId& app_id) const { |
| DCHECK(provider_); |
| return provider_->registrar().GetAppById(app_id); |
| } |
| |
| bool WebApps::Accepts(const std::string& app_id) const { |
| return WebAppPublisherHelper::Accepts(app_id); |
| } |
| |
| void WebApps::Initialize( |
| const mojo::Remote<apps::mojom::AppService>& app_service) { |
| DCHECK(profile_); |
| if (!AreWebAppsEnabled(profile_)) { |
| return; |
| } |
| |
| DCHECK(provider_); |
| |
| PublisherBase::Initialize(app_service, |
| apps::ConvertAppTypeToMojomAppType(app_type_)); |
| |
| provider_->on_registry_ready().Post( |
| FROM_HERE, base::BindOnce(&WebApps::InitWebApps, AsWeakPtr())); |
| } |
| |
| void WebApps::LoadIcon(const std::string& app_id, |
| const apps::IconKey& icon_key, |
| apps::IconType icon_type, |
| int32_t size_hint_in_dip, |
| bool allow_placeholder_icon, |
| apps::LoadIconCallback callback) { |
| publisher_helper().LoadIcon(app_id, icon_type, size_hint_in_dip, |
| static_cast<IconEffects>(icon_key.icon_effects), |
| std::move(callback)); |
| } |
| |
| void WebApps::LaunchAppWithParams(apps::AppLaunchParams&& params, |
| apps::LaunchCallback callback) { |
| publisher_helper().LaunchAppWithParams(std::move(params)); |
| // TODO(crbug.com/1244506): Add launch return value. |
| std::move(callback).Run(apps::LaunchResult()); |
| } |
| |
| void WebApps::Connect( |
| mojo::PendingRemote<apps::mojom::Subscriber> subscriber_remote, |
| apps::mojom::ConnectOptionsPtr opts) { |
| DCHECK(provider_); |
| |
| provider_->on_registry_ready().Post( |
| FROM_HERE, base::BindOnce(&WebApps::StartPublishingWebApps, AsWeakPtr(), |
| std::move(subscriber_remote))); |
| } |
| |
| void WebApps::LoadIcon(const std::string& app_id, |
| apps::mojom::IconKeyPtr icon_key, |
| apps::mojom::IconType icon_type, |
| int32_t size_hint_in_dip, |
| bool allow_placeholder_icon, |
| LoadIconCallback callback) { |
| if (!icon_key) { |
| // On failure, we still run the callback, with an empty IconValue. |
| std::move(callback).Run(apps::mojom::IconValue::New()); |
| return; |
| } |
| |
| publisher_helper().LoadIcon( |
| app_id, apps::ConvertMojomIconTypeToIconType(icon_type), size_hint_in_dip, |
| static_cast<IconEffects>(icon_key->icon_effects), |
| apps::IconValueToMojomIconValueCallback(std::move(callback))); |
| } |
| |
| void WebApps::Launch(const std::string& app_id, |
| int32_t event_flags, |
| apps::mojom::LaunchSource launch_source, |
| apps::mojom::WindowInfoPtr window_info) { |
| publisher_helper().Launch(app_id, event_flags, launch_source, |
| std::move(window_info)); |
| } |
| |
| void WebApps::LaunchAppWithFiles(const std::string& app_id, |
| int32_t event_flags, |
| apps::mojom::LaunchSource launch_source, |
| apps::mojom::FilePathsPtr file_paths) { |
| publisher_helper().LaunchAppWithFiles(app_id, event_flags, launch_source, |
| std::move(file_paths)); |
| } |
| |
| void WebApps::LaunchAppWithIntent(const std::string& app_id, |
| int32_t event_flags, |
| apps::mojom::IntentPtr intent, |
| apps::mojom::LaunchSource launch_source, |
| apps::mojom::WindowInfoPtr window_info, |
| LaunchAppWithIntentCallback callback) { |
| publisher_helper().LaunchAppWithIntent(app_id, event_flags, std::move(intent), |
| launch_source, std::move(window_info), |
| std::move(callback)); |
| } |
| |
| void WebApps::SetPermission(const std::string& app_id, |
| apps::mojom::PermissionPtr permission) { |
| publisher_helper().SetPermission(app_id, std::move(permission)); |
| } |
| |
| void WebApps::OpenNativeSettings(const std::string& app_id) { |
| publisher_helper().OpenNativeSettings(app_id); |
| } |
| |
| void WebApps::SetWindowMode(const std::string& app_id, |
| apps::mojom::WindowMode window_mode) { |
| publisher_helper().SetWindowMode(app_id, window_mode); |
| } |
| |
| void WebApps::SetRunOnOsLoginMode( |
| const std::string& app_id, |
| apps::mojom::RunOnOsLoginMode run_on_os_login_mode) { |
| publisher_helper().SetRunOnOsLoginMode(app_id, run_on_os_login_mode); |
| } |
| |
| void WebApps::PublishWebApps(std::vector<apps::AppPtr> apps) { |
| #if BUILDFLAG(IS_CHROMEOS_ASH) |
| const WebApp* web_app = GetWebApp(ash::kChromeUITrustedProjectorSwaAppId); |
| if (web_app) { |
| AddDefaultPreferredApp(ash::kChromeUITrustedProjectorSwaAppId, |
| GURL(ash::kChromeUIUntrustedProjectorPwaUrl), |
| app_service_); |
| } |
| #endif // BUILDFLAG(IS_CHROMEOS_ASH) |
| |
| if (apps.empty()) { |
| return; |
| } |
| |
| std::vector<apps::mojom::AppPtr> mojom_apps; |
| for (apps::AppPtr& app : apps) { |
| mojom_apps.push_back(apps::ConvertAppToMojomApp(app)); |
| } |
| |
| apps::AppPublisher::Publish(std::move(apps), app_type_, |
| /*should_notify_initialized=*/false); |
| |
| const bool should_notify_initialized = false; |
| if (subscribers_.size() == 1) { |
| auto& subscriber = *subscribers_.begin(); |
| subscriber->OnApps(std::move(mojom_apps), |
| apps::ConvertAppTypeToMojomAppType(app_type()), |
| should_notify_initialized); |
| return; |
| } |
| for (auto& subscriber : subscribers_) { |
| std::vector<apps::mojom::AppPtr> cloned_apps; |
| for (const auto& app : mojom_apps) |
| cloned_apps.push_back(app.Clone()); |
| subscriber->OnApps(std::move(cloned_apps), |
| apps::ConvertAppTypeToMojomAppType(app_type()), |
| should_notify_initialized); |
| } |
| } |
| |
| void WebApps::PublishWebApp(apps::AppPtr app) { |
| #if BUILDFLAG(IS_CHROMEOS_ASH) |
| if (app->app_id == ash::kChromeUITrustedProjectorSwaAppId) { |
| // After OOBE, PublishWebApps() above could execute before the intent filter |
| // has been registered. Since we need to call AddDefaultPreferredApp() after |
| // the intent filter has been registered, we need this call for the OOBE |
| // case. |
| AddDefaultPreferredApp(ash::kChromeUITrustedProjectorSwaAppId, |
| GURL(ash::kChromeUIUntrustedProjectorPwaUrl), |
| app_service_); |
| } |
| #endif // BUILDFLAG(IS_CHROMEOS_ASH) |
| |
| auto mojom_app = apps::ConvertAppToMojomApp(app); |
| apps::AppPublisher::Publish(std::move(app)); |
| PublisherBase::Publish(std::move(mojom_app), subscribers_); |
| } |
| |
| void WebApps::ModifyWebAppCapabilityAccess( |
| const std::string& app_id, |
| absl::optional<bool> accessing_camera, |
| absl::optional<bool> accessing_microphone) { |
| ModifyCapabilityAccess(subscribers_, app_id, std::move(accessing_camera), |
| std::move(accessing_microphone)); |
| } |
| |
| std::vector<apps::AppPtr> WebApps::CreateWebApps() { |
| DCHECK(provider_); |
| |
| std::vector<apps::AppPtr> apps; |
| for (const WebApp& web_app : provider_->registrar().GetApps()) { |
| if (Accepts(web_app.app_id())) { |
| apps.push_back(publisher_helper().CreateWebApp(&web_app)); |
| } |
| } |
| return apps; |
| } |
| |
| void WebApps::ConvertWebApps(std::vector<apps::mojom::AppPtr>* apps_out) { |
| DCHECK(provider_); |
| if (publisher_helper().IsShuttingDown()) { |
| return; |
| } |
| |
| for (const WebApp& web_app : provider_->registrar().GetApps()) { |
| if (Accepts(web_app.app_id())) { |
| apps_out->push_back(publisher_helper().ConvertWebApp(&web_app)); |
| } |
| } |
| } |
| |
| void WebApps::InitWebApps() { |
| RegisterPublisher(app_type_); |
| |
| std::vector<apps::AppPtr> apps = CreateWebApps(); |
| apps::AppPublisher::Publish(std::move(apps), app_type_, |
| /*should_notify_initialized=*/true); |
| } |
| |
| void WebApps::StartPublishingWebApps( |
| mojo::PendingRemote<apps::mojom::Subscriber> subscriber_remote) { |
| std::vector<apps::mojom::AppPtr> apps; |
| ConvertWebApps(&apps); |
| |
| mojo::Remote<apps::mojom::Subscriber> subscriber( |
| std::move(subscriber_remote)); |
| subscriber->OnApps(std::move(apps), |
| apps::ConvertAppTypeToMojomAppType(app_type_), |
| true /* should_notify_initialized */); |
| |
| subscribers_.Add(std::move(subscriber)); |
| } |
| |
| #if BUILDFLAG(IS_CHROMEOS_ASH) |
| void WebApps::Uninstall(const std::string& app_id, |
| apps::mojom::UninstallSource uninstall_source, |
| bool clear_site_data, |
| bool report_abuse) { |
| const WebApp* web_app = GetWebApp(app_id); |
| if (!web_app) { |
| return; |
| } |
| |
| publisher_helper().UninstallWebApp(web_app, uninstall_source, clear_site_data, |
| report_abuse); |
| } |
| |
| void WebApps::PauseApp(const std::string& app_id) { |
| publisher_helper().PauseApp(app_id); |
| } |
| |
| void WebApps::UnpauseApp(const std::string& app_id) { |
| publisher_helper().UnpauseApp(app_id); |
| } |
| |
| void WebApps::StopApp(const std::string& app_id) { |
| publisher_helper().StopApp(app_id); |
| } |
| |
| void WebApps::GetMenuModel(const std::string& app_id, |
| apps::mojom::MenuType menu_type, |
| int64_t display_id, |
| GetMenuModelCallback callback) { |
| const auto* web_app = GetWebApp(app_id); |
| if (!web_app) { |
| std::move(callback).Run(apps::mojom::MenuItems::New()); |
| return; |
| } |
| |
| apps::mojom::MenuItemsPtr menu_items = apps::mojom::MenuItems::New(); |
| if (web_app->IsSystemApp()) { |
| DCHECK(web_app->client_data().system_web_app_data.has_value()); |
| SystemAppType swa_type = |
| web_app->client_data().system_web_app_data->system_app_type; |
| |
| auto* system_app = WebAppProvider::GetForSystemWebApps(profile()) |
| ->system_web_app_manager() |
| .GetSystemApp(swa_type); |
| if (system_app && system_app->ShouldShowNewWindowMenuOption()) { |
| apps::AddCommandItem(ash::MENU_OPEN_NEW, |
| IDS_APP_LIST_CONTEXT_MENU_NEW_WINDOW, &menu_items); |
| } |
| } else { |
| apps::CreateOpenNewSubmenu(menu_type, |
| publisher_helper().GetWindowMode(app_id) == |
| apps::mojom::WindowMode::kBrowser |
| ? IDS_APP_LIST_CONTEXT_MENU_NEW_TAB |
| : IDS_APP_LIST_CONTEXT_MENU_NEW_WINDOW, |
| &menu_items); |
| } |
| |
| if (app_id == crostini::kCrostiniTerminalSystemAppId) { |
| DCHECK(base::FeatureList::IsEnabled(chromeos::features::kTerminalSSH)); |
| crostini::AddTerminalMenuItems(profile_, &menu_items); |
| } |
| |
| if (menu_type == apps::mojom::MenuType::kShelf && |
| instance_registry_->ContainsAppId(app_id)) { |
| apps::AddCommandItem(ash::MENU_CLOSE, IDS_SHELF_CONTEXT_MENU_CLOSE, |
| &menu_items); |
| } |
| |
| if (web_app->CanUserUninstallWebApp()) { |
| apps::AddCommandItem(ash::UNINSTALL, IDS_APP_LIST_UNINSTALL_ITEM, |
| &menu_items); |
| } |
| |
| if (!web_app->IsSystemApp()) { |
| apps::AddCommandItem(ash::SHOW_APP_INFO, IDS_APP_CONTEXT_MENU_SHOW_INFO, |
| &menu_items); |
| } |
| |
| if (app_id == crostini::kCrostiniTerminalSystemAppId) { |
| DCHECK(base::FeatureList::IsEnabled(chromeos::features::kTerminalSSH)); |
| crostini::AddTerminalMenuShortcuts(profile_, ash::LAUNCH_APP_SHORTCUT_FIRST, |
| std::move(menu_items), |
| std::move(callback)); |
| } else { |
| GetAppShortcutMenuModel(app_id, std::move(menu_items), std::move(callback)); |
| } |
| } |
| |
| void WebApps::GetAppShortcutMenuModel(const std::string& app_id, |
| apps::mojom::MenuItemsPtr menu_items, |
| GetMenuModelCallback callback) { |
| const WebApp* web_app = GetWebApp(app_id); |
| if (!web_app) { |
| std::move(callback).Run(apps::mojom::MenuItems::New()); |
| return; |
| } |
| |
| // Read shortcuts menu item icons from disk, if any. |
| if (!web_app->shortcuts_menu_item_infos().empty()) { |
| provider()->icon_manager().ReadAllShortcutsMenuIcons( |
| app_id, base::BindOnce(&WebApps::OnShortcutsMenuIconsRead, |
| base::AsWeakPtr<WebApps>(this), app_id, |
| std::move(menu_items), std::move(callback))); |
| } else { |
| std::move(callback).Run(std::move(menu_items)); |
| } |
| } |
| |
| void WebApps::OnShortcutsMenuIconsRead( |
| const std::string& app_id, |
| apps::mojom::MenuItemsPtr menu_items, |
| GetMenuModelCallback callback, |
| ShortcutsMenuIconBitmaps shortcuts_menu_icon_bitmaps) { |
| const WebApp* web_app = GetWebApp(app_id); |
| if (!web_app) { |
| std::move(callback).Run(apps::mojom::MenuItems::New()); |
| return; |
| } |
| |
| apps::AddSeparator(ui::DOUBLE_SEPARATOR, &menu_items); |
| |
| size_t menu_item_index = 0; |
| |
| for (const WebAppShortcutsMenuItemInfo& menu_item_info : |
| web_app->shortcuts_menu_item_infos()) { |
| const std::map<SquareSizePx, SkBitmap>* menu_item_icon_bitmaps = nullptr; |
| if (menu_item_index < shortcuts_menu_icon_bitmaps.size()) { |
| // We prefer |MASKABLE| icons, but fall back to icons with purpose |ANY|. |
| menu_item_icon_bitmaps = |
| &shortcuts_menu_icon_bitmaps[menu_item_index].maskable; |
| if (menu_item_icon_bitmaps->empty()) { |
| menu_item_icon_bitmaps = |
| &shortcuts_menu_icon_bitmaps[menu_item_index].any; |
| } |
| } |
| |
| if (menu_item_index != 0) { |
| apps::AddSeparator(ui::PADDED_SEPARATOR, &menu_items); |
| } |
| |
| gfx::ImageSkia icon; |
| if (menu_item_icon_bitmaps) { |
| IconEffects icon_effects = IconEffects::kNone; |
| |
| // We apply masking to each shortcut icon, regardless if the purpose is |
| // |MASKABLE| or |ANY|. |
| icon_effects = apps::kCrOsStandardBackground | apps::kCrOsStandardMask; |
| |
| icon = ConvertSquareBitmapsToImageSkia( |
| *menu_item_icon_bitmaps, icon_effects, |
| /*size_hint_in_dip=*/apps::kAppShortcutIconSizeDip); |
| } |
| |
| // Uses integer |command_id| to store menu item index. |
| const int command_id = ash::LAUNCH_APP_SHORTCUT_FIRST + menu_item_index; |
| |
| const std::string label = base::UTF16ToUTF8(menu_item_info.name); |
| std::string shortcut_id = publisher_helper().GenerateShortcutId(); |
| publisher_helper().StoreShortcutId(shortcut_id, menu_item_info); |
| |
| apps::AddShortcutCommandItem(command_id, shortcut_id, label, icon, |
| &menu_items); |
| |
| ++menu_item_index; |
| } |
| |
| std::move(callback).Run(std::move(menu_items)); |
| } |
| |
| void WebApps::ExecuteContextMenuCommand(const std::string& app_id, |
| int command_id, |
| const std::string& shortcut_id, |
| int64_t display_id) { |
| if (app_id == crostini::kCrostiniTerminalSystemAppId) { |
| DCHECK(base::FeatureList::IsEnabled(chromeos::features::kTerminalSSH)); |
| if (crostini::ExecuteTerminalMenuShortcutCommand(profile_, shortcut_id, |
| display_id)) { |
| return; |
| } |
| } |
| publisher_helper().ExecuteContextMenuCommand(app_id, shortcut_id, display_id); |
| } |
| |
| #endif |
| |
| } // namespace web_app |