// Copyright 2019 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/app_browser_controller.h"

#include <string_view>

#include "base/containers/contains.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/memory/values_equivalent.h"
#include "base/strings/escape.h"
#include "base/strings/utf_string_conversions.h"
#include "base/time/time.h"
#include "base/trace_event/trace_event.h"
#include "build/build_config.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/themes/browser_theme_pack.h"
#include "chrome/browser/themes/theme_properties.h"
#include "chrome/browser/themes/theme_service.h"
#include "chrome/browser/ui/actions/chrome_action_id.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_list.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/ui/browser_window/public/browser_window_features.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface_iterator.h"
#include "chrome/browser/ui/browser_window_state.h"
#include "chrome/browser/ui/color/chrome_color_id.h"
#include "chrome/browser/ui/color/chrome_color_provider_utils.h"
#include "chrome/browser/ui/tabs/tab_menu_model_factory.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/browser/ui/ui_features.h"
#include "chrome/browser/ui/views/page_action/action_ids.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/chrome_features.h"
#include "chrome/common/chrome_render_frame.mojom.h"
#include "chrome/common/pref_names.h"
#include "chrome/common/themes/autogenerated_theme_util.h"
#include "chrome/grit/generated_resources.h"
#include "components/security_state/content/security_state_tab_helper.h"
#include "components/security_state/core/security_state.h"
#include "components/url_formatter/url_formatter.h"
#include "components/webapps/browser/installable/installable_evaluator.h"
#include "content/public/browser/navigation_controller.h"
#include "content/public/browser/navigation_entry.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/page.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/url_constants.h"
#include "extensions/common/constants.h"
#include "mojo/public/cpp/bindings/associated_remote.h"
#include "third_party/blink/public/common/associated_interfaces/associated_interface_provider.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/common/renderer_preferences/renderer_preferences.h"
#include "third_party/blink/public/mojom/page/draggable_region.mojom.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "ui/base/models/image_model.h"
#include "ui/color/color_id.h"
#include "ui/color/color_provider.h"
#include "ui/color/color_recipe.h"
#include "ui/color/color_transform.h"
#include "ui/display/display.h"
#include "ui/display/screen.h"
#include "ui/gfx/color_palette.h"
#include "ui/gfx/color_utils.h"
#include "ui/gfx/favicon_size.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/resize_utils.h"
#include "ui/gfx/image/image_skia.h"
#include "ui/native_theme/native_theme.h"
#include "ui/native_theme/os_settings_provider.h"
#include "url/gurl.h"
#include "url/origin.h"
#include "url/url_constants.h"

#if BUILDFLAG(IS_CHROMEOS)
#include "chrome/browser/apps/icon_standardizer.h"
#include "chromeos/ash/experiences/system_web_apps/types/system_web_app_delegate.h"
#include "chromeos/ui/base/chromeos_ui_constants.h"
#endif

namespace {

SkColor GetAltColor(SkColor color) {
  return color_utils::BlendForMinContrast(
             color, color, std::nullopt,
             kAutogeneratedThemeActiveTabMinContrast)
      .color;
}

void SetWebContentsCanAcceptLoadDrops(content::WebContents* contents,
                                      bool can_accept) {
  contents->GetMutableRendererPrefs()->can_accept_load_drops = can_accept;
  contents->SyncRendererPrefs();
  contents->NotifyPreferencesChanged();
}

}  // namespace

