chromium/ash/style/color_palette_controller.cc

// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "ash/style/color_palette_controller.h"

#include <memory>

#include "ash/constants/ash_pref_names.h"
#include "ash/login/login_screen_controller.h"
#include "ash/public/cpp/login_types.h"
#include "ash/public/cpp/session/session_observer.h"
#include "ash/public/cpp/style/color_mode_observer.h"
#include "ash/public/cpp/style/dark_light_mode_controller.h"
#include "ash/public/cpp/wallpaper/wallpaper_controller.h"
#include "ash/public/cpp/wallpaper/wallpaper_controller_observer.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shell.h"
#include "ash/style/color_util.h"
#include "ash/style/dark_light_mode_controller_impl.h"
#include "ash/style/mojom/color_scheme.mojom-shared.h"
#include "ash/wallpaper/wallpaper_controller_impl.h"
#include "base/barrier_callback.h"
#include "base/check_is_test.h"
#include "base/functional/bind.h"
#include "base/json/values_util.h"
#include "base/logging.h"
#include "base/memory/raw_ptr.h"
#include "base/observer_list.h"
#include "base/scoped_observation.h"
#include "base/task/bind_post_task.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/task_runner.h"
#include "base/task/thread_pool.h"
#include "base/time/time.h"
#include "components/pref_registry/pref_registry_syncable.h"
#include "components/prefs/pref_change_registrar.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"
#include "components/user_manager/known_user.h"
#include "third_party/skia/include/core/SkColor.h"
#include "ui/color/color_provider_key.h"
#include "ui/color/dynamic_color/palette.h"
#include "ui/color/dynamic_color/palette_factory.h"
#include "ui/gfx/color_palette.h"

