910e62b5创建于 1月15日历史提交
// 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 <algorithm>

#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/url_pattern_with_regex_matcher.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_filter.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;

WebAppBrowserController* WebAppBrowserController::From(
    BrowserWindowInterface* browser) {
  auto* result = AppBrowserController::From(browser);
  return result ? result->AsWebAppBrowserController() : nullptr;
}

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::UrlMatchesBorderlessPattern(
    const GURL& url) const {
  const WebApp* app = registrar().GetAppById(app_id());
  if (app == nullptr) {
    return false;
  }
  return app->borderless_url_patterns().empty() ||
         std::ranges::any_of(app->borderless_url_patterns(),
                             [&url](const blink::SafeUrlPattern& p) {
                               return UrlPatternWithRegexMatcher(p).Match(url);
                             });
}

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().AppMatches(app_id(), WebAppFilter::IsIsolatedApp());
}

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());

  if (!did_notify_first_tab_) {
    did_notify_first_tab_ = true;
    tab_helper->NotifyIsFirstWebContentsInAppWindow(
        base::PassKey<WebAppBrowserController>());
  }
}

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