|  | // 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/dark_light_mode_controller_impl.h" | 
|  |  | 
|  | #include "ash/constants/ash_constants.h" | 
|  | #include "ash/constants/ash_features.h" | 
|  | #include "ash/constants/ash_pref_names.h" | 
|  | #include "ash/login/login_screen_controller.h" | 
|  | #include "ash/public/cpp/schedule_enums.h" | 
|  | #include "ash/public/cpp/style/color_mode_observer.h" | 
|  | #include "ash/session/session_controller_impl.h" | 
|  | #include "ash/shell.h" | 
|  | #include "ash/style/color_util.h" | 
|  | #include "components/account_id/account_id.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 "ui/chromeos/styles/cros_styles.h" | 
|  |  | 
|  | namespace ash { | 
|  |  | 
|  | namespace { | 
|  |  | 
|  | DarkLightModeControllerImpl* g_instance = nullptr; | 
|  |  | 
|  | // An array of OOBE screens which currently support dark theme. | 
|  | // In the future additional screens will be added. Eventually all screens | 
|  | // will support it and this array will not be needed anymore. | 
|  | constexpr OobeDialogState kStatesSupportingDarkTheme[] = { | 
|  | OobeDialogState::MARKETING_OPT_IN, OobeDialogState::THEME_SELECTION, | 
|  | OobeDialogState::CHOOBE}; | 
|  |  | 
|  | }  // namespace | 
|  |  | 
|  | DarkLightModeControllerImpl::DarkLightModeControllerImpl() | 
|  | : ScheduledFeature(prefs::kDarkModeEnabled, | 
|  | prefs::kDarkModeScheduleType, | 
|  | std::string(), | 
|  | std::string()) { | 
|  | DCHECK(!g_instance); | 
|  | g_instance = this; | 
|  |  | 
|  | // May be null in unit tests. | 
|  | if (Shell::HasInstance()) { | 
|  | auto* shell = Shell::Get(); | 
|  | shell->login_screen_controller()->data_dispatcher()->AddObserver(this); | 
|  | } | 
|  | } | 
|  |  | 
|  | DarkLightModeControllerImpl::~DarkLightModeControllerImpl() { | 
|  | DCHECK_EQ(g_instance, this); | 
|  | g_instance = nullptr; | 
|  |  | 
|  | // May be null in unit tests. | 
|  | if (Shell::HasInstance()) { | 
|  | auto* shell = Shell::Get(); | 
|  | auto* login_screen_controller = shell->login_screen_controller(); | 
|  | auto* data_dispatcher = login_screen_controller | 
|  | ? login_screen_controller->data_dispatcher() | 
|  | : nullptr; | 
|  | if (data_dispatcher) | 
|  | data_dispatcher->RemoveObserver(this); | 
|  | } | 
|  |  | 
|  | cros_styles::SetDebugColorsEnabled(false); | 
|  | cros_styles::SetDarkModeEnabled(false); | 
|  | } | 
|  |  | 
|  | // static | 
|  | DarkLightModeControllerImpl* DarkLightModeControllerImpl::Get() { | 
|  | return g_instance; | 
|  | } | 
|  |  | 
|  | // static | 
|  | void DarkLightModeControllerImpl::RegisterProfilePrefs( | 
|  | PrefRegistrySimple* registry) { | 
|  | registry->RegisterIntegerPref( | 
|  | prefs::kDarkModeScheduleType, | 
|  | static_cast<int>(ScheduleType::kSunsetToSunrise)); | 
|  |  | 
|  | registry->RegisterBooleanPref(prefs::kDarkModeEnabled, | 
|  | kDefaultDarkModeEnabled); | 
|  | } | 
|  |  | 
|  | void DarkLightModeControllerImpl::SetAutoScheduleEnabled(bool enabled) { | 
|  | SetScheduleType(enabled ? ScheduleType::kSunsetToSunrise | 
|  | : ScheduleType::kNone); | 
|  | } | 
|  |  | 
|  | bool DarkLightModeControllerImpl::GetAutoScheduleEnabled() const { | 
|  | const ScheduleType type = GetScheduleType(); | 
|  | // `DarkLightModeControllerImpl` does not support the custom scheduling. | 
|  | DCHECK_NE(type, ScheduleType::kCustom); | 
|  | return type == ScheduleType::kSunsetToSunrise; | 
|  | } | 
|  |  | 
|  | void DarkLightModeControllerImpl::ToggleColorMode() { | 
|  | DCHECK(active_user_pref_service_); | 
|  | active_user_pref_service_->SetBoolean(prefs::kDarkModeEnabled, | 
|  | !IsDarkModeEnabled()); | 
|  | active_user_pref_service_->CommitPendingWrite(); | 
|  | NotifyColorModeChanges(); | 
|  | } | 
|  |  | 
|  | void DarkLightModeControllerImpl::AddObserver(ColorModeObserver* observer) { | 
|  | observers_.AddObserver(observer); | 
|  | } | 
|  |  | 
|  | void DarkLightModeControllerImpl::RemoveObserver(ColorModeObserver* observer) { | 
|  | observers_.RemoveObserver(observer); | 
|  | } | 
|  |  | 
|  | bool DarkLightModeControllerImpl::IsDarkModeEnabled() const { | 
|  | // Dark mode is off during OOBE when the OobeDialogState is still unknown. | 
|  | // When the SessionState is OOBE, the OobeDialogState is HIDDEN until the | 
|  | // first screen is shown. This fixes a bug that caused dark colors to be | 
|  | // flashed when OOBE is loaded. See b/260008998 | 
|  | const auto session_state = | 
|  | Shell::Get()->session_controller()->GetSessionState(); | 
|  | if (oobe_state_ == OobeDialogState::HIDDEN && | 
|  | session_state == session_manager::SessionState::OOBE) { | 
|  | return false; | 
|  | } | 
|  |  | 
|  | // Disable dark mode for Shimless RMA. | 
|  | if (session_state == session_manager::SessionState::RMA) { | 
|  | return false; | 
|  | } | 
|  |  | 
|  | if (is_dark_mode_enabled_in_oobe_for_testing_.has_value()) { | 
|  | return is_dark_mode_enabled_in_oobe_for_testing_.value(); | 
|  | } | 
|  |  | 
|  | if (oobe_state_ != OobeDialogState::HIDDEN) { | 
|  | if (active_user_pref_service_) { | 
|  | // Managed users do not see the theme selection screen, so to avoid | 
|  | // confusion they should always see light colors during OOBE. | 
|  | if (const PrefService::Preference* pref = | 
|  | active_user_pref_service_->FindPreference( | 
|  | prefs::kDarkModeScheduleType); | 
|  | pref->IsManaged() || pref->IsRecommended()) { | 
|  | return false; | 
|  | } | 
|  |  | 
|  | if (!active_user_pref_service_->GetBoolean(prefs::kDarkModeEnabled)) { | 
|  | return false; | 
|  | } | 
|  | } | 
|  | return base::Contains(kStatesSupportingDarkTheme, oobe_state_); | 
|  | } | 
|  |  | 
|  | if (active_user_pref_service_) { | 
|  | return active_user_pref_service_->GetBoolean(prefs::kDarkModeEnabled); | 
|  | } | 
|  |  | 
|  | // On the login screen use the preference of the focused pod's user if they | 
|  | // had the preference stored in the known_user and the pod is focused. | 
|  | // Otherwise keep the color mode as DARK. | 
|  | return is_dark_mode_enabled_for_focused_pod_.value_or(true); | 
|  | } | 
|  |  | 
|  | void DarkLightModeControllerImpl::SetDarkModeEnabledForTest(bool enabled) { | 
|  | if (oobe_state_ != OobeDialogState::HIDDEN) { | 
|  | auto closure = GetNotifyOnDarkModeChangeClosure(); | 
|  | is_dark_mode_enabled_in_oobe_for_testing_ = enabled; | 
|  | return; | 
|  | } | 
|  |  | 
|  | if (IsDarkModeEnabled() != enabled) { | 
|  | ToggleColorMode(); | 
|  | } | 
|  | } | 
|  |  | 
|  | void DarkLightModeControllerImpl::OnOobeDialogStateChanged( | 
|  | OobeDialogState state) { | 
|  | auto closure = GetNotifyOnDarkModeChangeClosure(); | 
|  | oobe_state_ = state; | 
|  | } | 
|  |  | 
|  | void DarkLightModeControllerImpl::OnFocusPod(const AccountId& account_id) { | 
|  | auto closure = GetNotifyOnDarkModeChangeClosure(); | 
|  |  | 
|  | if (!account_id.is_valid()) { | 
|  | is_dark_mode_enabled_for_focused_pod_.reset(); | 
|  | return; | 
|  | } | 
|  | is_dark_mode_enabled_for_focused_pod_ = | 
|  | user_manager::KnownUser(ash::Shell::Get()->local_state()) | 
|  | .FindBoolPath(account_id, prefs::kDarkModeEnabled); | 
|  | } | 
|  |  | 
|  | void DarkLightModeControllerImpl::OnActiveUserPrefServiceChanged( | 
|  | PrefService* prefs) { | 
|  | active_user_pref_service_ = prefs; | 
|  | pref_change_registrar_ = std::make_unique<PrefChangeRegistrar>(); | 
|  | pref_change_registrar_->Init(prefs); | 
|  |  | 
|  | pref_change_registrar_->Add( | 
|  | prefs::kDarkModeEnabled, | 
|  | base::BindRepeating(&DarkLightModeControllerImpl::NotifyColorModeChanges, | 
|  | base::Unretained(this))); | 
|  |  | 
|  | // Immediately tell all the observers to load this user's saved preferences. | 
|  | NotifyColorModeChanges(); | 
|  |  | 
|  | ScheduledFeature::OnActiveUserPrefServiceChanged(prefs); | 
|  | } | 
|  |  | 
|  | void DarkLightModeControllerImpl::OnSessionStateChanged( | 
|  | session_manager::SessionState state) { | 
|  | auto closure = GetNotifyOnDarkModeChangeClosure(); | 
|  | if (state != session_manager::SessionState::OOBE && | 
|  | state != session_manager::SessionState::LOGIN_PRIMARY) { | 
|  | oobe_state_ = OobeDialogState::HIDDEN; | 
|  | } | 
|  | } | 
|  |  | 
|  | const char* DarkLightModeControllerImpl::GetFeatureName() const { | 
|  | return "DarkLightModeControllerImpl"; | 
|  | } | 
|  |  | 
|  | void DarkLightModeControllerImpl::NotifyColorModeChanges() { | 
|  | const bool is_enabled = IsDarkModeEnabled(); | 
|  | cros_styles::SetDarkModeEnabled(is_enabled); | 
|  | if (last_value_ == is_enabled) { | 
|  | // Updating the pref causes a notification. Skip it if it happens. | 
|  | return; | 
|  | } | 
|  | last_value_ = is_enabled; | 
|  | for (auto& observer : observers_) { | 
|  | observer.OnColorModeChanged(is_enabled); | 
|  | } | 
|  | } | 
|  |  | 
|  | base::ScopedClosureRunner | 
|  | DarkLightModeControllerImpl::GetNotifyOnDarkModeChangeClosure() { | 
|  | return base::ScopedClosureRunner( | 
|  | // Unretained is safe here because GetNotifyOnDarkModeChangeClosure is a | 
|  | // private function and callback should be called on going out of scope of | 
|  | // the calling method. | 
|  | base::BindOnce(&DarkLightModeControllerImpl::NotifyIfDarkModeChanged, | 
|  | base::Unretained(this), IsDarkModeEnabled())); | 
|  | } | 
|  |  | 
|  | void DarkLightModeControllerImpl::NotifyIfDarkModeChanged( | 
|  | bool old_is_dark_mode_enabled) { | 
|  | // If this is the first check, always notify. | 
|  | if (last_value_.has_value() && | 
|  | old_is_dark_mode_enabled == IsDarkModeEnabled()) { | 
|  | return; | 
|  | } | 
|  | NotifyColorModeChanges(); | 
|  | } | 
|  |  | 
|  | }  // namespace ash |