// 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/battery_notification.h"
#include <string>
#include "ash/constants/ash_features.h"
#include "ash/shell.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/system/power/battery_saver_controller.h"
#include "ash/system/power/power_notification_controller.h"
#include "ash/system/power/power_status.h"
#include "ash/system/system_notification_controller.h"
#include "ash/system/toast/toast_manager_impl.h"
#include "ash/system/toast/toast_overlay.h"
#include "ash/test/ash_test_base.h"
#include "base/test/scoped_feature_list.h"
#include "chromeos/dbus/power/fake_power_manager_client.h"
#include "chromeos/dbus/power_manager/power_supply_properties.pb.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/l10n/time_format.h"
#include "ui/message_center/message_center.h"
#include "ui/message_center/public/cpp/notification.h"
#include "ui/message_center/public/cpp/notification_types.h"
namespace ash {
namespace {
constexpr int kCriticalMinutes = 5;
constexpr int kLowPowerMinutes = 15;
} // namespace
using l10n_util::GetStringFUTF16;
using l10n_util::GetStringUTF16;
using message_center::FullscreenVisibility;
using message_center::SystemNotificationWarningLevel;
class BatteryNotificationTest : public AshTestBase {
public:
BatteryNotificationTest() = default;
BatteryNotificationTest(const BatteryNotificationTest&) = delete;
BatteryNotificationTest& operator=(const BatteryNotificationTest&) = delete;
~BatteryNotificationTest() override = default;
// AshTestBase:
void SetUp() override {
scoped_feature_list_ = std::make_unique<base::test::ScopedFeatureList>(
features::kBatterySaver);
chromeos::FakePowerManagerClient::InitializeFake();
AshTestBase::SetUp();
SetNotificationStateForTesting(
PowerNotificationController::NotificationState::
NOTIFICATION_BSM_ENABLING_AT_THRESHOLD);
battery_notification_ = std::make_unique<BatteryNotification>(
message_center::MessageCenter::Get(),
Shell::Get()
->system_notification_controller()
->power_notification_controller());
}
void TearDown() override {
OverrideIsBatterySaverAllowedForTesting(std::nullopt);
battery_notification_.reset();
AshTestBase::TearDown();
chromeos::PowerManagerClient::Shutdown();
scoped_feature_list_->Reset();
}
protected:
struct ExpectedNotificationValues {
size_t expected_button_size;
message_center::SystemNotificationWarningLevel expected_warning_level;
message_center::FullscreenVisibility expected_fullscreen_visibility;
std::u16string expected_title;
std::u16string expected_message;
std::u16string expected_button_title;
};
BatterySaverController* battery_saver_controller() {
return Shell::Get()->battery_saver_controller();
}
message_center::Notification* GetBatteryNotification() {
return message_center::MessageCenter::Get()->FindNotificationById(
BatteryNotification::kNotificationId);
}
ToastOverlay* GetCurrentToast() {
return Shell::Get()->toast_manager()->GetCurrentOverlayForTesting();
}
void TestBatterySaverNotification(
const ExpectedNotificationValues& expected_values,
PowerNotificationController::NotificationState notification_state,
bool expected_bsm_state_after_click) {
auto VerifyBatterySaverModeState =
[](base::RunLoop* run_loop, bool active,
std::optional<power_manager::BatterySaverModeState> state) {
ASSERT_TRUE(state);
EXPECT_EQ(state->enabled(), active);
run_loop->Quit();
};
PowerStatus::Get()->SetBatterySaverStateForTesting(
!expected_bsm_state_after_click);
// Display notification.
SetNotificationStateForTesting(notification_state);
battery_notification_->Update();
auto* notification = GetBatteryNotification();
ASSERT_TRUE(notification);
// Test expectations against actual values.
TestExpectedNotificationValues(expected_values, notification);
// Click the button to turn off/on battery saver mode depending on
// NotificationState.
notification->delegate()->Click(0, std::nullopt);
// Test that notification is dismissed after button is pressed.
EXPECT_EQ(GetBatteryNotification(), nullptr);
// Test Enable Toast on Button Click in Opt-In Branch.
if (notification_state ==
PowerNotificationController::NOTIFICATION_BSM_THRESHOLD_OPT_IN) {
EXPECT_NE(GetCurrentToast(), nullptr);
EXPECT_EQ(
GetCurrentToast()->GetText(),
l10n_util::GetStringUTF16(IDS_ASH_BATTERY_SAVER_ENABLED_TOAST_TEXT));
}
// Verify battery saver mode state changed respective to the
// NotificationState.
base::RunLoop run_loop;
chromeos::PowerManagerClient::Get()->GetBatterySaverModeState(
base::BindOnce(VerifyBatterySaverModeState, &run_loop,
expected_bsm_state_after_click));
run_loop.Run();
}
void TestExpectedNotificationValues(
const ExpectedNotificationValues& values,
const message_center::Notification* notification) {
const std::vector<message_center::ButtonInfo> buttons =
notification->buttons();
EXPECT_EQ(values.expected_warning_level,
notification->system_notification_warning_level());
EXPECT_EQ(values.expected_title, notification->title());
EXPECT_EQ(values.expected_message, notification->message());
EXPECT_EQ(values.expected_fullscreen_visibility,
notification->fullscreen_visibility());
EXPECT_FALSE(notification->pinned());
EXPECT_EQ(values.expected_button_size, buttons.size());
EXPECT_EQ(values.expected_button_title,
buttons.size() != 0 ? buttons[0].title : u"");
}
void SetNotificationStateForTesting(
PowerNotificationController::NotificationState new_state) {
Shell::Get()
->system_notification_controller()
->power_notification_controller()
->notification_state_ = new_state;
}
std::u16string GetLowPowerTitle() {
return GetStringUTF16(IDS_ASH_STATUS_TRAY_LOW_BATTERY_TITLE);
}
std::u16string GetLowPowerMessageBSMWithoutTime() {
return GetStringFUTF16(
IDS_ASH_STATUS_TRAY_LOW_BATTERY_BSM_GENERIC_MESSAGE_WITHOUT_TIME,
base::NumberToString16(PowerStatus::Get()->GetRoundedBatteryPercent()));
}
std::u16string GetLowPowerMessageBSM() {
return GetStringFUTF16(
IDS_ASH_STATUS_TRAY_LOW_BATTERY_BSM_GENERIC_MESSAGE,
base::NumberToString16(PowerStatus::Get()->GetRoundedBatteryPercent()),
GetRemainingTimeString());
}
std::u16string GetLowPowerMessage() {
return GetStringFUTF16(
IDS_ASH_STATUS_TRAY_LOW_BATTERY_MESSAGE, GetRemainingTimeString(),
base::NumberToString16(PowerStatus::Get()->GetRoundedBatteryPercent()));
}
std::u16string GetBatterySaverTitle() {
return GetStringUTF16(
IDS_ASH_STATUS_TRAY_LOW_BATTERY_BSM_AUTOENABLED_TITLE);
}
std::u16string GetBatterySaverMessageWithoutTime() {
return GetStringFUTF16(
IDS_ASH_STATUS_TRAY_LOW_BATTERY_BSM_AUTOENABLED_MESSAGE_WITHOUT_TIME,
base::NumberToString16(PowerStatus::Get()->GetRoundedBatteryPercent()));
}
std::u16string GetBatterySaverMessage() {
return GetStringFUTF16(
IDS_ASH_STATUS_TRAY_LOW_BATTERY_BSM_AUTOENABLED_MESSAGE,
base::NumberToString16(PowerStatus::Get()->GetRoundedBatteryPercent()),
GetRemainingTimeString());
}
std::u16string GetBatterySaverOptOutButtonString() {
return GetStringUTF16(IDS_ASH_STATUS_TRAY_LOW_BATTERY_BSM_BUTTON_OPT_OUT);
}
std::u16string GetBatterySaverOptInButtonString() {
return GetStringUTF16(IDS_ASH_STATUS_TRAY_LOW_BATTERY_BSM_BUTTON_OPT_IN);
}
void SetPowerStatus(double battery_percent = 100,
long time_to_empty_sec = 28800) {
power_manager::PowerSupplyProperties proto;
proto.set_battery_percent(battery_percent);
proto.set_battery_time_to_empty_sec(time_to_empty_sec);
PowerStatus::Get()->SetProtoForTesting(proto);
chromeos::FakePowerManagerClient::Get()->UpdatePowerProperties(proto);
}
std::unique_ptr<BatteryNotification> battery_notification_;
private:
std::u16string GetRemainingTimeString() {
return ui::TimeFormat::Simple(ui::TimeFormat::FORMAT_DURATION,
ui::TimeFormat::LENGTH_LONG,
*PowerStatus::Get()->GetBatteryTimeToEmpty());
}
std::unique_ptr<base::test::ScopedFeatureList> scoped_feature_list_;
};
// Keep test for backwards compatibility for time-based notifications.
TEST_F(BatteryNotificationTest, LowPowerNotification) {
// Disable Battery Saver feature to test original notification.
OverrideIsBatterySaverAllowedForTesting(false);
// Set the rounded value matches the low power threshold, percentage here is
// arbitrary.
SetPowerStatus(25, kLowPowerMinutes * 60 + 29);
SetNotificationStateForTesting(
PowerNotificationController::NotificationState::
NOTIFICATION_BSM_ENABLING_AT_THRESHOLD);
battery_notification_->Update();
auto* notification = GetBatteryNotification();
ASSERT_TRUE(notification);
// Expect a notification with 'Low Power', and no buttons to appear.
ExpectedNotificationValues expected_values{
0,
SystemNotificationWarningLevel::WARNING,
FullscreenVisibility::OVER_USER,
GetLowPowerTitle(),
GetLowPowerMessage(),
u""};
TestExpectedNotificationValues(expected_values, notification);
}
TEST_F(BatteryNotificationTest, ThresholdBatterySaverOptOutNotification) {
// Set the battery percentage to the threshold amount.
SetPowerStatus(features::kBatterySaverActivationChargePercent.Get());
// Expect a notification with 'turning on battery saver', and a
// 'turn off' button to appear.
ExpectedNotificationValues expected_values{
1,
SystemNotificationWarningLevel::WARNING,
FullscreenVisibility::OVER_USER,
GetBatterySaverTitle(),
GetBatterySaverMessage(),
GetBatterySaverOptOutButtonString()};
// Battery Saver should turn off when the button is clicked.
TestBatterySaverNotification(
expected_values,
PowerNotificationController::NOTIFICATION_BSM_ENABLING_AT_THRESHOLD,
/*expected_bsm_state_after_click=*/false);
}
TEST_F(BatteryNotificationTest,
ThresholdBatterySaverOptOutNotificationTimeCalculating) {
// Set the battery percentage to the threshold amount, and < 1 minute.
SetPowerStatus(features::kBatterySaverActivationChargePercent.Get(), 30);
// Expect a notification with 'turning on battery saver', with a message
// excluding the time remaining, and a 'turn off' button to appear.
ExpectedNotificationValues expected_values{
1,
SystemNotificationWarningLevel::WARNING,
FullscreenVisibility::OVER_USER,
GetBatterySaverTitle(),
GetBatterySaverMessageWithoutTime(),
GetBatterySaverOptOutButtonString()};
// Battery Saver should turn off when the button is clicked.
TestBatterySaverNotification(
expected_values,
PowerNotificationController::NOTIFICATION_BSM_ENABLING_AT_THRESHOLD,
/*expected_bsm_state_after_click=*/false);
}
TEST_F(BatteryNotificationTest, ThresholdBatterySaverOptInNotification) {
// Set the battery percentage to the threshold amount.
SetPowerStatus(features::kBatterySaverActivationChargePercent.Get());
// Expect a regular Low Power notification, and a 'turn on battery saver'
// button to appear.
ExpectedNotificationValues expected_values{
1,
SystemNotificationWarningLevel::WARNING,
FullscreenVisibility::OVER_USER,
GetLowPowerTitle(),
GetLowPowerMessageBSM(),
GetBatterySaverOptInButtonString()};
// Battery Saver should turn on when the button is clicked.
TestBatterySaverNotification(
expected_values,
PowerNotificationController::NOTIFICATION_BSM_THRESHOLD_OPT_IN,
/*expected_bsm_state_after_click=*/true);
}
TEST_F(BatteryNotificationTest,
ThresholdBatterySaverOptInNotificationTimeCalculating) {
// Set the battery percentage to the threshold amount, and < 1 minute.
SetPowerStatus(features::kBatterySaverActivationChargePercent.Get(), 30);
// Expect a regular Low Power notification, with a message
// excluding the time remaining, and a 'turn on battery saver'
// button to appear.
ExpectedNotificationValues expected_values{
1,
SystemNotificationWarningLevel::WARNING,
FullscreenVisibility::OVER_USER,
GetLowPowerTitle(),
GetLowPowerMessageBSMWithoutTime(),
GetBatterySaverOptInButtonString()};
// Battery Saver should turn on when the button is clicked.
TestBatterySaverNotification(
expected_values,
PowerNotificationController::NOTIFICATION_BSM_THRESHOLD_OPT_IN,
/*expected_bsm_state_after_click=*/true);
}
TEST_F(BatteryNotificationTest, CriticalPowerNotification) {
power_manager::PowerSupplyProperties proto;
// Set the rounded value matches the critical power threshold.
proto.set_battery_time_to_empty_sec(kCriticalMinutes * 60 + 29);
PowerStatus::Get()->SetProtoForTesting(proto);
SetNotificationStateForTesting(
PowerNotificationController::NotificationState::NOTIFICATION_CRITICAL);
battery_notification_->Update();
auto* notification = GetBatteryNotification();
ASSERT_TRUE(notification);
EXPECT_EQ(message_center::SystemNotificationWarningLevel::CRITICAL_WARNING,
notification->system_notification_warning_level());
EXPECT_EQ(
l10n_util::GetStringUTF16(IDS_ASH_STATUS_TRAY_CRITICAL_BATTERY_TITLE),
notification->title());
EXPECT_EQ(message_center::FullscreenVisibility::OVER_USER,
notification->fullscreen_visibility());
EXPECT_EQ(message_center::NotificationPriority::SYSTEM_PRIORITY,
notification->priority());
EXPECT_TRUE(notification->pinned());
}
} // namespace ash