| // 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/apps/app_service/web_apps_chromeos.h" |
| |
| #include <utility> |
| #include <vector> |
| |
| #include "ash/public/cpp/app_list/app_list_metrics.h" |
| #include "ash/public/cpp/app_menu_constants.h" |
| #include "base/bind.h" |
| #include "base/callback.h" |
| #include "base/callback_helpers.h" |
| #include "base/memory/weak_ptr.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/optional.h" |
| #include "base/stl_util.h" |
| #include "base/strings/string16.h" |
| #include "base/strings/stringprintf.h" |
| #include "chrome/browser/apps/app_service/app_launch_params.h" |
| #include "chrome/browser/apps/app_service/app_service_metrics.h" |
| #include "chrome/browser/apps/app_service/launch_utils.h" |
| #include "chrome/browser/apps/app_service/menu_util.h" |
| #include "chrome/browser/badging/badge_manager_factory.h" |
| #include "chrome/browser/chromeos/arc/arc_util.h" |
| #include "chrome/browser/chromeos/arc/arc_web_contents_data.h" |
| #include "chrome/browser/chromeos/crostini/crostini_util.h" |
| #include "chrome/browser/chromeos/extensions/gfx_utils.h" |
| #include "chrome/browser/notifications/notification_display_service_factory.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/ui/app_list/arc/arc_app_utils.h" |
| #include "chrome/browser/ui/browser.h" |
| #include "chrome/browser/ui/browser_finder.h" |
| #include "chrome/browser/ui/browser_list.h" |
| #include "chrome/browser/ui/web_applications/web_app_dialog_manager.h" |
| #include "chrome/browser/ui/web_applications/web_app_launch_manager.h" |
| #include "chrome/browser/ui/web_applications/web_app_ui_manager_impl.h" |
| #include "chrome/browser/web_applications/components/install_finalizer.h" |
| #include "chrome/browser/web_applications/components/web_app_constants.h" |
| #include "chrome/browser/web_applications/components/web_app_helpers.h" |
| #include "chrome/browser/web_applications/components/web_app_id.h" |
| #include "chrome/browser/web_applications/components/web_app_utils.h" |
| #include "chrome/browser/web_applications/system_web_app_manager.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_sync_bridge.h" |
| #include "chrome/common/chrome_features.h" |
| #include "chrome/common/chrome_switches.h" |
| #include "chrome/grit/generated_resources.h" |
| #include "components/arc/arc_service_manager.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/full_restore/app_launch_info.h" |
| #include "components/full_restore/full_restore_utils.h" |
| #include "components/services/app_service/public/cpp/intent_filter_util.h" |
| #include "components/sessions/core/session_id.h" |
| #include "content/public/browser/clear_site_data_utils.h" |
| #include "content/public/browser/web_contents.h" |
| #include "extensions/common/constants.h" |
| #include "ui/message_center/public/cpp/notification.h" |
| #include "url/origin.h" |
| |
| namespace { |
| |
| std::string GetDesktopPWAsAttentionBadgingFlag() { |
| const base::CommandLine* cmdline = base::CommandLine::ForCurrentProcess(); |
| if (cmdline->HasSwitch(switches::kDesktopPWAsAttentionBadgingCrOS)) { |
| return cmdline->GetSwitchValueASCII( |
| switches::kDesktopPWAsAttentionBadgingCrOS); |
| } |
| return ""; |
| } |
| |
| } // namespace |
| |
| namespace apps { |
| |
| WebAppsChromeOs::BadgeManagerDelegate::BadgeManagerDelegate( |
| const base::WeakPtr<WebAppsChromeOs>& web_apps_chrome_os) |
| : badging::BadgeManagerDelegate(web_apps_chrome_os->profile(), |
| web_apps_chrome_os->badge_manager_), |
| web_apps_chrome_os_(web_apps_chrome_os) {} |
| |
| WebAppsChromeOs::BadgeManagerDelegate::~BadgeManagerDelegate() = default; |
| |
| void WebAppsChromeOs::BadgeManagerDelegate::OnAppBadgeUpdated( |
| const web_app::AppId& app_id) { |
| if (!web_apps_chrome_os_) { |
| return; |
| } |
| apps::mojom::AppPtr app = |
| web_apps_chrome_os_->app_notifications_.GetAppWithHasBadgeStatus( |
| apps::mojom::AppType::kWeb, app_id); |
| app->has_badge = web_apps_chrome_os_->ShouldShowBadge(app_id, app->has_badge); |
| web_apps_chrome_os_->Publish(std::move(app), |
| web_apps_chrome_os_->subscribers()); |
| } |
| |
| WebAppsChromeOs::WebAppsChromeOs( |
| const mojo::Remote<apps::mojom::AppService>& app_service, |
| Profile* profile, |
| apps::InstanceRegistry* instance_registry) |
| : WebAppsBase(app_service, profile), instance_registry_(instance_registry) { |
| DCHECK(instance_registry_); |
| Initialize(); |
| badge_manager_ = badging::BadgeManagerFactory::GetForProfile(profile); |
| // badge_manager_ is nullptr in guest and incognito profiles. |
| if (badge_manager_) { |
| badge_manager_->SetDelegate( |
| std::make_unique<apps::WebAppsChromeOs::BadgeManagerDelegate>( |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| } |
| |
| WebAppsChromeOs::~WebAppsChromeOs() { |
| // In unit tests, AppServiceProxy might be ReInitializeForTesting, so |
| // WebApps might be destroyed without calling Shutdown, so arc_prefs_ |
| // needs to be removed from observer in the destructor function. |
| if (arc_prefs_) { |
| arc_prefs_->RemoveObserver(this); |
| arc_prefs_ = nullptr; |
| } |
| } |
| |
| void WebAppsChromeOs::Shutdown() { |
| if (arc_prefs_) { |
| arc_prefs_->RemoveObserver(this); |
| arc_prefs_ = nullptr; |
| } |
| |
| WebAppsBase::Shutdown(); |
| } |
| |
| void WebAppsChromeOs::ObserveArc() { |
| // Observe the ARC apps to set the badge on the equivalent web app's icon. |
| if (arc_prefs_) { |
| arc_prefs_->RemoveObserver(this); |
| } |
| |
| arc_prefs_ = ArcAppListPrefs::Get(profile()); |
| if (arc_prefs_) { |
| arc_prefs_->AddObserver(this); |
| } |
| } |
| |
| void WebAppsChromeOs::Initialize() { |
| DCHECK(profile()); |
| if (!web_app::AreWebAppsEnabled(profile())) { |
| return; |
| } |
| |
| notification_display_service_.Add( |
| NotificationDisplayServiceFactory::GetForProfile(profile())); |
| } |
| |
| void WebAppsChromeOs::LaunchAppWithIntent( |
| const std::string& app_id, |
| int32_t event_flags, |
| apps::mojom::IntentPtr intent, |
| apps::mojom::LaunchSource launch_source, |
| int64_t display_id) { |
| auto* tab = WebAppsChromeOs::LaunchAppWithIntentImpl( |
| app_id, event_flags, std::move(intent), launch_source, display_id); |
| |
| if (launch_source != apps::mojom::LaunchSource::kFromArc || !tab) { |
| return; |
| } |
| |
| // Add a flag to remember this tab originated in the ARC context. |
| tab->SetUserData(&arc::ArcWebContentsData::kArcTransitionFlag, |
| std::make_unique<arc::ArcWebContentsData>()); |
| } |
| |
| void WebAppsChromeOs::Uninstall(const std::string& app_id, |
| apps::mojom::UninstallSource uninstall_source, |
| bool clear_site_data, |
| bool report_abuse) { |
| const web_app::WebApp* web_app = GetWebApp(app_id); |
| if (!web_app) { |
| return; |
| } |
| |
| DCHECK(provider()); |
| DCHECK(provider()->install_finalizer().CanUserUninstallExternalApp(app_id)); |
| |
| auto origin = url::Origin::Create(web_app->start_url()); |
| // TODO(crbug.com/1104696): Update web_app::InstallFinalizer to accommodate |
| // when install_source == apps::mojom::UninstallSource::kMigration. |
| provider()->install_finalizer().UninstallExternalAppByUser(app_id, |
| base::DoNothing()); |
| web_app = nullptr; |
| |
| if (!clear_site_data) { |
| // TODO(loyso): Add UMA_HISTOGRAM_ENUMERATION here. |
| return; |
| } |
| |
| // TODO(loyso): Add UMA_HISTOGRAM_ENUMERATION here. |
| constexpr bool kClearCookies = true; |
| constexpr bool kClearStorage = true; |
| constexpr bool kClearCache = true; |
| constexpr bool kAvoidClosingConnections = false; |
| content::ClearSiteData(base::BindRepeating( |
| [](content::BrowserContext* browser_context) { |
| return browser_context; |
| }, |
| base::Unretained(profile())), |
| origin, kClearCookies, kClearStorage, kClearCache, |
| kAvoidClosingConnections, base::DoNothing()); |
| } |
| |
| void WebAppsChromeOs::PauseApp(const std::string& app_id) { |
| if (paused_apps_.MaybeAddApp(app_id)) { |
| SetIconEffect(app_id); |
| } |
| |
| constexpr bool kPaused = true; |
| Publish(paused_apps_.GetAppWithPauseStatus(apps::mojom::AppType::kWeb, app_id, |
| kPaused), |
| subscribers()); |
| |
| for (auto* browser : *BrowserList::GetInstance()) { |
| if (!browser->is_type_app()) { |
| continue; |
| } |
| if (web_app::GetAppIdFromApplicationName(browser->app_name()) == app_id) { |
| browser->tab_strip_model()->CloseAllTabs(); |
| } |
| } |
| } |
| |
| void WebAppsChromeOs::UnpauseApps(const std::string& app_id) { |
| if (paused_apps_.MaybeRemoveApp(app_id)) { |
| SetIconEffect(app_id); |
| } |
| |
| constexpr bool kPaused = false; |
| Publish(paused_apps_.GetAppWithPauseStatus(apps::mojom::AppType::kWeb, app_id, |
| kPaused), |
| subscribers()); |
| } |
| |
| void WebAppsChromeOs::GetMenuModel(const std::string& app_id, |
| apps::mojom::MenuType menu_type, |
| int64_t display_id, |
| GetMenuModelCallback callback) { |
| const web_app::WebApp* web_app = GetWebApp(app_id); |
| if (!web_app) { |
| std::move(callback).Run(apps::mojom::MenuItems::New()); |
| return; |
| } |
| |
| const bool is_system_web_app = web_app->IsSystemApp(); |
| apps::mojom::MenuItemsPtr menu_items = apps::mojom::MenuItems::New(); |
| |
| if (!is_system_web_app) { |
| CreateOpenNewSubmenu( |
| menu_type, |
| web_app->user_display_mode() == web_app::DisplayMode::kStandalone |
| ? IDS_APP_LIST_CONTEXT_MENU_NEW_WINDOW |
| : IDS_APP_LIST_CONTEXT_MENU_NEW_TAB, |
| &menu_items); |
| } |
| |
| if (menu_type == apps::mojom::MenuType::kShelf && |
| !instance_registry_->GetWindows(app_id).empty()) { |
| AddCommandItem(ash::MENU_CLOSE, IDS_SHELF_CONTEXT_MENU_CLOSE, &menu_items); |
| } |
| |
| if (provider()->install_finalizer().CanUserUninstallExternalApp(app_id)) { |
| 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); |
| } |
| |
| std::move(callback).Run(std::move(menu_items)); |
| } |
| |
| void WebAppsChromeOs::ExecuteContextMenuCommand(const std::string& app_id, |
| int command_id, |
| const std::string& shortcut_id, |
| int64_t display_id) { |
| // TODO(crbug.com/1129721) Implement it for shortcut menu in web apps. |
| NOTIMPLEMENTED(); |
| } |
| |
| void WebAppsChromeOs::OnWebAppWillBeUninstalled(const web_app::AppId& app_id) { |
| const web_app::WebApp* web_app = GetWebApp(app_id); |
| if (!web_app || !Accepts(app_id)) { |
| return; |
| } |
| |
| app_notifications_.RemoveNotificationsForApp(app_id); |
| paused_apps_.MaybeRemoveApp(app_id); |
| |
| WebAppsBase::OnWebAppWillBeUninstalled(app_id); |
| } |
| |
| // If is_disabled is set, the app backed by |app_id| is published with readiness |
| // kDisabledByPolicy, otherwise it's published with readiness kReady. |
| void WebAppsChromeOs::OnWebAppDisabledStateChanged(const web_app::AppId& app_id, |
| bool is_disabled) { |
| const web_app::WebApp* web_app = GetWebApp(app_id); |
| if (!web_app || !Accepts(app_id)) { |
| return; |
| } |
| // Sometimes OnWebAppDisabledStateChanged is called but |
| // WebApp::chromos_data().is_disabled isn't updated yet, that's why here we |
| // depend only on |is_disabled|. |
| apps::mojom::AppPtr app = WebAppsBase::ConvertImpl( |
| web_app, is_disabled ? apps::mojom::Readiness::kDisabledByPolicy |
| : apps::mojom::Readiness::kReady); |
| app->icon_key = icon_key_factory().MakeIconKey( |
| GetIconEffects(web_app, paused_apps_.IsPaused(app_id), is_disabled)); |
| Publish(std::move(app), subscribers()); |
| } |
| |
| void WebAppsChromeOs::OnPackageInstalled( |
| const arc::mojom::ArcPackageInfo& package_info) { |
| ApplyChromeBadge(package_info.package_name); |
| } |
| |
| void WebAppsChromeOs::OnPackageRemoved(const std::string& package_name, |
| bool uninstalled) { |
| ApplyChromeBadge(package_name); |
| } |
| |
| void WebAppsChromeOs::OnPackageListInitialRefreshed() { |
| if (!arc_prefs_) { |
| return; |
| } |
| |
| for (const auto& app_name : arc_prefs_->GetPackagesFromPrefs()) { |
| ApplyChromeBadge(app_name); |
| } |
| } |
| |
| void WebAppsChromeOs::OnArcAppListPrefsDestroyed() { |
| arc_prefs_ = nullptr; |
| } |
| |
| void WebAppsChromeOs::OnNotificationDisplayed( |
| const message_center::Notification& notification, |
| const NotificationCommon::Metadata* const metadata) { |
| if (notification.notifier_id().type != |
| message_center::NotifierType::WEB_PAGE) { |
| return; |
| } |
| MaybeAddWebPageNotifications(notification, metadata); |
| } |
| |
| void WebAppsChromeOs::OnNotificationClosed(const std::string& notification_id) { |
| auto app_ids = app_notifications_.GetAppIdsForNotification(notification_id); |
| if (app_ids.empty()) { |
| return; |
| } |
| |
| app_notifications_.RemoveNotification(notification_id); |
| |
| for (const auto& app_id : app_ids) { |
| apps::mojom::AppPtr app = app_notifications_.GetAppWithHasBadgeStatus( |
| apps::mojom::AppType::kWeb, app_id); |
| app->has_badge = ShouldShowBadge(app_id, app->has_badge); |
| Publish(std::move(app), subscribers()); |
| } |
| } |
| |
| void WebAppsChromeOs::OnNotificationDisplayServiceDestroyed( |
| NotificationDisplayService* service) { |
| notification_display_service_.Remove(service); |
| } |
| |
| bool WebAppsChromeOs::MaybeAddNotification(const std::string& app_id, |
| const std::string& notification_id) { |
| const web_app::WebApp* web_app = GetWebApp(app_id); |
| if (!web_app || !Accepts(app_id)) { |
| return false; |
| } |
| |
| app_notifications_.AddNotification(app_id, notification_id); |
| apps::mojom::AppPtr app = app_notifications_.GetAppWithHasBadgeStatus( |
| apps::mojom::AppType::kWeb, app_id); |
| app->has_badge = ShouldShowBadge(app_id, app->has_badge); |
| Publish(std::move(app), subscribers()); |
| return true; |
| } |
| |
| void WebAppsChromeOs::MaybeAddWebPageNotifications( |
| const message_center::Notification& notification, |
| const NotificationCommon::Metadata* const metadata) { |
| const GURL& url = |
| metadata |
| ? PersistentNotificationMetadata::From(metadata)->service_worker_scope |
| : notification.origin_url(); |
| |
| if (metadata) { |
| // For persistent notifications, find the web app with the scope url. |
| base::Optional<web_app::AppId> app_id = |
| web_app::FindInstalledAppWithUrlInScope(profile(), url, |
| /*window_only=*/false); |
| if (app_id.has_value()) { |
| MaybeAddNotification(app_id.value(), notification.id()); |
| } |
| } else { |
| // For non-persistent notifications, find all web apps that are installed |
| // under the origin url. |
| DCHECK(provider()); |
| auto app_ids = provider()->registrar().FindAppsInScope(url); |
| int count = 0; |
| for (const auto& app_id : app_ids) { |
| if (MaybeAddNotification(app_id, notification.id())) { |
| ++count; |
| } |
| } |
| RecordAppsPerNotification(count); |
| } |
| } |
| |
| apps::mojom::AppPtr WebAppsChromeOs::Convert(const web_app::WebApp* web_app, |
| apps::mojom::Readiness readiness) { |
| DCHECK(web_app->chromeos_data().has_value()); |
| bool is_disabled = web_app->chromeos_data()->is_disabled; |
| apps::mojom::AppPtr app = WebAppsBase::ConvertImpl( |
| web_app, |
| is_disabled ? apps::mojom::Readiness::kDisabledByPolicy : readiness); |
| |
| bool paused = paused_apps_.IsPaused(web_app->app_id()); |
| app->icon_key = icon_key_factory().MakeIconKey( |
| GetIconEffects(web_app, paused, is_disabled)); |
| |
| apps::mojom::OptionalBool has_notification = |
| app_notifications_.HasNotification(web_app->app_id()) |
| ? apps::mojom::OptionalBool::kTrue |
| : apps::mojom::OptionalBool::kFalse; |
| app->has_badge = ShouldShowBadge(web_app->app_id(), has_notification); |
| app->paused = paused ? apps::mojom::OptionalBool::kTrue |
| : apps::mojom::OptionalBool::kFalse; |
| return app; |
| } |
| |
| IconEffects WebAppsChromeOs::GetIconEffects(const web_app::WebApp* web_app, |
| bool paused, |
| bool is_disabled) { |
| IconEffects icon_effects = IconEffects::kNone; |
| if (base::FeatureList::IsEnabled(features::kAppServiceAdaptiveIcon)) { |
| icon_effects |= web_app->is_generated_icon() |
| ? IconEffects::kCrOsStandardMask |
| : IconEffects::kCrOsStandardIcon; |
| } else { |
| icon_effects |= IconEffects::kResizeAndPad; |
| } |
| if (extensions::util::ShouldApplyChromeBadgeToWebApp(profile(), |
| web_app->app_id())) { |
| icon_effects |= IconEffects::kChromeBadge; |
| } |
| icon_effects |= WebAppsBase::GetIconEffects(web_app); |
| if (paused) { |
| icon_effects |= IconEffects::kPaused; |
| } |
| |
| if (is_disabled) { |
| icon_effects |= IconEffects::kBlocked; |
| } |
| |
| return icon_effects; |
| } |
| |
| void WebAppsChromeOs::ApplyChromeBadge(const std::string& package_name) { |
| const std::vector<std::string> app_ids = |
| extensions::util::GetEquivalentInstalledAppIds(package_name); |
| |
| for (auto& app_id : app_ids) { |
| if (GetWebApp(app_id)) { |
| SetIconEffect(app_id); |
| } |
| } |
| } |
| |
| void WebAppsChromeOs::SetIconEffect(const std::string& app_id) { |
| const web_app::WebApp* web_app = GetWebApp(app_id); |
| if (!web_app) { |
| return; |
| } |
| |
| apps::mojom::AppPtr app = apps::mojom::App::New(); |
| app->app_type = apps::mojom::AppType::kWeb; |
| app->app_id = app_id; |
| DCHECK(web_app->chromeos_data().has_value()); |
| app->icon_key = icon_key_factory().MakeIconKey( |
| GetIconEffects(web_app, paused_apps_.IsPaused(app_id), |
| web_app->chromeos_data()->is_disabled)); |
| Publish(std::move(app), subscribers()); |
| } |
| |
| content::WebContents* WebAppsChromeOs::LaunchAppWithParams( |
| AppLaunchParams params) { |
| AppLaunchParams params_for_restore(params.app_id, params.container, |
| params.disposition, params.display_id, |
| params.launch_files, params.intent); |
| |
| auto* web_contents = WebAppsBase::LaunchAppWithParams(std::move(params)); |
| |
| int session_id = GetSessionIdForRestoreFromWebContents( |
| params_for_restore.container, web_contents); |
| if (!SessionID::IsValidValue(session_id)) { |
| return web_contents; |
| } |
| |
| const web_app::WebApp* web_app = GetWebApp(params_for_restore.app_id); |
| std::unique_ptr<full_restore::AppLaunchInfo> launch_info; |
| if (web_app && web_app->IsSystemApp()) { |
| // Save all launch information for system web apps, because the browser |
| // session restore can't restore system web apps. |
| launch_info = std::make_unique<full_restore::AppLaunchInfo>( |
| params_for_restore.app_id, session_id, params_for_restore.container, |
| params_for_restore.disposition, params_for_restore.display_id, |
| std::move(params_for_restore.launch_files), |
| std::move(params_for_restore.intent)); |
| } else { |
| // If the app is not a system web app, the browser session restore can |
| // restore the app after reboot, so we don't need to save the launch |
| // parameters to launch the app after reboot. Only the browser session id is |
| // saved as the window id, to restore the window stack, snap, etc. The app |
| // id is modified as the Chrome browser id, so that it won't be launched |
| // after reboot. Also for web apps opened with tabs in one browser window, |
| // we don't need to save multiple records in the full restore data. |
| launch_info = std::make_unique<full_restore::AppLaunchInfo>( |
| extension_misc::kChromeAppId, session_id); |
| } |
| |
| full_restore::SaveAppLaunchInfo(profile()->GetPath(), std::move(launch_info)); |
| return web_contents; |
| } |
| |
| bool WebAppsChromeOs::Accepts(const std::string& app_id) { |
| // Crostini Terminal System App is handled by Crostini Apps. |
| return app_id != crostini::kCrostiniTerminalSystemAppId; |
| } |
| |
| apps::mojom::OptionalBool WebAppsChromeOs::ShouldShowBadge( |
| const std::string& app_id, |
| apps::mojom::OptionalBool has_notification) { |
| std::string flag = GetDesktopPWAsAttentionBadgingFlag(); |
| if (flag == switches::kDesktopPWAsAttentionBadgingCrOSApiOnly) { |
| // Show a badge based only on the Web Badging API. |
| return badge_manager_ && badge_manager_->GetBadgeValue(app_id).has_value() |
| ? apps::mojom::OptionalBool::kTrue |
| : apps::mojom::OptionalBool::kFalse; |
| } else if (flag == |
| switches::kDesktopPWAsAttentionBadgingCrOSNotificationsOnly) { |
| // Show a badge only if a notification is showing. |
| return has_notification; |
| } else { |
| // When the flag is not set or set to "api-and-notifications" we show a |
| // badge if either a notification is showing or the Web Badging API has a |
| // badge set. |
| return badge_manager_ && badge_manager_->GetBadgeValue(app_id).has_value() |
| ? apps::mojom::OptionalBool::kTrue |
| : has_notification; |
| } |
| } |
| } // namespace apps |