| // Copyright 2013 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_status.h" |
| |
| #include <algorithm> |
| #include <cmath> |
| |
| #include "ash/constants/ash_features.h" |
| #include "ash/public/cpp/power_utils.h" |
| #include "ash/resources/vector_icons/vector_icons.h" |
| #include "ash/strings/grit/ash_strings.h" |
| #include "ash/system/power/battery_image_source.h" |
| #include "base/i18n/number_formatting.h" |
| #include "base/i18n/time_formatting.h" |
| #include "base/logging.h" |
| #include "base/memory/ptr_util.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "chromeos/dbus/power/power_manager_client.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "ui/base/l10n/time_format.h" |
| #include "ui/chromeos/styles/cros_tokens_color_mappings.h" |
| #include "ui/color/color_provider.h" |
| #include "ui/display/display.h" |
| #include "ui/display/screen.h" |
| #include "ui/gfx/color_palette.h" |
| #include "ui/gfx/image/image.h" |
| |
| namespace ash { |
| namespace { |
| |
| static PowerStatus* g_power_status = nullptr; |
| |
| std::u16string GetBatteryTimeAccessibilityString(int hour, int min) { |
| DCHECK(hour || min); |
| if (hour && !min) { |
| return ui::TimeFormat::Simple(ui::TimeFormat::FORMAT_DURATION, |
| ui::TimeFormat::LENGTH_LONG, |
| base::Hours(hour)); |
| } |
| if (min && !hour) { |
| return ui::TimeFormat::Simple(ui::TimeFormat::FORMAT_DURATION, |
| ui::TimeFormat::LENGTH_LONG, |
| base::Minutes(min)); |
| } |
| return l10n_util::GetStringFUTF16( |
| IDS_ASH_STATUS_TRAY_BATTERY_TIME_ACCESSIBLE, |
| ui::TimeFormat::Simple(ui::TimeFormat::FORMAT_DURATION, |
| ui::TimeFormat::LENGTH_LONG, base::Hours(hour)), |
| ui::TimeFormat::Simple(ui::TimeFormat::FORMAT_DURATION, |
| ui::TimeFormat::LENGTH_LONG, base::Minutes(min))); |
| } |
| |
| int PowerSourceToMessageID( |
| const power_manager::PowerSupplyProperties_PowerSource& source) { |
| switch (source.port()) { |
| case power_manager::PowerSupplyProperties_PowerSource_Port_UNKNOWN: |
| return IDS_ASH_POWER_SOURCE_PORT_UNKNOWN; |
| case power_manager::PowerSupplyProperties_PowerSource_Port_LEFT: |
| return IDS_ASH_POWER_SOURCE_PORT_LEFT; |
| case power_manager::PowerSupplyProperties_PowerSource_Port_RIGHT: |
| return IDS_ASH_POWER_SOURCE_PORT_RIGHT; |
| case power_manager::PowerSupplyProperties_PowerSource_Port_BACK: |
| return IDS_ASH_POWER_SOURCE_PORT_BACK; |
| case power_manager::PowerSupplyProperties_PowerSource_Port_FRONT: |
| return IDS_ASH_POWER_SOURCE_PORT_FRONT; |
| case power_manager::PowerSupplyProperties_PowerSource_Port_LEFT_FRONT: |
| return IDS_ASH_POWER_SOURCE_PORT_LEFT_FRONT; |
| case power_manager::PowerSupplyProperties_PowerSource_Port_LEFT_BACK: |
| return IDS_ASH_POWER_SOURCE_PORT_LEFT_BACK; |
| case power_manager::PowerSupplyProperties_PowerSource_Port_RIGHT_FRONT: |
| return IDS_ASH_POWER_SOURCE_PORT_RIGHT_FRONT; |
| case power_manager::PowerSupplyProperties_PowerSource_Port_RIGHT_BACK: |
| return IDS_ASH_POWER_SOURCE_PORT_RIGHT_BACK; |
| case power_manager::PowerSupplyProperties_PowerSource_Port_BACK_LEFT: |
| return IDS_ASH_POWER_SOURCE_PORT_BACK_LEFT; |
| case power_manager::PowerSupplyProperties_PowerSource_Port_BACK_RIGHT: |
| return IDS_ASH_POWER_SOURCE_PORT_BACK_RIGHT; |
| } |
| NOTREACHED_IN_MIGRATION(); |
| return 0; |
| } |
| |
| SkColor GetDefaultAlertColor(const ui::ColorProvider* color_provider) { |
| return color_provider->GetColor(cros_tokens::kColorAlert); |
| } |
| |
| } // namespace |
| |
| BatteryColors PowerStatus::BatteryImageInfo::ResolveColors( |
| const PowerStatus::BatteryImageInfo& info, |
| const ui::ColorProvider* color_provider) { |
| BatteryColors resolved_colors; |
| |
| resolved_colors.foreground_color = |
| info.battery_color_preferences.foreground_color; |
| |
| // If there is a preference for badge color, use it. Otherwise, default to the |
| // foreground color used for drawing the battery icon. |
| resolved_colors.badge_color = |
| info.battery_color_preferences.badge_color.value_or( |
| info.battery_color_preferences.foreground_color); |
| |
| resolved_colors.alert_color = GetDefaultAlertColor(color_provider); |
| |
| return resolved_colors; |
| } |
| |
| bool PowerStatus::BatteryImageInfo::ApproximatelyEqual( |
| const BatteryImageInfo& o) const { |
| // 100% is distinct from all else. |
| if ((charge_percent != o.charge_percent) && |
| (charge_percent == 100 || o.charge_percent == 100)) { |
| return false; |
| } |
| |
| // Otherwise, consider close values such as 42% and 45% as about the same. |
| return icon_badge == o.icon_badge && alert_if_low == o.alert_if_low && |
| std::abs(charge_percent - o.charge_percent) < 5; |
| } |
| |
| const int PowerStatus::kMaxBatteryTimeToDisplaySec = 24 * 60 * 60; |
| |
| const double PowerStatus::kCriticalBatteryChargePercentage = 5; |
| |
| // static |
| void PowerStatus::Initialize() { |
| CHECK(!g_power_status); |
| g_power_status = new PowerStatus(); |
| } |
| |
| // static |
| void PowerStatus::Shutdown() { |
| CHECK(g_power_status); |
| delete g_power_status; |
| g_power_status = nullptr; |
| } |
| |
| // static |
| bool PowerStatus::IsInitialized() { |
| return g_power_status != nullptr; |
| } |
| |
| // static |
| PowerStatus* PowerStatus::Get() { |
| CHECK(g_power_status) << "PowerStatus::Get() called before Initialize()."; |
| return g_power_status; |
| } |
| |
| void PowerStatus::AddObserver(Observer* observer) { |
| DCHECK(observer); |
| observers_.AddObserver(observer); |
| } |
| |
| void PowerStatus::RemoveObserver(Observer* observer) { |
| DCHECK(observer); |
| observers_.RemoveObserver(observer); |
| } |
| |
| void PowerStatus::RequestStatusUpdate() { |
| chromeos::PowerManagerClient::Get()->RequestStatusUpdate(); |
| } |
| |
| bool PowerStatus::IsBatteryPresent() const { |
| return proto_.battery_state() != |
| power_manager::PowerSupplyProperties_BatteryState_NOT_PRESENT; |
| } |
| |
| bool PowerStatus::IsBatteryFull() const { |
| return proto_.battery_state() == |
| power_manager::PowerSupplyProperties_BatteryState_FULL; |
| } |
| |
| bool PowerStatus::IsBatteryCharging() const { |
| return proto_.battery_state() == |
| power_manager::PowerSupplyProperties_BatteryState_CHARGING; |
| } |
| |
| bool PowerStatus::IsBatteryDischargingOnLinePower() const { |
| return IsLinePowerConnected() && |
| proto_.battery_state() == |
| power_manager::PowerSupplyProperties_BatteryState_DISCHARGING; |
| } |
| |
| double PowerStatus::GetBatteryPercent() const { |
| return proto_.battery_percent(); |
| } |
| |
| int PowerStatus::GetRoundedBatteryPercent() const { |
| return power_utils::GetRoundedBatteryPercent(GetBatteryPercent()); |
| } |
| |
| bool PowerStatus::IsBatteryTimeBeingCalculated() const { |
| return proto_.is_calculating_battery_time(); |
| } |
| |
| std::optional<base::TimeDelta> PowerStatus::GetBatteryTimeToEmpty() const { |
| // powerd omits the field if no battery is present and sends -1 if it couldn't |
| // compute a reasonable estimate. |
| if (!proto_.has_battery_time_to_empty_sec() || |
| proto_.battery_time_to_empty_sec() < 0) { |
| return std::nullopt; |
| } |
| return base::Seconds(proto_.battery_time_to_empty_sec()); |
| } |
| |
| std::optional<base::TimeDelta> PowerStatus::GetBatteryTimeToFull() const { |
| // powerd omits the field if no battery is present and sends -1 if it couldn't |
| // compute a reasonable estimate. |
| if (!proto_.has_battery_time_to_full_sec() || |
| proto_.battery_time_to_full_sec() < 0) { |
| return std::nullopt; |
| } |
| return base::Seconds(proto_.battery_time_to_full_sec()); |
| } |
| |
| bool PowerStatus::IsLinePowerConnected() const { |
| return proto_.external_power() != |
| power_manager::PowerSupplyProperties_ExternalPower_DISCONNECTED; |
| } |
| |
| bool PowerStatus::IsMainsChargerConnected() const { |
| return proto_.external_power() == |
| power_manager::PowerSupplyProperties_ExternalPower_AC; |
| } |
| |
| bool PowerStatus::IsUsbChargerConnected() const { |
| return proto_.external_power() == |
| power_manager::PowerSupplyProperties_ExternalPower_USB; |
| } |
| |
| bool PowerStatus::SupportsDualRoleDevices() const { |
| return proto_.supports_dual_role_devices(); |
| } |
| |
| bool PowerStatus::HasDualRoleDevices() const { |
| if (!SupportsDualRoleDevices()) { |
| return false; |
| } |
| |
| for (int i = 0; i < proto_.available_external_power_source_size(); i++) { |
| if (!proto_.available_external_power_source(i).active_by_default()) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| std::vector<PowerStatus::PowerSource> PowerStatus::GetPowerSources() const { |
| std::vector<PowerSource> sources; |
| for (int i = 0; i < proto_.available_external_power_source_size(); i++) { |
| const auto& source = proto_.available_external_power_source(i); |
| sources.push_back( |
| {source.id(), |
| source.active_by_default() ? DEDICATED_CHARGER : DUAL_ROLE_USB, |
| PowerSourceToMessageID(source)}); |
| } |
| return sources; |
| } |
| |
| std::string PowerStatus::GetCurrentPowerSourceID() const { |
| return proto_.external_power_source_id(); |
| } |
| |
| PowerStatus::BatteryImageInfo PowerStatus::GenerateBatteryImageInfo( |
| const SkColor foreground_color, |
| std::optional<SkColor> badge_color) const { |
| BatteryImageInfo info(foreground_color, badge_color); |
| CalculateBatteryImageInfo(&info); |
| return info; |
| } |
| |
| void PowerStatus::CalculateBatteryImageInfo(BatteryImageInfo* info) const { |
| // We only alert if we are on battery, and battery saver mode is disabled. |
| if (features::IsBatterySaverAvailable()) { |
| info->alert_if_low = !IsLinePowerConnected() && !IsBatterySaverActive(); |
| } else { |
| info->alert_if_low = !IsLinePowerConnected(); |
| } |
| |
| if (!IsUsbChargerConnected() && !IsBatteryPresent()) { |
| info->icon_badge = &kUnifiedMenuBatteryXIcon; |
| info->badge_outline = &kUnifiedMenuBatteryXOutlineMaskIcon; |
| info->charge_percent = 0; |
| return; |
| } |
| |
| if (IsUsbChargerConnected()) { |
| info->icon_badge = &kUnifiedMenuBatteryUnreliableIcon; |
| info->badge_outline = &kUnifiedMenuBatteryUnreliableOutlineMaskIcon; |
| } else if (IsLinePowerConnected()) { |
| info->icon_badge = &kUnifiedMenuBatteryBoltIcon; |
| info->badge_outline = &kUnifiedMenuBatteryBoltOutlineMaskIcon; |
| } else if (IsBatterySaverActive()) { |
| info->icon_badge = &kBatterySaverPlusIcon; |
| info->badge_outline = &kBatterySaverPlusOutlineIcon; |
| } else { |
| info->icon_badge = nullptr; |
| info->badge_outline = nullptr; |
| } |
| |
| info->charge_percent = GetBatteryPercent(); |
| |
| // Use an alert badge if the battery is critically low and does not already |
| // have a badge assigned. |
| if (GetBatteryPercent() < kCriticalBatteryChargePercentage && |
| !info->icon_badge) { |
| info->icon_badge = &kUnifiedMenuBatteryAlertIcon; |
| info->badge_outline = &kUnifiedMenuBatteryAlertOutlineMaskIcon; |
| } |
| } |
| |
| // static |
| ui::ImageModel PowerStatus::GetBatteryImageModel(const BatteryImageInfo& info, |
| int height) { |
| return ui::ImageModel::FromImageGenerator( |
| base::BindRepeating(&PowerStatus::GetBatteryImage, info, height), |
| gfx::Size(height, height)); |
| } |
| |
| // static |
| gfx::ImageSkia PowerStatus::GetBatteryImage( |
| const BatteryImageInfo& info, |
| int height, |
| const ui::ColorProvider* color_provider) { |
| BatteryColors colors = |
| PowerStatus::BatteryImageInfo::ResolveColors(info, color_provider); |
| auto* source = new BatteryImageSource(info, height, colors); |
| return gfx::ImageSkia(base::WrapUnique(source), source->size()); |
| } |
| |
| std::u16string PowerStatus::GetAccessibleNameString( |
| bool full_description) const { |
| if (IsBatteryFull()) { |
| return l10n_util::GetStringUTF16( |
| IDS_ASH_STATUS_TRAY_BATTERY_FULL_CHARGE_ACCESSIBLE); |
| } |
| |
| int percentage_accessibility_token = -1; |
| if (features::IsBatterySaverAvailable()) { |
| if (IsBatteryCharging()) { |
| percentage_accessibility_token = |
| IsBatterySaverActive() |
| ? IDS_ASH_STATUS_TRAY_BATTERY_PERCENT_CHARGING_BSM_ON_ACCESSIBLE |
| : IDS_ASH_STATUS_TRAY_BATTERY_PERCENT_CHARGING_ACCESSIBLE; |
| } else { |
| percentage_accessibility_token = |
| IsBatterySaverActive() |
| ? IDS_ASH_STATUS_TRAY_BATTERY_PERCENT_BSM_ON_ACCESSIBLE |
| : IDS_ASH_STATUS_TRAY_BATTERY_PERCENT_ACCESSIBLE; |
| } |
| } else { // Backwards compatibility with battery saver feature flag disabled. |
| percentage_accessibility_token = |
| IsBatteryCharging() |
| ? IDS_ASH_STATUS_TRAY_BATTERY_PERCENT_CHARGING_ACCESSIBLE |
| : IDS_ASH_STATUS_TRAY_BATTERY_PERCENT_ACCESSIBLE; |
| } |
| |
| std::u16string battery_percentage_accessible = l10n_util::GetStringFUTF16( |
| percentage_accessibility_token, |
| base::NumberToString16(GetRoundedBatteryPercent())); |
| if (!full_description) |
| return battery_percentage_accessible; |
| |
| std::u16string battery_time_accessible = std::u16string(); |
| const std::optional<base::TimeDelta> time = |
| IsBatteryCharging() ? GetBatteryTimeToFull() : GetBatteryTimeToEmpty(); |
| |
| if (IsUsbChargerConnected()) { |
| battery_time_accessible = l10n_util::GetStringUTF16( |
| IDS_ASH_STATUS_TRAY_BATTERY_CHARGING_UNRELIABLE_ACCESSIBLE); |
| } else if (IsBatteryTimeBeingCalculated()) { |
| battery_time_accessible = l10n_util::GetStringUTF16( |
| IDS_ASH_STATUS_TRAY_BATTERY_CALCULATING_ACCESSIBLE); |
| } else if (time && power_utils::ShouldDisplayBatteryTime(*time) && |
| !IsBatteryDischargingOnLinePower()) { |
| int hour = 0, min = 0; |
| power_utils::SplitTimeIntoHoursAndMinutes(*time, &hour, &min); |
| std::u16string minute = min < 10 ? u"0" + base::NumberToString16(min) |
| : base::NumberToString16(min); |
| battery_time_accessible = l10n_util::GetStringFUTF16( |
| IsBatteryCharging() |
| ? IDS_ASH_STATUS_TRAY_BATTERY_TIME_UNTIL_FULL_ACCESSIBLE |
| : IDS_ASH_STATUS_TRAY_BATTERY_TIME_LEFT_ACCESSIBLE, |
| GetBatteryTimeAccessibilityString(hour, min)); |
| } |
| return battery_time_accessible.empty() |
| ? battery_percentage_accessible |
| : battery_percentage_accessible + u" " + battery_time_accessible; |
| } |
| |
| std::pair<std::u16string, std::u16string> PowerStatus::GetStatusStrings() |
| const { |
| std::u16string percentage; |
| std::u16string status; |
| if (IsBatteryFull()) { |
| status = l10n_util::GetStringUTF16(IDS_ASH_STATUS_TRAY_BATTERY_FULL); |
| } else { |
| percentage = base::FormatPercent(GetRoundedBatteryPercent()); |
| if (IsUsbChargerConnected()) { |
| status = l10n_util::GetStringUTF16( |
| IDS_ASH_STATUS_TRAY_BATTERY_CHARGING_UNRELIABLE); |
| } else if (IsBatteryTimeBeingCalculated()) { |
| status = |
| l10n_util::GetStringUTF16(IDS_ASH_STATUS_TRAY_BATTERY_CALCULATING); |
| } else { |
| std::optional<base::TimeDelta> time = IsBatteryCharging() |
| ? GetBatteryTimeToFull() |
| : GetBatteryTimeToEmpty(); |
| if (time && power_utils::ShouldDisplayBatteryTime(*time) && |
| !IsBatteryDischargingOnLinePower()) { |
| std::u16string duration; |
| if (!base::TimeDurationFormat(*time, base::DURATION_WIDTH_NUMERIC, |
| &duration)) { |
| LOG(ERROR) << "Failed to format duration " << *time; |
| } |
| status = l10n_util::GetStringFUTF16( |
| IsBatteryCharging() |
| ? IDS_ASH_STATUS_TRAY_BATTERY_TIME_UNTIL_FULL_SHORT |
| : IDS_ASH_STATUS_TRAY_BATTERY_TIME_LEFT_SHORT, |
| duration); |
| } |
| } |
| } |
| |
| return std::make_pair(percentage, status); |
| } |
| |
| std::u16string PowerStatus::GetInlinedStatusString() const { |
| auto [percentage_text, status_text] = GetStatusStrings(); |
| |
| if (!percentage_text.empty() && !status_text.empty()) { |
| return percentage_text + |
| l10n_util::GetStringUTF16( |
| IDS_ASH_STATUS_TRAY_BATTERY_STATUS_SEPARATOR) + |
| status_text; |
| } else if (!percentage_text.empty()) { |
| return percentage_text; |
| } else { |
| return status_text; |
| } |
| } |
| |
| double PowerStatus::GetPreferredMinimumPower() const { |
| return proto_.preferred_minimum_external_power(); |
| } |
| |
| bool PowerStatus::IsBatterySaverActive() const { |
| return battery_saver_active_; |
| } |
| |
| base::WeakPtr<PowerStatus> PowerStatus::GetWeakPtr() { |
| return weak_ptr_factory_.GetWeakPtr(); |
| } |
| |
| PowerStatus::PowerStatus() { |
| chromeos::PowerManagerClient::Get()->AddObserver(this); |
| chromeos::PowerManagerClient::Get()->RequestStatusUpdate(); |
| chromeos::PowerManagerClient::Get()->GetBatterySaverModeState(base::BindOnce( |
| &PowerStatus::OnGotBatterySaverState, weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| PowerStatus::~PowerStatus() { |
| chromeos::PowerManagerClient::Get()->RemoveObserver(this); |
| } |
| |
| void PowerStatus::SetProtoForTesting( |
| const power_manager::PowerSupplyProperties& proto) { |
| proto_ = proto; |
| proto_initialized_ = true; |
| } |
| |
| void PowerStatus::SetBatterySaverStateForTesting(bool active) { |
| battery_saver_active_ = active; |
| } |
| |
| void PowerStatus::PowerChanged( |
| const power_manager::PowerSupplyProperties& proto) { |
| proto_ = proto; |
| proto_initialized_ = true; |
| for (auto& observer : observers_) { |
| observer.OnPowerStatusChanged(); |
| } |
| } |
| |
| void PowerStatus::BatterySaverModeStateChanged( |
| const power_manager::BatterySaverModeState& state) { |
| const bool prev_active = battery_saver_active_; |
| battery_saver_active_ = state.enabled(); |
| if (prev_active == battery_saver_active_) { |
| return; |
| } |
| if (!proto_initialized_) { |
| // Don't update clients |
| return; |
| } |
| for (auto& observer : observers_) { |
| observer.OnPowerStatusChanged(); |
| } |
| } |
| |
| void PowerStatus::OnGotBatterySaverState( |
| std::optional<power_manager::BatterySaverModeState> state) { |
| if (state) { |
| BatterySaverModeStateChanged(*state); |
| } |
| } |
| |
| } // namespace ash |