namespace web_app {

DEFINE_USER_DATA(AppBrowserController);

// static
const AppBrowserController* AppBrowserController::From(
    const BrowserWindowInterface* browser) {
  return Get(browser->GetUnownedUserDataHost());
}

// static
AppBrowserController* AppBrowserController::From(
    BrowserWindowInterface* browser) {
  return Get(browser->GetUnownedUserDataHost());
}

// static
bool AppBrowserController::IsWebApp(const BrowserWindowInterface* browser) {
  return browser && From(browser);
}

// static
bool AppBrowserController::IsIsolatedWebApp(
    const BrowserWindowInterface* browser) {
  return IsWebApp(browser) && From(browser)->IsIsolatedWebApp();
}

// static
bool AppBrowserController::IsForWebApp(const BrowserWindowInterface* browser,
                                       const webapps::AppId& app_id) {
  return IsWebApp(browser) && From(browser)->app_id() == app_id;
}

// static
BrowserWindowInterface* AppBrowserController::FindForWebApp(
    const Profile& profile,
    const webapps::AppId& app_id) {
  BrowserWindowInterface* browser_for_web_app = nullptr;
  ForEachCurrentBrowserWindowInterfaceOrderedByActivation(
      [&](BrowserWindowInterface* browser) {
        if (browser->GetBrowserForMigrationOnly()
                ->IsAttemptingToCloseBrowser()) {
          return true;  // continue iterating
        }
        if (browser->GetType() != BrowserWindowInterface::TYPE_APP) {
          return true;  // continue iterating
        }
        if (browser->GetProfile() != &profile) {
          return true;  // continue iterating
        }
        if (!IsForWebApp(browser, app_id)) {
          return true;  // continue iterating
        }
        browser_for_web_app = browser;
        return false;  // stop iterating
      });
  return browser_for_web_app;
}

// static
std::optional<int> AppBrowserController::FindTabIndexForApp(
    BrowserWindowInterface* browser,
    const webapps::AppId& app_id,
    bool for_focus_existing,
    HomeTabScope home_tab_scope) {
  auto is_valid_tab = [&app_id, home_tab_scope, for_focus_existing,
                       browser](content::WebContents* contents) {
    WebAppTabHelper* tab_helper = WebAppTabHelper::FromWebContents(contents);
    if (app_id != tab_helper->app_id() || contents->HasOpener()) {
      return false;
    }
    if (for_focus_existing && !tab_helper->CanBeUsedForFocusExisting()) {
      return false;
    }
    if (home_tab_scope == HomeTabScope::kDontCare) {
      return true;
    }
    return (home_tab_scope == HomeTabScope::kInScope) ==
           (From(browser)->GetPinnedHomeTab() == contents);
  };
  // The active web contents should have preference if it is in scope.
  if (browser->GetFeatures().tab_strip_model()->active_index() !=
      TabStripModel::kNoTab) {
    if (is_valid_tab(
            browser->GetFeatures().tab_strip_model()->GetActiveWebContents())) {
      return {browser->GetFeatures().tab_strip_model()->active_index()};
    }
  }
  // Otherwise, use the first one for the app.
  for (int i = 0; i < browser->GetFeatures().tab_strip_model()->count(); ++i) {
    if (is_valid_tab(
            browser->GetFeatures().tab_strip_model()->GetWebContentsAt(i))) {
      return {i};
    }
  }
  return std::nullopt;
}

// static
std::optional<AppBrowserController::BrowserAndTabIndex>
AppBrowserController::FindTopLevelBrowsingContextForWebApp(
    const Profile& profile,
    const webapps::AppId& app_id,
    bool for_app_browser,
    bool for_focus_existing,
    HomeTabScope home_tab_scope) {
  std::optional<AppBrowserController::BrowserAndTabIndex>
      browser_and_tab_index = std::nullopt;
  ForEachCurrentBrowserWindowInterfaceOrderedByActivation(
      [&](BrowserWindowInterface* browser) {
        if (browser->GetBrowserForMigrationOnly()
                ->IsAttemptingToCloseBrowser()) {
          return true;  // continue iterating
        }
        if (IsWebApp(browser) != for_app_browser) {
          return true;  // continue iterating
        }
        if (browser->GetProfile() != &profile) {
          return true;  // continue iterating
        }
        if (IsWebApp(browser) && !IsForWebApp(browser, app_id)) {
          return true;  // continue iterating
        }
        std::optional<int> tab_index = FindTabIndexForApp(
            browser, app_id, for_focus_existing, home_tab_scope);
        if (tab_index.has_value()) {
          browser_and_tab_index = {{browser, *tab_index}};
          return false;  // stop iterating
        }
        return true;  // continue iterating
      });
  return browser_and_tab_index;
}

// static
std::u16string AppBrowserController::FormatUrlOrigin(
    const GURL& url,
    url_formatter::FormatUrlTypes format_types) {
  auto origin = url::Origin::Create(url);
  return url_formatter::FormatUrl(origin.opaque() ? url : origin.GetURL(),
                                  format_types, base::UnescapeRule::SPACES,
                                  nullptr, nullptr, nullptr);
}

const ui::ThemeProvider* AppBrowserController::GetThemeProvider() const {
  return theme_provider_.get();
}

AppBrowserController::AppBrowserController(Browser* browser,
                                           webapps::AppId app_id,
                                           bool has_tab_strip)
    : content::WebContentsObserver(nullptr),
      browser_(browser),
      app_id_(std::move(app_id)),
      has_tab_strip_(has_tab_strip),
      theme_provider_(
          ThemeService::CreateBoundThemeProvider(browser_->profile(), this)),
      scoped_unowned_user_data_(browser->GetUnownedUserDataHost(), *this) {
  CHECK(browser->tab_strip_model()->empty());
  browser->tab_strip_model()->AddObserver(this);
}

AppBrowserController::AppBrowserController(Browser* browser,
                                           webapps::AppId app_id)
    : AppBrowserController(browser, std::move(app_id), false) {}

void AppBrowserController::Init() {
  UpdateThemePack();
}

AppBrowserController::~AppBrowserController() {
  browser()->tab_strip_model()->RemoveObserver(this);
}

bool AppBrowserController::ShouldShowCustomTabBar() const {
  if (!IsInstalled()) {
    return false;
  }

  content::WebContents* web_contents =
      browser()->tab_strip_model()->GetActiveWebContents();

  if (!web_contents) {
    return false;
  }

  GURL start_url = GetAppStartUrl();
  std::string_view start_url_scheme = start_url.scheme();

  bool is_internal_start_url_scheme =
#if BUILDFLAG(ARKWEB_ARKWEB_EXTENSIONS)
      start_url_scheme == extensions::kArkwebExtensionScheme ||
#endif
      start_url_scheme == extensions::kExtensionScheme ||
      start_url_scheme == content::kChromeUIScheme ||
      start_url_scheme == content::kChromeUIUntrustedScheme;

  auto should_show_toolbar_for_url = [&](const GURL& url) -> bool {
    // If the url is unset, it doesn't give a signal as to whether the toolbar
    // should be shown or not. In lieu of more information, do not show the
    // toolbar.
    if (url.is_empty()) {
      return false;
    }

    // Show toolbar when not using 'https', unless this is an internal app,
    // or origin is secure (e.g. localhost).
    if (!is_internal_start_url_scheme && !url.SchemeIs(url::kHttpsScheme) &&
        !webapps::InstallableEvaluator::IsOriginConsideredSecure(url)) {
      return true;
    }

    // Page URLs that are not within scope
    // (https://www.w3.org/TR/appmanifest/#dfn-within-scope) of the app
    // corresponding to |start_url| show the toolbar.
    return !IsUrlInAppScope(url);
  };

  GURL visible_url = web_contents->GetVisibleURL();
  GURL last_committed_url = web_contents->GetLastCommittedURL();

  if (last_committed_url.is_empty() && visible_url.is_empty()) {
    return should_show_toolbar_for_url(initial_url());
  }

  // Special case for about:blank app popup windows. If an app window creates a
  // popup window to about:blank from a document within app scope, the toolbar
  // should not be shown.
  if (last_committed_url.spec() == url::kAboutBlankURL) {
    auto* primary_main_frame = web_contents->GetPrimaryMainFrame();
    if (primary_main_frame &&
        primary_main_frame->GetLastCommittedOrigin().IsSameOriginWith(
            start_url) &&
        browser()->is_type_app_popup()) {
      return false;
    }
  }

  if (should_show_toolbar_for_url(visible_url) ||
      should_show_toolbar_for_url(last_committed_url)) {
    return true;
  }

  // Insecure external web sites show the toolbar.
  // Note: IsContentSecure is false until a navigation is committed.
  if (!last_committed_url.is_empty() && !is_internal_start_url_scheme &&
      !webapps::InstallableEvaluator::IsContentSecure(web_contents)) {
    return true;
  }

  return false;
}

bool AppBrowserController::has_tab_strip() const {
  return has_tab_strip_;
}

bool AppBrowserController::HasTitlebarMenuButton() const {
#if BUILDFLAG(IS_CHROMEOS)
  // Hide for system apps.
  return !system_app();
#else
  return true;
#endif  // BUILDFLAG(IS_CHROMEOS)
}

bool AppBrowserController::HasTitlebarAppOriginText() const {
#if BUILDFLAG(IS_CHROMEOS)
  // Do not show origin text for System Apps.
  if (system_app()) {
    return false;
  }
#endif  // BUILDFLAG(IS_CHROMEOS)
  return true;
}

bool AppBrowserController::HasTitlebarContentSettings() const {
#if BUILDFLAG(IS_CHROMEOS)
  // Do not show content settings for System Apps.
  return !system_app();
#else
  return true;
#endif  // BUILDFLAG(IS_CHROMEOS)
}

std::vector<actions::ActionId> AppBrowserController::GetTitleBarPageActions()
    const {
  if (!base::FeatureList::IsEnabled(features::kPageActionsMigration)) {
    return {};
  }
#if BUILDFLAG(IS_CHROMEOS)
  if (system_app()) {
    return {
        kActionFind,
        kActionZoomNormal,
    };
  }
#endif  // BUILDFLAG(IS_CHROMEOS)

  std::vector<actions::ActionId> types_enabled = {
      kActionFind,
      kActionShowPasswordsBubbleOrPage,
      kActionShowTranslate,
      kActionZoomNormal,
      kActionShowFileSystemAccess,
      kActionShowCookieControls,
  };

#if DCHECK_IS_ON()
  for (auto action_id : types_enabled) {
    DCHECK(base::Contains(page_actions::kActionIds, action_id));
  }
#endif

  return types_enabled;
}

std::vector<PageActionIconType>
AppBrowserController::GetTitleBarPageActionTypes() const {
#if BUILDFLAG(IS_CHROMEOS)
  if (system_app()) {
    return {PageActionIconType::kFind, PageActionIconType::kZoom};
  }
#endif  // BUILDFLAG(IS_CHROMEOS)

  std::vector<PageActionIconType> types_enabled;
  types_enabled.push_back(PageActionIconType::kFind);
  types_enabled.push_back(PageActionIconType::kManagePasswords);
  types_enabled.push_back(PageActionIconType::kTranslate);
  types_enabled.push_back(PageActionIconType::kZoom);
  types_enabled.push_back(PageActionIconType::kFileSystemAccess);
  types_enabled.push_back(PageActionIconType::kCookieControls);
  types_enabled.push_back(PageActionIconType::kSaveCard);

  return types_enabled;
}

bool AppBrowserController::IsInstalled() const {
  return false;
}

std::unique_ptr<TabMenuModelFactory>
AppBrowserController::GetTabMenuModelFactory() const {
  return nullptr;
}

bool AppBrowserController::AppUsesWindowControlsOverlay() const {
  return false;
}

bool AppBrowserController::AppUsesBorderlessMode() const {
  return false;
}

bool AppBrowserController::UrlMatchesBorderlessPattern(const GURL& url) const {
  return false;
}

bool AppBrowserController::AppUsesTabbed() const {
  return false;
}

bool AppBrowserController::IsIsolatedWebApp() const {
  return false;
}

void AppBrowserController::SetIsolatedWebAppTrueForTesting() {}

bool AppBrowserController::IsWindowControlsOverlayEnabled() const {
  return false;
}

void AppBrowserController::ToggleWindowControlsOverlayEnabled(
    base::OnceClosure on_complete) {
  std::move(on_complete).Run();
}

gfx::Rect AppBrowserController::GetDefaultBounds() const {
  return gfx::Rect();
}

bool AppBrowserController::HasReloadButton() const {
  return true;
}

bool AppBrowserController::HasPendingUpdate() const {
  return false;
}

bool AppBrowserController::HasPendingUpdateNotIgnoredByUser() const {
  return false;
}

void AppBrowserController::CreateMetadataAndTriggerAppUpdateDialog(
    base::TimeTicks start_time) const {}

bool AppBrowserController::IsPreventCloseEnabled() const {
  auto* provider = WebAppProvider::GetForWebApps(browser()->profile());
  if (!provider) {
    return false;
  }
  return provider->registrar_unsafe().IsPreventCloseEnabled(app_id());
}

#if !BUILDFLAG(IS_CHROMEOS)
bool AppBrowserController::HasProfileMenuButton() const {
  return false;
}
bool AppBrowserController::IsProfileMenuButtonVisible() const {
  return false;
}
#endif  // !BUILDFLAG(IS_CHROMEOS)

#if BUILDFLAG(IS_CHROMEOS)
const ash::SystemWebAppDelegate* AppBrowserController::system_app() const {
  return nullptr;
}
#endif  // BUILDFLAG(IS_CHROMEOS)

std::u16string AppBrowserController::GetLaunchFlashText() const {
  // Isolated Web Apps should show the app's name instead of the origin.
  // App Short Name is considered trustworthy because manifest comes from signed
  // web bundle. The flash text is not needed on platforms that already display
  // the app name in the title bar (e.g. Mac, Windows, and Linux).
  if (IsIsolatedWebApp()) {
#if BUILDFLAG(IS_MAC) || BUILDFLAG(IS_WIN) || BUILDFLAG(IS_LINUX)
    return std::u16string();
#else   // !(BUILDFLAG(IS_MAC) || BUILDFLAG(IS_WIN) || BUILDFLAG(IS_LINUX))
    return GetAppShortName();
#endif  // BUILDFLAG(IS_MAC) || BUILDFLAG(IS_WIN) || BUILDFLAG(IS_LINUX)
  }
  return GetFormattedUrlOrigin();
}

WebAppBrowserController* AppBrowserController::AsWebAppBrowserController() {
  return nullptr;
}

bool AppBrowserController::CanUserUninstall() const {
  return false;
}

void AppBrowserController::Uninstall(
    webapps::WebappUninstallSource webapp_uninstall_source) {
  NOTREACHED();
}

void AppBrowserController::UpdateCustomTabBarVisibility(bool animate) const {
  browser()->window()->UpdateCustomTabBarVisibility(ShouldShowCustomTabBar(),
                                                    animate);
}

void AppBrowserController::DidStartNavigation(
    content::NavigationHandle* navigation_handle) {
  if (!initial_url().is_empty()) {
    return;
  }
  if (!navigation_handle->IsInPrimaryMainFrame()) {
    return;
  }
  if (navigation_handle->GetURL().is_empty()) {
    return;
  }
  SetInitialURL(navigation_handle->GetURL());
}

void AppBrowserController::DOMContentLoaded(
    content::RenderFrameHost* render_frame_host) {
  // We hold off changing theme color for a new tab until the page is loaded.
  UpdateThemePack();
}

void AppBrowserController::DidChangeThemeColor() {
  UpdateThemePack();
}

void AppBrowserController::OnBackgroundColorChanged() {
  UpdateThemePack();
}

void AppBrowserController::PrimaryPageChanged(content::Page& page) {
  // Reset the draggable regions for window controls overlay apps so they are
  // not cached on navigation. Note that these are not cleared for borderless
  // apps because when we navigate out of scope and then back to scope, the
  // draggable regions stay same and nothing triggers to re-initialize them.
  // So if they are cleared, they don't work anymore when coming back to scope.
  if (AppUsesWindowControlsOverlay()) {
    draggable_region_ = std::nullopt;
  }
}

std::optional<SkColor> AppBrowserController::GetThemeColor() const {
  if (ui::NativeTheme::GetInstanceForNativeUi()->preferred_contrast() ==
      ui::NativeTheme::PreferredContrast::kMore) {
    if (const std::optional<SkColor> window_color =
            ui::OsSettingsProvider::Get().Color(
                ui::OsSettingsProvider::ColorId::kWindow)) {
      return window_color;
    }
  }

  if (content::WebContents* const web_contents =
          browser()->tab_strip_model()->GetActiveWebContents()) {
    // HTML meta theme-color tag overrides manifest theme_color, see spec:
    // https://www.w3.org/TR/appmanifest/#theme_color-member
    if (const std::optional<SkColor> color = web_contents->GetThemeColor()) {
      // The frame/tabstrip code expects an opaque color.
      return SkColorSetA(*color, SK_AlphaOPAQUE);
    }
  }

  return std::nullopt;
}

std::optional<SkColor> AppBrowserController::GetBackgroundColor() const {
  std::optional<SkColor> color;
  if (auto* web_contents =
          browser()->tab_strip_model()->GetActiveWebContents()) {
    color = web_contents->GetBackgroundColor();
  }
  return color ? SkColorSetA(*color, SK_AlphaOPAQUE) : color;
}

std::u16string AppBrowserController::GetTitle() const {
  content::WebContents* web_contents =
      browser()->tab_strip_model()->GetActiveWebContents();
  if (!web_contents) {
    return std::u16string();
  }

  content::NavigationEntry* entry =
      web_contents->GetController().GetVisibleEntry();
  return entry ? entry->GetTitle() : std::u16string();
}

std::string AppBrowserController::GetTitleForMediaControls() const {
#if BUILDFLAG(IS_CHROMEOS)
  // Only return the app name if we're a System Web App.
  if (system_app()) {
    return base::UTF16ToUTF8(GetAppShortName());
  }
#endif  // BUILDFLAG(IS_CHROMEOS)
  return std::string();
}

const GURL& AppBrowserController::GetAppNewTabUrl() const {
  return GetAppStartUrl();
}

content::WebContents* AppBrowserController::GetPinnedHomeTab() const {
  return nullptr;
}

bool AppBrowserController::ShouldHideNewTabButton() const {
  return false;
}

bool AppBrowserController::IsUrlInHomeTabScope(const GURL& url) const {
  return false;
}

bool AppBrowserController::ShouldShowAppIconOnTab(int index) const {
  return false;
}

#if BUILDFLAG(IS_MAC)
bool AppBrowserController::AlwaysShowToolbarInFullscreen() const {
  return true;
}

void AppBrowserController::ToggleAlwaysShowToolbarInFullscreen() {}
#endif

void AppBrowserController::OnTabStripModelChanged(
    TabStripModel* tab_strip_model,
    const TabStripModelChange& change,
    const TabStripSelectionChange& selection) {
  if (selection.active_tab_changed()) {
    content::WebContentsObserver::Observe(selection.new_contents);
    // Update theme when tabs change unless there are no tabs, or if the tab has
    // not finished loading, we will update later in DOMContentLoaded().
    if (tab_strip_model->count() > 0 &&
        selection.new_contents->IsDocumentOnLoadCompletedInPrimaryMainFrame()) {
      UpdateThemePack();
    }
  }
  if (change.type() == TabStripModelChange::kInserted) {
    for (const auto& contents : change.GetInsert()->contents) {
      OnTabInserted(contents.contents);
    }
  } else if (change.type() == TabStripModelChange::kRemoved) {
    for (const auto& contents : change.GetRemove()->contents) {
      OnTabRemoved(contents.contents);
    }
    // WebContents should be null when the last tab is closed.
    DCHECK_EQ(web_contents() == nullptr, tab_strip_model->empty());
  }

  // Do not update the UI during window shutdown.
  if (!selection.new_contents) {
    return;
  }

  UpdateCustomTabBarVisibility(/*animate=*/false);
}

CustomThemeSupplier* AppBrowserController::GetThemeSupplier() const {
  return theme_pack_.get();
}

bool AppBrowserController::ShouldUseCustomFrame() const {
  return true;
}

void AppBrowserController::AddColorMixers(
    ui::ColorProvider* provider,
    const ui::ColorProviderKey& key) const {
  constexpr SkAlpha kSeparatorOpacity = 0.15f * 255.0f;
#if !BUILDFLAG(IS_CHROMEOS)
  // This color is the same as the default active frame color.
  const std::optional<SkColor> theme_color = GetThemeColor();
  ui::ColorTransform default_background =
      key.color_mode == ui::ColorProviderKey::ColorMode::kLight
          ? ui::ColorTransform(ui::kColorFrameActiveUnthemed)
          : ui::HSLShift(ui::kColorFrameActiveUnthemed,
                         ThemeProperties::GetDefaultTint(
                             ThemeProperties::TINT_FRAME, true));
#endif
  ui::ColorMixer& mixer = provider->AddMixer();
  std::optional<SkColor> bg_color = GetBackgroundColor();
  // TODO(kylixrd): The definition of kColorPwaBackground isn't fully fleshed
  // out yet. Whether or not the PWA background color is set is used in many
  // locations to derive other colors. Those specific locations would need to be
  // addressed in their own context.
  if (bg_color) {
    mixer[kColorPwaBackground] = {bg_color.value()};
  }
  mixer[kColorPwaMenuButtonIcon] = {kColorToolbarButtonIcon};
  mixer[kColorPwaSecurityChipForeground] = {ui::kColorSecondaryForeground};
  mixer[kColorPwaSecurityChipForegroundDangerous] = {
      ui::kColorAlertHighSeverity};
  mixer[kColorPwaSecurityChipForegroundSecure] = {
      kColorPwaSecurityChipForeground};
  auto separator_color =
      ui::GetColorWithMaxContrast(kColorPwaToolbarBackground);
  mixer[kColorPwaTabBarBottomSeparator] = ui::AlphaBlend(
      separator_color, kColorPwaToolbarBackground, kSeparatorOpacity);
  mixer[kColorPwaTabBarTopSeparator] =
      ui::AlphaBlend(separator_color, kColorPwaTheme, kSeparatorOpacity);
#if BUILDFLAG(IS_CHROMEOS)
  // Ash system frames differ from ChromeOS browser frames.
  mixer[kColorPwaTheme] = {chromeos::kDefaultFrameColor};
#else
  mixer[kColorPwaTheme] = theme_color ? ui::ColorTransform(theme_color.value())
                                      : default_background;
#endif
  mixer[kColorPwaToolbarBackground] = {ui::kColorEndpointBackground};
  mixer[kColorPwaToolbarButtonIcon] =
      ui::DeriveDefaultIconColor(ui::kColorEndpointForeground);
  mixer[kColorPwaToolbarButtonIconDisabled] =
      ui::SetAlpha(kColorPwaToolbarButtonIcon, gfx::kDisabledControlAlpha);
  if (bg_color) {
    mixer[kColorWebContentsBackground] =
        ui::SetAlpha(kColorPwaBackground, SK_AlphaOPAQUE);
  }

  mixer[kColorInfoBarBackground] = {kColorPwaToolbarBackground};
  mixer[kColorInfoBarForeground] = {kColorPwaToolbarButtonIcon};
  mixer[kColorInfoBarButtonIcon] = {kColorPwaToolbarButtonIcon};
  mixer[kColorInfoBarButtonIconDisabled] = {kColorPwaToolbarButtonIconDisabled};

  // Omnibox icon colors in PWA windows are used both for the LocationIconView
  // in the CustomTabBarView as well as page action and info icons in the title
  // bar. In case of LocationIconView, CustomTabBarView overrides the color ID
  // to use for its background, so here we define the colors to use for the
  // icons that appear in the title bar. Note that all three of these colors are
  // "background" colors, i.e. the color that is shown behind the icon/text.
  // Making them equal to the toolbar ink drop colors will make these icons look
  // similar to other icons in the PWA title bar.
  mixer[kColorOmniboxIconBackground] = {kColorToolbarInkDropHover};
  mixer[kColorOmniboxIconHover] = {kColorToolbarInkDropHover};
  mixer[kColorOmniboxIconPressed] = {kColorToolbarInkDropRipple};

  // The Material Design color mixer hardcodes various toolbar colors to certain
  // colors, ignoring the toolbar colors set in the BrowserThemePack. Since in
  // web apps the toolbar is part of the frame/titlebar, we set them to match
  // the frame colors here. Because BrowserFrameViewWin overrides
  // GetCaptionColor, special handling is needed to ensure ToolbarButton
  // foreground color matches the rest of the title bar elements on Windows.
#if BUILDFLAG(IS_WIN)
  mixer[kColorFrameCaptionActive] = {kColorCaptionForegroundActive};
  mixer[kColorFrameCaptionInactive] = {kColorCaptionForegroundInactive};
#endif  // BUILDFLAG(IS_WIN)
  mixer[kColorToolbar] = {ui::kColorFrameActive};
  mixer[kColorToolbarTextDefault] = {kColorFrameCaptionActive};
  mixer[kColorToolbarTextDisabledDefault] = {kColorFrameCaptionInactive};
  mixer[kColorToolbarButtonIconDefault] = {kColorFrameCaptionActive};
  mixer[kColorToolbarButtonIcon] = {kColorToolbarButtonIconDefault};
  mixer[kColorToolbarButtonIconHovered] = {kColorToolbarButtonIcon};
  mixer[kColorToolbarButtonIconPressed] = {kColorToolbarButtonIcon};
  // While there are separate color IDs for disabled in inactive toolbar button
  // icons, in reality toolbar buttons don't distinguish between disabled and
  // inactive states. We want to make sure that the disabled state if visually
  // distinct from the active state. On windows this is always the case for
  // kColorFrameCaptionInactive, however on other platforms this might be the
  // same color as the active caption color. So on non-windows we derive the
  // disabled color from the regular toolbar color.
#if BUILDFLAG(IS_WIN)
  mixer[kColorToolbarButtonIconDisabled] = {kColorFrameCaptionInactive};
#else
  mixer[kColorToolbarButtonIconDisabled] = {ui::GetResultingPaintColor(
      {ui::kColorSysStateDisabled}, {kColorToolbar})};
#endif
  mixer[kColorToolbarButtonIconInactive] = {kColorToolbarButtonIconDisabled};

  // App menu highlight colors in PWA window should be derived from the (active)
  // frame color, as that is what it is drawn on top of.
  mixer[kColorAppMenuHighlightDefault] =
      ui::PickGoogleColor(ui::kColorFrameActiveUnthemed, kColorToolbar,
                          color_utils::kMinimumVisibleContrastRatio);
  mixer[kColorAppMenuExpandedForegroundDefault] =
      ui::GetColorWithMaxContrast(kColorAppMenuHighlightDefault);
}

void AppBrowserController::OnReceivedInitialURL() {
  UpdateCustomTabBarVisibility(/*animate=*/false);

  // Browsers of picture in picture type already take care of setting the proper
  // window bounds.
  if (browser()->is_type_picture_in_picture()) {
    return;
  }

  // If the window bounds have not been overridden, there is no need to resize
  // the window.
  if (!browser()->bounds_overridden()) {
    return;
  }

  // The saved bounds will only be wrong if they are content bounds.
  if (!chrome::SavedBoundsAreContentBounds(browser())) {
    return;
  }

  // TODO(crbug.com/41459774): Correctly set the window size at creation time.
  // This is currently not possible because the current url is not easily known
  // at popup construction time.
  //
  // Note that any potential fix should take into account that
  // `override_bounds()` represent the outer window bounds, not the content
  // size.
  browser()->window()->SetContentsSize(browser()->override_bounds().size());
}

void AppBrowserController::OnTabInserted(content::WebContents* contents) {
  if (!contents->GetVisibleURL().is_empty() && initial_url_.is_empty()) {
    SetInitialURL(contents->GetVisibleURL());
  }

  // Collect draggable app regions if the app supports Window Controls Overlay
  // or Borderless mode. This is required in addition to the use in
  // RenderFrameCreated to handle existing web contents being reparented into an
  // app window.
  if (AppUsesWindowControlsOverlay() || AppUsesBorderlessMode()) {
    contents->SetSupportsDraggableRegions(/*supports_draggable_regions=*/true);
  }

  SetWebContentsCanAcceptLoadDrops(contents, false);
}

void AppBrowserController::OnTabRemoved(content::WebContents* contents) {
  // Stop collecting draggable app regions when the web contents is removed
  // since it may be reparented to a tab in the browser.
  contents->SetSupportsDraggableRegions(/*supports_draggable_regions=*/false);

  SetWebContentsCanAcceptLoadDrops(contents, true);
}

ui::ImageModel AppBrowserController::GetFallbackAppIcon() const {
  TRACE_EVENT0("ui", "TaskManagerView::GetFallbackAppIcon");
  gfx::ImageSkia page_icon = browser()->GetCurrentPageIcon().AsImageSkia();
  if (!page_icon.isNull()) {
#if BUILDFLAG(IS_CHROMEOS)
    return ui::ImageModel::FromImageSkia(
        apps::CreateStandardIconImage(page_icon));
#else
    return ui::ImageModel::FromImageSkia(page_icon);
#endif
  }

  // The icon may be loading still. Return a transparent icon rather
  // than using a placeholder to avoid flickering.
  SkBitmap bitmap;
  bitmap.allocN32Pixels(gfx::kFaviconSize, gfx::kFaviconSize);
  bitmap.eraseColor(SK_ColorTRANSPARENT);
  return ui::ImageModel::FromImageSkia(
      gfx::ImageSkia::CreateFrom1xBitmap(bitmap));
}

void AppBrowserController::DraggableRegionsChanged(
    const std::vector<blink::mojom::DraggableRegionPtr>& regions,
    content::WebContents* contents) {
  content::WebContents* active_contents =
      browser()->tab_strip_model()->GetActiveWebContents();

  if (contents != active_contents) {
    return;
  }

  SkRegion sk_region;
  for (const blink::mojom::DraggableRegionPtr& region : regions) {
    sk_region.op(
        SkIRect::MakeLTRB(region->bounds.x(), region->bounds.y(),
                          region->bounds.x() + region->bounds.width(),
                          region->bounds.y() + region->bounds.height()),
        region->draggable ? SkRegion::kUnion_Op : SkRegion::kDifference_Op);
  }

  draggable_region_ = sk_region;

  if (on_draggable_region_set_for_testing_) {
    std::move(on_draggable_region_set_for_testing_).Run();
  }
}

void AppBrowserController::SetOnUpdateDraggableRegionForTesting(
    base::OnceClosure done) {
  on_draggable_region_set_for_testing_ = std::move(done);
}

void AppBrowserController::MaybeSetInitialUrlOnReparentTab() {
  if (initial_url_.is_empty() || !IsUrlInAppScope(initial_url_)) {
    initial_url_ = GURL();
    SetInitialURL(GetAppStartUrl());
  }
}

void AppBrowserController::UpdateThemePack() {
  std::optional<SkColor> theme_color = GetThemeColor();

  // TODO(crbug.com/40119262): Add tests for theme properties being set in this
  // branch.
  std::optional<SkColor> background_color = GetBackgroundColor();
  if (theme_color == last_theme_color_ &&
      background_color == last_background_color_) {
    return;
  }
  last_theme_color_ = theme_color;
  last_background_color_ = background_color;

  bool ignore_custom_colors = false;
#if BUILDFLAG(IS_CHROMEOS)
  // Some system web apps use the system theme color, and should not update
  // the theme pack here. Otherwise the colorIds for the window caption bar will
  // be remapped through `BrowserThemePack::BuildFromColors`, and colors will be
  // resolved differently than the colors set in the function `AddUiColorMixer`.
  if (system_app() && system_app()->UseSystemThemeColor()) {
    ignore_custom_colors = true;
  }
#endif  // BUILDFLAG(IS_CHROMEOS)

  bool no_custom_colors = !theme_color && !background_color;
  bool non_tabbed_no_frame_color = !has_tab_strip_ && !theme_color;

  if (ignore_custom_colors || no_custom_colors || non_tabbed_no_frame_color) {
    theme_pack_ = nullptr;
    if (browser_->window()) {
      browser_->window()->UserChangedTheme(
          BrowserThemeChangeType::kWebAppTheme);
    }
    return;
  }

  if (!theme_color) {
    theme_color = GetAltColor(*background_color);
  } else if (!background_color) {
    background_color =
        (ui::NativeTheme::GetInstanceForNativeUi()->preferred_color_scheme() ==
         ui::NativeTheme::PreferredColorScheme::kDark)
            ? gfx::kGoogleGrey900
            : SK_ColorWHITE;
  }

  theme_pack_ = base::MakeRefCounted<BrowserThemePack>(
      ui::ColorProviderKey::ThemeInitializerSupplier::ThemeType::
          kAutogenerated);
  BrowserThemePack::BuildFromWebAppColors(*theme_color, *background_color,
                                          theme_pack_.get());
  if (browser_->window()) {
    browser_->window()->UserChangedTheme(BrowserThemeChangeType::kWebAppTheme);
  }
}

void AppBrowserController::SetInitialURL(const GURL& initial_url) {
  DCHECK(initial_url_.is_empty());
  initial_url_ = initial_url;

  OnReceivedInitialURL();
}

}  // namespace web_app