// Copyright 2017 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <cmath>
#include <limits>
#include <optional>
#include <sstream>
#include <string>
#include "ash/constants/ash_features.h"
#include "ash/constants/ash_pref_names.h"
#include "ash/display/cursor_window_controller.h"
#include "ash/display/window_tree_host_manager.h"
#include "ash/public/cpp/session/session_types.h"
#include "ash/root_window_controller.h"
#include "ash/session/session_controller_impl.h"
#include "ash/session/test_session_controller_client.h"
#include "ash/shell.h"
#include "ash/system/geolocation/geolocation_controller.h"
#include "ash/system/geolocation/geolocation_controller_test_util.h"
#include "ash/system/geolocation/test_geolocation_url_loader_factory.h"
#include "ash/system/night_light/night_light_controller_impl.h"
#include "ash/system/scheduled_feature/scheduled_feature.h"
#include "ash/system/time/calendar_unittest_utils.h"
#include "ash/system/time/time_of_day.h"
#include "ash/test/ash_test_base.h"
#include "ash/test/ash_test_helper.h"
#include "ash/test/time_of_day_test_util.h"
#include "ash/test_shell_delegate.h"
#include "base/command_line.h"
#include "base/functional/bind.h"
#include "base/memory/ptr_util.h"
#include "base/memory/raw_ptr.h"
#include "base/strings/pattern.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/simple_test_clock.h"
#include "base/test/simple_test_tick_clock.h"
#include "base/time/time.h"
#include "chromeos/ash/components/geolocation/simple_geolocation_provider.h"
#include "components/prefs/pref_service.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/compositor/layer.h"
#include "ui/display/manager/display_change_observer.h"
#include "ui/display/manager/display_manager.h"
#include "ui/display/manager/test/action_logger_util.h"
#include "ui/display/manager/test/fake_display_snapshot.h"
#include "ui/display/manager/test/test_native_display_delegate.h"
#include "ui/gfx/geometry/vector3d_f.h"
#include "ui/message_center/public/cpp/notification.h"
namespace ash {
namespace {
constexpr char kUser1Email[] = "user1@nightlight";
constexpr char kUser2Email[] = "user2@nightlight";
constexpr char kPDTTimezone[] = "America/Los_Angeles";
constexpr char kEDTTimezone[] = "America/New_York";
// All sunrise/set times are based on the timestamp returned by
// `GetMidnightForTestGeopositions()`.
//
// San Jose. Sunrise is approximately 7:00 AM and sunset is approximately
// 7:00 PM PDT.
constexpr SimpleGeoposition kPDTGeoposition1 = {37.335480, -121.893028};
// Tatshenshini-Alsek Provincial Park, HWY-3, Stikine, BC, V0W, CAN
// Sunrise is approximately 8:00 AM and sunset is approximately
// 8:00 PM PDT.
constexpr SimpleGeoposition kPDTGeoposition2 = {59.95353, -137.31462};
// Washington DC. Sunrise/set is approximately 7:00 AM/PM EDT.
constexpr SimpleGeoposition kEDTGeoposition = {38.89037, -77.03196};
enum AmPm { kAM, kPM };
template <class T>
bool IsSunRiseSetTimeNear(T input, T target) {
// The sunrise/set times for the test geopositions above are just approximate
// and do not occur exactly on the hour specified. Sunrise/set times also
// change by a couple minutes day to day if the test spans multiple days.
// So use a small tolerance when comparing timestamps involving sunrise/set.
static constexpr base::TimeDelta kSunriseSunsetTolerance = base::Minutes(10);
return target - kSunriseSunsetTolerance <= input &&
input <= target + kSunriseSunsetTolerance;
}
MATCHER_P(SunRiseSetTimeNear, target, "") {
return IsSunRiseSetTimeNear(arg, target);
}
// Sunset/sunrise times for all `k*Geoposition*` coordinates above are based
// on this timestamp.
base::Time GetMidnightForTestGeopositions() {
base::Time time;
CHECK(base::Time::FromString("26 Sep 2023 00:00:00 PDT", &time));
return time;
}
// Convenience function for constructing a TimeOfDay object for exact hours
// during the day. |hour| is between 1 and 12.
TimeOfDay MakeTimeOfDay(int hour, AmPm am_pm) {
DCHECK_GE(hour, 1);
DCHECK_LE(hour, 12);
if (am_pm == kAM) {
hour %= 12;
} else {
if (hour != 12)
hour += 12;
hour %= 24;
}
return TimeOfDay(hour * 60);
}
NightLightControllerImpl* GetController() {
return Shell::Get()->night_light_controller();
}
// Returns RGB scaling factors already applied on the display's compositor in a
// Vector3df given a |display_id|.
gfx::Vector3dF GetDisplayCompositorRGBScaleFactors(int64_t display_id) {
WindowTreeHostManager* wth_manager = Shell::Get()->window_tree_host_manager();
aura::Window* root_window =
wth_manager->GetRootWindowForDisplayId(display_id);
DCHECK(root_window);
aura::WindowTreeHost* host = root_window->GetHost();
DCHECK(host);
ui::Compositor* compositor = host->compositor();
DCHECK(compositor);
const SkM44& matrix = compositor->display_color_matrix();
return gfx::Vector3dF(matrix.rc(0, 0), matrix.rc(1, 1), matrix.rc(2, 2));
}
// Returns a vector with a Vector3dF for each compositor.
// Each element contains RGB scaling factors.
std::vector<gfx::Vector3dF> GetAllDisplaysCompositorsRGBScaleFactors() {
std::vector<gfx::Vector3dF> scale_factors;
for (int64_t display_id :
Shell::Get()->display_manager()->GetConnectedDisplayIdList()) {
scale_factors.push_back(GetDisplayCompositorRGBScaleFactors(display_id));
}
return scale_factors;
}
// Tests that the given display with |display_id| has the expected color matrix
// on its compositor that corresponds to the given |temperature|.
void TestDisplayCompositorTemperature(int64_t display_id, float temperature) {
const gfx::Vector3dF& scaling_factors =
GetDisplayCompositorRGBScaleFactors(display_id);
const float blue_scale = scaling_factors.z();
const float green_scale = scaling_factors.y();
EXPECT_FLOAT_EQ(
blue_scale,
NightLightControllerImpl::BlueColorScaleFromTemperature(temperature));
EXPECT_FLOAT_EQ(
green_scale,
NightLightControllerImpl::GreenColorScaleFromTemperature(temperature));
}
// Tests that the display color matrices of all compositors correctly correspond
// to the given |temperature|.
void TestCompositorsTemperature(float temperature) {
for (int64_t display_id :
Shell::Get()->display_manager()->GetConnectedDisplayIdList()) {
TestDisplayCompositorTemperature(display_id, temperature);
}
}
class TestObserver : public NightLightController::Observer {
public:
TestObserver() { GetController()->AddObserver(this); }
TestObserver(const TestObserver& other) = delete;
TestObserver& operator=(const TestObserver& rhs) = delete;
~TestObserver() override { GetController()->RemoveObserver(this); }
// ash::NightLightController::Observer:
void OnNightLightEnabledChanged(bool enabled) override { status_ = enabled; }
bool status() const { return status_; }
private:
bool status_ = false;
};
class NightLightTest : public NoSessionAshTestBase,
public ScheduledFeature::Clock {
public:
NightLightTest() : NightLightTest(GetMidnightForTestGeopositions()) {}
explicit NightLightTest(base::Time test_start_time)
: timezone_pdt_(kPDTTimezone),
test_start_time_(test_start_time),
geolocation_url_loader_factory_(
base::MakeRefCounted<TestGeolocationUrlLoaderFactory>()) {}
NightLightTest(const NightLightTest& other) = delete;
NightLightTest& operator=(const NightLightTest& rhs) = delete;
~NightLightTest() override = default;
PrefService* user1_pref_service() {
return Shell::Get()->session_controller()->GetUserPrefServiceForUser(
AccountId::FromUserEmail(kUser1Email));
}
PrefService* user2_pref_service() {
return Shell::Get()->session_controller()->GetUserPrefServiceForUser(
AccountId::FromUserEmail(kUser2Email));
}
base::Time test_start_time() const { return test_start_time_; }
// AshTestBase:
void SetUp() override {
ASSERT_TRUE(timezone_pdt_.is_success());
clock_.SetNow(test_start_time_);
// `tick_clock_`'s starting time is irrelevant. It should be advanced in
// unison with the wall `clock_` though to reflect reality.
tick_clock_.SetNowTicks(base::TimeTicks::Now());
NoSessionAshTestBase::SetUp();
geolocation_controller()->SetClockForTesting(&clock_);
GetController()->SetClockForTesting(this);
CreateTestUserSessions();
// Simulate user 1 login.
SimulateNewUserFirstLogin(kUser1Email);
// Start with ambient color pref disabled.
SetAmbientColorPrefEnabled(false);
// `GeolocationController` uses `SimpleGeolocationProvider` singleton
// instance, which is initialized by `AshTestHelper`.
SimpleGeolocationProvider::GetInstance()
->SetSharedUrlLoaderFactoryForTesting(geolocation_url_loader_factory_);
}
void CreateTestUserSessions() {
GetSessionControllerClient()->Reset();
GetSessionControllerClient()->AddUserSession(kUser1Email);
GetSessionControllerClient()->AddUserSession(kUser2Email);
}
void SwitchActiveUser(const std::string& email) {
GetSessionControllerClient()->SwitchActiveUser(
AccountId::FromUserEmail(email));
}
void SetNightLightEnabled(bool enabled) {
GetController()->SetEnabled(enabled);
}
void SetAmbientColorPrefEnabled(bool enabled) {
GetController()->SetAmbientColorEnabled(enabled);
}
// Simulate powerd sending multiple times an ambient temperature of
// |powerd_temperature|. The remapped ambient temperature should eventually
// reach |target_remapped_temperature|.
float SimulateAmbientColorFromPowerd(int32_t powerd_temperature,
float target_remapped_temperature) {
auto* controller = GetController();
int max_steps = 1000;
float ambient_temperature = 0.0f;
const float initial_difference =
controller->ambient_temperature() - target_remapped_temperature;
do {
controller->AmbientColorChanged(powerd_temperature);
ambient_temperature = controller->ambient_temperature();
} while (max_steps-- &&
((ambient_temperature - target_remapped_temperature) *
initial_difference) > 0.0f);
// We should reach the expected remapped temperature.
EXPECT_GT(max_steps, 0);
return ambient_temperature;
}
void AdvanceTimeTo(TimeOfDay time) {
time.SetClock(&clock_);
base::Time new_time = ToTimeToday(time);
if (new_time < Now()) {
new_time += base::Days(1);
}
AdvanceTimeBy(new_time - Now());
}
void AdvanceTimeBy(base::TimeDelta amount) {
CHECK_GE(amount, base::TimeDelta());
clock_.Advance(amount);
tick_clock_.Advance(amount);
}
// ScheduledFeature::Clock:
base::Time Now() const override { return clock_.Now(); }
base::TimeTicks NowTicks() const override { return tick_clock_.NowTicks(); }
GeolocationController* geolocation_controller() {
return Shell::Get()->geolocation_controller();
}
void SetPosition(const SimpleGeoposition& position) {
geolocation_url_loader_factory_->SetValidPosition(
position.latitude, position.longitude, Now());
geolocation_controller()->RequestImmediateGeopositionForTesting();
// This is a signal that `NightLightControllerImpl` must have received the
// geoposition observer notification as well and updated its internal
// schedule.
GeopositionResponsesWaiter waiter(geolocation_controller());
waiter.Wait();
}
void AdvanceControllerToNextTask(
base::TimeDelta expected_offset_from_start_time) {
AdvanceTimeBy(GetController()->timer()->GetCurrentDelay());
GetController()->timer()->FireNow();
const base::Time expected_now =
test_start_time() + expected_offset_from_start_time;
switch (GetController()->GetScheduleType()) {
case ScheduleType::kSunsetToSunrise:
ASSERT_THAT(Now(), SunRiseSetTimeNear(expected_now));
break;
case ScheduleType::kCustom:
case ScheduleType::kNone:
ASSERT_EQ(Now(), expected_now);
break;
}
// `ScheduledFeature` has a known weakness: If sunrise tomorrow is later in
// the day that sunrise today (ex: 6:59 AM and 7:00 AM), it will wake up at
// both 6:59 AM and 7:00 AM the next day to update the feature state. There
// are no user visible effects and no change in feature status at 7:00 AM,
// but this method needs to advance the clock to the later update so that
// tests can reason about the expected checkpoints easily.
while (IsSunRiseSetTimeNear(GetController()->timer()->GetCurrentDelay(),
base::TimeDelta())) {
const bool feature_status_before = GetController()->GetEnabled();
AdvanceTimeBy(GetController()->timer()->GetCurrentDelay());
GetController()->timer()->FireNow();
ASSERT_EQ(GetController()->GetEnabled(), feature_status_before);
}
}
private:
// Some tests may not actually need this setup, but it's significantly easier
// to debug test cases when they start with definitive time settings.
calendar_test_utils::ScopedLibcTimeZone timezone_pdt_;
const base::Time test_start_time_;
base::SimpleTestClock clock_;
base::SimpleTestTickClock tick_clock_;
const scoped_refptr<TestGeolocationUrlLoaderFactory>
geolocation_url_loader_factory_;
};
// Tests toggling NightLight on / off and makes sure the observer is updated and
// the layer temperatures are modified.
TEST_F(NightLightTest, TestToggle) {
UpdateDisplay("800x600,800x600");
TestObserver observer;
NightLightControllerImpl* controller = GetController();
SetNightLightEnabled(false);
ASSERT_FALSE(controller->IsNightLightEnabled());
TestCompositorsTemperature(0.0f);
controller->Toggle();
EXPECT_TRUE(controller->IsNightLightEnabled());
EXPECT_TRUE(observer.status());
TestCompositorsTemperature(GetController()->GetColorTemperature());
controller->Toggle();
EXPECT_FALSE(controller->IsNightLightEnabled());
EXPECT_FALSE(observer.status());
TestCompositorsTemperature(0.0f);
}
// Tests setting the temperature in various situations.
TEST_F(NightLightTest, TestSetTemperature) {
UpdateDisplay("800x600,800x600");
TestObserver observer;
NightLightControllerImpl* controller = GetController();
SetNightLightEnabled(false);
ASSERT_FALSE(controller->IsNightLightEnabled());
// Setting the temperature while NightLight is disabled only changes the
// color temperature field, but root layers temperatures are not affected, nor
// the status of NightLight itself.
const float temperature1 = 0.2f;
controller->SetColorTemperature(temperature1);
EXPECT_EQ(temperature1, controller->GetColorTemperature());
TestCompositorsTemperature(0.0f);
// When NightLight is enabled, temperature changes actually affect the root
// layers temperatures.
SetNightLightEnabled(true);
ASSERT_TRUE(controller->IsNightLightEnabled());
const float temperature2 = 0.7f;
controller->SetColorTemperature(temperature2);
EXPECT_EQ(temperature2, controller->GetColorTemperature());
TestCompositorsTemperature(temperature2);
// When NightLight is disabled, the value of the color temperature field
// doesn't change, however the temperatures set on the root layers are all
// 0.0f. Observers only receive an enabled status change notification; no
// temperature change notification.
SetNightLightEnabled(false);
ASSERT_FALSE(controller->IsNightLightEnabled());
EXPECT_FALSE(observer.status());
EXPECT_EQ(temperature2, controller->GetColorTemperature());
TestCompositorsTemperature(0.0f);
// When re-enabled, the stored temperature is re-applied.
SetNightLightEnabled(true);
EXPECT_TRUE(observer.status());
ASSERT_TRUE(controller->IsNightLightEnabled());
TestCompositorsTemperature(temperature2);
}
TEST_F(NightLightTest, TestNightLightWithDisplayConfigurationChanges) {
// Start with one display and enable NightLight.
NightLightControllerImpl* controller = GetController();
SetNightLightEnabled(true);
ASSERT_TRUE(controller->IsNightLightEnabled());
const float temperature = 0.2f;
controller->SetColorTemperature(temperature);
EXPECT_EQ(temperature, controller->GetColorTemperature());
TestCompositorsTemperature(temperature);
// Add a new display, and expect that its compositor gets the already set from
// before color temperature.
display_manager()->AddRemoveDisplay();
base::RunLoop().RunUntilIdle();
ASSERT_EQ(2u, RootWindowController::root_window_controllers().size());
TestCompositorsTemperature(temperature);
// While we have the second display, enable mirror mode, the compositors
// should still have the same temperature.
display_manager()->SetMirrorMode(display::MirrorMode::kNormal, std::nullopt);
EXPECT_TRUE(display_manager()->IsInMirrorMode());
base::RunLoop().RunUntilIdle();
TestCompositorsTemperature(temperature);
// Exit mirror mode, temperature is still applied.
display_manager()->SetMirrorMode(display::MirrorMode::kOff, std::nullopt);
EXPECT_FALSE(display_manager()->IsInMirrorMode());
base::RunLoop().RunUntilIdle();
TestCompositorsTemperature(temperature);
// Enter unified mode, temperature is still applied.
display_manager()->SetUnifiedDesktopEnabled(true);
EXPECT_TRUE(display_manager()->IsInUnifiedMode());
base::RunLoop().RunUntilIdle();
TestCompositorsTemperature(temperature);
// The unified host should never have a temperature on its compositor.
TestDisplayCompositorTemperature(display::kUnifiedDisplayId, 0.0f);
// Exit unified mode, and remove the display, temperature should remain the
// same.
display_manager()->SetUnifiedDesktopEnabled(false);
EXPECT_FALSE(display_manager()->IsInUnifiedMode());
base::RunLoop().RunUntilIdle();
TestCompositorsTemperature(temperature);
display_manager()->AddRemoveDisplay();
base::RunLoop().RunUntilIdle();
ASSERT_EQ(1u, RootWindowController::root_window_controllers().size());
TestCompositorsTemperature(temperature);
}
// Tests that switching users retrieves NightLight settings for the active
// user's prefs.
TEST_F(NightLightTest, TestUserSwitchAndSettingsPersistence) {
// Test start with user1 logged in.
NightLightControllerImpl* controller = GetController();
SetNightLightEnabled(true);
EXPECT_TRUE(controller->IsNightLightEnabled());
const float user1_temperature = 0.8f;
controller->SetColorTemperature(user1_temperature);
EXPECT_EQ(user1_temperature, controller->GetColorTemperature());
TestCompositorsTemperature(user1_temperature);
// Switch to user 2, and expect NightLight to be disabled.
SwitchActiveUser(kUser2Email);
EXPECT_FALSE(controller->IsNightLightEnabled());
// Changing user_2's color temperature shouldn't affect user_1's settings.
const float user2_temperature = 0.2f;
user2_pref_service()->SetDouble(prefs::kNightLightTemperature,
user2_temperature);
EXPECT_EQ(user2_temperature, controller->GetColorTemperature());
TestCompositorsTemperature(0.0f);
EXPECT_EQ(user1_temperature,
user1_pref_service()->GetDouble(prefs::kNightLightTemperature));
// Switch back to user 1, to find NightLight is still enabled, and the same
// user's color temperature are re-applied.
SwitchActiveUser(kUser1Email);
EXPECT_TRUE(controller->IsNightLightEnabled());
EXPECT_EQ(user1_temperature, controller->GetColorTemperature());
TestCompositorsTemperature(user1_temperature);
}
// Tests that changes from outside NightLightControlled to the NightLight
// Preferences are seen by the controlled and applied properly.
TEST_F(NightLightTest, TestOutsidePreferencesChangesAreApplied) {
// Test start with user1 logged in.
NightLightControllerImpl* controller = GetController();
user1_pref_service()->SetBoolean(prefs::kNightLightEnabled, true);
EXPECT_TRUE(controller->IsNightLightEnabled());
const float temperature1 = 0.65f;
user1_pref_service()->SetDouble(prefs::kNightLightTemperature, temperature1);
EXPECT_EQ(temperature1, controller->GetColorTemperature());
TestCompositorsTemperature(temperature1);
const float temperature2 = 0.23f;
user1_pref_service()->SetDouble(prefs::kNightLightTemperature, temperature2);
EXPECT_EQ(temperature2, controller->GetColorTemperature());
TestCompositorsTemperature(temperature2);
user1_pref_service()->SetBoolean(prefs::kNightLightEnabled, false);
EXPECT_FALSE(controller->IsNightLightEnabled());
}
// Tests transitioning from kNone to kCustom and back to kNone schedule types.
TEST_F(NightLightTest, TestScheduleNoneToCustomTransition) {
NightLightControllerImpl* controller = GetController();
// Now is 6:00 PM.
AdvanceTimeTo(TimeOfDay(18 * 60));
SetNightLightEnabled(false);
controller->SetScheduleType(ScheduleType::kNone);
// Start time is at 3:00 PM and end time is at 8:00 PM.
controller->SetCustomStartTime(TimeOfDay(15 * 60));
controller->SetCustomEndTime(TimeOfDay(20 * 60));
// 15:00 18:00 20:00
// <----- + ----------- + ----------- + ----->
// | | |
// start now end
//
// Even though "Now" is inside the NightLight interval, nothing should change,
// since the schedule type is "none".
EXPECT_FALSE(controller->IsNightLightEnabled());
TestCompositorsTemperature(0.0f);
// Now change the schedule type to custom, NightLight should turn on
// immediately with a short animation duration, and the timer should be
// running with a delay of exactly 2 hours scheduling the end.
controller->SetScheduleType(ScheduleType::kCustom);
EXPECT_TRUE(controller->IsNightLightEnabled());
TestCompositorsTemperature(controller->GetColorTemperature());
EXPECT_EQ(NightLightControllerImpl::AnimationDuration::kShort,
controller->last_animation_duration());
EXPECT_TRUE(controller->timer()->IsRunning());
EXPECT_EQ(base::Hours(2), controller->timer()->GetCurrentDelay());
// If the user changes the schedule type to "none", the NightLight status
// should not change, but the timer should not be running.
controller->SetScheduleType(ScheduleType::kNone);
EXPECT_TRUE(controller->IsNightLightEnabled());
TestCompositorsTemperature(controller->GetColorTemperature());
EXPECT_FALSE(controller->timer()->IsRunning());
}
// Tests what happens when the time now reaches the end of the NightLight
// interval when NightLight mode is on.
TEST_F(NightLightTest, TestCustomScheduleReachingEndTime) {
NightLightControllerImpl* controller = GetController();
AdvanceTimeTo(TimeOfDay(18 * 60));
controller->SetCustomStartTime(TimeOfDay(15 * 60));
controller->SetCustomEndTime(TimeOfDay(20 * 60));
controller->SetScheduleType(ScheduleType::kCustom);
EXPECT_TRUE(controller->IsNightLightEnabled());
TestCompositorsTemperature(controller->GetColorTemperature());
// Simulate reaching the end time by triggering the timer's user task. Make
// sure that NightLight ended with a long animation.
//
// 15:00 20:00
// <----- + ------------------------ + ----->
// | |
// start end & now
//
// Now is 8:00 PM.
AdvanceTimeTo(TimeOfDay(20 * 60));
controller->timer()->FireNow();
EXPECT_FALSE(controller->IsNightLightEnabled());
TestCompositorsTemperature(0.0f);
EXPECT_EQ(NightLightControllerImpl::AnimationDuration::kLong,
controller->last_animation_duration());
// The timer should still be running, but now scheduling the start at 3:00 PM
// tomorrow which is 19 hours from "now" (8:00 PM).
EXPECT_TRUE(controller->timer()->IsRunning());
EXPECT_EQ(base::Hours(19), controller->timer()->GetCurrentDelay());
}
// Tests that user toggles from the system menu or system settings override any
// status set by an automatic schedule.
TEST_F(NightLightTest, TestExplicitUserTogglesWhileScheduleIsActive) {
// Start with the below custom schedule, where NightLight is off.
//
// 15:00 20:00 23:00
// <----- + ----------------- + ------------ + ---->
// | | |
// start end now
//
NightLightControllerImpl* controller = GetController();
AdvanceTimeTo(TimeOfDay(23 * 60));
controller->SetCustomStartTime(TimeOfDay(15 * 60));
controller->SetCustomEndTime(TimeOfDay(20 * 60));
controller->SetScheduleType(ScheduleType::kCustom);
EXPECT_FALSE(controller->IsNightLightEnabled());
TestCompositorsTemperature(0.0f);
// What happens if the user manually turns NightLight on while the schedule
// type says it should be off?
// User toggles either from the system menu or the System Settings toggle
// button must override any automatic schedule, and should be performed with
// the short animation duration.
controller->Toggle();
EXPECT_TRUE(controller->IsNightLightEnabled());
TestCompositorsTemperature(controller->GetColorTemperature());
EXPECT_EQ(NightLightControllerImpl::AnimationDuration::kShort,
controller->last_animation_duration());
// Feature status shouldn't change at next start time.
AdvanceControllerToNextTask(base::Days(1) + base::Hours(15));
EXPECT_TRUE(controller->GetEnabled());
// Feature status changes back to regular schedule at next end time.
AdvanceControllerToNextTask(base::Days(1) + base::Hours(20));
EXPECT_FALSE(controller->GetEnabled());
TestCompositorsTemperature(0.0f);
// Back to 11:00 PM the next day.
AdvanceTimeBy(base::Hours(3));
// Manually turning it on then back off should be respected, and this time the
// start is scheduled at 3:00 PM tomorrow after 19 hours from "now" (8:00 PM).
controller->Toggle();
controller->Toggle();
EXPECT_FALSE(controller->IsNightLightEnabled());
TestCompositorsTemperature(0.0f);
EXPECT_EQ(NightLightControllerImpl::AnimationDuration::kShort,
controller->last_animation_duration());
// Feature status should change at next start time.
AdvanceControllerToNextTask(base::Days(2) + base::Hours(15));
EXPECT_TRUE(controller->GetEnabled());
}
// Tests that changing the custom start and end times, in such a way that
// shouldn't change the current status, only updates the timer but doesn't
// change the status.
TEST_F(NightLightTest, TestChangingStartTimesThatDontChangeTheStatus) {
// 16:00 18:00 22:00
// <----- + ----------- + ----------- + ----->
// | | |
// now start end
//
NightLightControllerImpl* controller = GetController();
AdvanceTimeTo(TimeOfDay(16 * 60)); // 4:00 PM.
SetNightLightEnabled(false);
controller->SetScheduleType(ScheduleType::kNone);
controller->SetCustomStartTime(TimeOfDay(18 * 60)); // 6:00 PM.
controller->SetCustomEndTime(TimeOfDay(22 * 60)); // 10:00 PM.
// Since now is outside the NightLight interval, changing the schedule type
// to kCustom, shouldn't affect the status. Validate the timer is running with
// a 2-hour delay.
controller->SetScheduleType(ScheduleType::kCustom);
EXPECT_FALSE(controller->IsNightLightEnabled());
TestCompositorsTemperature(0.0f);
EXPECT_TRUE(controller->timer()->IsRunning());
EXPECT_EQ(base::Hours(2), controller->timer()->GetCurrentDelay());
// Change the start time in such a way that doesn't change the status, but
// despite that, confirm that schedule has been updated.
controller->SetCustomStartTime(TimeOfDay(19 * 60)); // 7:00 PM.
EXPECT_FALSE(controller->IsNightLightEnabled());
TestCompositorsTemperature(0.0f);
EXPECT_TRUE(controller->timer()->IsRunning());
EXPECT_EQ(base::Hours(3), controller->timer()->GetCurrentDelay());
// Changing the end time in a similar fashion to the above and expect no
// change.
controller->SetCustomEndTime(TimeOfDay(23 * 60)); // 11:00 PM.
EXPECT_FALSE(controller->IsNightLightEnabled());
TestCompositorsTemperature(0.0f);
EXPECT_TRUE(controller->timer()->IsRunning());
EXPECT_EQ(base::Hours(3), controller->timer()->GetCurrentDelay());
}
// Tests the behavior of the sunset to sunrise automatic schedule type.
TEST_F(NightLightTest, TestSunsetSunrise) {
SetPosition(kPDTGeoposition1);
// 16:00 18:00 19:00 22:00 7:00
// <----- + ----------- + ------- + -------- + --------------- + ------->
// | | | | |
// now custom start sunset custom end sunrise
//
NightLightControllerImpl* controller = GetController();
AdvanceTimeBy(base::Hours(16));
SetNightLightEnabled(false);
controller->SetScheduleType(ScheduleType::kNone);
controller->SetCustomStartTime(TimeOfDay(18 * 60)); // 6:00 PM.
controller->SetCustomEndTime(TimeOfDay(22 * 60)); // 10:00 PM.
// Custom times should have no effect when the schedule type is sunset to
// sunrise.
controller->SetScheduleType(ScheduleType::kSunsetToSunrise);
EXPECT_FALSE(controller->IsNightLightEnabled());
TestCompositorsTemperature(0.0f);
EXPECT_TRUE(controller->timer()->IsRunning());
EXPECT_THAT(controller->timer()->GetCurrentDelay(),
SunRiseSetTimeNear(base::Hours(1)));
// Simulate reaching late afternoon (17:00)
AdvanceControllerToNextTask(base::Hours(17));
EXPECT_FALSE(controller->GetEnabled());
TestCompositorsTemperature(0.0f);
// Simulate reaching sunset.
AdvanceControllerToNextTask(base::Hours(19));
EXPECT_TRUE(controller->IsNightLightEnabled());
TestCompositorsTemperature(controller->GetColorTemperature());
EXPECT_EQ(NightLightControllerImpl::AnimationDuration::kLong,
controller->last_animation_duration());
// Timer is running scheduling the end at sunrise.
EXPECT_TRUE(controller->timer()->IsRunning());
EXPECT_THAT(controller->timer()->GetCurrentDelay(),
SunRiseSetTimeNear(base::Hours(12)));
// Simulate reaching sunrise.
AdvanceControllerToNextTask(base::Days(1) + base::Hours(7));
EXPECT_FALSE(controller->IsNightLightEnabled());
TestCompositorsTemperature(0.0f);
EXPECT_EQ(NightLightControllerImpl::AnimationDuration::kLong,
controller->last_animation_duration());
EXPECT_TRUE(controller->timer()->IsRunning());
ASSERT_THAT(controller->timer()->GetCurrentDelay(),
SunRiseSetTimeNear(base::Hours(4)));
}
// Tests the behavior of the sunset to sunrise automatic schedule type when the
// client sets the geoposition.
TEST_F(NightLightTest, TestSunsetSunriseGeoposition) {
SetPosition(kPDTGeoposition1);
// Position 1 sunset and sunrise times.
//
// 16:00 19:00 7:00
// <----- + --------- + ---------------- + ------->
// | | |
// now sunset sunrise
//
NightLightControllerImpl* controller = GetController();
AdvanceTimeBy(base::Hours(16));
// Expect that timer is running and the start is scheduled after 4 hours.
controller->SetScheduleType(ScheduleType::kSunsetToSunrise);
EXPECT_FALSE(controller->IsNightLightEnabled());
TestCompositorsTemperature(0.0f);
EXPECT_TRUE(controller->timer()->IsRunning());
EXPECT_THAT(controller->timer()->GetCurrentDelay(),
SunRiseSetTimeNear(base::Hours(1)));
// Simulate reaching late afternoon (17:00)
AdvanceControllerToNextTask(base::Hours(17));
EXPECT_FALSE(controller->GetEnabled());
TestCompositorsTemperature(0.0f);
// Simulate reaching sunset.
AdvanceControllerToNextTask(base::Hours(19));
EXPECT_TRUE(controller->IsNightLightEnabled());
TestCompositorsTemperature(controller->GetColorTemperature());
EXPECT_EQ(NightLightControllerImpl::AnimationDuration::kLong,
controller->last_animation_duration());
// Timer is running scheduling the end at sunrise.
EXPECT_TRUE(controller->timer()->IsRunning());
EXPECT_THAT(controller->timer()->GetCurrentDelay(),
SunRiseSetTimeNear(base::Hours(12)));
// Now simulate user changing position.
// Position 2 sunset and sunrise times.
//
// 19:00 20:00 08:00
// <----- + --------- + ---------------- + ------->
// | | |
// now sunset sunrise
//
SetPosition(kPDTGeoposition2);
EXPECT_FALSE(controller->IsNightLightEnabled());
TestCompositorsTemperature(0.0f);
EXPECT_TRUE(controller->timer()->IsRunning());
EXPECT_THAT(controller->timer()->GetCurrentDelay(),
SunRiseSetTimeNear(base::Hours(1)));
// Simulate reaching sunset is new location.
AdvanceControllerToNextTask(base::Hours(20));
EXPECT_TRUE(controller->IsNightLightEnabled());
TestCompositorsTemperature(controller->GetColorTemperature());
EXPECT_TRUE(controller->timer()->IsRunning());
EXPECT_THAT(controller->timer()->GetCurrentDelay(),
SunRiseSetTimeNear(base::Hours(12)));
// Simulate reaching sunrise is new location.
AdvanceControllerToNextTask(base::Days(1) + base::Hours(8));
EXPECT_FALSE(controller->IsNightLightEnabled());
TestCompositorsTemperature(0.0f);
EXPECT_EQ(NightLightControllerImpl::AnimationDuration::kLong,
controller->last_animation_duration());
// Timer is scheduling morning.
EXPECT_TRUE(controller->timer()->IsRunning());
EXPECT_THAT(controller->timer()->GetCurrentDelay(),
SunRiseSetTimeNear(base::Hours(4)));
}
// Tests the behavior when the client sets the geoposition while in custom
// schedule setting. Current time is simulated to be updated accordingly. The
// current time change should bring the controller into or take it out of the
// night light mode accordingly if necessary, based on the settings.
TEST_F(NightLightTest, TestCustomScheduleGeopositionChanges) {
constexpr int kCustom_Start = 19 * 60;
constexpr int kCustom_End = 2 * 60;
SetPosition(kPDTGeoposition1);
NightLightControllerImpl* controller = GetController();
controller->SetCustomStartTime(TimeOfDay(kCustom_Start));
controller->SetCustomEndTime(TimeOfDay(kCustom_End));
// Position 1 current time and custom start and end time.
//
// 17:00 19:00 2:00
// <----- + --------- + --------------- + ------------->
// | | |
// now custom start custom end
//
AdvanceTimeTo(TimeOfDay(17 * 60));
// Expect that timer is running and is scheduled at next custom start time.
controller->SetScheduleType(ScheduleType::kCustom);
EXPECT_FALSE(controller->IsNightLightEnabled());
TestCompositorsTemperature(0.0f);
EXPECT_TRUE(controller->timer()->IsRunning());
EXPECT_EQ(controller->timer()->GetCurrentDelay(), base::Hours(2));
// Simulate a timezone + geoposition change. Custom start/end times should
// remain the same in local time. But it was 5:00 PM PDT before, which is
// 8:00 EDT now.
//
// 19:00 20:00 2:00
// <----- + --------- + --------------- + ------------->
// | | |
// custom start now custom end
//
{
calendar_test_utils::ScopedLibcTimeZone timezone_edt(kEDTTimezone);
ASSERT_TRUE(timezone_edt.is_success());
SetPosition(kEDTGeoposition);
// Expect the controller to enter night light mode and the scheduled end
// delay has been updated.
EXPECT_TRUE(controller->IsNightLightEnabled());
TestCompositorsTemperature(controller->GetColorTemperature());
EXPECT_EQ(NightLightControllerImpl::AnimationDuration::kShort,
controller->last_animation_duration());
EXPECT_TRUE(controller->timer()->IsRunning());
EXPECT_EQ(controller->timer()->GetCurrentDelay(), base::Hours(6));
}
// Simulate user changing position back to location 1 and current local time
// goes back to 5 PM.
SetPosition(kPDTGeoposition1);
EXPECT_FALSE(controller->IsNightLightEnabled());
TestCompositorsTemperature(0.0f);
EXPECT_EQ(NightLightControllerImpl::AnimationDuration::kShort,
controller->last_animation_duration());
// Timer is running and is scheduled at next custom start time.
EXPECT_TRUE(controller->timer()->IsRunning());
EXPECT_EQ(controller->timer()->GetCurrentDelay(), base::Hours(2));
}
// Tests that on device resume from sleep, the NightLight status is updated
// correctly if the time has changed meanwhile.
TEST_F(NightLightTest, TestCustomScheduleOnResume) {
NightLightControllerImpl* controller = GetController();
// Now is 4:00 PM.
AdvanceTimeTo(TimeOfDay(16 * 60));
SetNightLightEnabled(false);
// Start time is at 6:00 PM and end time is at 10:00 PM. NightLight should be
// off.
// 16:00 18:00 22:00
// <----- + ----------- + ----------- + ----->
// | | |
// now start end
//
controller->SetColorTemperature(0.4f);
controller->SetCustomStartTime(TimeOfDay(18 * 60));
controller->SetCustomEndTime(TimeOfDay(22 * 60));
controller->SetScheduleType(ScheduleType::kCustom);
EXPECT_FALSE(controller->IsNightLightEnabled());
TestCompositorsTemperature(0.0f);
EXPECT_TRUE(controller->timer()->IsRunning());
// NightLight should start in 2 hours.
EXPECT_EQ(base::Hours(2), controller->timer()->GetCurrentDelay());
// Now simulate that the device was suspended for 3 hours, and the time now
// is 7:00 PM when the devices was resumed. Expect that NightLight turns on.
AdvanceTimeTo(TimeOfDay(19 * 60));
controller->SuspendDone(base::TimeDelta::Max());
EXPECT_TRUE(controller->IsNightLightEnabled());
TestCompositorsTemperature(0.4f);
EXPECT_TRUE(controller->timer()->IsRunning());
// NightLight should end in 3 hours.
EXPECT_EQ(base::Hours(3), controller->timer()->GetCurrentDelay());
}
// The following tests ensure that the NightLight schedule is correctly
// refreshed when the start and end times are inverted (i.e. the "start time" as
// a time of day today is in the future with respect to the "end time" also as a
// time of day today).
//
// Case 1: "Now" is less than both "end" and "start".
TEST_F(NightLightTest, TestCustomScheduleInvertedStartAndEndTimesCase1) {
NightLightControllerImpl* controller = GetController();
// Now is 4:00 AM.
AdvanceTimeTo(TimeOfDay(4 * 60));
SetNightLightEnabled(false);
// Start time is at 9:00 PM and end time is at 6:00 AM. "Now" is less than
// both. NightLight should be on.
// 4:00 6:00 21:00
// <----- + ----------- + ----------- + ----->
// | | |
// now end start
//
controller->SetColorTemperature(0.4f);
controller->SetCustomStartTime(TimeOfDay(21 * 60));
controller->SetCustomEndTime(TimeOfDay(6 * 60));
controller->SetScheduleType(ScheduleType::kCustom);
EXPECT_TRUE(controller->IsNightLightEnabled());
TestCompositorsTemperature(0.4f);
EXPECT_TRUE(controller->timer()->IsRunning());
// NightLight should end in two hours.
EXPECT_EQ(base::Hours(2), controller->timer()->GetCurrentDelay());
}
// Case 2: "Now" is between "end" and "start".
TEST_F(NightLightTest, TestCustomScheduleInvertedStartAndEndTimesCase2) {
NightLightControllerImpl* controller = GetController();
// Now is 6:00 AM.
AdvanceTimeTo(TimeOfDay(6 * 60));
SetNightLightEnabled(false);
// Start time is at 9:00 PM and end time is at 4:00 AM. "Now" is between both.
// NightLight should be off.
// 4:00 6:00 21:00
// <----- + ----------- + ----------- + ----->
// | | |
// end now start
//
controller->SetColorTemperature(0.4f);
controller->SetCustomStartTime(TimeOfDay(21 * 60));
controller->SetCustomEndTime(TimeOfDay(4 * 60));
controller->SetScheduleType(ScheduleType::kCustom);
EXPECT_FALSE(controller->IsNightLightEnabled());
TestCompositorsTemperature(0.0f);
EXPECT_TRUE(controller->timer()->IsRunning());
// NightLight should start in 15 hours.
EXPECT_EQ(base::Hours(15), controller->timer()->GetCurrentDelay());
}
// Case 3: "Now" is greater than both "start" and "end".
TEST_F(NightLightTest, TestCustomScheduleInvertedStartAndEndTimesCase3) {
NightLightControllerImpl* controller = GetController();
// Now is 11:00 PM.
AdvanceTimeTo(TimeOfDay(23 * 60));
SetNightLightEnabled(false);
// Start time is at 9:00 PM and end time is at 4:00 AM. "Now" is greater than
// both. NightLight should be on.
// 4:00 21:00 23:00
// <----- + ----------- + ----------- + ----->
// | | |
// end start now
//
controller->SetColorTemperature(0.4f);
controller->SetCustomStartTime(TimeOfDay(21 * 60));
controller->SetCustomEndTime(TimeOfDay(4 * 60));
controller->SetScheduleType(ScheduleType::kCustom);
EXPECT_TRUE(controller->IsNightLightEnabled());
TestCompositorsTemperature(0.4f);
EXPECT_TRUE(controller->timer()->IsRunning());
// NightLight should end in 5 hours.
EXPECT_EQ(base::Hours(5), controller->timer()->GetCurrentDelay());
}
TEST_F(NightLightTest, TestAmbientLightEnabledSetting_FeatureOn) {
base::test::ScopedFeatureList features;
features.InitAndEnableFeature(features::kAllowAmbientEQ);
// Feature enabled, Pref disabled -> disabled
SetAmbientColorPrefEnabled(false);
EXPECT_FALSE(GetController()->GetAmbientColorEnabled());
// Feature enabled, Pref enabled -> enabled
SetAmbientColorPrefEnabled(true);
EXPECT_TRUE(GetController()->GetAmbientColorEnabled());
}
TEST_F(NightLightTest, TestAmbientLightEnabledSetting_FeatureOff) {
// With the feature disabled it should always be disabled.
base::test::ScopedFeatureList features;
features.InitAndDisableFeature(features::kAllowAmbientEQ);
// Feature disabled, Pref disabled -> disabled
SetAmbientColorPrefEnabled(false);
EXPECT_FALSE(GetController()->GetAmbientColorEnabled());
// Feature disabled, Pref enabled -> disabled
SetAmbientColorPrefEnabled(true);
EXPECT_FALSE(GetController()->GetAmbientColorEnabled());
}
TEST_F(NightLightTest, TestAmbientLightRemappingTemperature) {
NightLightControllerImpl* controller = GetController();
// Test that at the beginning the ambient temperature is neutral.
constexpr float kNeutralColorTemperatureInKelvin = 6500;
EXPECT_EQ(kNeutralColorTemperatureInKelvin,
controller->ambient_temperature());
controller->SetAmbientColorEnabled(true);
EXPECT_EQ(kNeutralColorTemperatureInKelvin,
controller->ambient_temperature());
// Simulate powerd sending multiple times an ambient temperature of 8000.
// The remapped ambient temperature should grow and eventually reach ~7400.
float ambient_temperature = SimulateAmbientColorFromPowerd(8000, 7400.0f);
// If powerd sends the same temperature, the remapped temperature should not
// change.
controller->AmbientColorChanged(8000);
EXPECT_EQ(ambient_temperature, controller->ambient_temperature());
// Simulate powerd sending multiple times an ambient temperature of 2700.
// The remapped ambient temperature should grow and eventually reach 4500.
ambient_temperature = SimulateAmbientColorFromPowerd(2700, 4500.0f);
// Disabling ambient color should not affect the returned temperature.
controller->SetAmbientColorEnabled(false);
EXPECT_EQ(ambient_temperature, controller->ambient_temperature());
// Re-enabling should still keep the same temperature.
controller->SetAmbientColorEnabled(true);
EXPECT_EQ(ambient_temperature, controller->ambient_temperature());
}
// Tests that manual changes to NightLight status while a schedule is being used
// will be remembered and reapplied across user switches.
TEST_F(NightLightTest, MultiUserManualStatusToggleWithSchedules) {
// Setup user 1 to use a custom schedule from 3pm till 8pm, and user 2 to use
// a sunset-to-sunrise schedule from 7pm till 7am.
//
//
// |<--- User 1 NL on --->|
// | |
// <--------+-----------------+----+----------------------------+----------->
// 3pm 7pm 8pm 7am
// | |
// |<--------- User 2 NL on -------->|
//
// Test cases at:
//
// <---+---------+--------------+------------+----------------------------+-->
// 2pm 4pm 7:30pm 10pm 9am
//
AdvanceTimeBy(base::Hours(14));
constexpr float kUser1Temperature = 0.6f;
constexpr float kUser2Temperature = 0.8f;
NightLightControllerImpl* controller = GetController();
controller->SetCustomStartTime(MakeTimeOfDay(3, kPM));
controller->SetCustomEndTime(MakeTimeOfDay(8, kPM));
controller->SetScheduleType(ScheduleType::kCustom);
controller->SetColorTemperature(kUser1Temperature);
SwitchActiveUser(kUser2Email);
controller->SetScheduleType(ScheduleType::kSunsetToSunrise);
controller->SetColorTemperature(kUser2Temperature);
SwitchActiveUser(kUser1Email);
struct {
TimeOfDay fake_now;
bool user_1_expected_status;
bool user_2_expected_status;
} kTestCases[] = {
{MakeTimeOfDay(2, kPM), false, false},
{MakeTimeOfDay(4, kPM), true, false},
{TimeOfDay(19 * 60 + 30), true, true},
{MakeTimeOfDay(10, kPM), false, true},
{MakeTimeOfDay(9, kAM), // 9:00 AM tomorrow.
false, false},
};
// Verifies that NightLight status is |expected_status| and the given
// |user_temperature| is applied only when NightLight is expected to be
// enabled.
auto verify_night_light_state = [controller](bool expected_status,
float user_temperature) {
EXPECT_EQ(expected_status, controller->IsNightLightEnabled());
TestCompositorsTemperature(expected_status ? user_temperature : 0.0f);
};
bool user_1_previous_status = false;
for (const auto& test_case : kTestCases) {
// Each test case begins when user_1 is active.
SCOPED_TRACE(test_case.fake_now.ToString());
const bool user_1_toggled_status = !test_case.user_1_expected_status;
const bool user_2_toggled_status = !test_case.user_2_expected_status;
// Apply the test's case fake time, and fire the timer if there's a change
// expected in NightLight's status.
AdvanceTimeTo(test_case.fake_now);
if (user_1_previous_status != test_case.user_1_expected_status)
controller->timer()->FireNow();
user_1_previous_status = test_case.user_1_expected_status;
// The untoggled states for both users should match the expected ones
// according to their schedules.
verify_night_light_state(test_case.user_1_expected_status,
kUser1Temperature);
SwitchActiveUser(kUser2Email);
verify_night_light_state(test_case.user_2_expected_status,
kUser2Temperature);
// Manually toggle NightLight for user_2 and expect that it will be
// remembered when we switch to user_1 and back.
controller->Toggle();
verify_night_light_state(user_2_toggled_status, kUser2Temperature);
SwitchActiveUser(kUser1Email);
verify_night_light_state(test_case.user_1_expected_status,
kUser1Temperature);
SwitchActiveUser(kUser2Email);
verify_night_light_state(user_2_toggled_status, kUser2Temperature);
// Toggle it for user_1 as well, and expect it will be remembered and won't
// affect the already toggled state for user_2.
SwitchActiveUser(kUser1Email);
verify_night_light_state(test_case.user_1_expected_status,
kUser1Temperature);
controller->Toggle();
verify_night_light_state(user_1_toggled_status, kUser1Temperature);
SwitchActiveUser(kUser2Email);
verify_night_light_state(user_2_toggled_status, kUser2Temperature);
// Toggle both users back to their original states in preparation for the
// next test case.
controller->Toggle();
verify_night_light_state(test_case.user_2_expected_status,
kUser2Temperature);
SwitchActiveUser(kUser1Email);
verify_night_light_state(user_1_toggled_status, kUser1Temperature);
controller->Toggle();
verify_night_light_state(test_case.user_1_expected_status,
kUser1Temperature);
}
}
TEST_F(NightLightTest, ManualStatusToggleCanPersistAfterResumeFromSuspend) {
AdvanceTimeTo(MakeTimeOfDay(11, kAM));
NightLightControllerImpl* controller = GetController();
controller->SetCustomStartTime(MakeTimeOfDay(3, kPM));
controller->SetCustomEndTime(MakeTimeOfDay(8, kPM));
controller->SetScheduleType(ScheduleType::kCustom);
EXPECT_FALSE(controller->IsNightLightEnabled());
// Toggle the status manually and expect that NightLight is scheduled to
// turn back off at 8:00 PM.
controller->Toggle();
EXPECT_TRUE(controller->IsNightLightEnabled());
// Simulate suspend and then resume at 2:00 PM (which is outside the user's
// custom schedule). However, the manual toggle to on should be kept.
AdvanceTimeTo(MakeTimeOfDay(2, kPM));
controller->SuspendDone(base::TimeDelta{});
EXPECT_TRUE(controller->IsNightLightEnabled());
// Suspend again and resume at 5:00 PM (which is within the user's custom
// schedule). The schedule should be applied normally.
AdvanceTimeTo(MakeTimeOfDay(5, kPM));
controller->SuspendDone(base::TimeDelta{});
EXPECT_TRUE(controller->IsNightLightEnabled());
// Suspend and resume at 9:00 PM and expect NightLight to be off.
AdvanceTimeTo(MakeTimeOfDay(9, kPM));
controller->SuspendDone(base::TimeDelta{});
EXPECT_FALSE(controller->IsNightLightEnabled());
}
// Fixture for testing behavior of Night Light when displays support hardware
// CRTC matrices.
class NightLightCrtcTest : public NightLightTest {
public:
NightLightCrtcTest()
: logger_(std::make_unique<display::test::ActionLogger>()) {}
NightLightCrtcTest(const NightLightCrtcTest& other) = delete;
NightLightCrtcTest& operator=(const NightLightCrtcTest& rhs) = delete;
~NightLightCrtcTest() override = default;
static constexpr gfx::Size kDisplaySize{1024, 768};
static constexpr int64_t kId1 = 123;
static constexpr int64_t kId2 = 456;
// NightLightTest:
void SetUp() override {
NightLightTest::SetUp();
native_display_delegate_ =
new display::test::TestNativeDisplayDelegate(logger_.get());
display_manager()->configurator()->SetDelegateForTesting(
std::unique_ptr<display::NativeDisplayDelegate>(
native_display_delegate_));
display_change_observer_ =
std::make_unique<display::DisplayChangeObserver>(display_manager());
test_api_ = std::make_unique<display::DisplayConfigurator::TestApi>(
display_manager()->configurator());
}
void TearDown() override {
// DisplayChangeObserver access DeviceDataManager in its destructor, so
// destroy it first.
display_change_observer_ = nullptr;
NightLightTest::TearDown();
}
struct TestSnapshotParams {
bool has_ctm_support;
};
// Builds two display snapshots returns a list of owned unique pointers to
// them. |snapshot_params| should contain exactly 2 elements that correspond
// to capabilities of both displays.
std::vector<std::unique_ptr<display::DisplaySnapshot>>
BuildAndGetDisplaySnapshots(
const std::vector<TestSnapshotParams>& snapshot_params) const {
DCHECK_EQ(2u, snapshot_params.size());
std::vector<std::unique_ptr<display::DisplaySnapshot>> snapshots;
snapshots.emplace_back(
display::FakeDisplaySnapshot::Builder()
.SetId(kId1)
.SetNativeMode(kDisplaySize)
.SetCurrentMode(kDisplaySize)
.SetHasColorCorrectionMatrix(snapshot_params[0].has_ctm_support)
.SetType(display::DISPLAY_CONNECTION_TYPE_INTERNAL)
.Build());
snapshots.back()->set_origin({0, 0});
snapshots.emplace_back(
display::FakeDisplaySnapshot::Builder()
.SetId(kId2)
.SetNativeMode(kDisplaySize)
.SetCurrentMode(kDisplaySize)
.SetHasColorCorrectionMatrix(snapshot_params[1].has_ctm_support)
.Build());
snapshots.back()->set_origin({1030, 0});
return snapshots;
}
// Takes ownership of |outputs| and updates the display configurator and
// display manager with them.
void UpdateDisplays(
std::vector<std::unique_ptr<display::DisplaySnapshot>> outputs) {
native_display_delegate_->SetOutputs(std::move(outputs));
display_manager()->configurator()->OnConfigurationChanged();
EXPECT_TRUE(test_api_->TriggerConfigureTimeout());
display_change_observer_->GetStateForDisplayIds(
native_display_delegate_->GetOutputs());
display_change_observer_->OnDisplayConfigurationChanged(
native_display_delegate_->GetOutputs());
}
// Returns true if the software cursor is turned on.
bool IsCursorCompositingEnabled() const {
return Shell::Get()
->window_tree_host_manager()
->cursor_window_controller()
->is_cursor_compositing_enabled();
}
std::string GetLoggerActionsAndClear() {
return logger_->GetActionsAndClear();
}
private:
std::unique_ptr<display::test::ActionLogger> logger_;
// Not owned.
raw_ptr<display::test::TestNativeDisplayDelegate, DanglingUntriaged>
native_display_delegate_;
std::unique_ptr<display::DisplayChangeObserver> display_change_observer_;
std::unique_ptr<display::DisplayConfigurator::TestApi> test_api_;
};
// static
constexpr gfx::Size NightLightCrtcTest::kDisplaySize;
// All displays support CRTC matrices.
TEST_F(NightLightCrtcTest, TestAllDisplaysSupportCrtcMatrix) {
// Create two displays with both having support for CRTC matrices.
std::vector<std::unique_ptr<display::DisplaySnapshot>> outputs =
BuildAndGetDisplaySnapshots({{true}, {true}});
UpdateDisplays(std::move(outputs));
EXPECT_EQ(2u, display_manager()->GetNumDisplays());
ASSERT_EQ(2u, RootWindowController::root_window_controllers().size());
// Turn on Night Light:
NightLightControllerImpl* controller = GetController();
SetNightLightEnabled(true);
float temperature = 0.2f;
GetLoggerActionsAndClear();
controller->SetColorTemperature(temperature);
EXPECT_EQ(temperature, controller->GetColorTemperature());
// Since both displays support CRTC matrices, no compositor matrix should be
// set (i.e. compositor matrix is identity which corresponds to the zero
// temperature).
TestCompositorsTemperature(0.0f);
// Hence software cursor should not be used.
EXPECT_FALSE(IsCursorCompositingEnabled());
// Setting a new temperature is applied.
temperature = 0.65f;
controller->SetColorTemperature(temperature);
EXPECT_EQ(temperature, controller->GetColorTemperature());
TestCompositorsTemperature(0.0f);
EXPECT_FALSE(IsCursorCompositingEnabled());
// Test the cursor compositing behavior when Night Light is on (and doesn't
// require the software cursor) while other accessibility settings that affect
// the cursor are toggled.
for (const auto* const pref : {prefs::kAccessibilityLargeCursorEnabled,
prefs::kAccessibilityHighContrastEnabled}) {
user1_pref_service()->SetBoolean(pref, true);
EXPECT_TRUE(IsCursorCompositingEnabled());
// Disabling the accessibility feature should revert back to the hardware
// cursor.
user1_pref_service()->SetBoolean(pref, false);
EXPECT_FALSE(IsCursorCompositingEnabled());
}
}
// All displays support CRTC matrices in the compressed gamma space.
TEST_F(NightLightCrtcTest,
TestAllDisplaysSupportCrtcMatrixCompressedGammaSpace) {
// Create two displays with both having support for CRTC matrices that are
// applied in the compressed gamma space.
std::vector<std::unique_ptr<display::DisplaySnapshot>> outputs =
BuildAndGetDisplaySnapshots({{true}, {true}});
UpdateDisplays(std::move(outputs));
EXPECT_EQ(2u, display_manager()->GetNumDisplays());
ASSERT_EQ(2u, RootWindowController::root_window_controllers().size());
// Turn on Night Light:
NightLightControllerImpl* controller = GetController();
SetNightLightEnabled(true);
float temperature = 0.2f;
GetLoggerActionsAndClear();
controller->SetColorTemperature(temperature);
EXPECT_EQ(temperature, controller->GetColorTemperature());
// Since both displays support CRTC matrices, no compositor matrix should be
// set (i.e. compositor matrix is identity which corresponds to the zero
// temperature).
TestCompositorsTemperature(0.0f);
// Hence software cursor should not be used.
EXPECT_FALSE(IsCursorCompositingEnabled());
// Setting a new temperature is applied.
temperature = 0.65f;
controller->SetColorTemperature(temperature);
EXPECT_EQ(temperature, controller->GetColorTemperature());
TestCompositorsTemperature(0.0f);
EXPECT_FALSE(IsCursorCompositingEnabled());
}
// One display supports CRTC matrix and the other doesn't.
TEST_F(NightLightCrtcTest, TestMixedCrtcMatrixSupport) {
std::vector<std::unique_ptr<display::DisplaySnapshot>> outputs =
BuildAndGetDisplaySnapshots({{true}, {false}});
UpdateDisplays(std::move(outputs));
EXPECT_EQ(2u, display_manager()->GetNumDisplays());
ASSERT_EQ(2u, RootWindowController::root_window_controllers().size());
// Turn on Night Light:
NightLightControllerImpl* controller = GetController();
SetNightLightEnabled(true);
const float temperature = 0.2f;
GetLoggerActionsAndClear();
controller->SetColorTemperature(temperature);
EXPECT_EQ(temperature, controller->GetColorTemperature());
// The first display supports CRTC matrix, so its compositor has identity
// matrix.
TestDisplayCompositorTemperature(kId1, 0.0f);
// However, the second display doesn't support CRTC matrix, Night Light is
// using the compositor matrix on this display.
TestDisplayCompositorTemperature(kId2, temperature);
// With mixed CRTC support, software cursor must be on.
EXPECT_TRUE(IsCursorCompositingEnabled());
}
// All displays don't support CRTC matrices.
TEST_F(NightLightCrtcTest, TestNoCrtcMatrixSupport) {
std::vector<std::unique_ptr<display::DisplaySnapshot>> outputs =
BuildAndGetDisplaySnapshots({{false}, {false}});
UpdateDisplays(std::move(outputs));
EXPECT_EQ(2u, display_manager()->GetNumDisplays());
ASSERT_EQ(2u, RootWindowController::root_window_controllers().size());
// Turn on Night Light:
NightLightControllerImpl* controller = GetController();
SetNightLightEnabled(true);
const float temperature = 0.2f;
GetLoggerActionsAndClear();
controller->SetColorTemperature(temperature);
EXPECT_EQ(temperature, controller->GetColorTemperature());
// Compositor matrices are used on both displays.
TestCompositorsTemperature(temperature);
// With no CRTC support, software cursor must be on.
EXPECT_TRUE(IsCursorCompositingEnabled());
}
// Tests that switching CRTC matrix support on while Night Light is enabled
// doesn't result in the matrix being applied twice.
TEST_F(NightLightCrtcTest, TestNoDoubleNightLightEffect) {
std::vector<std::unique_ptr<display::DisplaySnapshot>> outputs =
BuildAndGetDisplaySnapshots({{false}, {false}});
UpdateDisplays(std::move(outputs));
EXPECT_EQ(2u, display_manager()->GetNumDisplays());
ASSERT_EQ(2u, RootWindowController::root_window_controllers().size());
// Turn on Night Light:
NightLightControllerImpl* controller = GetController();
SetNightLightEnabled(true);
const float temperature = 0.2f;
GetLoggerActionsAndClear();
controller->SetColorTemperature(temperature);
EXPECT_EQ(temperature, controller->GetColorTemperature());
// Compositor matrices are used on both displays.
TestCompositorsTemperature(temperature);
// With no CRTC support, software cursor must be on.
EXPECT_TRUE(IsCursorCompositingEnabled());
// Simulate that the two displays suddenly became able to support CRTC matrix.
// This shouldn't happen in practice, but we noticed multiple times on resume
// from suspend, or after the display turned on after it was off as a result
// of no user activity, we noticed that the display can get into a transient
// state where it is wrongly believed to have no CTM matrix capability, then
// later corrected. When this happened, we noticed that the Night Light effect
// is applied twice; once via the CRTC CTM matrix, and another via the
// compositor matrix. When this happens, we need to assert that the compositor
// matrix is set to identity, and the cursor compositing is updated correctly.
// TODO(afakhry): Investigate the root cause of this https://crbug.com/844067.
std::vector<std::unique_ptr<display::DisplaySnapshot>> outputs2 =
BuildAndGetDisplaySnapshots({{true}, {true}});
UpdateDisplays(std::move(outputs2));
TestCompositorsTemperature(0.0f);
EXPECT_FALSE(IsCursorCompositingEnabled());
}
// The following tests are for ambient color temperature conversions
// needed to go from a powerd ambient temperature reading in Kelvin to three
// RGB factors that can be used for a CTM to match the ambient color
// temperature.
// The table for the mapping was created with internal user studies, refer to
// kTable in
// NightLightControllerImpl::RemapAmbientColorTemperature to
// verify the assertion in the following tests.
TEST(AmbientTemperature, RemapAmbientColorTemperature) {
// Neutral temperature
float temperature =
NightLightControllerImpl::RemapAmbientColorTemperature(6500);
EXPECT_GT(temperature, 6000);
EXPECT_LT(temperature, 7000);
// Warm color temperature
temperature = NightLightControllerImpl::RemapAmbientColorTemperature(3000);
EXPECT_GT(temperature, 4500);
EXPECT_LT(temperature, 5000);
// Daylight color temperature
temperature = NightLightControllerImpl::RemapAmbientColorTemperature(7500);
EXPECT_GT(temperature, 6800);
EXPECT_LT(temperature, 7500);
// Extremely high color temperature
temperature = NightLightControllerImpl::RemapAmbientColorTemperature(20000);
EXPECT_GT(temperature, 7000);
EXPECT_LT(temperature, 8000);
}
// The following tests Kelvin temperatures to RGB scale factors.
// The values are from the calculation of white point based on Planckian locus.
// For each RGB vector we compute the distance from the expected value
// and check it's within a threshold of 0.01f;
TEST(AmbientTemperature, AmbientTemperatureToRGBScaleFactors) {
constexpr float allowed_difference = 0.01f;
// Neutral temperature
gfx::Vector3dF vec =
NightLightControllerImpl::ColorScalesFromRemappedTemperatureInKevin(6500);
EXPECT_LT((vec - gfx::Vector3dF(1.0f, 1.0f, 1.0f)).Length(),
allowed_difference);
// Warm
vec =
NightLightControllerImpl::ColorScalesFromRemappedTemperatureInKevin(4500);
EXPECT_LT((vec - gfx::Vector3dF(1.0f, 0.8816f, 0.7313f)).Length(),
allowed_difference);
// Daylight
vec =
NightLightControllerImpl::ColorScalesFromRemappedTemperatureInKevin(7000);
EXPECT_LT((vec - gfx::Vector3dF(0.949f, 0.971f, 1.0f)).Length(),
allowed_difference);
}
class AutoNightLightTest : public NightLightTest {
public:
AutoNightLightTest() : AutoNightLightTest(TimeOfDay(16 * 60)) {}
explicit AutoNightLightTest(TimeOfDay starting_time_of_day)
: NightLightTest(
GetMidnightForTestGeopositions() +
base::Minutes(
starting_time_of_day.offset_minutes_from_zero_hour())) {}
AutoNightLightTest(const AutoNightLightTest& other) = delete;
AutoNightLightTest& operator=(const AutoNightLightTest& rhs) = delete;
~AutoNightLightTest() override = default;
// NightLightTest:
void SetUp() override {
scoped_feature_list_.InitAndEnableFeature(features::kAutoNightLight);
// Now is at 4 PM.
//
// 16:00 19:00 7:00
// <----- + ----------------- + ----------------------- + ------->
// | | |
// now sunset sunrise
//
NightLightTest::SetUp();
SetPosition(kPDTGeoposition1);
}
private:
base::test::ScopedFeatureList scoped_feature_list_;
};
TEST_F(AutoNightLightTest, Notification) {
// Unblock the user session in order to be able to stop showing the Auto Night
// Light notification.
GetSessionControllerClient()->UnlockScreen();
// Since Auto Night Light is enabled, the schedule should be automatically set
// to sunset-to-sunrise, even though the user never set that pref.
NightLightControllerImpl* controller = GetController();
EXPECT_EQ(ScheduleType::kSunsetToSunrise, controller->GetScheduleType());
EXPECT_FALSE(
user1_pref_service()->HasPrefPath(prefs::kNightLightScheduleType));
// Simulate reaching late afternoon (17:00).
AdvanceControllerToNextTask(base::Hours(1));
// Simulate reaching sunset.
AdvanceControllerToNextTask(base::Hours(3));
EXPECT_TRUE(controller->IsNightLightEnabled());
auto* notification = controller->GetAutoNightLightNotificationForTesting();
ASSERT_TRUE(notification);
ASSERT_TRUE(notification->delegate());
// Simulate the user clicking the notification body to go to settings, and
// turning off Night Light manually for tonight. The notification should be
// dismissed.
notification->delegate()->Click(std::nullopt, std::nullopt);
controller->SetEnabled(false);
EXPECT_FALSE(controller->IsNightLightEnabled());
EXPECT_FALSE(controller->GetAutoNightLightNotificationForTesting());
// Simulate reaching sunrise (7:00).
AdvanceControllerToNextTask(base::Hours(15));
EXPECT_FALSE(controller->GetEnabled());
// Simulate reaching morning (11:00).
AdvanceControllerToNextTask(base::Hours(19));
EXPECT_FALSE(controller->GetEnabled());
// Simulate reaching late afternoon (17:00).
AdvanceControllerToNextTask(base::Days(1) + base::Hours(1));
EXPECT_FALSE(controller->GetEnabled());
// Simulate reaching next sunset. The notification should no longer show.
AdvanceControllerToNextTask(base::Days(1) + base::Hours(3));
EXPECT_TRUE(controller->IsNightLightEnabled());
EXPECT_FALSE(controller->GetAutoNightLightNotificationForTesting());
}
TEST_F(AutoNightLightTest, DismissNotificationOnTurningOff) {
GetSessionControllerClient()->UnlockScreen();
NightLightControllerImpl* controller = GetController();
EXPECT_EQ(ScheduleType::kSunsetToSunrise, controller->GetScheduleType());
// Simulate reaching late afternoon (5:00 PM).
AdvanceControllerToNextTask(base::Hours(1));
// Simulate reaching sunset (7:00 PM).
AdvanceControllerToNextTask(base::Hours(3));
EXPECT_TRUE(controller->IsNightLightEnabled());
auto* notification = controller->GetAutoNightLightNotificationForTesting();
ASSERT_TRUE(notification);
ASSERT_TRUE(notification->delegate());
// Simulate receiving an updated geoposition with sunset/sunrise times at
// 8pm/8am, so now is before sunset. Night Light should turn off, and the
// stale notification from above should be removed. However, its removal
// should not affect kAutoNightLightNotificationDismissed.
SetPosition(kPDTGeoposition2);
EXPECT_FALSE(controller->IsNightLightEnabled());
EXPECT_FALSE(controller->GetAutoNightLightNotificationForTesting());
// Simulate reaching next sunset. The notification should still show, since it
// was never dismissed by the user.
AdvanceControllerToNextTask(base::Hours(4));
EXPECT_TRUE(controller->IsNightLightEnabled());
EXPECT_TRUE(controller->GetAutoNightLightNotificationForTesting());
}
TEST_F(AutoNightLightTest, CannotDisableNotificationWhenSessionIsBlocked) {
BlockUserSession(BLOCKED_BY_LOCK_SCREEN);
EXPECT_TRUE(Shell::Get()->session_controller()->IsUserSessionBlocked());
// Simulate reaching late afternoon (17:00).
AdvanceControllerToNextTask(base::Hours(1));
// Simulate reaching sunset.
NightLightControllerImpl* controller = GetController();
AdvanceControllerToNextTask(base::Hours(3));
EXPECT_TRUE(controller->IsNightLightEnabled());
auto* notification = controller->GetAutoNightLightNotificationForTesting();
ASSERT_TRUE(notification);
ASSERT_TRUE(notification->delegate());
// Simulate user closing the notification.
notification->delegate()->Close(/*by_user=*/true);
EXPECT_FALSE(user1_pref_service()->GetBoolean(
prefs::kAutoNightLightNotificationDismissed));
}
TEST_F(AutoNightLightTest, OverriddenByUser) {
// Once the user sets the schedule to anything, even sunset-to-sunrise, the
// auto-night light will never show.
NightLightControllerImpl* controller = GetController();
controller->SetScheduleType(ScheduleType::kSunsetToSunrise);
// Simulate reaching late afternoon (17:00).
AdvanceControllerToNextTask(base::Hours(1));
// Simulate reaching sunset.
AdvanceControllerToNextTask(base::Hours(3));
EXPECT_TRUE(controller->IsNightLightEnabled());
EXPECT_FALSE(controller->GetAutoNightLightNotificationForTesting());
}
TEST_F(AutoNightLightTest, NoNotificationWhenManuallyEnabledFromSettings) {
NightLightControllerImpl* controller = GetController();
EXPECT_FALSE(controller->IsNightLightEnabled());
user1_pref_service()->SetBoolean(prefs::kNightLightEnabled, true);
EXPECT_TRUE(controller->IsNightLightEnabled());
EXPECT_FALSE(controller->GetAutoNightLightNotificationForTesting());
}
TEST_F(AutoNightLightTest, NoNotificationWhenManuallyEnabledFromSystemMenu) {
NightLightControllerImpl* controller = GetController();
EXPECT_FALSE(controller->IsNightLightEnabled());
controller->Toggle();
EXPECT_TRUE(controller->IsNightLightEnabled());
EXPECT_FALSE(controller->GetAutoNightLightNotificationForTesting());
}
// Now is at 11 PM.
//
// 20:00 23:00 5:00
// <----- + ----------------- + ----------------------- + ------->
// | | |
// sunset now sunrise
//
// Tests that when the user logs in for the first time between sunset and
// sunrise with Auto Night Light enabled, and the notification has never been
// dismissed before, the notification should be shown.
class AutoNightLightOnFirstLogin : public AutoNightLightTest {
public:
AutoNightLightOnFirstLogin() : AutoNightLightTest(TimeOfDay(23 * 60)) {}
AutoNightLightOnFirstLogin(const AutoNightLightOnFirstLogin& other) = delete;
AutoNightLightOnFirstLogin& operator=(const AutoNightLightOnFirstLogin& rhs) =
delete;
~AutoNightLightOnFirstLogin() override = default;
};
TEST_F(AutoNightLightOnFirstLogin, NotifyOnFirstLogin) {
NightLightControllerImpl* controller = GetController();
EXPECT_TRUE(controller->IsNightLightEnabled());
EXPECT_TRUE(controller->GetAutoNightLightNotificationForTesting());
}
// Fixture for testing Ambient EQ.
class AmbientEQTest : public NightLightTest {
public:
AmbientEQTest() : logger_(std::make_unique<display::test::ActionLogger>()) {}
AmbientEQTest(const AmbientEQTest& other) = delete;
AmbientEQTest& operator=(const AmbientEQTest& rhs) = delete;
~AmbientEQTest() override = default;
static constexpr gfx::Vector3dF kDefaultScalingFactors{1.0f, 1.0f, 1.0f};
static constexpr int64_t kInternalDisplayId = 123;
static constexpr int64_t kExternalDisplayId = 456;
// NightLightTest:
void SetUp() override {
NightLightTest::SetUp();
features_.InitAndEnableFeature(features::kAllowAmbientEQ);
controller_ = GetController();
native_display_delegate_ =
new display::test::TestNativeDisplayDelegate(logger_.get());
display_manager()->configurator()->SetDelegateForTesting(
std::unique_ptr<display::NativeDisplayDelegate>(
native_display_delegate_));
display_change_observer_ =
std::make_unique<display::DisplayChangeObserver>(display_manager());
test_api_ = std::make_unique<display::DisplayConfigurator::TestApi>(
display_manager()->configurator());
}
void ConfigureMultipleDisplaySetup() {
const gfx::Size kDisplaySize{1024, 768};
std::vector<std::unique_ptr<display::DisplaySnapshot>> snapshots;
snapshots.emplace_back(
display::FakeDisplaySnapshot::Builder()
.SetId(kInternalDisplayId)
.SetNativeMode(kDisplaySize)
.SetCurrentMode(kDisplaySize)
.SetType(display::DISPLAY_CONNECTION_TYPE_INTERNAL)
.SetOrigin({0, 0})
.Build());
snapshots.emplace_back(display::FakeDisplaySnapshot::Builder()
.SetId(kExternalDisplayId)
.SetNativeMode(kDisplaySize)
.SetCurrentMode(kDisplaySize)
.SetOrigin({1030, 0})
.Build());
native_display_delegate_->SetOutputs(std::move(snapshots));
display_manager()->configurator()->OnConfigurationChanged();
EXPECT_TRUE(test_api_->TriggerConfigureTimeout());
display_change_observer_->GetStateForDisplayIds(
native_display_delegate_->GetOutputs());
display_change_observer_->OnDisplayConfigurationChanged(
native_display_delegate_->GetOutputs());
}
void TearDown() override {
// DisplayChangeObserver access DeviceDataManager in its destructor, so
// destroy it first.
display_change_observer_ = nullptr;
NightLightTest::TearDown();
}
protected:
base::test::ScopedFeatureList features_;
std::unique_ptr<display::test::ActionLogger> logger_;
// Not owned.
raw_ptr<NightLightControllerImpl, DanglingUntriaged> controller_;
raw_ptr<display::test::TestNativeDisplayDelegate, DanglingUntriaged>
native_display_delegate_;
std::unique_ptr<display::DisplayChangeObserver> display_change_observer_;
std::unique_ptr<display::DisplayConfigurator::TestApi> test_api_;
};
// static
constexpr gfx::Vector3dF AmbientEQTest::kDefaultScalingFactors;
TEST_F(AmbientEQTest, TestAmbientRgbScalingUpdatesOnPrefChanged) {
// Start with the pref disabled.
controller_->SetAmbientColorEnabled(false);
// Shift to the coolest temperature and the temperature updates even with the
// pref disabled but the scaling factors don't.
float ambient_temperature = SimulateAmbientColorFromPowerd(8000, 7350.0f);
EXPECT_EQ(ambient_temperature, controller_->ambient_temperature());
EXPECT_EQ(kDefaultScalingFactors, controller_->ambient_rgb_scaling_factors());
// Enabling the pref and the scaling factors update.
controller_->SetAmbientColorEnabled(true);
const auto coolest_scaling_factors =
controller_->ambient_rgb_scaling_factors();
EXPECT_NE(kDefaultScalingFactors, coolest_scaling_factors);
// Shift to the warmest temp and the the scaling factors should update along
// with the temperature while the pref is enabled.
ambient_temperature = SimulateAmbientColorFromPowerd(2700, 5800.0f);
EXPECT_EQ(ambient_temperature, controller_->ambient_temperature());
const auto warmest_scaling_factors =
controller_->ambient_rgb_scaling_factors();
EXPECT_NE(warmest_scaling_factors, coolest_scaling_factors);
EXPECT_NE(warmest_scaling_factors, kDefaultScalingFactors);
}
TEST_F(AmbientEQTest, TestAmbientRgbScalingUpdatesOnUserChangedToEnabled) {
// Start with user1 logged in with pref disabled.
controller_->SetAmbientColorEnabled(false);
// Shift to the coolest temperature and the temperature updates even with the
// pref disabled but the scaling factors don't.
float ambient_temperature = SimulateAmbientColorFromPowerd(8000, 7350.0f);
EXPECT_EQ(ambient_temperature, controller_->ambient_temperature());
EXPECT_EQ(kDefaultScalingFactors, controller_->ambient_rgb_scaling_factors());
// Enable the pref for user 2 then switch to user2 and the factors update.
user2_pref_service()->SetBoolean(prefs::kAmbientColorEnabled, true);
SwitchActiveUser(kUser2Email);
const auto coolest_scaling_factors =
controller_->ambient_rgb_scaling_factors();
EXPECT_NE(kDefaultScalingFactors, coolest_scaling_factors);
}
TEST_F(AmbientEQTest, TestAmbientRgbScalingUpdatesOnUserChangedBothDisabled) {
// Start with user1 logged in with pref disabled.
controller_->SetAmbientColorEnabled(false);
// Shift to the coolest temperature and the temperature updates even with the
// pref disabled but the scaling factors don't.
float ambient_temperature = SimulateAmbientColorFromPowerd(8000, 7350.0f);
EXPECT_EQ(ambient_temperature, controller_->ambient_temperature());
EXPECT_EQ(kDefaultScalingFactors, controller_->ambient_rgb_scaling_factors());
// Disable the pref for user 2 then switch to user2 and the factors still
// shouldn't update.
user2_pref_service()->SetBoolean(prefs::kAmbientColorEnabled, false);
SwitchActiveUser(kUser2Email);
EXPECT_EQ(kDefaultScalingFactors, controller_->ambient_rgb_scaling_factors());
}
TEST_F(AmbientEQTest, TestAmbientColorMatrix) {
ConfigureMultipleDisplaySetup();
SetNightLightEnabled(false);
SetAmbientColorPrefEnabled(true);
auto scaling_factors = GetAllDisplaysCompositorsRGBScaleFactors();
// If no temperature is set, we expect 1.0 for each scaling factor.
for (const gfx::Vector3dF& rgb : scaling_factors) {
EXPECT_TRUE((rgb - gfx::Vector3dF(1.0f, 1.0f, 1.0f)).IsZero());
}
// Turn color temperature down.
SimulateAmbientColorFromPowerd(8000, 7350.0f);
auto internal_rgb = GetDisplayCompositorRGBScaleFactors(kInternalDisplayId);
auto external_rgb = GetDisplayCompositorRGBScaleFactors(kExternalDisplayId);
// A cool temperature should affect only red and green.
EXPECT_LT(internal_rgb.x(), 1.0f);
EXPECT_LT(internal_rgb.y(), 1.0f);
EXPECT_EQ(internal_rgb.z(), 1.0f);
// The external display should not be affected.
EXPECT_TRUE((external_rgb - gfx::Vector3dF(1.0f, 1.0f, 1.0f)).IsZero());
// Turn color temperature up.
SimulateAmbientColorFromPowerd(2700, 5800.0f);
internal_rgb = GetDisplayCompositorRGBScaleFactors(kInternalDisplayId);
external_rgb = GetDisplayCompositorRGBScaleFactors(kExternalDisplayId);
// A warm temperature should affect only green and blue.
EXPECT_EQ(internal_rgb.x(), 1.0f);
EXPECT_LT(internal_rgb.y(), 1.0f);
EXPECT_LT(internal_rgb.z(), 1.0f);
// The external display should not be affected.
EXPECT_TRUE((external_rgb - gfx::Vector3dF(1.0f, 1.0f, 1.0f)).IsZero());
}
TEST_F(AmbientEQTest, TestNightLightAndAmbientColorInteraction) {
ConfigureMultipleDisplaySetup();
SetNightLightEnabled(true);
auto night_light_rgb = GetAllDisplaysCompositorsRGBScaleFactors().front();
SetAmbientColorPrefEnabled(true);
auto night_light_and_ambient_rgb =
GetDisplayCompositorRGBScaleFactors(kInternalDisplayId);
// Ambient color with neutral temperature should not affect night light.
EXPECT_TRUE((night_light_rgb - night_light_and_ambient_rgb).IsZero());
SimulateAmbientColorFromPowerd(2700, 5800.0f);
night_light_and_ambient_rgb =
GetDisplayCompositorRGBScaleFactors(kInternalDisplayId);
// Red should not be affected by a warmed ambient temperature.
EXPECT_EQ(night_light_and_ambient_rgb.x(), night_light_rgb.x());
// Green and blue should be lowered instead.
EXPECT_LT(night_light_and_ambient_rgb.y(), night_light_rgb.y());
EXPECT_LT(night_light_and_ambient_rgb.z(), night_light_rgb.z());
}
} // namespace
} // namespace ash