chromium/ash/system/power/power_sounds_controller.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 "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