|  | // Copyright 2015 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/ui/web_applications/web_app_browser_controller.h" | 
|  |  | 
|  | #include "ash/constants/web_app_id_constants.h" | 
|  | #include "base/callback_list.h" | 
|  | #include "base/check_is_test.h" | 
|  | #include "base/containers/flat_set.h" | 
|  | #include "base/feature_list.h" | 
|  | #include "base/functional/callback.h" | 
|  | #include "base/functional/callback_helpers.h" | 
|  | #include "base/memory/raw_ptr.h" | 
|  | #include "base/metrics/histogram_functions.h" | 
|  | #include "base/strings/strcat.h" | 
|  | #include "base/strings/string_util.h" | 
|  | #include "base/strings/utf_string_conversions.h" | 
|  | #include "base/time/time.h" | 
|  | #include "build/build_config.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/ash/browser_delegate/browser_controller.h" | 
|  | #include "chrome/browser/profiles/profile.h" | 
|  | #include "chrome/browser/ui/browser.h" | 
|  | #include "chrome/browser/ui/browser_commands.h" | 
|  | #include "chrome/browser/ui/browser_window.h" | 
|  | #include "chrome/browser/ui/browser_window/public/browser_window_interface.h" | 
|  | #include "chrome/browser/ui/tabs/tab_menu_model_factory.h" | 
|  | #include "chrome/browser/ui/web_applications/web_app_dialogs.h" | 
|  | #include "chrome/browser/ui/web_applications/web_app_launch_utils.h" | 
|  | #include "chrome/browser/ui/web_applications/web_app_tabbed_utils.h" | 
|  | #include "chrome/browser/web_applications/locks/app_lock.h" | 
|  | #include "chrome/browser/web_applications/proto/web_app.pb.h" | 
|  | #include "chrome/browser/web_applications/ui_manager/update_dialog_types.h" | 
|  | #include "chrome/browser/web_applications/web_app_command_scheduler.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_icon_manager.h" | 
|  | #include "chrome/browser/web_applications/web_app_install_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_scope.h" | 
|  | #include "chrome/browser/web_applications/web_app_sync_bridge.h" | 
|  | #include "chrome/browser/web_applications/web_app_tab_helper.h" | 
|  | #include "chrome/browser/web_applications/web_app_ui_manager.h" | 
|  | #include "chrome/grit/generated_resources.h" | 
|  | #include "components/password_manager/core/common/password_manager_features.h" | 
|  | #include "components/services/app_service/public/cpp/app_registry_cache.h" | 
|  | #include "components/services/app_service/public/cpp/app_types.h" | 
|  | #include "components/webapps/browser/installable/installable_metrics.h" | 
|  | #include "components/webapps/common/web_app_id.h" | 
|  | #include "content/public/browser/site_isolation_policy.h" | 
|  | #include "content/public/browser/web_contents.h" | 
|  | #include "content/public/common/content_features.h" | 
|  | #include "third_party/blink/public/common/features.h" | 
|  | #include "ui/base/l10n/l10n_util.h" | 
|  | #include "ui/gfx/favicon_size.h" | 
|  | #include "ui/gfx/image/image.h" | 
|  | #include "ui/native_theme/native_theme.h" | 
|  | #include "url/gurl.h" | 
|  | #include "url/origin.h" | 
|  |  | 
|  | #if BUILDFLAG(IS_CHROMEOS) | 
|  | #include "ash/constants/ash_features.h" | 
|  | #include "chrome/browser/ash/apps/apk_web_app_service.h" | 
|  | #include "chrome/browser/ash/system_web_apps/color_helpers.h" | 
|  | #include "chrome/browser/web_applications/chromeos_web_app_experiments.h" | 
|  | #include "chrome/common/chrome_features.h" | 
|  | #include "chromeos/ash/experiences/system_web_apps/types/system_web_app_delegate.h" | 
|  | #include "chromeos/constants/chromeos_features.h" | 
|  | #endif | 
|  |  | 
|  | namespace web_app { | 
|  |  | 
|  | namespace { | 
|  |  | 
|  | #if BUILDFLAG(IS_CHROMEOS) | 
|  | constexpr char kRelationship[] = "delegate_permission/common.handle_all_urls"; | 
|  |  | 
|  | // SystemWebAppDelegate provides menu. | 
|  | class SystemAppTabMenuModelFactory : public TabMenuModelFactory { | 
|  | public: | 
|  | explicit SystemAppTabMenuModelFactory( | 
|  | const ash::SystemWebAppDelegate* system_app) | 
|  | : system_app_(system_app) {} | 
|  | SystemAppTabMenuModelFactory(const SystemAppTabMenuModelFactory&) = delete; | 
|  | SystemAppTabMenuModelFactory& operator=(const SystemAppTabMenuModelFactory&) = | 
|  | delete; | 
|  | ~SystemAppTabMenuModelFactory() override = default; | 
|  |  | 
|  | std::unique_ptr<ui::SimpleMenuModel> Create( | 
|  | ui::SimpleMenuModel::Delegate* delegate, | 
|  | TabMenuModelDelegate* tab_menu_model_delegate, | 
|  | TabStripModel*, | 
|  | int) override { | 
|  | return system_app_->GetTabMenuModel(delegate); | 
|  | } | 
|  |  | 
|  | private: | 
|  | const raw_ptr<const ash::SystemWebAppDelegate> system_app_; | 
|  | }; | 
|  | #endif  // BUILDFLAG(IS_CHROMEOS) | 
|  |  | 
|  | base::OnceClosure& IconLoadCallbackForTesting() { | 
|  | static base::NoDestructor<base::OnceClosure> callback; | 
|  | return *callback; | 
|  | } | 
|  |  | 
|  | base::OnceClosure& ManifestUpdateAppliedCallbackForTesting() { | 
|  | static base::NoDestructor<base::OnceClosure> callback; | 
|  | return *callback; | 
|  | } | 
|  |  | 
|  | }  // namespace | 
|  |  | 
|  | WebAppBrowserController::WebAppBrowserController( | 
|  | WebAppProvider& provider, | 
|  | Browser* browser, | 
|  | webapps::AppId app_id, | 
|  | #if BUILDFLAG(IS_CHROMEOS) | 
|  | const ash::SystemWebAppDelegate* system_app, | 
|  | #endif  // BUILDFLAG(IS_CHROMEOS) | 
|  | bool has_tab_strip) | 
|  | : AppBrowserController(browser, std::move(app_id), has_tab_strip), | 
|  | provider_(provider), | 
|  | #if BUILDFLAG(IS_CHROMEOS) | 
|  | system_app_(system_app), | 
|  | #endif  // BUILDFLAG(IS_CHROMEOS) | 
|  | has_pinned_home_tab_(has_tab_strip && | 
|  | provider.registrar_unsafe() | 
|  | .GetAppPinnedHomeTabUrl(this->app_id()) | 
|  | .has_value()) { | 
|  | effective_display_mode_ = | 
|  | registrar().GetAppEffectiveDisplayMode(this->app_id()); | 
|  | install_manager_observation_.Observe(&provider.install_manager()); | 
|  | registrar_observation_.Observe(&provider.registrar_unsafe()); | 
|  | PerformDigitalAssetLinkVerification(browser); | 
|  | } | 
|  |  | 
|  | WebAppBrowserController::~WebAppBrowserController() = default; | 
|  |  | 
|  | bool WebAppBrowserController::HasMinimalUiButtons() const { | 
|  | if (has_tab_strip()) { | 
|  | return false; | 
|  | } | 
|  | return effective_display_mode_ == DisplayMode::kMinimalUi; | 
|  | } | 
|  |  | 
|  | std::unique_ptr<TabMenuModelFactory> | 
|  | WebAppBrowserController::GetTabMenuModelFactory() const { | 
|  | #if BUILDFLAG(IS_CHROMEOS) | 
|  | if (system_app() && system_app()->HasCustomTabMenuModel()) { | 
|  | return std::make_unique<SystemAppTabMenuModelFactory>(system_app()); | 
|  | } | 
|  | #endif  // BUILDFLAG(IS_CHROMEOS) | 
|  | return nullptr; | 
|  | } | 
|  |  | 
|  | bool WebAppBrowserController::AppUsesWindowControlsOverlay() const { | 
|  | return effective_display_mode_ == DisplayMode::kWindowControlsOverlay; | 
|  | } | 
|  |  | 
|  | bool WebAppBrowserController::IsWindowControlsOverlayEnabled() const { | 
|  | return AppUsesWindowControlsOverlay() && | 
|  | registrar().GetWindowControlsOverlayEnabled(app_id()); | 
|  | } | 
|  |  | 
|  | void WebAppBrowserController::ToggleWindowControlsOverlayEnabled( | 
|  | base::OnceClosure on_complete) { | 
|  | DCHECK(AppUsesWindowControlsOverlay()); | 
|  |  | 
|  | provider_->scheduler().ScheduleCallback( | 
|  | "WebAppBrowserController::ToggleWindowControlsOverlayEnabled", | 
|  | AppLockDescription(app_id()), | 
|  | base::BindOnce( | 
|  | [](const webapps::AppId& app_id, AppLock& lock, | 
|  | base::Value::Dict& debug_value) { | 
|  | lock.sync_bridge().SetAppWindowControlsOverlayEnabled( | 
|  | app_id, | 
|  | !lock.registrar().GetWindowControlsOverlayEnabled(app_id)); | 
|  | }, | 
|  | app_id()), | 
|  | /*on_complete=*/std::move(on_complete)); | 
|  | } | 
|  |  | 
|  | bool WebAppBrowserController::AppUsesBorderlessMode() const { | 
|  | return IsIsolatedWebApp() && | 
|  | effective_display_mode_ == DisplayMode::kBorderless; | 
|  | } | 
|  |  | 
|  | bool WebAppBrowserController::AppUsesTabbed() const { | 
|  | if (!base::FeatureList::IsEnabled(blink::features::kDesktopPWAsTabStrip)) { | 
|  | return false; | 
|  | } | 
|  | return effective_display_mode_ == DisplayMode::kTabbed; | 
|  | } | 
|  |  | 
|  | bool WebAppBrowserController::IsIsolatedWebApp() const { | 
|  | return is_isolated_web_app_for_testing_ || registrar().IsIsolated(app_id()); | 
|  | } | 
|  |  | 
|  | void WebAppBrowserController::SetIsolatedWebAppTrueForTesting() { | 
|  | is_isolated_web_app_for_testing_ = true; | 
|  | } | 
|  |  | 
|  | gfx::Rect WebAppBrowserController::GetDefaultBounds() const { | 
|  | #if BUILDFLAG(IS_CHROMEOS) | 
|  | if (system_app_) { | 
|  | return system_app_->GetDefaultBounds( | 
|  | ash::BrowserController::GetInstance()->GetDelegate(browser())); | 
|  | } | 
|  | #endif  // BUILDFLAG(IS_CHROMEOS) | 
|  | return gfx::Rect(); | 
|  | } | 
|  |  | 
|  | bool WebAppBrowserController::HasReloadButton() const { | 
|  | #if BUILDFLAG(IS_CHROMEOS) | 
|  | if (system_app_) { | 
|  | return system_app_->ShouldHaveReloadButtonInMinimalUi(); | 
|  | } | 
|  | #endif  // BUILDFLAG(IS_CHROMEOS) | 
|  | return true; | 
|  | } | 
|  |  | 
|  | bool WebAppBrowserController::HasPendingUpdate() const { | 
|  | if (!base::FeatureList::IsEnabled(features::kWebAppPredictableAppUpdating)) { | 
|  | return false; | 
|  | } | 
|  | const WebApp* app = registrar().GetAppById(app_id()); | 
|  | return app && app->pending_update_info().has_value(); | 
|  | } | 
|  |  | 
|  | bool WebAppBrowserController::HasPendingUpdateNotIgnoredByUser() const { | 
|  | if (!base::FeatureList::IsEnabled(features::kWebAppPredictableAppUpdating)) { | 
|  | return false; | 
|  | } | 
|  | const WebApp* app = registrar().GetAppById(app_id()); | 
|  | if (!app || !app->pending_update_info().has_value()) { | 
|  | return false; | 
|  | } | 
|  | CHECK(app->pending_update_info()->has_was_ignored()); | 
|  | return !app->pending_update_info()->was_ignored(); | 
|  | } | 
|  |  | 
|  | void WebAppBrowserController::CreateMetadataAndTriggerAppUpdateDialog( | 
|  | base::TimeTicks start_time) const { | 
|  | provider_->scheduler().ReadAppUpdateDataFromDisk( | 
|  | app_id(), | 
|  | base::BindOnce( | 
|  | &WebAppBrowserController::OnMetadataObtainedTriggerUpdateDialog, | 
|  | weak_ptr_factory_.GetWeakPtr(), start_time)); | 
|  | } | 
|  |  | 
|  | #if !BUILDFLAG(IS_CHROMEOS) | 
|  | bool WebAppBrowserController::HasProfileMenuButton() const { | 
|  | #if BUILDFLAG(IS_MAC) | 
|  | return true; | 
|  | #else | 
|  | return app_id() == ash::kPasswordManagerAppId; | 
|  | #endif | 
|  | } | 
|  |  | 
|  | bool WebAppBrowserController::IsProfileMenuButtonVisible() const { | 
|  | CHECK(HasProfileMenuButton()); | 
|  | if (app_id() == ash::kPasswordManagerAppId) { | 
|  | return true; | 
|  | } | 
|  | #if BUILDFLAG(IS_MAC) | 
|  | return AppShimRegistry::Get()->GetInstalledProfilesForApp(app_id()).size() > | 
|  | 1; | 
|  | #else | 
|  | NOTREACHED(); | 
|  | #endif | 
|  | } | 
|  | #endif  // !BUILDFLAG(IS_CHROMEOS) | 
|  |  | 
|  | #if BUILDFLAG(IS_CHROMEOS) | 
|  | const ash::SystemWebAppDelegate* WebAppBrowserController::system_app() const { | 
|  | return system_app_; | 
|  | } | 
|  | #endif  // BUILDFLAG(IS_CHROMEOS) | 
|  |  | 
|  | #if BUILDFLAG(IS_MAC) | 
|  | bool WebAppBrowserController::AlwaysShowToolbarInFullscreen() const { | 
|  | // Reading this setting synchronously rather than going through the command | 
|  | // manager greatly simplifies where this is read. This should be fine, since | 
|  | // this is only persisted in the web app db. | 
|  | return registrar().AlwaysShowToolbarInFullscreen(app_id()); | 
|  | } | 
|  |  | 
|  | void WebAppBrowserController::ToggleAlwaysShowToolbarInFullscreen() { | 
|  | provider_->scheduler().ScheduleCallback( | 
|  | "WebAppBrowserController::ToggleAlwaysShowToolbarInFullscreen", | 
|  | AppLockDescription(app_id()), | 
|  | base::BindOnce( | 
|  | [](const webapps::AppId& app_id, AppLock& lock, | 
|  | base::Value::Dict& debug_value) { | 
|  | lock.sync_bridge().SetAlwaysShowToolbarInFullscreen( | 
|  | app_id, | 
|  | !lock.registrar().AlwaysShowToolbarInFullscreen(app_id)); | 
|  | }, | 
|  | app_id()), | 
|  | /*on_complete=*/base::DoNothing()); | 
|  | } | 
|  | #endif | 
|  |  | 
|  | #if BUILDFLAG(IS_CHROMEOS) | 
|  | bool WebAppBrowserController::ShouldShowCustomTabBar() const { | 
|  | if (AppBrowserController::ShouldShowCustomTabBar()) { | 
|  | return true; | 
|  | } | 
|  |  | 
|  | return is_verified_.value_or(false); | 
|  | } | 
|  |  | 
|  | void WebAppBrowserController::CheckDigitalAssetLinkRelationshipForAndroidApp( | 
|  | const std::string& package_name, | 
|  | const std::string& fingerprint) { | 
|  | // base::Unretained is safe as |asset_link_handler_| is owned by this object | 
|  | // and will be destroyed if this object is destroyed. | 
|  | // TODO(swestphal): Support passing several fingerprints for verification. | 
|  | std::vector<std::string> fingerprints{fingerprint}; | 
|  | asset_link_handler_->CheckDigitalAssetLinkRelationshipForAndroidApp( | 
|  | url::Origin::Create(GetAppStartUrl()), kRelationship, | 
|  | std::move(fingerprints), package_name, | 
|  | base::BindOnce(&WebAppBrowserController::OnRelationshipCheckComplete, | 
|  | base::Unretained(this))); | 
|  | } | 
|  |  | 
|  | void WebAppBrowserController::OnRelationshipCheckComplete( | 
|  | content_relationship_verification::RelationshipCheckResult result) { | 
|  | bool should_show_cct = false; | 
|  | switch (result) { | 
|  | case content_relationship_verification::RelationshipCheckResult::kSuccess: | 
|  | should_show_cct = false; | 
|  | break; | 
|  | case content_relationship_verification::RelationshipCheckResult::kFailure: | 
|  | case content_relationship_verification::RelationshipCheckResult:: | 
|  | kNoConnection: | 
|  | should_show_cct = true; | 
|  | break; | 
|  | } | 
|  | is_verified_ = should_show_cct; | 
|  | browser()->window()->UpdateCustomTabBarVisibility(should_show_cct, | 
|  | false /* animate */); | 
|  | } | 
|  | #endif  // BUILDFLAG(IS_CHROMEOS) | 
|  |  | 
|  | void WebAppBrowserController::OnWebAppUninstalled( | 
|  | const webapps::AppId& uninstalled_app_id, | 
|  | webapps::WebappUninstallSource uninstall_source) { | 
|  | if (uninstalled_app_id == app_id()) { | 
|  | chrome::CloseWindow(browser()); | 
|  | } | 
|  | } | 
|  |  | 
|  | void WebAppBrowserController::OnWebAppManifestUpdated( | 
|  | const webapps::AppId& updated_app_id) { | 
|  | if (updated_app_id == app_id()) { | 
|  | UpdateThemePack(); | 
|  | app_icon_.reset(); | 
|  | browser()->window()->UpdateTitleBar(); | 
|  |  | 
|  | if (ManifestUpdateAppliedCallbackForTesting()) { | 
|  | std::move(ManifestUpdateAppliedCallbackForTesting()).Run(); | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | void WebAppBrowserController::OnWebAppInstallManagerDestroyed() { | 
|  | install_manager_observation_.Reset(); | 
|  | } | 
|  |  | 
|  | void WebAppBrowserController::OnWebAppEffectiveScopeChanged( | 
|  | const webapps::AppId& app_id, | 
|  | const WebAppScope& new_scope) { | 
|  | if (app_id != this->app_id()) { | 
|  | return; | 
|  | } | 
|  |  | 
|  | // When the HostedAppController finally goes away, pipe through the | 
|  | // WebAppScope appropriately to remove another registrar lookup. | 
|  | UpdateCustomTabBarVisibility(/*animate=*/true); | 
|  | } | 
|  |  | 
|  | void WebAppBrowserController::OnAppRegistrarDestroyed() { | 
|  | registrar_observation_.Reset(); | 
|  | } | 
|  |  | 
|  | ui::ImageModel WebAppBrowserController::GetWindowAppIcon() const { | 
|  | if (app_icon_) { | 
|  | return *app_icon_; | 
|  | } | 
|  | app_icon_ = GetFallbackAppIcon(); | 
|  |  | 
|  | #if BUILDFLAG(IS_CHROMEOS) | 
|  | if (apps::AppServiceProxyFactory::IsAppServiceAvailableForProfile( | 
|  | browser()->profile())) { | 
|  | LoadAppIcon(true /* allow_placeholder_icon */); | 
|  | return *app_icon_; | 
|  | } | 
|  | #endif | 
|  |  | 
|  | if (provider_->icon_manager().HasSmallestIcon(app_id(), {IconPurpose::ANY}, | 
|  | kWebAppIconSmall)) { | 
|  | provider_->icon_manager().ReadSmallestIcon( | 
|  | app_id(), {IconPurpose::ANY}, kWebAppIconSmall, | 
|  | base::BindOnce(&WebAppBrowserController::OnReadIcon, | 
|  | weak_ptr_factory_.GetWeakPtr())); | 
|  | } | 
|  |  | 
|  | return *app_icon_; | 
|  | } | 
|  |  | 
|  | gfx::ImageSkia WebAppBrowserController::GetHomeTabIcon() const { | 
|  | return provider_->icon_manager().GetMonochromeFavicon(app_id()); | 
|  | } | 
|  |  | 
|  | gfx::ImageSkia WebAppBrowserController::GetFallbackHomeTabIcon() const { | 
|  | return provider_->icon_manager().GetFaviconImageSkia(app_id()); | 
|  | } | 
|  |  | 
|  | gfx::ImageSkia WebAppBrowserController::GetAppMenuIcon() const { | 
|  | return provider_->icon_manager().GetFaviconImageSkia(app_id()); | 
|  | } | 
|  |  | 
|  | ui::ImageModel WebAppBrowserController::GetWindowIcon() const { | 
|  | return GetWindowAppIcon(); | 
|  | } | 
|  |  | 
|  | std::optional<SkColor> WebAppBrowserController::GetThemeColor() const { | 
|  | #if BUILDFLAG(IS_CHROMEOS) | 
|  | // System App popups (settings pages) always use default theme. | 
|  | if (system_app() && browser()->is_type_app_popup()) { | 
|  | return std::nullopt; | 
|  | } | 
|  | #endif  // BUILDFLAG(IS_CHROMEOS) | 
|  |  | 
|  | std::optional<SkColor> web_theme_color = | 
|  | AppBrowserController::GetThemeColor(); | 
|  | if (web_theme_color) { | 
|  | return web_theme_color; | 
|  | } | 
|  |  | 
|  | #if BUILDFLAG(IS_CHROMEOS) | 
|  | if (chromeos::features::IsUploadOfficeToCloudEnabled() && | 
|  | ChromeOsWebAppExperiments::IgnoreManifestColor(app_id())) { | 
|  | return std::nullopt; | 
|  | } | 
|  |  | 
|  | // System Apps with dynamic color ignore manifest and pull theme color from | 
|  | // the OS. | 
|  | if (system_app() && system_app()->UseSystemThemeColor()) { | 
|  | return ash::GetSystemThemeColor(); | 
|  | } | 
|  | #endif  // BUILDFLAG(IS_CHROMEOS) | 
|  |  | 
|  | if (ui::NativeTheme::GetInstanceForNativeUi()->preferred_color_scheme() == | 
|  | ui::NativeTheme::PreferredColorScheme::kDark) { | 
|  | if (std::optional<SkColor> dark_mode_color = | 
|  | registrar().GetAppDarkModeThemeColor(app_id())) { | 
|  | return dark_mode_color; | 
|  | } | 
|  | } | 
|  |  | 
|  | return registrar().GetAppThemeColor(app_id()); | 
|  | } | 
|  |  | 
|  | std::optional<SkColor> WebAppBrowserController::GetBackgroundColor() const { | 
|  | std::optional<SkColor> web_contents_color = | 
|  | AppBrowserController::GetBackgroundColor(); | 
|  | std::optional<SkColor> manifest_color = GetResolvedManifestBackgroundColor(); | 
|  |  | 
|  | #if BUILDFLAG(IS_CHROMEOS) | 
|  | if (chromeos::features::IsUploadOfficeToCloudEnabled() && | 
|  | ChromeOsWebAppExperiments::IgnoreManifestColor(app_id())) { | 
|  | manifest_color = std::nullopt; | 
|  | } | 
|  | #endif  // BUILDFLAG(IS_CHROMEOS) | 
|  |  | 
|  | // Prefer an available web contents color but when such a color is | 
|  | // unavailable (i.e. in the time between when a window launches and it's web | 
|  | // content loads) attempt to pull the background color from the manifest. | 
|  | std::optional<SkColor> result = | 
|  | web_contents_color ? web_contents_color : manifest_color; | 
|  |  | 
|  | #if BUILDFLAG(IS_CHROMEOS) | 
|  | if (system_app() && system_app()->UseSystemThemeColor()) { | 
|  | // With jelly enabled, some system apps prefer system color over manifest. | 
|  | SkColor os_color = ash::GetSystemBackgroundColor(); | 
|  | result = web_contents_color ? web_contents_color : os_color; | 
|  | } | 
|  | #endif  // BUILDFLAG(IS_CHROMEOS) | 
|  |  | 
|  | return result; | 
|  | } | 
|  |  | 
|  | const GURL& WebAppBrowserController::GetAppStartUrl() const { | 
|  | return registrar().GetAppStartUrl(app_id()); | 
|  | } | 
|  |  | 
|  | const GURL& WebAppBrowserController::GetAppNewTabUrl() const { | 
|  | return registrar().GetAppNewTabUrl(app_id()); | 
|  | } | 
|  |  | 
|  | content::WebContents* WebAppBrowserController::GetPinnedHomeTab() const { | 
|  | return has_pinned_home_tab_ | 
|  | ? browser()->tab_strip_model()->GetWebContentsAt(0) | 
|  | : nullptr; | 
|  | } | 
|  |  | 
|  | bool WebAppBrowserController::ShouldHideNewTabButton() const { | 
|  | #if BUILDFLAG(IS_CHROMEOS) | 
|  | // Configure new tab button visibility for system apps based on their delegate | 
|  | // implementation. | 
|  | if (system_app() && system_app()->ShouldHaveTabStrip()) { | 
|  | return system_app()->ShouldHideNewTabButton(); | 
|  | } | 
|  | #endif  // BUILDFLAG(IS_CHROMEOS) | 
|  |  | 
|  | if (!registrar().IsTabbedWindowModeEnabled(app_id())) { | 
|  | return true; | 
|  | } | 
|  |  | 
|  | // If the app added a pinned home tab without changing their new tab URL, we | 
|  | // hide the new tab button to avoid the start_url being opened in a non home | 
|  | // tab. | 
|  | return IsUrlInHomeTabScope(GetAppNewTabUrl()); | 
|  | } | 
|  |  | 
|  | bool WebAppBrowserController::IsUrlInHomeTabScope(const GURL& url) const { | 
|  | return registrar().IsUrlInHomeTabScope(url, app_id()); | 
|  | } | 
|  |  | 
|  | bool WebAppBrowserController::ShouldShowAppIconOnTab(int index) const { | 
|  | #if BUILDFLAG(IS_CHROMEOS) | 
|  | return !system_app() && | 
|  | web_app::IsPinnedHomeTab(browser()->tab_strip_model(), index); | 
|  | #else | 
|  | return web_app::IsPinnedHomeTab(browser()->tab_strip_model(), index); | 
|  | #endif | 
|  | } | 
|  |  | 
|  | bool WebAppBrowserController::IsUrlInAppScope(const GURL& url) const { | 
|  | #if BUILDFLAG(IS_CHROMEOS) | 
|  | if (system_app() && system_app()->IsUrlInSystemAppScope(url)) { | 
|  | return true; | 
|  | } | 
|  | #endif  // BUILDFLAG(IS_CHROMEOS) | 
|  |  | 
|  | std::optional<WebAppScope> scope = registrar().GetEffectiveScope(app_id()); | 
|  | if (!scope.has_value()) { | 
|  | return false; | 
|  | } | 
|  | return scope->IsInScope(url, {.allow_http_to_https_upgrade = true}); | 
|  | } | 
|  |  | 
|  | WebAppBrowserController* WebAppBrowserController::AsWebAppBrowserController() { | 
|  | return this; | 
|  | } | 
|  |  | 
|  | std::u16string WebAppBrowserController::GetTitle() const { | 
|  | // When showing the toolbar, display the name of the app, instead of the | 
|  | // current page as the title. | 
|  | if (ShouldShowCustomTabBar()) { | 
|  | // TODO(crbug.com/40118430): Use name instead of short_name. | 
|  | return base::UTF8ToUTF16(registrar().GetAppShortName(app_id())); | 
|  | } | 
|  |  | 
|  | std::u16string raw_title = AppBrowserController::GetTitle(); | 
|  |  | 
|  | std::u16string app_name = base::UTF8ToUTF16( | 
|  | provider_->registrar_unsafe().GetAppShortName(app_id())); | 
|  |  | 
|  | // If app title is set, then use that with the app name as the title. | 
|  | std::optional<std::u16string> application_title; | 
|  | content::WebContents* web_contents = | 
|  | browser()->tab_strip_model()->GetActiveWebContents(); | 
|  | if (web_contents) { | 
|  | application_title = web_contents->GetApplicationTitle(); | 
|  | } | 
|  |  | 
|  | // If the app title is empty, then use the app name. | 
|  | if (application_title.has_value()) { | 
|  | return application_title.value().empty() | 
|  | ? app_name | 
|  | : l10n_util::GetStringFUTF16(IDS_WEB_APP_WITH_APP_TITLE, | 
|  | app_name, | 
|  | application_title.value()); | 
|  | } | 
|  | if (base::StartsWith(raw_title, app_name)) { | 
|  | return raw_title; | 
|  | } | 
|  |  | 
|  | if (raw_title.empty()) { | 
|  | return app_name; | 
|  | } | 
|  |  | 
|  | return base::StrCat({app_name, u" - ", raw_title}); | 
|  | } | 
|  |  | 
|  | std::u16string WebAppBrowserController::GetAppShortName() const { | 
|  | return base::UTF8ToUTF16(registrar().GetAppShortName(app_id())); | 
|  | } | 
|  |  | 
|  | std::u16string WebAppBrowserController::GetFormattedUrlOrigin() const { | 
|  | if (registrar().GetScopeExtensions(app_id()).empty()) { | 
|  | return FormatUrlOrigin(GetAppStartUrl()); | 
|  | } | 
|  |  | 
|  | CHECK(browser() != nullptr && browser()->tab_strip_model() != nullptr); | 
|  | content::WebContents* contents = | 
|  | browser()->tab_strip_model()->GetActiveWebContents(); | 
|  | if (contents == nullptr) { | 
|  | return FormatUrlOrigin(GetAppStartUrl()); | 
|  | } | 
|  | GURL last_committed_url = contents->GetLastCommittedURL(); | 
|  | if (last_committed_url.is_empty()) { | 
|  | return FormatUrlOrigin(GetAppStartUrl()); | 
|  | } | 
|  | return FormatUrlOrigin(last_committed_url); | 
|  | } | 
|  |  | 
|  | bool WebAppBrowserController::CanUserUninstall() const { | 
|  | return registrar().CanUserUninstallWebApp(app_id()); | 
|  | } | 
|  |  | 
|  | void WebAppBrowserController::Uninstall( | 
|  | webapps::WebappUninstallSource webapp_uninstall_source) { | 
|  | provider_->ui_manager().PresentUserUninstallDialog( | 
|  | app_id(), webapps::WebappUninstallSource::kAppMenu, browser()->window(), | 
|  | base::DoNothing()); | 
|  | } | 
|  |  | 
|  | bool WebAppBrowserController::IsInstalled() const { | 
|  | return registrar().IsInstallState( | 
|  | app_id(), {proto::InstallState::SUGGESTED_FROM_ANOTHER_DEVICE, | 
|  | proto::InstallState::INSTALLED_WITHOUT_OS_INTEGRATION, | 
|  | proto::InstallState::INSTALLED_WITH_OS_INTEGRATION}); | 
|  | } | 
|  |  | 
|  | void WebAppBrowserController::SetIconLoadCallbackForTesting( | 
|  | base::OnceClosure callback) { | 
|  | IconLoadCallbackForTesting() = std::move(callback); | 
|  | } | 
|  |  | 
|  | void WebAppBrowserController::SetManifestUpdateAppliedCallbackForTesting( | 
|  | base::OnceClosure callback) { | 
|  | ManifestUpdateAppliedCallbackForTesting() = std::move(callback); | 
|  | } | 
|  |  | 
|  | void WebAppBrowserController::OnTabInserted(content::WebContents* contents) { | 
|  | AppBrowserController::OnTabInserted(contents); | 
|  |  | 
|  | WebAppTabHelper* tab_helper = WebAppTabHelper::FromWebContents(contents); | 
|  | tab_helper->SetIsInAppWindow(app_id()); | 
|  | } | 
|  |  | 
|  | void WebAppBrowserController::OnTabRemoved(content::WebContents* contents) { | 
|  | AppBrowserController::OnTabRemoved(contents); | 
|  | WebAppTabHelper::FromWebContents(contents)->SetIsInAppWindow( | 
|  | /*window_app_id=*/std::nullopt); | 
|  | } | 
|  |  | 
|  | const WebAppRegistrar& WebAppBrowserController::registrar() const { | 
|  | return provider_->registrar_unsafe(); | 
|  | } | 
|  |  | 
|  | const WebAppInstallManager& WebAppBrowserController::install_manager() const { | 
|  | return provider_->install_manager(); | 
|  | } | 
|  |  | 
|  | void WebAppBrowserController::LoadAppIcon(bool allow_placeholder_icon) const { | 
|  | apps::AppServiceProxy* proxy = | 
|  | apps::AppServiceProxyFactory::GetForProfile(browser()->profile()); | 
|  | proxy->LoadIcon(app_id(), apps::IconType::kStandard, kWebAppIconSmall, | 
|  | allow_placeholder_icon, | 
|  | base::BindOnce(&WebAppBrowserController::OnLoadIcon, | 
|  | weak_ptr_factory_.GetWeakPtr())); | 
|  | } | 
|  |  | 
|  | void WebAppBrowserController::OnLoadIcon(apps::IconValuePtr icon_value) { | 
|  | if (!icon_value || icon_value->icon_type != apps::IconType::kStandard) { | 
|  | return; | 
|  | } | 
|  |  | 
|  | app_icon_ = ui::ImageModel::FromImageSkia(icon_value->uncompressed); | 
|  |  | 
|  | if (icon_value->is_placeholder_icon) { | 
|  | LoadAppIcon(false /* allow_placeholder_icon */); | 
|  | } | 
|  |  | 
|  | if (auto* contents = web_contents()) { | 
|  | contents->NotifyNavigationStateChanged(content::INVALIDATE_TYPE_TAB); | 
|  | } | 
|  | if (IconLoadCallbackForTesting()) { | 
|  | std::move(IconLoadCallbackForTesting()).Run(); | 
|  | } | 
|  | } | 
|  |  | 
|  | void WebAppBrowserController::OnReadIcon(IconPurpose purpose, SkBitmap bitmap) { | 
|  | // We request only IconPurpose::ANY icons. | 
|  | DCHECK_EQ(purpose, IconPurpose::ANY); | 
|  |  | 
|  | if (bitmap.empty()) { | 
|  | DLOG(ERROR) << "Failed to read icon for web app"; | 
|  | return; | 
|  | } | 
|  |  | 
|  | app_icon_ = | 
|  | ui::ImageModel::FromImageSkia(gfx::ImageSkia::CreateFrom1xBitmap(bitmap)); | 
|  | if (auto* contents = web_contents()) { | 
|  | contents->NotifyNavigationStateChanged(content::INVALIDATE_TYPE_TAB); | 
|  | } | 
|  | if (IconLoadCallbackForTesting()) { | 
|  | std::move(IconLoadCallbackForTesting()).Run(); | 
|  | } | 
|  | } | 
|  |  | 
|  | void WebAppBrowserController::PerformDigitalAssetLinkVerification( | 
|  | Browser* browser) { | 
|  | #if BUILDFLAG(IS_CHROMEOS) | 
|  | asset_link_handler_ = std::make_unique< | 
|  | content_relationship_verification::DigitalAssetLinksHandler>( | 
|  | browser->profile()->GetURLLoaderFactory()); | 
|  | is_verified_ = std::nullopt; | 
|  |  | 
|  | ash::ApkWebAppService* apk_web_app_service = | 
|  | ash::ApkWebAppService::Get(browser->profile()); | 
|  | if (!apk_web_app_service || !apk_web_app_service->IsWebOnlyTwa(app_id())) { | 
|  | return; | 
|  | } | 
|  |  | 
|  | const std::optional<std::string> package_name = | 
|  | apk_web_app_service->GetPackageNameForWebApp(app_id()); | 
|  | const std::optional<std::string> fingerprint = | 
|  | apk_web_app_service->GetCertificateSha256Fingerprint(app_id()); | 
|  |  | 
|  | // Any web-only TWA should have an associated package name and fingerprint. | 
|  | DCHECK(package_name.has_value()); | 
|  | DCHECK(fingerprint.has_value()); | 
|  |  | 
|  | CheckDigitalAssetLinkRelationshipForAndroidApp(*package_name, *fingerprint); | 
|  | #endif | 
|  | } | 
|  |  | 
|  | void WebAppBrowserController::OnMetadataObtainedTriggerUpdateDialog( | 
|  | base::TimeTicks start_time, | 
|  | std::optional<WebAppIdentityUpdate> identity_update) const { | 
|  | if (!identity_update) { | 
|  | return; | 
|  | } | 
|  |  | 
|  | // TODO(crbug.com/436868803): Pipe calling of the final update command to this | 
|  | // function. | 
|  | web_app::ShowWebAppReviewUpdateDialog( | 
|  | app_id(), *identity_update, browser(), start_time, | 
|  | base::BindOnce([](WebAppIdentityUpdateResult result) { | 
|  | base::UmaHistogramEnumeration("WebApp.PredictableUpdateDialog.Result", | 
|  | result); | 
|  | })); | 
|  | } | 
|  |  | 
|  | std::optional<SkColor> | 
|  | WebAppBrowserController::GetResolvedManifestBackgroundColor() const { | 
|  | if (ui::NativeTheme::GetInstanceForNativeUi()->preferred_color_scheme() == | 
|  | ui::NativeTheme::PreferredColorScheme::kDark) { | 
|  | if (std::optional<SkColor> dark_mode_color = | 
|  | registrar().GetAppDarkModeBackgroundColor(app_id())) { | 
|  | return dark_mode_color; | 
|  | } | 
|  | } | 
|  | return registrar().GetAppBackgroundColor(app_id()); | 
|  | } | 
|  |  | 
|  | }  // namespace web_app |