blob: 7941d901fb178b20b4489552bc33ef077024c3a5 [file] [log] [blame]
// 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 "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/power/power_status.h"
#include "base/check.h"
#include "base/check_is_test.h"
#include "base/metrics/histogram_functions.h"
#include "base/time/time.h"
#include "chromeos/ash/components/audio/sounds.h"
#include "chromeos/dbus/power/power_manager_client.h"
#include "components/prefs/pref_registry_simple.h"
#include "ui/message_center/message_center.h"
namespace ash {
namespace {
constexpr ExternalPower kAcPower =
power_manager::PowerSupplyProperties_ExternalPower_AC;
constexpr ExternalPower kUsbPower =
power_manager::PowerSupplyProperties_ExternalPower_USB;
// Percentage-based thresholds for remaining battery charge to play sounds when
// plugging in a charger cable.
constexpr int kMidPercentageForCharging = 16;
constexpr int kNormalPercentageForCharging = 80;
// Percentage-based threshold for remaining battery level when using a low-power
// charger.
constexpr int kCriticalWarningPercentage = 5;
constexpr int kLowPowerWarningPercentage = 10;
// Time-based threshold for remaining time when disconnected with the line
// power (any type of charger).
constexpr base::TimeDelta kCriticalWarningMinutes = base::Minutes(5);
constexpr base::TimeDelta kLowPowerWarningMinutes = base::Minutes(15);
// Gets the sound for plugging in an AC charger at different battery levels.
Sound GetSoundKeyForBatteryLevel(int level) {
if (level >= kNormalPercentageForCharging)
return Sound::kChargeHighBattery;
const int threshold =
IsBatterySaverAllowed()
? features::kBatterySaverActivationChargePercent.Get() + 1
: kMidPercentageForCharging;
return level >= threshold ? Sound::kChargeMediumBattery
: Sound::kChargeLowBattery;
}
} // namespace
// static
const char PowerSoundsController::kPluggedInBatteryLevelHistogramName[] =
"Ash.PowerSoundsController.PluggedInBatteryLevel";
// static
const char PowerSoundsController::kUnpluggedBatteryLevelHistogramName[] =
"Ash.PowerSoundsController.UnpluggedBatteryLevel";
PowerSoundsController::PowerSoundsController() {
chromeos::PowerManagerClient* client = chromeos::PowerManagerClient::Get();
DCHECK(client);
client->AddObserver(this);
// Get the initial lid state.
client->GetSwitchStates(
base::BindOnce(&PowerSoundsController::OnReceiveSwitchStates,
weak_factory_.GetWeakPtr()));
PowerStatus* power_status = PowerStatus::Get();
power_status->AddObserver(this);
battery_level_ = power_status->GetRoundedBatteryPercent();
is_ac_charger_connected_ = power_status->IsMainsChargerConnected();
local_state_ = Shell::Get()->local_state();
// `local_state_` could be null in tests.
if (local_state_) {
low_battery_sound_enabled_.Init(prefs::kLowBatterySoundEnabled,
local_state_);
charging_sounds_enabled_.Init(prefs::kChargingSoundsEnabled, local_state_);
}
}
PowerSoundsController::~PowerSoundsController() {
PowerStatus::Get()->RemoveObserver(this);
chromeos::PowerManagerClient::Get()->RemoveObserver(this);
}
void PowerSoundsController::RegisterLocalStatePrefs(
PrefRegistrySimple* registry) {
registry->RegisterBooleanPref(prefs::kChargingSoundsEnabled,
/*default_value=*/false);
registry->RegisterBooleanPref(prefs::kLowBatterySoundEnabled,
/*default_value=*/false);
}
void PowerSoundsController::OnPowerStatusChanged() {
if (!local_state_) {
CHECK_IS_TEST();
return;
}
const PowerStatus& status = *PowerStatus::Get();
SetPowerStatus(status.GetRoundedBatteryPercent(),
status.IsBatteryTimeBeingCalculated(), status.external_power(),
status.GetBatteryTimeToEmpty());
}
void PowerSoundsController::LidEventReceived(
chromeos::PowerManagerClient::LidState state,
base::TimeTicks timestamp) {
lid_state_ = state;
}
void PowerSoundsController::OnReceiveSwitchStates(
std::optional<chromeos::PowerManagerClient::SwitchStates> switch_states) {
if (switch_states.has_value()) {
lid_state_ = switch_states->lid_state;
}
}
bool PowerSoundsController::CanPlaySounds() const {
// Do not play any sound if the device is in DND mode, or if the lid is not
// open.
return !message_center::MessageCenter::Get()->IsQuietMode() &&
lid_state_ == chromeos::PowerManagerClient::LidState::OPEN;
}
void PowerSoundsController::SetPowerStatus(
int battery_level,
bool is_calculating_battery_time,
ExternalPower external_power,
std::optional<base::TimeDelta> remaining_time) {
battery_level_ = battery_level;
const bool old_ac_charger_connected = is_ac_charger_connected_;
is_ac_charger_connected_ = external_power == kAcPower;
// Records the battery level only for the device plugged in or Unplugged.
if (old_ac_charger_connected != is_ac_charger_connected_) {
base::UmaHistogramPercentage(is_ac_charger_connected_
? kPluggedInBatteryLevelHistogramName
: kUnpluggedBatteryLevelHistogramName,
battery_level_);
}
MaybePlaySoundsForCharging(old_ac_charger_connected);
if (UpdateBatteryState(is_calculating_battery_time, external_power,
remaining_time) &&
ShouldPlayLowBatterySound()) {
Shell::Get()->system_sounds_delegate()->Play(Sound::kNoChargeLowBattery);
}
}
void PowerSoundsController::MaybePlaySoundsForCharging(
bool old_ac_charger_connected) {
// Don't play the charging sound if the toggle button is disabled by user in
// the Settings UI.
if (!charging_sounds_enabled_.GetValue() || !CanPlaySounds()) {
return;
}
// Returns when it isn't a plug in event.
bool is_plugging_in = !old_ac_charger_connected && is_ac_charger_connected_;
if (!is_plugging_in)
return;
Shell::Get()->system_sounds_delegate()->Play(
GetSoundKeyForBatteryLevel(battery_level_));
}
bool PowerSoundsController::ShouldPlayLowBatterySound() const {
if (!low_battery_sound_enabled_.GetValue() || !CanPlaySounds()) {
return false;
}
return current_state_ == BatteryState::kCriticalPower ||
current_state_ == BatteryState::kLowPower;
}
bool PowerSoundsController::UpdateBatteryState(
bool is_calculating_battery_time,
ExternalPower external_power,
std::optional<base::TimeDelta> remaining_time) {
const auto new_state = CalculateBatteryState(is_calculating_battery_time,
external_power, remaining_time);
if (new_state == current_state_) {
return false;
}
current_state_ = new_state;
return true;
}
PowerSoundsController::BatteryState
PowerSoundsController::CalculateBatteryState(
bool is_calculating_battery_time,
ExternalPower external_power,
std::optional<base::TimeDelta> remaining_time) const {
const bool is_battery_saver_allowed = IsBatterySaverAllowed();
if ((is_calculating_battery_time && !is_battery_saver_allowed) ||
is_ac_charger_connected_) {
return BatteryState::kNone;
}
// The battery state calculation should follow the same logic used by the
// power notification (Please see
// `PowerNotificationController::UpdateNotificationState()`). Hence, when a
// low-power charger (i.e. a USB charger) is connected, or we are using
// battery saver notifications, we calculate the state based on the remaining
// `battery_level_` percentage. GetBatteryStateFromBatteryLevel()
// automatically reflects this differentiation in its logic. Otherwise, when
// the device is disconnected, we calculate it based on the remaining time
// until the battery is empty.
if (is_battery_saver_allowed || external_power == kUsbPower) {
return GetBatteryStateFromBatteryLevel();
}
return GetBatteryStateFromRemainingTime(remaining_time);
}
PowerSoundsController::BatteryState
PowerSoundsController::GetBatteryStateFromBatteryLevel() const {
if (battery_level_ <= kCriticalWarningPercentage) {
return BatteryState::kCriticalPower;
}
const int low_power_warning_percentage =
IsBatterySaverAllowed()
? features::kBatterySaverActivationChargePercent.Get()
: kLowPowerWarningPercentage;
if (battery_level_ <= low_power_warning_percentage) {
return BatteryState::kLowPower;
}
return BatteryState::kNone;
}
PowerSoundsController::BatteryState
PowerSoundsController::GetBatteryStateFromRemainingTime(
std::optional<base::TimeDelta> remaining_time) const {
if (!remaining_time) {
return BatteryState::kNone;
}
if (*remaining_time <= kCriticalWarningMinutes) {
return BatteryState::kCriticalPower;
}
if (*remaining_time <= kLowPowerWarningMinutes) {
return BatteryState::kLowPower;
}
return BatteryState::kNone;
}
} // namespace ash