namespace ash {

namespace {

class ColorPaletteControllerImpl;

using ColorMode = ui::ColorProviderKey::ColorMode;

const SkColor kDefaultWallpaperColor = gfx::kGoogleBlue400;

PrefService* GetUserPrefService(const AccountId& account_id) {
  if (!account_id.is_valid()) {
    CHECK_IS_TEST();
    return nullptr;
  }
  return Shell::Get()->session_controller()->GetUserPrefServiceForUser(
      account_id);
}

// Returns the currently active user session (at index 0).
const UserSession* GetActiveUserSession() {
  return Shell::Get()->session_controller()->GetUserSession(/*index=*/0);
}

const AccountId& AccountFromSession(const UserSession* session) {
  CHECK(session);
  return session->user_info.account_id;
}

using SchemeVariant = ui::ColorProviderKey::SchemeVariant;

SchemeVariant ToVariant(style::mojom::ColorScheme scheme) {
  switch (scheme) {
    case style::mojom::ColorScheme::kStatic:
    case style::mojom::ColorScheme::kNeutral:
      return SchemeVariant::kNeutral;
    case style::mojom::ColorScheme::kTonalSpot:
      return SchemeVariant::kTonalSpot;
    case style::mojom::ColorScheme::kExpressive:
      return SchemeVariant::kExpressive;
    case style::mojom::ColorScheme::kVibrant:
      return SchemeVariant::kVibrant;
  }
}

SampleColorScheme GenerateSampleColorScheme(bool dark,
                                            SkColor seed_color,
                                            style::mojom::ColorScheme scheme) {
  DCHECK_NE(scheme, style::mojom::ColorScheme::kStatic)
      << "Requesting a static scheme doesn't make sense since there is no "
         "seed color";

  std::unique_ptr<ui::Palette> palette =
      ui::GeneratePalette(seed_color, ToVariant(scheme));
  SampleColorScheme sample;
  sample.scheme = scheme;
  // Tertiary is cros.ref.teratiary-70 for all color schemes.
  sample.tertiary = palette->tertiary().get(70.f);  // tertiary 70

  if (scheme == style::mojom::ColorScheme::kVibrant) {
    // Vibrant uses cros.ref.primary-70 and cros.ref.primary-50.
    sample.primary = palette->primary().get(70.f);    // primary 70
    sample.secondary = palette->primary().get(50.f);  // primary 50
  } else {
    // All other schemes use cros.ref.primary-80 and cros.ref.primary-60.
    sample.primary = palette->primary().get(80.f);    // primary 80
    sample.secondary = palette->primary().get(60.f);  // primary 60
  }

  return sample;
}

void SortSampleColorSchemes(
    ColorPaletteController::SampleColorSchemeCallback callback,
    base::span<const style::mojom::ColorScheme> color_scheme_buttons,
    std::vector<SampleColorScheme> sample_color_schemes) {
  std::vector<SampleColorScheme> sorted_sample_color_schemes;
  for (const auto scheme : color_scheme_buttons) {
    auto color_scheme_sample =
        std::find_if(sample_color_schemes.begin(), sample_color_schemes.end(),
                     [&scheme](SampleColorScheme sample) {
                       return scheme == sample.scheme;
                     });
    sorted_sample_color_schemes.push_back(*color_scheme_sample);
  }
  std::move(callback).Run(sorted_sample_color_schemes);
}

// Refresh colors of the system on the current color mode. Not only the SysUI,
// but also all the other components like WebUI. This will trigger
// View::OnThemeChanged to live update the colors. The colors live update can
// happen when color mode changes or wallpaper changes. It is needed when
// wallpaper changes as the background color is calculated from current
// wallpaper.
void RefreshNativeTheme(const ColorPaletteSeed& seed) {
  const SkColor themed_color = seed.seed_color;
  bool is_dark_mode_enabled = seed.color_mode == ColorMode::kDark;
  auto* native_theme = ui::NativeTheme::GetInstanceForNativeUi();
  native_theme->set_use_dark_colors(is_dark_mode_enabled);
  native_theme->set_user_color(themed_color);
  native_theme->set_scheme_variant(ToVariant(seed.scheme));
  native_theme->NotifyOnNativeThemeUpdated();

  auto* native_theme_web = ui::NativeTheme::GetInstanceForWeb();
  if (!native_theme_web->IsForcedDarkMode()) {
    native_theme_web->set_use_dark_colors(is_dark_mode_enabled);
    native_theme_web->set_preferred_color_scheme(
        is_dark_mode_enabled ? ui::NativeTheme::PreferredColorScheme::kDark
                             : ui::NativeTheme::PreferredColorScheme::kLight);
  }
  native_theme_web->set_scheme_variant(ToVariant(seed.scheme));
  native_theme_web->set_user_color(themed_color);
  native_theme_web->NotifyOnNativeThemeUpdated();
}

class ColorPaletteControllerImpl : public ColorPaletteController,
                                   public WallpaperControllerObserver,
                                   public ColorModeObserver {
 public:
  ColorPaletteControllerImpl(
      DarkLightModeController* dark_light_mode_controller,
      WallpaperControllerImpl* wallpaper_controller,
      PrefService* local_state)
      : wallpaper_controller_(wallpaper_controller),
        dark_light_mode_controller_(dark_light_mode_controller),
        local_state_(local_state) {
    dark_light_observation_.Observe(dark_light_mode_controller);
    wallpaper_observation_.Observe(wallpaper_controller);
    Shell::Get()->login_screen_controller()->data_dispatcher()->AddObserver(
        this);
    if (!local_state) {
      // The local state should only be null in tests.
      CHECK_IS_TEST();
    }
  }
  ~ColorPaletteControllerImpl() override {
    DCHECK_EQ(0, notification_pauser_count_);
  }

  void AddObserver(Observer* observer) override {
    observers_.AddObserver(observer);
  }

  void RemoveObserver(Observer* observer) override {
    observers_.RemoveObserver(observer);
  }

  void SetColorScheme(style::mojom::ColorScheme scheme,
                      const AccountId& account_id,
                      base::OnceClosure on_complete) override {
    DVLOG(1) << "Setting color scheme to: " << (int)scheme;
    PrefService* pref_service = GetUserPrefService(account_id);
    if (!pref_service) {
      DVLOG(1) << "No user pref service available.";
      return;
    }
    pref_service->SetInteger(prefs::kDynamicColorColorScheme,
                             static_cast<int>(scheme));
    NotifyObservers(GetColorPaletteSeed(account_id));
    base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
        FROM_HERE, std::move(on_complete));
  }

