chromium/ash/system/power/power_sounds_controller_unittest.cc

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

#include "ash/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