// 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/system/power/power_sounds_controller.h"
#include <memory>
#include "ash/constants/ash_features.h"
#include "ash/constants/ash_pref_names.h"
#include "ash/shell.h"
#include "ash/system/power/battery_saver_controller.h"
#include "ash/system/system_notification_controller.h"
#include "ash/system/test_system_sounds_delegate.h"
#include "ash/test/ash_test_base.h"
#include "base/containers/flat_map.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "chromeos/dbus/power/fake_power_manager_client.h"
using power_manager::PowerSupplyProperties;
namespace ash {
namespace {
using ExternalPower = power_manager::PowerSupplyProperties_ExternalPower;
constexpr ExternalPower kAcPower =
power_manager::PowerSupplyProperties_ExternalPower_AC;
constexpr ExternalPower kUsbPower =
power_manager::PowerSupplyProperties_ExternalPower_USB;
constexpr ExternalPower kDisconnectedPower =
power_manager::PowerSupplyProperties_ExternalPower_DISCONNECTED;
constexpr int kCriticalPercentage = 5;
constexpr int kLowPowerPercentage = 10;
constexpr int kCriticalMinutes = 5;
constexpr int kLowPowerMinutes = 15;
} // namespace
class PowerSoundsControllerTest : public AshTestBase {
public:
explicit PowerSoundsControllerTest(
std::optional<bool> battery_saver_allowed = false)
: battery_saver_allowed_(battery_saver_allowed) {}
PowerSoundsControllerTest(const PowerSoundsControllerTest&) = delete;
PowerSoundsControllerTest& operator=(const PowerSoundsControllerTest&) =
delete;
~PowerSoundsControllerTest() override = default;
// AshTestBase:
void SetUp() override {
scoped_feature_.InitWithFeatures({features::kBatterySaver}, {});
AshTestBase::SetUp();
OverrideIsBatterySaverAllowedForTesting(battery_saver_allowed_);
SetInitialPowerStatus();
}
void TearDown() override {
AshTestBase::TearDown();
OverrideIsBatterySaverAllowedForTesting(std::nullopt);
}
TestSystemSoundsDelegate* GetSystemSoundsDelegate() const {
return static_cast<TestSystemSoundsDelegate*>(
Shell::Get()->system_sounds_delegate());
}
bool VerifySounds(const std::vector<Sound>& expected_sounds) const {
const auto& actual_sounds =
GetSystemSoundsDelegate()->last_played_sound_keys();
if (actual_sounds.size() != expected_sounds.size()) {
return false;
}
for (size_t i = 0; i < expected_sounds.size(); ++i) {
if (expected_sounds[i] != actual_sounds[i]) {
return false;
}
}
return true;
}
// Returns true if the lid is closed.
bool SetLidState(bool closed) {
chromeos::FakePowerManagerClient::Get()->SetLidState(
closed ? chromeos::PowerManagerClient::LidState::CLOSED
: chromeos::PowerManagerClient::LidState::OPEN,
base::TimeTicks::Now());
return Shell::Get()
->system_notification_controller()
->power_sounds_->lid_state_ ==
chromeos::PowerManagerClient::LidState::CLOSED;
}
void SetPowerStatus(int battery_level,
ExternalPower external_power,
int minutes_to_empty = 180) {
ASSERT_GE(battery_level, 0);
ASSERT_LE(battery_level, 100);
const bool old_ac_charger_connected = is_ac_charger_connected_;
is_ac_charger_connected_ = external_power == kAcPower;
PowerSupplyProperties proto;
proto.set_external_power(external_power);
proto.set_battery_percent(battery_level);
proto.set_battery_time_to_empty_sec(minutes_to_empty * 60);
proto.set_battery_time_to_full_sec(2 * 60 * 60);
proto.set_is_calculating_battery_time(false);
chromeos::FakePowerManagerClient::Get()->UpdatePowerProperties(proto);
// Records the battery level only when it's a plugin or unplug event.
if (old_ac_charger_connected != is_ac_charger_connected_) {
if (is_ac_charger_connected_) {
plugged_in_levels_samples_[battery_level]++;
} else {
unplugged_levels_samples_[battery_level]++;
}
}
}
void SetInitialPowerStatus() {
// The two toggle buttons are disabled as default, to test features, we will
// initialize it as enabled.
local_state()->SetBoolean(prefs::kChargingSoundsEnabled, true);
local_state()->SetBoolean(prefs::kLowBatterySoundEnabled, true);
// The default status for power is connected with a charger and the battery
// level is 1%. We set the initial power status for each unit test to
// disconnected with a charger and 5% battery level.
is_ac_charger_connected_ = true;
SetPowerStatus(5, kDisconnectedPower);
EXPECT_FALSE(SetLidState(/*closed=*/false));
}
protected:
base::HistogramTester histogram_tester_;
base::flat_map</*battery_level=*/int, /*sample_count=*/int>
plugged_in_levels_samples_;
base::flat_map</*battery_level=*/int, /*sample_count=*/int>
unplugged_levels_samples_;
base::test::ScopedFeatureList scoped_feature_;
private:
bool is_ac_charger_connected_;
std::optional<bool> battery_saver_allowed_;
};
class PowerSoundsControllerWithBatterySaverTest
: public PowerSoundsControllerTest,
public testing::WithParamInterface<
features::BatterySaverNotificationBehavior> {
public:
PowerSoundsControllerWithBatterySaverTest()
: PowerSoundsControllerTest(true) {}
};
INSTANTIATE_TEST_SUITE_P(
All,
PowerSoundsControllerWithBatterySaverTest,
testing::Values(features::BatterySaverNotificationBehavior::kBSMAutoEnable,
features::BatterySaverNotificationBehavior::kBSMOptIn));
TEST_P(PowerSoundsControllerWithBatterySaverTest,
PlayLowBatterySoundForBatterySaver) {
// Don't play warning sound if the battery level is no less than the low power
// threshold for battery saver.
const int battery_saver_threshold =
features::kBatterySaverActivationChargePercent.Get();
GetSystemSoundsDelegate()->reset();
SetPowerStatus(battery_saver_threshold + 1, kDisconnectedPower);
EXPECT_TRUE(GetSystemSoundsDelegate()->empty());
// When the battery drops below the low power threshold at the first time, the
// device should play the sound for warning.
SetPowerStatus(battery_saver_threshold, kDisconnectedPower);
EXPECT_TRUE(VerifySounds({Sound::kNoChargeLowBattery}));
GetSystemSoundsDelegate()->reset();
// When the battery level keeps dropping but no less than the critical power
// threshold, the device shouldn't play sound for warning.
SetPowerStatus(battery_saver_threshold - 1, kDisconnectedPower);
EXPECT_TRUE(GetSystemSoundsDelegate()->empty());
// The device will play the sound if its battery level keeps dropping to the
// critical power threshold.
SetPowerStatus(kCriticalPercentage, kDisconnectedPower);
EXPECT_TRUE(VerifySounds({Sound::kNoChargeLowBattery}));
}
// Tests if sounds are played correctedly when the device is plugged at three
// different battery ranges with a AC charger.
TEST_F(PowerSoundsControllerTest, PlaySoundsForCharging) {
// Expect no sounds at the initial status when a device has a battery level of
// 5%.
EXPECT_TRUE(GetSystemSoundsDelegate()->empty());
// When the battery level is in low range, from 0% to 15%, the sound for
// plugging in should be `Sound::kChargeLowBattery`.
SetPowerStatus(5, kAcPower);
EXPECT_TRUE(VerifySounds({Sound::kChargeLowBattery}));
// We should reset the sound key if the sound played successfully each time
// for not affecting the next sound key.
GetSystemSoundsDelegate()->reset();
// Unplug the ac charger when battery level reaches out to 50%.
SetPowerStatus(50, kDisconnectedPower);
// When the battery level is in medium range, from 16% to 79%, the sound for
// plugging in should be `Sound::kChargeMediumBattery`.
SetPowerStatus(50, kAcPower);
EXPECT_TRUE(VerifySounds({Sound::kChargeMediumBattery}));
GetSystemSoundsDelegate()->reset();
// Unplug the ac charger when battery level reaches out to 90%.
SetPowerStatus(90, kDisconnectedPower);
// When the battery level is in high range, from 80% to 100%, the sound for
// plugging in should be `Sound::kChargeHighBattery`.
SetPowerStatus(90, kAcPower);
EXPECT_TRUE(VerifySounds({Sound::kChargeHighBattery}));
GetSystemSoundsDelegate()->reset();
SetPowerStatus(95, kDisconnectedPower);
// Verifies no charging sound will be played if the device is connected with a
// USB charger(low-power charger).
SetPowerStatus(95, kUsbPower);
EXPECT_TRUE(GetSystemSoundsDelegate()->empty());
}
// Tests that when the user disables the toggle button for charging sounds, when
// plugging in a charger, the device won't play any charging sound.
TEST_F(PowerSoundsControllerTest, NoChargingSoundPlayedIfToggleButtonDisabled) {
local_state()->SetBoolean(prefs::kChargingSoundsEnabled, false);
ASSERT_FALSE(local_state()->GetBoolean(prefs::kChargingSoundsEnabled));
// Charge the device after disabling the button, and no sounds will be played.
SetPowerStatus(5, kAcPower);
EXPECT_TRUE(GetSystemSoundsDelegate()->empty());
}
// Tests if the warning sound can be played when the battery level drops below
// the threshold when connecting a low-power charger.
TEST_F(PowerSoundsControllerTest, PlayLowBatterySoundForPercentage) {
// Don't play warning sound if the battery level is no less than the low power
// threshold.
SetPowerStatus(kLowPowerPercentage + 1, kUsbPower);
EXPECT_TRUE(GetSystemSoundsDelegate()->empty());
// When the battery drops below the low power threshold at the first time, the
// device should play the sound for warning.
SetPowerStatus(kLowPowerPercentage, kUsbPower);
EXPECT_TRUE(VerifySounds({Sound::kNoChargeLowBattery}));
GetSystemSoundsDelegate()->reset();
// When the battery level keeps dropping but no less than the critical power
// threshold, the device shouldn't play sound for warning.
SetPowerStatus(kLowPowerPercentage - 1, kUsbPower);
EXPECT_TRUE(GetSystemSoundsDelegate()->empty());
// The device will play the sound if its battery level keeps dropping to the
// critical power threshold.
SetPowerStatus(kCriticalPercentage, kUsbPower);
EXPECT_TRUE(VerifySounds({Sound::kNoChargeLowBattery}));
}
// Tests if the warning sound can be played when the battery level drops below
// the threshold when disconnecting a line power.
TEST_F(PowerSoundsControllerTest, PlayLowBatterySoundForRemainingTime) {
// Set the remaining minutes value higher than the low power threshold.
SetPowerStatus(50, kDisconnectedPower, kLowPowerMinutes + 1);
EXPECT_TRUE(GetSystemSoundsDelegate()->empty());
// Set the remaining minutes value to the low power threshold.
SetPowerStatus(50, kDisconnectedPower, kLowPowerMinutes);
EXPECT_TRUE(VerifySounds({Sound::kNoChargeLowBattery}));
GetSystemSoundsDelegate()->reset();
// When the rounded value keeps dropping but no less than the critical power
// threshold, the device shouldn't play sound for warning.
SetPowerStatus(50, kDisconnectedPower, kCriticalMinutes + 1);
EXPECT_TRUE(GetSystemSoundsDelegate()->empty());
// Set the rounded value lower than the critical power threshold.
SetPowerStatus(50, kDisconnectedPower, kCriticalMinutes);
EXPECT_TRUE(VerifySounds({Sound::kNoChargeLowBattery}));
}
// When the toggle button for the low battery sound is disabled, the sound won't
// be played if the battery drops below 15% when connecting with a low-power
// charger.
TEST_F(PowerSoundsControllerTest,
NoLowBatterySoundPlayedIfToggleButtonDisabled) {
local_state()->SetBoolean(prefs::kLowBatterySoundEnabled, false);
ASSERT_FALSE(local_state()->GetBoolean(prefs::kLowBatterySoundEnabled));
// Don't play warning sound if the battery level is no less than 15% when
// connecting with a low-power charger.
SetPowerStatus(16, kUsbPower);
EXPECT_TRUE(GetSystemSoundsDelegate()->empty());
// When the battery drops below 15% at the first time, e.g. 15%, the device
// shouldn't play the sound for warning.
SetPowerStatus(15, kUsbPower);
EXPECT_TRUE(GetSystemSoundsDelegate()->empty());
}
// Tests that the charging sound and the low battery sound will be played
// sequentially, because the charging sound is only played if connecting with an
// AC charger; however, the low battery sound won't be played if it's AC
// charger.
TEST_F(PowerSoundsControllerTest, PlaySoundsSequentially) {
// 1. Tests that the low power minutes threshold come first, and then charging
// the device.
SetPowerStatus(10, kDisconnectedPower, kLowPowerMinutes + 1);
EXPECT_TRUE(GetSystemSoundsDelegate()->empty());
SetPowerStatus(10, kDisconnectedPower, kLowPowerMinutes);
EXPECT_TRUE(VerifySounds({Sound::kNoChargeLowBattery}));
GetSystemSoundsDelegate()->reset();
SetPowerStatus(10, kAcPower, kLowPowerMinutes);
EXPECT_TRUE(VerifySounds({Sound::kChargeLowBattery}));
GetSystemSoundsDelegate()->reset();
// 2. Tests that charging the device first and then disconnecting the device
// at the threshold.
SetPowerStatus(10, kDisconnectedPower, kLowPowerMinutes + 1);
EXPECT_TRUE(GetSystemSoundsDelegate()->empty());
SetPowerStatus(10, kAcPower, kLowPowerMinutes);
EXPECT_TRUE(VerifySounds({Sound::kChargeLowBattery}));
GetSystemSoundsDelegate()->reset();
SetPowerStatus(10, kDisconnectedPower, kLowPowerMinutes);
EXPECT_TRUE(VerifySounds({Sound::kNoChargeLowBattery}));
}
// Tests that the recording when the device is plugged in or Unplugged are
// recorded correctly.
TEST_F(PowerSoundsControllerTest,
RecordingBatteryLevelWhenPluggedInOrUnplugged) {
SetPowerStatus(5, kAcPower);
SetPowerStatus(50, kDisconnectedPower);
SetPowerStatus(50, kAcPower);
SetPowerStatus(100, kDisconnectedPower);
SetPowerStatus(100, kAcPower);
SetPowerStatus(100, kDisconnectedPower);
SetPowerStatus(100, kAcPower);
SetPowerStatus(100, kDisconnectedPower);
// Compare the number of samples for battery level from 0% to 100%.
for (int i = 0; i <= 100; i++) {
histogram_tester_.ExpectBucketCount(
PowerSoundsController::kPluggedInBatteryLevelHistogramName, i,
plugged_in_levels_samples_[i]);
histogram_tester_.ExpectBucketCount(
PowerSoundsController::kUnpluggedBatteryLevelHistogramName, i,
unplugged_levels_samples_[i]);
}
}
// Tests that the sounds can only be played if the lid is opened; otherwise, we
// don't play any sounds.
TEST_F(PowerSoundsControllerTest, PlaySoundsOnlyIfLidIsOpened) {
// When the lid is closed, plugging in a ac charger, the device don't play any
// sound.
EXPECT_TRUE(SetLidState(/*closed=*/true));
SetPowerStatus(5, kAcPower);
EXPECT_TRUE(GetSystemSoundsDelegate()->empty());
// When we open the lid, it doesn't play the delayed sound.
EXPECT_FALSE(SetLidState(/*closed=*/false));
EXPECT_TRUE(GetSystemSoundsDelegate()->empty());
// Under the condition of the lid opening, the device will play a sound when
// charging it.
SetPowerStatus(10, kDisconnectedPower);
SetPowerStatus(5, kAcPower);
EXPECT_TRUE(VerifySounds({Sound::kChargeLowBattery}));
}
} // namespace ash