  void SetStaticColor(SkColor seed_color,
                      const AccountId& account_id,
                      base::OnceClosure on_complete) override {
    DVLOG(1) << "Static color scheme: " << (int)seed_color;
    PrefService* pref_service = GetUserPrefService(account_id);
    if (!pref_service) {
      DVLOG(1) << "No user pref service available.";
      return;
    }
    // Set the color scheme before the seed color because there is a check in
    // |GetStaticColor| to only return a color if the color scheme is kStatic.
    pref_service->SetInteger(
        prefs::kDynamicColorColorScheme,
        static_cast<int>(style::mojom::ColorScheme::kStatic));
    pref_service->SetUint64(prefs::kDynamicColorSeedColor, seed_color);
    NotifyObservers(GetColorPaletteSeed(account_id));
    base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
        FROM_HERE, std::move(on_complete));
  }

  std::optional<ColorPaletteSeed> GetColorPaletteSeed(
      const AccountId& account_id) const override {
    ColorPaletteSeed seed;
    std::optional<SkColor> seed_color =
        UsesWallpaperSeedColor(account_id)
            ? GetWallpaperColorForUser(account_id)
            : GetStaticSeedColor(account_id);
    if (!seed_color) {
      return {};
    }

    seed.color_mode = dark_light_mode_controller_->IsDarkModeEnabled()
                          ? ui::ColorProviderKey::ColorMode::kDark
                          : ui::ColorProviderKey::ColorMode::kLight;
    seed.seed_color = *seed_color;
    seed.scheme = GetColorScheme(account_id);

    return seed;
  }

  std::optional<SkColor> GetWallpaperColorForUser(
      const AccountId& account_id) const {
    if (GetActiveUserSession()) {
      return CurrentWallpaperColor(
          dark_light_mode_controller_->IsDarkModeEnabled());
    }
    const bool should_use_k_means = ShouldUseKMeans(account_id);
    std::optional<SkColor> seed_color =
        wallpaper_controller_->GetCachedWallpaperColorForUser(
            account_id, should_use_k_means);
    if (seed_color.has_value()) {
      if (should_use_k_means) {
        bool dark = dark_light_mode_controller_->IsDarkModeEnabled();
        return ColorUtil::AdjustKMeansColor(seed_color.value(), dark);
      }
      return seed_color.value();
    }
    DVLOG(1)
        << "No wallpaper color for user. Returning default wallpaper color.";
    return kDefaultWallpaperColor;
  }

  std::optional<ColorPaletteSeed> GetCurrentSeed() const override {
    const auto* session = GetActiveUserSession();
    if (!session) {
      return {};
    }

    return GetColorPaletteSeed(AccountFromSession(session));
  }

  bool UsesWallpaperSeedColor(const AccountId& account_id) const override {
    // Scheme tracks if wallpaper color is used.
    return GetColorScheme(account_id) != style::mojom::ColorScheme::kStatic;
  }

  style::mojom::ColorScheme GetColorScheme(
      const AccountId& account_id) const override {
    PrefService* pref_service = GetUserPrefService(account_id);
    if (pref_service) {
      const PrefService::Preference* pref =
          pref_service->FindPreference(prefs::kDynamicColorColorScheme);
      if (!pref->IsDefaultValue()) {
        return static_cast<style::mojom::ColorScheme>(
            pref->GetValue()->GetInt());
      }
    } else {
      CHECK(local_state_);
      const auto scheme =
          user_manager::KnownUser(local_state_)
              .FindIntPath(account_id, prefs::kDynamicColorColorScheme);
      if (scheme.has_value()) {
        return static_cast<style::mojom::ColorScheme>(scheme.value());
      }
    }

    DVLOG(1) << "No user pref service or local pref service available. "
                "Returning default color scheme.";
    // The preferred default color scheme for the time of day wallpaper instead
    // of tonal spot.
    return features::IsTimeOfDayWallpaperEnabled() &&
                   wallpaper_controller_->IsTimeOfDayWallpaper()
               ? style::mojom::ColorScheme::kNeutral
               : style::mojom::ColorScheme::kTonalSpot;
  }

