blob: 8d53265772610f98bdb3809ecf91380e3666f467 [file] [log] [blame]
// 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/peripheral_battery_notifier.h"
#include <string>
#include <vector>
#include "ash/constants/ash_features.h"
#include "ash/constants/notifier_catalogs.h"
#include "ash/power/hid_battery_util.h"
#include "ash/public/cpp/notification_utils.h"
#include "ash/public/cpp/system_tray_client.h"
#include "ash/resources/vector_icons/vector_icons.h"
#include "ash/shell.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/system/model/system_tray_model.h"
#include "base/functional/bind.h"
#include "base/logging.h"
#include "base/memory/raw_ptr.h"
#include "base/strings/utf_string_conversions.h"
#include "base/time/time.h"
#include "components/device_event_log/device_event_log.h"
#include "device/bluetooth/bluetooth_adapter_factory.h"
#include "device/bluetooth/bluetooth_device.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/events/devices/device_data_manager.h"
#include "ui/events/devices/touchscreen_device.h"
#include "ui/gfx/image/image.h"
#include "ui/message_center/message_center.h"
#include "ui/message_center/public/cpp/notification.h"
#include "ui/message_center/public/cpp/notification_delegate.h"
namespace ash {
namespace {
// When a peripheral device's battery level is <= kLowBatteryLevel, consider
// it to be in low battery condition.
const uint8_t kLowBatteryLevel = 16;
// Don't show 2 low battery notification within |kNotificationInterval|.
constexpr base::TimeDelta kNotificationInterval = base::Seconds(60);
constexpr char kNotifierStylusBattery[] = "ash.stylus-battery";
constexpr char kNotificationOriginUrl[] = "chrome://peripheral-battery";
constexpr char kNotifierNonStylusBattery[] = "power.peripheral-battery";
// Prefix added to the key of a device to generate a unique ID when posting
// a notification to the Message Center.
constexpr char kPeripheralDeviceIdPrefix[] = "battery_notification-";
// Struct containing parameters for the notification which vary between the
// stylus notifications and the non stylus notifications.
struct NotificationParams {
std::string id;
std::u16string title;
std::u16string message;
std::string notifier_name;
GURL url;
raw_ptr<const gfx::VectorIcon> icon;
};
NotificationParams GetNonStylusNotificationParams(const std::string& map_key,
const std::u16string& name,
uint8_t battery_level,
bool is_bluetooth) {
return NotificationParams{
kPeripheralDeviceIdPrefix + map_key,
name,
l10n_util::GetStringFUTF16Int(
IDS_ASH_LOW_PERIPHERAL_BATTERY_NOTIFICATION_TEXT, battery_level),
kNotifierNonStylusBattery,
GURL(kNotificationOriginUrl),
is_bluetooth ? &kNotificationBluetoothBatteryWarningIcon
: &kNotificationBatteryCriticalIcon};
}
NotificationParams GetStylusNotificationParams() {
return NotificationParams{
PeripheralBatteryNotifier::kStylusNotificationId,
l10n_util::GetStringUTF16(IDS_ASH_LOW_STYLUS_BATTERY_NOTIFICATION_TITLE),
l10n_util::GetStringUTF16(IDS_ASH_LOW_STYLUS_BATTERY_NOTIFICATION_BODY),
kNotifierStylusBattery,
GURL(),
&kNotificationStylusBatteryWarningIcon};
}
} // namespace
const char PeripheralBatteryNotifier::kStylusNotificationId[] =
"stylus-battery";
PeripheralBatteryNotifier::NotificationInfo::NotificationInfo() = default;
PeripheralBatteryNotifier::NotificationInfo::NotificationInfo(
std::optional<uint8_t> level,
base::TimeTicks last_notification_timestamp)
: level(level),
last_notification_timestamp(last_notification_timestamp),
ever_notified(false) {}
PeripheralBatteryNotifier::NotificationInfo::~NotificationInfo() = default;
PeripheralBatteryNotifier::NotificationInfo::NotificationInfo(
const NotificationInfo& info) {
level = info.level;
last_notification_timestamp = info.last_notification_timestamp;
ever_notified = info.ever_notified;
}
PeripheralBatteryNotifier::PeripheralBatteryNotifier(
PeripheralBatteryListener* listener)
: peripheral_battery_listener_(listener) {
peripheral_battery_listener_->AddObserver(this);
}
PeripheralBatteryNotifier::~PeripheralBatteryNotifier() {
peripheral_battery_listener_->RemoveObserver(this);
}
void PeripheralBatteryNotifier::OnUpdatedBatteryLevel(
const PeripheralBatteryListener::BatteryInfo& battery_info) {
// TODO(b/187703348): it is worth listening to charger events if they
// might remove the notification: we want to clear it as soon as
// we believe the battery to have been charged, or at least starting
// charging.
if (battery_info.type == PeripheralBatteryListener::BatteryInfo::
PeripheralType::kStylusViaCharger) {
return;
}
UpdateBattery(battery_info);
}
void PeripheralBatteryNotifier::OnAddingBattery(
const PeripheralBatteryListener::BatteryInfo& battery) {}
void PeripheralBatteryNotifier::OnRemovingBattery(
const PeripheralBatteryListener::BatteryInfo& battery_info) {
const std::string& map_key = battery_info.key;
CancelNotification(battery_info);
battery_notifications_.erase(map_key);
BLUETOOTH_LOG(EVENT)
<< "Battery has been removed, erasing notification, battery key: "
<< map_key;
}
void PeripheralBatteryNotifier::UpdateBattery(
const PeripheralBatteryListener::BatteryInfo& battery_info) {
if (!battery_info.level || !battery_info.battery_report_eligible) {
CancelNotification(battery_info);
return;
}
const std::string& map_key = battery_info.key;
bool was_old_battery_level_low = false;
auto it = battery_notifications_.find(map_key);
if (it == battery_notifications_.end()) {
NotificationInfo new_notification_info;
new_notification_info.level = battery_info.level;
new_notification_info.last_notification_timestamp =
battery_info.last_update_timestamp;
new_notification_info.ever_notified = false;
battery_notifications_[map_key] = new_notification_info;
} else {
NotificationInfo& existing_notification_info = it->second;
std::optional<uint8_t> old_level = existing_notification_info.level;
was_old_battery_level_low = old_level && *old_level <= kLowBatteryLevel;
existing_notification_info.level = battery_info.level;
BLUETOOTH_LOG(EVENT)
<< "Battery level updated, battery key: " << map_key
<< " new battery level: "
<< (battery_info.level.has_value()
? base::NumberToString(battery_info.level.value())
: "empty value")
<< " old battery level: "
<< (old_level.has_value() ? base::NumberToString(old_level.value())
: "empty value")
<< " battery type: " << static_cast<int>(battery_info.type);
}
if (*battery_info.level > kLowBatteryLevel) {
CancelNotification(battery_info);
return;
}
// If low battery was on record, check if there is a notification, otherwise
// the user dismissed it and we shouldn't create another one.
if (was_old_battery_level_low)
UpdateBatteryNotificationIfVisible(battery_info);
else
ShowNotification(battery_info);
}
void PeripheralBatteryNotifier::UpdateBatteryNotificationIfVisible(
const PeripheralBatteryListener::BatteryInfo& battery_info) {
const std::string& map_key = battery_info.key;
std::string notification_map_key =
(battery_info.type ==
PeripheralBatteryListener::BatteryInfo::PeripheralType::kStylusViaScreen)
? kStylusNotificationId
: (kPeripheralDeviceIdPrefix + map_key);
message_center::Notification* notification =
message_center::MessageCenter::Get()->FindVisibleNotificationById(
notification_map_key);
if (notification)
ShowOrUpdateNotification(battery_info);
}
void PeripheralBatteryNotifier::ShowNotification(
const PeripheralBatteryListener::BatteryInfo& battery_info) {
const std::string& map_key = battery_info.key;
NotificationInfo& notification_info = battery_notifications_[map_key];
base::TimeTicks now = base::TimeTicks::Now();
if (!notification_info.ever_notified ||
(now - notification_info.last_notification_timestamp >=
kNotificationInterval)) {
ShowOrUpdateNotification(battery_info);
notification_info.last_notification_timestamp = base::TimeTicks::Now();
notification_info.ever_notified = true;
}
}
void PeripheralBatteryNotifier::ShowOrUpdateNotification(
const PeripheralBatteryListener::BatteryInfo& battery_info) {
const std::string& map_key = battery_info.key;
BLUETOOTH_LOG(EVENT)
<< "Battery notification shown or updated, battery key: " << map_key
<< " battery level: "
<< (battery_info.level.has_value()
? base::NumberToString(battery_info.level.value())
: "empty value")
<< " battery type: " << static_cast<int>(battery_info.type);
// Stylus battery notifications differ slightly.
NotificationParams params =
(battery_info.type ==
PeripheralBatteryListener::BatteryInfo::PeripheralType::kStylusViaScreen)
? GetStylusNotificationParams()
: GetNonStylusNotificationParams(
map_key, battery_info.name, *battery_info.level,
!battery_info.bluetooth_address.empty());
auto delegate = base::MakeRefCounted<
message_center::HandleNotificationClickDelegate>(base::BindRepeating(
[](const NotificationParams& params) {
Shell::Get()->system_tray_model()->client()->ShowBluetoothSettings();
message_center::MessageCenter::Get()->RemoveNotification(
params.id, /*by_user=*/false);
},
params));
auto notification = CreateSystemNotificationPtr(
message_center::NOTIFICATION_TYPE_SIMPLE, params.id, params.title,
params.message, std::u16string(), params.url,
message_center::NotifierId(message_center::NotifierType::SYSTEM_COMPONENT,
params.notifier_name,
NotificationCatalogName::kPeripheralBattery),
message_center::RichNotificationData(), std::move(delegate), *params.icon,
message_center::SystemNotificationWarningLevel::WARNING);
message_center::MessageCenter::Get()->AddNotification(
std::move(notification));
}
void PeripheralBatteryNotifier::CancelNotification(
const PeripheralBatteryListener::BatteryInfo& battery_info) {
const std::string& map_key = battery_info.key;
const auto it = battery_notifications_.find(map_key);
if (it == battery_notifications_.end()) {
return;
}
BLUETOOTH_LOG(EVENT)
<< "Battery notification canceled, battery key: " << map_key
<< " battery level: "
<< (battery_info.level.has_value()
? base::NumberToString(battery_info.level.value())
: "empty value")
<< " battery type: " << static_cast<int>(battery_info.type);
std::string notification_map_key =
(battery_info.type ==
PeripheralBatteryListener::BatteryInfo::PeripheralType::kStylusViaScreen)
? kStylusNotificationId
: (kPeripheralDeviceIdPrefix + map_key);
message_center::MessageCenter::Get()->RemoveNotification(notification_map_key,
/*by_user=*/false);
// Resetting this value allows a new low battery level to post a
// notification if the old one was also under the threshold.
it->second.level.reset();
}
} // namespace ash