blob: 188d27830f1861d0357729aba78e80b7d67ab316 [file] [log] [blame]
// Copyright (c) 2013 The Chromium Authors. All rights reserved.
// 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 <vector>
#include "ash/power/hid_battery_util.h"
#include "ash/public/cpp/notification_utils.h"
#include "ash/resources/vector_icons/vector_icons.h"
#include "ash/shell.h"
#include "ash/strings/grit/ash_strings.h"
#include "base/bind.h"
#include "base/logging.h"
#include "base/macros.h"
#include "base/strings/string16.h"
#include "base/strings/string_piece.h"
#include "base/strings/utf_string_conversions.h"
#include "base/time/default_tick_clock.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"
namespace ash {
namespace {
// When a peripheral device's battery level is <= kLowBatteryLevel, consider
// it to be in low battery condition.
const uint8_t kLowBatteryLevel = 15;
// Don't show 2 low battery notification within |kNotificationInterval|.
constexpr base::TimeDelta kNotificationInterval =
base::TimeDelta::FromSeconds(60);
const char kNotifierStylusBattery[] = "ash.stylus-battery";
// TODO(sammiequon): Add a notification url to chrome://settings/stylus once
// battery related information is shown there.
const char kNotificationOriginUrl[] = "chrome://peripheral-battery";
const char kNotifierNonStylusBattery[] = "power.peripheral-battery";
// Prefix added to the address of a Bluetooth device to generate an unique ID
// when posting a notification to the Message Center.
const char kBluetoothDeviceIdPrefix[] = "battery_notification_bluetooth-";
// Checks if the device is an external stylus.
bool IsStylusDevice(const std::string& path, const std::string& model_name) {
std::string identifier = ExtractHIDBatteryIdentifier(path);
for (const ui::TouchscreenDevice& device :
ui::DeviceDataManager::GetInstance()->GetTouchscreenDevices()) {
if (device.has_stylus &&
(device.name == model_name ||
device.name.find(model_name) != std::string::npos) &&
device.sys_path.value().find(identifier) != std::string::npos) {
return true;
}
}
return false;
}
// Struct containing parameters for the notification which vary between the
// stylus notifications and the non stylus notifications.
struct NotificationParams {
std::string id;
base::string16 title;
base::string16 message;
std::string notifier_name;
GURL url;
const gfx::VectorIcon* icon;
};
NotificationParams GetNonStylusNotificationParams(const std::string& map_key,
const base::string16& name,
uint8_t battery_level,
bool is_bluetooth) {
return NotificationParams{
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};
}
std::string GetMapKeyForBluetoothAddress(const std::string& bluetooth_address) {
return kBluetoothDeviceIdPrefix + base::ToLowerASCII(bluetooth_address);
}
// Returns the corresponding map key for a HID device.
std::string GetBatteryMapKey(const std::string& path) {
// Check if the HID path corresponds to a Bluetooth device.
const std::string bluetooth_address =
ExtractBluetoothAddressFromHIDBatteryPath(path);
return bluetooth_address.empty()
? path
: GetMapKeyForBluetoothAddress(bluetooth_address);
}
std::string GetBatteryMapKey(device::BluetoothDevice* device) {
return GetMapKeyForBluetoothAddress(device->GetAddress());
}
} // namespace
const char PeripheralBatteryNotifier::kStylusNotificationId[] =
"stylus-battery";
PeripheralBatteryNotifier::BatteryInfo::BatteryInfo() = default;
PeripheralBatteryNotifier::BatteryInfo::BatteryInfo(
const base::string16& name,
base::Optional<uint8_t> level,
base::TimeTicks last_notification_timestamp,
bool is_stylus,
const std::string& bluetooth_address)
: name(name),
level(level),
last_notification_timestamp(last_notification_timestamp),
is_stylus(is_stylus),
bluetooth_address(bluetooth_address) {}
PeripheralBatteryNotifier::BatteryInfo::~BatteryInfo() = default;
PeripheralBatteryNotifier::BatteryInfo::BatteryInfo(const BatteryInfo& info) {
name = info.name;
level = info.level;
last_notification_timestamp = info.last_notification_timestamp;
is_stylus = info.is_stylus;
bluetooth_address = info.bluetooth_address;
}
PeripheralBatteryNotifier::PeripheralBatteryNotifier()
: clock_(base::DefaultTickClock::GetInstance()),
weakptr_factory_(
new base::WeakPtrFactory<PeripheralBatteryNotifier>(this)) {
chromeos::PowerManagerClient::Get()->AddObserver(this);
device::BluetoothAdapterFactory::Get()->GetAdapter(
base::BindOnce(&PeripheralBatteryNotifier::InitializeOnBluetoothReady,
weakptr_factory_->GetWeakPtr()));
}
PeripheralBatteryNotifier::~PeripheralBatteryNotifier() {
if (bluetooth_adapter_.get())
bluetooth_adapter_->RemoveObserver(this);
chromeos::PowerManagerClient::Get()->RemoveObserver(this);
}
void PeripheralBatteryNotifier::PeripheralBatteryStatusReceived(
const std::string& path,
const std::string& name,
int level) {
// TODO(sammiequon): Powerd never sends negative levels. Investigate changing
// this check and the one below.
if (level < -1 || level > 100) {
LOG(ERROR) << "Invalid battery level " << level << " for device " << name
<< " at path " << path;
return;
}
if (!IsHIDBattery(path)) {
LOG(ERROR) << "Unsupported battery path " << path;
return;
}
// If unknown battery level received, cancel any existing notification.
if (level == -1) {
CancelNotification(GetBatteryMapKey(path));
return;
}
BatteryInfo battery{base::ASCIIToUTF16(name), level, base::TimeTicks(),
IsStylusDevice(path, name),
ExtractBluetoothAddressFromHIDBatteryPath(path)};
UpdateBattery(GetBatteryMapKey(path), battery);
}
void PeripheralBatteryNotifier::DeviceBatteryChanged(
device::BluetoothAdapter* adapter,
device::BluetoothDevice* device,
base::Optional<uint8_t> new_battery_percentage) {
if (!new_battery_percentage) {
CancelNotification(kBluetoothDeviceIdPrefix +
base::ToLowerASCII(device->GetAddress()));
return;
}
DCHECK_LE(new_battery_percentage.value(), 100);
BatteryInfo battery{device->GetNameForDisplay(),
new_battery_percentage.value(), base::TimeTicks(),
false /* is_stylus */, device->GetAddress()};
UpdateBattery(GetBatteryMapKey(device), battery);
}
void PeripheralBatteryNotifier::DeviceConnectedStateChanged(
device::BluetoothAdapter* adapter,
device::BluetoothDevice* device,
bool is_now_connected) {
if (!is_now_connected)
RemoveBluetoothBattery(device->GetAddress());
}
void PeripheralBatteryNotifier::DeviceRemoved(device::BluetoothAdapter* adapter,
device::BluetoothDevice* device) {
RemoveBluetoothBattery(device->GetAddress());
}
void PeripheralBatteryNotifier::InitializeOnBluetoothReady(
scoped_refptr<device::BluetoothAdapter> adapter) {
bluetooth_adapter_ = adapter;
CHECK(bluetooth_adapter_.get());
bluetooth_adapter_->AddObserver(this);
}
void PeripheralBatteryNotifier::RemoveBluetoothBattery(
const std::string& bluetooth_address) {
std::string address_lowercase = base::ToLowerASCII(bluetooth_address);
auto it = batteries_.find(kBluetoothDeviceIdPrefix + address_lowercase);
if (it != batteries_.end()) {
CancelNotification(it->first);
batteries_.erase(it);
}
}
void PeripheralBatteryNotifier::UpdateBattery(const std::string& map_key,
const BatteryInfo& battery_info) {
bool was_old_battery_level_low = false;
auto it = batteries_.find(map_key);
if (it == batteries_.end()) {
batteries_[map_key] = battery_info;
} else {
BatteryInfo& existing_battery_info = it->second;
base::Optional<uint8_t> old_level = existing_battery_info.level;
was_old_battery_level_low = old_level && *old_level < kLowBatteryLevel;
existing_battery_info.name = battery_info.name;
existing_battery_info.level = battery_info.level;
}
const BatteryInfo& info = batteries_[map_key];
if (!info.level || *info.level > kLowBatteryLevel) {
CancelNotification(map_key);
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(map_key, info);
else
ShowNotification(map_key, info);
}
void PeripheralBatteryNotifier::UpdateBatteryNotificationIfVisible(
const std::string& map_key,
const BatteryInfo& battery) {
message_center::Notification* notification =
message_center::MessageCenter::Get()->FindVisibleNotificationById(
map_key);
if (notification)
ShowOrUpdateNotification(map_key, battery);
}
void PeripheralBatteryNotifier::ShowNotification(const std::string& map_key,
const BatteryInfo& battery) {
base::TimeTicks now = clock_->NowTicks();
if (now - battery.last_notification_timestamp >= kNotificationInterval) {
ShowOrUpdateNotification(map_key, battery);
batteries_[map_key].last_notification_timestamp = clock_->NowTicks();
}
}
void PeripheralBatteryNotifier::ShowOrUpdateNotification(
const std::string& map_key,
const BatteryInfo& battery) {
// Stylus battery notifications differ slightly.
NotificationParams params = battery.is_stylus
? GetStylusNotificationParams()
: GetNonStylusNotificationParams(
map_key, battery.name, *battery.level,
!battery.bluetooth_address.empty());
auto notification = CreateSystemNotification(
message_center::NOTIFICATION_TYPE_SIMPLE, params.id, params.title,
params.message, base::string16(), params.url,
message_center::NotifierId(message_center::NotifierType::SYSTEM_COMPONENT,
params.notifier_name),
message_center::RichNotificationData(), nullptr, *params.icon,
message_center::SystemNotificationWarningLevel::WARNING);
message_center::MessageCenter::Get()->AddNotification(
std::move(notification));
}
void PeripheralBatteryNotifier::CancelNotification(const std::string& map_key) {
const auto it = batteries_.find(map_key);
if (it != batteries_.end()) {
std::string notification_map_key =
it->second.is_stylus ? kStylusNotificationId : map_key;
message_center::MessageCenter::Get()->RemoveNotification(
notification_map_key, false /* by_user */);
// 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