  std::optional<SkColor> GetStaticColor(
      const AccountId& account_id) const override {
    if (GetColorScheme(account_id) == style::mojom::ColorScheme::kStatic) {
      return GetStaticSeedColor(account_id);
    }

    return std::nullopt;
  }

  void GenerateSampleColorSchemes(
      base::span<const style::mojom::ColorScheme> color_scheme_buttons,
      SampleColorSchemeCallback callback) const override {
    bool dark = dark_light_mode_controller_->IsDarkModeEnabled();
    std::optional<SkColor> celebi_seed_color = GetCurrentCelebiColor();
    if (!celebi_seed_color) {
      LOG(WARNING) << "Using default color due to missing wallpaper sample";
      celebi_seed_color.emplace(kDefaultWallpaperColor);
    }
    CHECK(celebi_seed_color.has_value());
    auto* session = GetActiveUserSession();
    DCHECK(session);
    auto account_id = AccountFromSession(session);
    bool use_k_means = GetUseKMeansPref(account_id);
    const SkColor tonal_spot_color =
        use_k_means ? GetCurrentKMeanColor() : *celebi_seed_color;
    // Schemes need to be copied as the underlying memory for the span could go
    // out of scope.
    const std::vector<style::mojom::ColorScheme> schemes_copy(
        color_scheme_buttons.begin(), color_scheme_buttons.end());
    const auto barrier_callback = base::BarrierCallback<SampleColorScheme>(
        color_scheme_buttons.size(),
        base::BindOnce(&SortSampleColorSchemes,
                       base::BindPostTaskToCurrentDefault(std::move(callback)),
                       std::move(schemes_copy)));

    for (unsigned int i = 0; i < color_scheme_buttons.size(); i++) {
      SkColor seed_color =
          color_scheme_buttons[i] == style::mojom::ColorScheme::kTonalSpot
              ? tonal_spot_color
              : *celebi_seed_color;
      base::ThreadPool::PostTaskAndReplyWithResult(
          FROM_HERE,
          base::BindOnce(&GenerateSampleColorScheme, dark, seed_color,
                         color_scheme_buttons[i]),
          base::BindOnce(std::move(barrier_callback)));
    }
  }

  // LoginDataDispatcher::Observer overrides:
  void OnOobeDialogStateChanged(OobeDialogState state) override {
    oobe_state_ = state;
  }

  // WallpaperControllerObserver overrides:
  void OnWallpaperColorsChanged() override {
    NotifyObservers(BestEffortSeed(GetActiveUserSession()));
  }

  // WallpaperControllerObserver overrides:
  void OnUserSetWallpaper(const AccountId& account_id) override {
    if (oobe_state_ == OobeDialogState::HIDDEN) {
      UpdateUseKMeanColor(account_id, /* value= */ false);
    }
  }

  void SelectLocalAccount(const AccountId& account_id) override {
    NotifyObservers(GetColorPaletteSeed(account_id));
  }

  // ColorModeObserver overrides:
  void OnColorModeChanged(bool) override {
    NotifyObservers(BestEffortSeed(GetActiveUserSession()));
  }

  // SessionObserver overrides:
  void OnActiveUserPrefServiceChanged(PrefService* prefs) override {
    ScopedNotificationPauser scoped_notification_pauser(this);

    MaybeSetUseKMeansPref(prefs);
    NotifyObservers(BestEffortSeed(GetActiveUserSession()));

    pref_change_registrar_ = std::make_unique<PrefChangeRegistrar>();
    pref_change_registrar_->Init(prefs);
    OnColorSchemePrefChanged();
    OnSeedColorPrefChanged();
    OnUseKMeansPrefChanged();

    pref_change_registrar_->Add(
        prefs::kDynamicColorColorScheme,
        base::BindRepeating(
            &ColorPaletteControllerImpl::OnColorSchemePrefChanged,
            base::Unretained(this)));
    pref_change_registrar_->Add(
        prefs::kDynamicColorSeedColor,
        base::BindRepeating(&ColorPaletteControllerImpl::OnSeedColorPrefChanged,
                            base::Unretained(this)));
    pref_change_registrar_->Add(
        prefs::kDynamicColorUseKMeans,
        base::BindRepeating(&ColorPaletteControllerImpl::OnUseKMeansPrefChanged,
                            base::Unretained(this)));
  }

