| // Copyright 2024 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/webapps/webapps_client_desktop.h" |
| |
| #include "base/check_is_test.h" |
| #include "base/no_destructor.h" |
| #include "build/build_config.h" |
| #include "chrome/browser/banners/app_banner_manager_desktop.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/segmentation_platform/segmentation_platform_service_factory.h" |
| #include "chrome/browser/user_education/user_education_service.h" |
| #include "chrome/browser/user_education/user_education_service_factory.h" |
| #include "chrome/browser/web_applications/visited_manifest_manager.h" |
| #include "chrome/browser/web_applications/web_app_filter.h" |
| #include "chrome/browser/web_applications/web_app_helpers.h" |
| #include "chrome/browser/web_applications/web_app_pref_guardrails.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_tab_helper.h" |
| #include "chrome/common/url_constants.h" |
| #include "components/infobars/content/content_infobar_manager.h" |
| #include "components/security_state/content/security_state_tab_helper.h" |
| #include "components/webapps/browser/features.h" |
| #include "components/webapps/browser/installable/installable_metrics.h" |
| #include "content/public/browser/web_contents.h" |
| #include "url/origin.h" |
| |
| namespace webapps { |
| |
| namespace { |
| bool CheckNewWebAppConflictsWithExistingInstallation( |
| content::BrowserContext* browser_context, |
| const GURL& start_url, |
| const ManifestId& manifest_id) { |
| CHECK(browser_context); |
| |
| // We prompt the user to re-install if the site wants to be in a |
| // standalone window but the user has opted for opening in browser tab. This |
| // is to support the situation where a site is not a PWA, users have installed |
| // it via Create Shortcut action, the site becomes a standalone PWA later and |
| // we want to prompt them to "install" the new PWA experience. |
| // TODO(crbug.com/40180519): Showing an install button when it's already |
| // installed is confusing. Perhaps different UX would be best. |
| |
| Profile* profile = Profile::FromBrowserContext(browser_context); |
| web_app::WebAppProvider* provider = |
| web_app::WebAppProvider::GetForWebApps(profile); |
| |
| // We can install if it's not installed, or this is crafted app and already |
| // installed but opens in a tab. |
| std::optional<web_app::mojom::UserDisplayMode> user_display_mode = |
| provider->registrar_unsafe().GetAppUserDisplayMode( |
| web_app::GenerateAppIdFromManifestId(manifest_id)); |
| if (user_display_mode == web_app::mojom::UserDisplayMode::kBrowser) { |
| return false; |
| } |
| |
| // If there is an existing crafted or DIY app that has the same manifest_id, |
| // do not promote installation. |
| if (provider->registrar_unsafe().IsInstallState( |
| web_app::GenerateAppIdFromManifestId(manifest_id), |
| {web_app::proto::InstallState::INSTALLED_WITH_OS_INTEGRATION, |
| web_app::proto::InstallState::INSTALLED_WITHOUT_OS_INTEGRATION})) { |
| return true; |
| } |
| |
| // We cannot install if we are in scope of an installed crafted app, no matter |
| // the user display type. |
| std::optional<AppId> non_diy_app_id = |
| provider->registrar_unsafe().FindBestAppWithUrlInScope( |
| start_url, web_app::WebAppFilter::IsCraftedApp()); |
| |
| if (non_diy_app_id) { |
| return true; |
| } |
| |
| // Otherwise there is no app installed here, or there is a DIY app that |
| // controls this URL but that's fine. |
| return false; |
| } |
| } // namespace |
| |
| // static |
| void WebappsClientDesktop::CreateSingleton() { |
| static base::NoDestructor<WebappsClientDesktop> instance; |
| instance.get(); |
| } |
| |
| WebappInstallSource WebappsClientDesktop::GetInstallSource( |
| content::WebContents* web_contents, |
| InstallTrigger trigger) { |
| switch (trigger) { |
| case InstallTrigger::AMBIENT_BADGE: |
| return WebappInstallSource::AMBIENT_BADGE_BROWSER_TAB; |
| case InstallTrigger::API: |
| return WebappInstallSource::API_BROWSER_TAB; |
| case InstallTrigger::AUTOMATIC_PROMPT: |
| return WebappInstallSource::AUTOMATIC_PROMPT_BROWSER_TAB; |
| case InstallTrigger::MENU: |
| return WebappInstallSource::MENU_BROWSER_TAB; |
| case InstallTrigger::CREATE_SHORTCUT: |
| return WebappInstallSource::MENU_CREATE_SHORTCUT; |
| } |
| } |
| |
| AppBannerManager* WebappsClientDesktop::GetAppBannerManager( |
| content::WebContents* web_contents) { |
| CHECK(web_contents); |
| return AppBannerManagerDesktop::FromWebContents(web_contents); |
| } |
| |
| void WebappsClientDesktop::DoesNewWebAppConflictWithExistingInstallation( |
| content::BrowserContext* browser_context, |
| const GURL& start_url, |
| const ManifestId& manifest_id, |
| WebAppInstallationConflictCallback callback) const { |
| std::move(callback).Run( |
| /* does_conflict= */ CheckNewWebAppConflictsWithExistingInstallation( |
| browser_context, start_url, manifest_id)); |
| } |
| |
| bool WebappsClientDesktop::IsInAppBrowsingContext( |
| content::WebContents* web_contents) const { |
| CHECK(web_contents); |
| web_app::WebAppTabHelper* tab_helper = |
| web_app::WebAppTabHelper::FromWebContents(web_contents); |
| return tab_helper && tab_helper->is_in_app_window(); |
| } |
| |
| bool WebappsClientDesktop::IsAppPartiallyInstalledForSiteUrl( |
| content::BrowserContext* browser_context, |
| const GURL& site_url) const { |
| CHECK(browser_context); |
| return web_app::IsNonLocallyInstalledAppWithUrlInScope( |
| Profile::FromBrowserContext(browser_context), site_url); |
| } |
| |
| bool WebappsClientDesktop::IsAppFullyInstalledForSiteUrl( |
| content::BrowserContext* browser_context, |
| const GURL& site_url) const { |
| CHECK(browser_context); |
| return web_app::FindInstalledAppWithUrlInScope( |
| Profile::FromBrowserContext(browser_context), site_url) |
| .has_value(); |
| } |
| |
| bool WebappsClientDesktop::IsUrlControlledBySeenManifest( |
| content::BrowserContext* browsing_context, |
| const GURL& site_url) const { |
| auto* provider = web_app::WebAppProvider::GetForWebApps( |
| Profile::FromBrowserContext(browsing_context)); |
| return provider && provider->is_registry_ready() |
| ? provider->visited_manifest_manager() |
| .IsUrlControlledBySeenManifest(site_url) |
| : false; |
| } |
| |
| void WebappsClientDesktop::OnManifestSeen( |
| content::BrowserContext* browsing_context, |
| const blink::mojom::Manifest& manifest) const { |
| auto* provider = web_app::WebAppProvider::GetForWebApps( |
| Profile::FromBrowserContext(browsing_context)); |
| if (provider && provider->is_registry_ready()) { |
| provider->visited_manifest_manager().OnManifestSeen(manifest); |
| } |
| } |
| |
| void WebappsClientDesktop::SaveInstallationDismissedForMl( |
| content::BrowserContext* browser_context, |
| const GURL& manifest_id) const { |
| CHECK(browser_context); |
| Profile* profile = Profile::FromBrowserContext(browser_context); |
| CHECK(profile); |
| web_app::WebAppPrefGuardrails::GetForMlInstallPrompt(profile->GetPrefs()) |
| .RecordDismiss(web_app::GenerateAppIdFromManifestId(manifest_id), |
| base::Time::Now()); |
| } |
| |
| void WebappsClientDesktop::SaveInstallationIgnoredForMl( |
| content::BrowserContext* browser_context, |
| const GURL& manifest_id) const { |
| CHECK(browser_context); |
| Profile* profile = Profile::FromBrowserContext(browser_context); |
| CHECK(profile); |
| web_app::WebAppPrefGuardrails::GetForMlInstallPrompt(profile->GetPrefs()) |
| .RecordIgnore(web_app::GenerateAppIdFromManifestId(manifest_id), |
| base::Time::Now()); |
| } |
| |
| void WebappsClientDesktop::SaveInstallationAcceptedForMl( |
| content::BrowserContext* browser_context, |
| const GURL& manifest_id) const { |
| CHECK(browser_context); |
| Profile* profile = Profile::FromBrowserContext(browser_context); |
| CHECK(profile); |
| web_app::WebAppPrefGuardrails::GetForMlInstallPrompt(profile->GetPrefs()) |
| .RecordAccept(web_app::GenerateAppIdFromManifestId(manifest_id)); |
| } |
| |
| bool WebappsClientDesktop::IsMlPromotionBlockedByHistoryGuardrail( |
| content::BrowserContext* browser_context, |
| const GURL& manifest_id) const { |
| CHECK(browser_context); |
| Profile* profile = Profile::FromBrowserContext(browser_context); |
| CHECK(profile); |
| |
| if (!manifest_id.is_empty() && |
| web_app::WebAppPrefGuardrails::GetForMlInstallPrompt(profile->GetPrefs()) |
| .IsBlockedByGuardrails( |
| web_app::GenerateAppIdFromManifestId(manifest_id))) { |
| return true; |
| } |
| |
| // Do not copy this. This is a temporary hack to help not bother users before |
| // the ML triggering is moved to triggering IPH for a new install entry point. |
| // crbug.com/356401517 |
| UserEducationService* const user_education_service = |
| UserEducationServiceFactory::GetForBrowserContext(profile); |
| user_education::FeaturePromoResult synthetic_result = |
| user_education_service->feature_promo_session_policy().CanShowPromo( |
| {.weight = |
| user_education::FeaturePromoSessionPolicy::PromoWeight::kHeavy, |
| .priority = |
| user_education::FeaturePromoSessionPolicy::PromoPriority::kLow}, |
| /*currently_showing=*/std::nullopt); |
| return synthetic_result.failure() == |
| user_education::FeaturePromoResult::kBlockedByGracePeriod; |
| } |
| |
| segmentation_platform::SegmentationPlatformService* |
| WebappsClientDesktop::GetSegmentationPlatformService( |
| content::BrowserContext* browser_context) const { |
| if (segmentation_platform_for_testing()) { // IN-TEST |
| CHECK_IS_TEST(); |
| return segmentation_platform_for_testing(); // IN-TEST |
| } |
| CHECK(browser_context); |
| return segmentation_platform::SegmentationPlatformServiceFactory:: |
| GetForProfile(Profile::FromBrowserContext(browser_context)); |
| } |
| |
| std::optional<webapps::AppId> WebappsClientDesktop::GetAppIdForWebContents( |
| content::WebContents* web_contents) { |
| CHECK(web_contents); |
| web_app::WebAppTabHelper* helper = |
| web_app::WebAppTabHelper::FromWebContents(web_contents); |
| if (!helper) { |
| return std::nullopt; |
| } |
| return helper->app_id(); |
| } |
| |
| } // namespace webapps |