// 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_) {
const PrefService::Preference* pref =
active_user_pref_service_->FindPreference(
prefs::kDarkModeScheduleType);
// Managed users do not see the theme selection screen, so to avoid
// confusion they should always see light colors during OOBE
if (pref->IsManaged() || pref->IsRecommended()) {
return false;
}
if (!active_user_pref_service_->GetBoolean(prefs::kDarkModeEnabled)) {
return false;
}
}
return base::Contains(kStatesSupportingDarkTheme, oobe_state_);
}
// 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.
if (!active_user_pref_service_ &&
is_dark_mode_enabled_for_focused_pod_.has_value()) {
return is_dark_mode_enabled_for_focused_pod_.value();
}
// Keep the color mode as DARK in login screen.
if (!active_user_pref_service_) {
return true;
}
return active_user_pref_service_->GetBoolean(prefs::kDarkModeEnabled);
}
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