  // Sets the UseKMeans pref to false if the user is new, and
  // true if the user does not have a set pref. This will give users updating
  // the device the k means color for tonal spot, while new users will only see
  // celebi colors.
  void MaybeSetUseKMeansPref(PrefService* prefs) {
    DCHECK(prefs);
    auto* session = GetActiveUserSession();
    DCHECK(session);
    if (session->user_info.is_ephemeral) {
      return;
    }

    auto account_id = AccountFromSession(session);
    if (Shell::Get()->session_controller()->IsUserFirstLogin()) {
      // New users should use celebi color calculation.
      UpdateUseKMeanColor(account_id, /* value= */ false);
      return;
    }
    const PrefService::Preference* pref =
        prefs->FindPreference(prefs::kDynamicColorUseKMeans);
    if (pref->IsDefaultValue()) {
      // Any user without a set pref is an existing user logging in for the
      // first time after updating their device. They should use the kmeans
      // color calculation.
      UpdateUseKMeanColor(account_id, /* value= */ true);
    }
  }

  SkColor GetUserWallpaperColorOrDefault(SkColor default_color) const override {
    const auto& calculated_colors = wallpaper_controller_->calculated_colors();
    if (!calculated_colors) {
      DVLOG(1) << "Failed to get wallpaper color";
      const bool dark_mode = dark_light_mode_controller_->IsDarkModeEnabled();
      return ColorUtil::AdjustKMeansColor(default_color, dark_mode);
    }

    std::optional<AccountId> account_id;
    auto* session = GetActiveUserSession();
    if (session) {
      account_id = AccountFromSession(session);
    }

    // Return KMeans if the account is kept in legacy mode i.e. they haven't
    // changed their wallpaper since the new sampling algorithm was introduced.
    if (account_id.has_value() && ShouldUseKMeans(*account_id)) {
      return GetCurrentKMeanColor();
    }

    const auto celebi_color = GetCurrentCelebiColor();
    if (celebi_color.has_value()) {
      return *celebi_color;
    }
    DVLOG(1) << "Failed to get wallpaper color";
    return default_color;
  }

  bool GetUseKMeansPref(const AccountId& account_id) const override {
    PrefService* pref_service = GetUserPrefService(account_id);
    if (pref_service) {
      return pref_service->GetBoolean(prefs::kDynamicColorUseKMeans);
    }
    CHECK(local_state_);
    const base::Value* value =
        user_manager::KnownUser(local_state_)
            .FindPath(account_id, prefs::kDynamicColorUseKMeans);
    if (value && value->GetIfBool().has_value()) {
      return value->GetBool();
    }
    DVLOG(1) << "No user pref service or local pref service available. "
                "Returning UseKMeans pref as false.";
    return false;
  }

 private:
  // Helper class to pause observer notifications when there is one instance
  // is live. The last one of the skipped notifications is sent out when the
  // last instances of this class is going away.
  class ScopedNotificationPauser {
   public:
    explicit ScopedNotificationPauser(ColorPaletteControllerImpl* controller)
        : controller_(controller) {
      controller_->AddNotificationPauser();
    }
    ~ScopedNotificationPauser() { controller_->RemoveNotificationPauser(); }

   private:
    // `controller_` must out live this class.
    const raw_ptr<ColorPaletteControllerImpl> controller_;
  };

  // Gets the user's current wallpaper color.
  // TODO(b/289106519): Combine this function with |GetUserWallpaperColor|.
  std::optional<SkColor> CurrentWallpaperColor(bool dark) const {
    const std::optional<WallpaperCalculatedColors>& calculated_colors =
        wallpaper_controller_->calculated_colors();
    if (!calculated_colors) {
      return {};
    }
    auto* session = GetActiveUserSession();
    if (session) {
      auto account_id = AccountFromSession(session);
      if (ShouldUseKMeans(account_id)) {
        return GetCurrentKMeanColor();
      }
    }
    return GetCurrentCelebiColor();
  }

  std::optional<SkColor> GetCurrentCelebiColor() const {
    const std::optional<WallpaperCalculatedColors>& calculated_colors =
        wallpaper_controller_->calculated_colors();
    if (!calculated_colors) {
      return {};
    }
    return calculated_colors->celebi_color;
  }

  SkColor GetCurrentKMeanColor() const {
    bool dark = dark_light_mode_controller_->IsDarkModeEnabled();
    const SkColor default_color = dark ? gfx::kGoogleGrey900 : SK_ColorWHITE;
    SkColor k_mean_color = wallpaper_controller_->GetKMeanColor();
    SkColor color =
        k_mean_color == kInvalidWallpaperColor ? default_color : k_mean_color;
    return ColorUtil::AdjustKMeansColor(color, dark);
  }

  bool ShouldUseKMeans(const AccountId& account_id) const {
    return GetColorScheme(account_id) ==
               style::mojom::ColorScheme::kTonalSpot &&
           GetUseKMeansPref(account_id);
  }

  void UpdateUseKMeanColor(const AccountId& account_id, bool value) {
    PrefService* pref_service = GetUserPrefService(account_id);
    if (pref_service) {
      pref_service->SetBoolean(prefs::kDynamicColorUseKMeans, value);
    }
  }

  SkColor GetStaticSeedColor(const AccountId& account_id) const {
    PrefService* pref_service = GetUserPrefService(account_id);
    if (pref_service) {
      return static_cast<SkColor>(
          pref_service->GetUint64(prefs::kDynamicColorSeedColor));
    }
    CHECK(local_state_);
    const base::Value* value =
        user_manager::KnownUser(local_state_)
            .FindPath(account_id, prefs::kDynamicColorSeedColor);
    if (value) {
      const auto seed_color = base::ValueToInt64(value);
      if (seed_color.has_value()) {
        return static_cast<SkColor>(seed_color.value());
      }
    }
    DVLOG(1) << "No user pref service or local pref service available. "
                "Returning default color palette seed.";
    return kDefaultWallpaperColor;
  }

  // Returns the seed for `session` if it's present.  Otherwise, returns a seed
  // for backward compatibility with just dark/light and seed color filled.
  std::optional<ColorPaletteSeed> BestEffortSeed(const UserSession* session) {
    if (session) {
      return GetColorPaletteSeed(AccountFromSession(session));
    }

    session_manager::SessionState session_state =
        Shell::Get()->session_controller()->GetSessionState();
    const bool is_oobe =
        session_state == session_manager::SessionState::OOBE ||
        (session_state == session_manager::SessionState::LOGIN_PRIMARY &&
         oobe_state_ != OobeDialogState::HIDDEN);

    if (!is_oobe) {
      // This early return prevents overwriting colors. OOBE has a special
      // wallpaper and needs to sample the color. In any other case, like on the
      // login screen, the calculated colors may not have been updated and so
      // the colors should not be updated.
      return {};
    }

    // Generate a seed where we assume TonalSpot and ignore static colors.
    ColorPaletteSeed seed;
    bool dark = dark_light_mode_controller_->IsDarkModeEnabled();
    // The user is in OOBE and should see the celebi color.
    std::optional<SkColor> seed_color = GetCurrentCelebiColor();
    if (!seed_color) {
      // If `seed_color` is not available, we expect to have it shortly
      // the color computation is done and this will be called again.
      return {};
    }
    seed.color_mode = dark ? ui::ColorProviderKey::ColorMode::kDark
                           : ui::ColorProviderKey::ColorMode::kLight;
    seed.seed_color = *seed_color;
    seed.scheme = style::mojom::ColorScheme::kTonalSpot;

    return seed;
  }

  void NotifyObservers(const std::optional<ColorPaletteSeed>& seed) {
    if (notification_pauser_count_) {
      last_notification_seed_ = seed;
      return;
    }

    if (!seed) {
      // If the seed wasn't valid, skip notifications.
      return;
    }

    for (auto& observer : observers_) {
      observer.OnColorPaletteChanging(*seed);
    }

    RefreshNativeTheme(*seed);
  }

  void OnColorSchemePrefChanged() {
    if (!local_state_) {
      CHECK_IS_TEST();
      return;
    }
    auto account_id = AccountFromSession(GetActiveUserSession());
    auto color_scheme = GetColorScheme(account_id);
    user_manager::KnownUser(local_state_)
        .SetIntegerPref(account_id, prefs::kDynamicColorColorScheme,
                        static_cast<int>(color_scheme));
    NotifyObservers(BestEffortSeed(GetActiveUserSession()));
  }

  void OnSeedColorPrefChanged() {
    if (!local_state_) {
      CHECK_IS_TEST();
      return;
    }
    auto account_id = AccountFromSession(GetActiveUserSession());
    auto seed_color = GetStaticSeedColor(account_id);
    user_manager::KnownUser(local_state_)
        .SetPath(account_id, prefs::kDynamicColorSeedColor,
                 base::Int64ToValue(seed_color));
    NotifyObservers(BestEffortSeed(GetActiveUserSession()));
  }

  void OnUseKMeansPrefChanged() {
    if (!local_state_) {
      CHECK_IS_TEST();
      return;
    }
    auto account_id = AccountFromSession(GetActiveUserSession());
    auto use_k_means = GetUseKMeansPref(account_id);
    user_manager::KnownUser(local_state_)
        .SetBooleanPref(account_id, prefs::kDynamicColorUseKMeans, use_k_means);
    NotifyObservers(BestEffortSeed(GetActiveUserSession()));
  }

  void AddNotificationPauser() { ++notification_pauser_count_; }

  void RemoveNotificationPauser() {
    --notification_pauser_count_;

    if (notification_pauser_count_ == 0 && last_notification_seed_) {
      NotifyObservers(last_notification_seed_);
      last_notification_seed_.reset();
    }
  }

  base::ScopedObservation<DarkLightModeController, ColorModeObserver>
      dark_light_observation_{this};

  base::ScopedObservation<WallpaperController, WallpaperControllerObserver>
      wallpaper_observation_{this};

  ScopedSessionObserver scoped_session_observer_{this};

  raw_ptr<WallpaperControllerImpl> wallpaper_controller_;  // unowned

  raw_ptr<DarkLightModeController> dark_light_mode_controller_;  // unowned

  // May be null in tests.
  const raw_ptr<PrefService> local_state_;

  base::ObserverList<ColorPaletteController::Observer> observers_;

  std::unique_ptr<PrefChangeRegistrar> pref_change_registrar_;

  OobeDialogState oobe_state_ = OobeDialogState::HIDDEN;

  // Number of live ScopedNotificationPausers.
  int notification_pauser_count_ = 0;
  std::optional<ColorPaletteSeed> last_notification_seed_;
};

}  // namespace

// static
std::unique_ptr<ColorPaletteController> ColorPaletteController::Create(
    DarkLightModeController* dark_light_mode_controller,
    WallpaperControllerImpl* wallpaper_controller,
    PrefService* local_state) {
  return std::make_unique<ColorPaletteControllerImpl>(
      dark_light_mode_controller, wallpaper_controller, local_state);
}

// static
void ColorPaletteController::RegisterPrefs(PrefRegistrySimple* registry) {
  registry->RegisterIntegerPref(
      prefs::kDynamicColorColorScheme,
      static_cast<int>(style::mojom::ColorScheme::kTonalSpot),
      user_prefs::PrefRegistrySyncable::SYNCABLE_OS_PREF);
  registry->RegisterUint64Pref(
      prefs::kDynamicColorSeedColor, 0,
      user_prefs::PrefRegistrySyncable::SYNCABLE_OS_PREF);
  registry->RegisterBooleanPref(
      prefs::kDynamicColorUseKMeans, false,
      user_prefs::PrefRegistrySyncable::SYNCABLE_OS_PREF);
}

// static
void ColorPaletteController::RegisterLocalStatePrefs(
    PrefRegistrySimple* registry) {
  registry->RegisterIntegerPref(
      prefs::kDynamicColorColorScheme,
      static_cast<int>(style::mojom::ColorScheme::kTonalSpot));
  registry->RegisterUint64Pref(prefs::kDynamicColorSeedColor, 0);
  registry->RegisterBooleanPref(prefs::kDynamicColorUseKMeans, false);
}

}  // namespace ash