Commit 18b2a915 authored by Alberto Herrera's avatar Alberto Herrera Committed by Commit Bot

[Bluetooth] Show low battery notifications for GATT Bluetooth Devices.

Use the existing PeripheralBatteryNotifier to show low battery
notifications of connected device::BluetoothDevice objects. This CL
adds functionality to trigger for GATT connections that support the
Battery Service.

Also added DeviceBatteryChanged() to device::BluetoothAdapter::Observer
so device::BluetoothDevice can notify about battery level changes and
PeripheralBatteryNotifier gets notified about those changes.

Also modified BluetoothDevice to make the battery percentage property
OS_CHROMEOS-only.

Design doc at go/cros-bt-battery.

Bug: 785758
Change-Id: I325e6bf47032fe5fc2a7471d2a1be9850fd89030
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1703341
Commit-Queue: Alberto Herrera <ahrfgb@google.com>
Reviewed-by: default avatarJames Cook <jamescook@chromium.org>
Reviewed-by: default avatarReilly Grant <reillyg@chromium.org>
Reviewed-by: default avatarRyan Hansberry <hansberry@chromium.org>
Reviewed-by: default avatarKyle Horimoto <khorimoto@chromium.org>
Cr-Commit-Position: refs/heads/master@{#682791}
parent e0d76169
......@@ -76,7 +76,7 @@ void GattBatteryPoller::OnBatteryPercentageFetched(
if (battery_percentage) {
device::BluetoothDevice* device = adapter_->GetDevice(device_address_);
if (device)
device->set_battery_percentage(*battery_percentage);
device->SetBatteryPercentage(*battery_percentage);
}
ScheduleNextAttempt(battery_percentage.has_value());
......@@ -100,7 +100,7 @@ void GattBatteryPoller::ScheduleNextAttempt(bool was_last_attempt_successful) {
StartNextAttemptTimer();
} else {
// Reset battery field after exceeding the retry count.
device->set_battery_percentage(base::nullopt);
device->SetBatteryPercentage(base::nullopt);
}
}
......
......@@ -165,7 +165,7 @@ TEST_F(GattBatteryPollerTest, RetryPollingAfterAnError) {
}
TEST_F(GattBatteryPollerTest, DoesNotModifyBatteryValueAfterAnError) {
mock_device_->set_battery_percentage(kBatteryPercentage);
mock_device_->SetBatteryPercentage(kBatteryPercentage);
CreateGattBatteryPoller();
EXPECT_EQ(1, fetchers_created_count());
......@@ -180,7 +180,7 @@ TEST_F(GattBatteryPollerTest, DoesNotModifyBatteryValueAfterAnError) {
TEST_F(GattBatteryPollerTest, StopsRetryingAfterMaxRetryCount) {
// Set a battery level to the device. Expect it resets after maximum retry
// count is exceeded.
mock_device_->set_battery_percentage(kBatteryPercentage);
mock_device_->SetBatteryPercentage(kBatteryPercentage);
CreateGattBatteryPoller();
const int kMaxRetryCount = 3;
......
......@@ -11,12 +11,14 @@
#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/string_split.h"
#include "base/strings/string_util.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 "third_party/re2/src/re2/re2.h"
......@@ -34,7 +36,7 @@ namespace {
// When a peripheral device's battery level is <= kLowBatteryLevel, consider
// it to be in low battery condition.
const int kLowBatteryLevel = 15;
const uint8_t kLowBatteryLevel = 15;
// Don't show 2 low battery notification within |kNotificationInterval|.
constexpr base::TimeDelta kNotificationInterval =
......@@ -54,6 +56,10 @@ const char kNotifierNonStylusBattery[] = "power.peripheral-battery";
const char kHIDBatteryPathPrefix[] = "/sys/class/power_supply/hid-";
const char kHIDBatteryPathSuffix[] = "-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-";
// Regex to check for valid bluetooth addresses.
constexpr char kBluetoothAddressRegex[] =
"^([0-9A-Fa-f]{2}:){5}([0-9A-Fa-f]{2})$";
......@@ -130,13 +136,13 @@ struct NotificationParams {
const gfx::VectorIcon* icon;
};
NotificationParams GetNonStylusNotificationParams(const std::string& address,
const std::string& name,
int battery_level,
NotificationParams GetNonStylusNotificationParams(const std::string& map_key,
const base::string16& name,
uint8_t battery_level,
bool is_bluetooth) {
return NotificationParams{
address,
base::ASCIIToUTF16(name),
map_key,
name,
l10n_util::GetStringFUTF16Int(
IDS_ASH_LOW_PERIPHERAL_BATTERY_NOTIFICATION_TEXT, battery_level),
kNotifierNonStylusBattery,
......@@ -155,13 +161,55 @@ NotificationParams GetStylusNotificationParams() {
&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 = ExtractBluetoothAddressFromPath(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()
: weakptr_factory_(
: clock_(base::DefaultTickClock::GetInstance()),
weakptr_factory_(
new base::WeakPtrFactory<PeripheralBatteryNotifier>(this)) {
chromeos::PowerManagerClient::Get()->AddObserver(this);
device::BluetoothAdapterFactory::GetAdapter(
......@@ -194,44 +242,38 @@ void PeripheralBatteryNotifier::PeripheralBatteryStatusReceived(
// If unknown battery level received, cancel any existing notification.
if (level == -1) {
CancelNotification(path);
CancelNotification(GetBatteryMapKey(path));
return;
}
// Post the notification in 2 cases:
// 1. It's the first time the battery level is received, and it is below
// kLowBatteryLevel.
// 2. The battery level is in record and it drops below kLowBatteryLevel.
if (batteries_.find(path) == batteries_.end()) {
BatteryInfo battery{name, level, base::TimeTicks(),
IsStylusDevice(path, name),
ExtractBluetoothAddressFromPath(path)};
if (level <= kLowBatteryLevel) {
if (PostNotification(path, battery)) {
battery.last_notification_timestamp = testing_clock_
? testing_clock_->NowTicks()
: base::TimeTicks::Now();
}
}
batteries_[path] = battery;
} else {
BatteryInfo* battery = &batteries_[path];
battery->name = name;
int old_level = battery->level;
battery->level = level;
if (old_level > kLowBatteryLevel && level <= kLowBatteryLevel) {
if (PostNotification(path, *battery)) {
battery->last_notification_timestamp = testing_clock_
? testing_clock_->NowTicks()
: base::TimeTicks::Now();
}
}
BatteryInfo battery{base::ASCIIToUTF16(name), level, base::TimeTicks(),
IsStylusDevice(path, name),
ExtractBluetoothAddressFromPath(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::DeviceChanged(device::BluetoothAdapter* adapter,
device::BluetoothDevice* device) {
if (!device->IsPaired())
void PeripheralBatteryNotifier::DeviceConnectedStateChanged(
device::BluetoothAdapter* adapter,
device::BluetoothDevice* device,
bool is_now_connected) {
if (!is_now_connected)
RemoveBluetoothBattery(device->GetAddress());
}
......@@ -250,33 +292,72 @@ void PeripheralBatteryNotifier::InitializeOnBluetoothReady(
void PeripheralBatteryNotifier::RemoveBluetoothBattery(
const std::string& bluetooth_address) {
std::string address_lowercase = base::ToLowerASCII(bluetooth_address);
for (auto it = batteries_.begin(); it != batteries_.end(); ++it) {
if (it->second.bluetooth_address == address_lowercase) {
CancelNotification(it->first);
batteries_.erase(it);
return;
}
auto it = batteries_.find(kBluetoothDeviceIdPrefix + address_lowercase);
if (it != batteries_.end()) {
CancelNotification(it->first);
batteries_.erase(it);
}
}
bool PeripheralBatteryNotifier::PostNotification(const std::string& path,
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) {
// Only post notification if kNotificationInterval seconds have passed since
// last notification showed, avoiding the case where the battery level
// oscillates around the threshold level.
base::TimeTicks now =
testing_clock_ ? testing_clock_->NowTicks() : base::TimeTicks::Now();
if (now - battery.last_notification_timestamp < kNotificationInterval)
return false;
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(path, battery.name, battery.level,
!battery.bluetooth_address.empty());
NotificationParams params = battery.is_stylus
? GetStylusNotificationParams()
: GetNonStylusNotificationParams(
map_key, battery.name, *battery.level,
!battery.bluetooth_address.empty());
auto notification = ash::CreateSystemNotification(
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,
......@@ -287,16 +368,19 @@ bool PeripheralBatteryNotifier::PostNotification(const std::string& path,
message_center::MessageCenter::Get()->AddNotification(
std::move(notification));
return true;
}
void PeripheralBatteryNotifier::CancelNotification(const std::string& path) {
const auto it = batteries_.find(path);
void PeripheralBatteryNotifier::CancelNotification(const std::string& map_key) {
const auto it = batteries_.find(map_key);
if (it != batteries_.end()) {
std::string notification_id =
it->second.is_stylus ? kStylusNotificationId : path;
std::string notification_map_key =
it->second.is_stylus ? kStylusNotificationId : map_key;
message_center::MessageCenter::Get()->RemoveNotification(
notification_id, false /* by_user */);
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();
}
}
......
......@@ -5,6 +5,7 @@
#ifndef ASH_SYSTEM_POWER_PERIPHERAL_BATTERY_NOTIFIER_H_
#define ASH_SYSTEM_POWER_PERIPHERAL_BATTERY_NOTIFIER_H_
#include <cstdint>
#include <map>
#include "ash/ash_export.h"
......@@ -12,6 +13,7 @@
#include "base/gtest_prod_util.h"
#include "base/macros.h"
#include "base/memory/weak_ptr.h"
#include "base/optional.h"
#include "base/time/tick_clock.h"
#include "chromeos/dbus/power/power_manager_client.h"
#include "device/bluetooth/bluetooth_adapter.h"
......@@ -33,9 +35,6 @@ class ASH_EXPORT PeripheralBatteryNotifier
PeripheralBatteryNotifier();
~PeripheralBatteryNotifier() override;
void set_testing_clock(const base::TickClock* clock) {
testing_clock_ = clock;
}
// chromeos::PowerManagerClient::Observer:
void PeripheralBatteryStatusReceived(const std::string& path,
......@@ -43,8 +42,13 @@ class ASH_EXPORT PeripheralBatteryNotifier
int level) override;
// device::BluetoothAdapter::Observer:
void DeviceChanged(device::BluetoothAdapter* adapter,
device::BluetoothDevice* device) override;
void DeviceBatteryChanged(
device::BluetoothAdapter* adapter,
device::BluetoothDevice* device,
base::Optional<uint8_t> new_battery_percentage) override;
void DeviceConnectedStateChanged(device::BluetoothAdapter* adapter,
device::BluetoothDevice* device,
bool is_now_connected) override;
void DeviceRemoved(device::BluetoothAdapter* adapter,
device::BluetoothDevice* device) override;
......@@ -57,10 +61,19 @@ class ASH_EXPORT PeripheralBatteryNotifier
FRIEND_TEST_ALL_PREFIXES(PeripheralBatteryNotifierTest, DeviceRemove);
struct BatteryInfo {
BatteryInfo();
BatteryInfo(const base::string16& name,
base::Optional<uint8_t> level,
base::TimeTicks last_notification_timestamp,
bool is_stylus,
const std::string& bluetooth_address);
~BatteryInfo();
BatteryInfo(const BatteryInfo& info);
// Human readable name for the device. It is changeable.
std::string name;
// Battery level within range [0, 100], and -1 for unknown level.
int level = -1;
base::string16 name;
// Battery level within range [0, 100].
base::Optional<uint8_t> level;
base::TimeTicks last_notification_timestamp;
bool is_stylus = false;
// Peripheral's Bluetooth address. Empty for non-Bluetooth devices.
......@@ -75,21 +88,39 @@ class ASH_EXPORT PeripheralBatteryNotifier
// changed or removed.
void RemoveBluetoothBattery(const std::string& bluetooth_address);
// Posts a low battery notification with unique id |path|. Returns true
// if the notification is posted, false if not.
bool PostNotification(const std::string& path, const BatteryInfo& battery);
// Updates the battery information of the peripheral with the corresponding
// |map_key|, and calls to post a notification if the battery level is under
// the threshold.
void UpdateBattery(const std::string& map_key,
const BatteryInfo& battery_info);
// Updates the battery percentage in the corresponding notification.
void UpdateBatteryNotificationIfVisible(const std::string& map_key,
const BatteryInfo& battery);
// Calls to display a notification only if kNotificationInterval seconds have
// passed since the last notification showed, avoiding the case where the
// battery level oscillates around the threshold level.
void ShowNotification(const std::string& map_key, const BatteryInfo& battery);
// Posts a low battery notification with id as |map_key|. If a notification
// with the same id exists, its content gets updated.
void ShowOrUpdateNotification(const std::string& map_key,
const BatteryInfo& battery);
void CancelNotification(const std::string& path);
void CancelNotification(const std::string& map_key);
// Record of existing battery infomation. The key is the device path.
// Record of existing battery information. For Bluetooth Devices, the key is
// kBluetoothDeviceIdPrefix + the device's address. For HID devices, the key
// is the device path. If a device uses HID over Bluetooth, it is indexed as a
// Bluetooth device.
std::map<std::string, BatteryInfo> batteries_;
// PeripheralBatteryNotifier is an observer of |bluetooth_adapter_| for
// bluetooth device change/remove events.
scoped_refptr<device::BluetoothAdapter> bluetooth_adapter_;
// Used only for helping test. Not owned and can be nullptr.
const base::TickClock* testing_clock_ = nullptr;
const base::TickClock* clock_;
std::unique_ptr<base::WeakPtrFactory<PeripheralBatteryNotifier>>
weakptr_factory_;
......
......@@ -9,40 +9,113 @@
#include "ash/shell.h"
#include "ash/test/ash_test_base.h"
#include "base/macros.h"
#include "base/strings/string16.h"
#include "base/strings/string_piece.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/simple_test_tick_clock.h"
#include "device/bluetooth/bluetooth_device.h"
#include "device/bluetooth/test/mock_bluetooth_adapter.h"
#include "device/bluetooth/test/mock_bluetooth_device.h"
#include "ui/events/devices/device_data_manager_test_api.h"
#include "ui/events/devices/touchscreen_device.h"
#include "ui/message_center/message_center.h"
#include "ui/message_center/public/cpp/notification.h"
using testing::NiceMock;
namespace {
// HID device.
const char kTestBatteryPath[] =
"/sys/class/power_supply/hid-AA:BB:CC:DD:EE:FF-battery";
const char kTestBatteryAddress[] = "ff:ee:dd:cc:bb:aa";
const char kTestDeviceName[] = "test device";
const char kTestBatteryId[] =
"battery_notification_bluetooth-ff:ee:dd:cc:bb:aa";
// Bluetooth devices.
const char kBluetoothDeviceAddress1[] = "aa:bb:cc:dd:ee:ff";
const char kBluetoothDeviceAddress2[] = "11:22:33:44:55:66";
const char kBluetoothDeviceId1[] =
"battery_notification_bluetooth-aa:bb:cc:dd:ee:ff";
const char kBluetoothDeviceId2[] =
"battery_notification_bluetooth-11:22:33:44:55:66";
const base::string16& NotificationMessagePrefix() {
static const base::string16 prefix(base::ASCIIToUTF16("Battery low ("));
return prefix;
}
const base::string16& NotificationMessageSuffix() {
static const base::string16 suffix(base::ASCIIToUTF16("%)"));
return suffix;
}
} // namespace
namespace ash {
class PeripheralBatteryNotifierTest : public ash::AshTestBase {
class PeripheralBatteryNotifierTest : public AshTestBase {
public:
PeripheralBatteryNotifierTest() = default;
~PeripheralBatteryNotifierTest() override = default;
void SetUp() override {
ash::AshTestBase::SetUp();
observer_ = std::make_unique<PeripheralBatteryNotifier>();
AshTestBase::SetUp();
mock_adapter_ =
base::MakeRefCounted<NiceMock<device::MockBluetoothAdapter>>();
mock_device_1_ = std::make_unique<NiceMock<device::MockBluetoothDevice>>(
mock_adapter_.get(), 0 /* bluetooth_class */, "device_name_1",
kBluetoothDeviceAddress1, true /* paired */, true /* connected */);
mock_device_2_ = std::make_unique<NiceMock<device::MockBluetoothDevice>>(
mock_adapter_.get(), 0 /* bluetooth_class */, "device_name_2",
kBluetoothDeviceAddress2, true /* paired */, true /* connected */);
message_center_ = message_center::MessageCenter::Get();
battery_notifier_ = std::make_unique<PeripheralBatteryNotifier>();
// No notifications should have been posted yet.
EXPECT_EQ(0u, message_center_->NotificationCount());
}
void TearDown() override {
observer_.reset();
ash::AshTestBase::TearDown();
battery_notifier_.reset();
AshTestBase::TearDown();
}
// Extracts the battery percentage from the message of a notification.
uint8_t ExtractBatteryPercentage(message_center::Notification* notification) {
const base::string16& message = notification->message();
EXPECT_TRUE(base::StartsWith(message, NotificationMessagePrefix(),
base::CompareCase::SENSITIVE));
EXPECT_TRUE(base::EndsWith(message, NotificationMessageSuffix(),
base::CompareCase::SENSITIVE));
int prefix_size = NotificationMessagePrefix().size();
int suffix_size = NotificationMessageSuffix().size();
int key_len = message.size() - prefix_size - suffix_size;
EXPECT_GT(key_len, 0);
int battery_percentage;
EXPECT_TRUE(base::StringToInt(message.substr(prefix_size, key_len),
&battery_percentage));
EXPECT_GE(battery_percentage, 0);
EXPECT_LE(battery_percentage, 100);
return battery_percentage;
}
void SetTestingClock(base::SimpleTestTickClock* clock) {
battery_notifier_->clock_ = clock;
}
protected:
std::unique_ptr<PeripheralBatteryNotifier> observer_;
scoped_refptr<NiceMock<device::MockBluetoothAdapter>> mock_adapter_;
std::unique_ptr<device::MockBluetoothDevice> mock_device_1_;
std::unique_ptr<device::MockBluetoothDevice> mock_device_2_;
message_center::MessageCenter* message_center_;
std::unique_ptr<PeripheralBatteryNotifier> battery_notifier_;
private:
DISALLOW_COPY_AND_ASSIGN(PeripheralBatteryNotifierTest);
......@@ -50,69 +123,66 @@ class PeripheralBatteryNotifierTest : public ash::AshTestBase {
TEST_F(PeripheralBatteryNotifierTest, Basic) {
base::SimpleTestTickClock clock;
observer_->set_testing_clock(&clock);
message_center::MessageCenter* message_center =
message_center::MessageCenter::Get();
SetTestingClock(&clock);
// Level 50 at time 100, no low-battery notification.
clock.Advance(base::TimeDelta::FromSeconds(100));
observer_->PeripheralBatteryStatusReceived(kTestBatteryPath, kTestDeviceName,
50);
EXPECT_EQ(1u, observer_->batteries_.count(kTestBatteryPath));
battery_notifier_->PeripheralBatteryStatusReceived(kTestBatteryPath,
kTestDeviceName, 50);
EXPECT_EQ(1u, battery_notifier_->batteries_.count(kTestBatteryId));
const PeripheralBatteryNotifier::BatteryInfo& info =
observer_->batteries_[kTestBatteryPath];
battery_notifier_->batteries_[kTestBatteryId];
EXPECT_EQ(kTestDeviceName, info.name);
EXPECT_EQ(50, info.level);
EXPECT_EQ(base::ASCIIToUTF16(kTestDeviceName), info.name);
EXPECT_EQ(base::nullopt, info.level);
EXPECT_EQ(base::TimeTicks(), info.last_notification_timestamp);
EXPECT_EQ(kTestBatteryAddress, info.bluetooth_address);
EXPECT_TRUE(message_center->FindVisibleNotificationById(kTestBatteryPath) ==
EXPECT_TRUE(message_center_->FindVisibleNotificationById(kTestBatteryId) ==
nullptr);
// Level 5 at time 110, low-battery notification.
clock.Advance(base::TimeDelta::FromSeconds(10));
observer_->PeripheralBatteryStatusReceived(kTestBatteryPath, kTestDeviceName,
5);
battery_notifier_->PeripheralBatteryStatusReceived(kTestBatteryPath,
kTestDeviceName, 5);
EXPECT_EQ(5, info.level);
EXPECT_EQ(clock.NowTicks(), info.last_notification_timestamp);
EXPECT_TRUE(message_center->FindVisibleNotificationById(kTestBatteryPath) !=
EXPECT_TRUE(message_center_->FindVisibleNotificationById(kTestBatteryId) !=
nullptr);
// Verify that the low-battery notification for stylus does not show up.
EXPECT_FALSE(message_center->FindVisibleNotificationById(
EXPECT_FALSE(message_center_->FindVisibleNotificationById(
PeripheralBatteryNotifier::kStylusNotificationId) !=
nullptr);
// Level -1 at time 115, cancel previous notification
clock.Advance(base::TimeDelta::FromSeconds(5));
observer_->PeripheralBatteryStatusReceived(kTestBatteryPath, kTestDeviceName,
-1);
EXPECT_EQ(5, info.level);
battery_notifier_->PeripheralBatteryStatusReceived(kTestBatteryPath,
kTestDeviceName, -1);
EXPECT_EQ(base::nullopt, info.level);
EXPECT_EQ(clock.NowTicks() - base::TimeDelta::FromSeconds(5),
info.last_notification_timestamp);
EXPECT_TRUE(message_center->FindVisibleNotificationById(kTestBatteryPath) ==
EXPECT_TRUE(message_center_->FindVisibleNotificationById(kTestBatteryId) ==
nullptr);
// Level 50 at time 120, no low-battery notification.
clock.Advance(base::TimeDelta::FromSeconds(5));
observer_->PeripheralBatteryStatusReceived(kTestBatteryPath, kTestDeviceName,
50);
EXPECT_EQ(50, info.level);
battery_notifier_->PeripheralBatteryStatusReceived(kTestBatteryPath,
kTestDeviceName, 50);
EXPECT_EQ(base::nullopt, info.level);
EXPECT_EQ(clock.NowTicks() - base::TimeDelta::FromSeconds(10),
info.last_notification_timestamp);
EXPECT_TRUE(message_center->FindVisibleNotificationById(kTestBatteryPath) ==
EXPECT_TRUE(message_center_->FindVisibleNotificationById(kTestBatteryId) ==
nullptr);
// Level 5 at time 130, no low-battery notification (throttling).
clock.Advance(base::TimeDelta::FromSeconds(10));
observer_->PeripheralBatteryStatusReceived(kTestBatteryPath, kTestDeviceName,
5);
battery_notifier_->PeripheralBatteryStatusReceived(kTestBatteryPath,
kTestDeviceName, 5);
EXPECT_EQ(5, info.level);
EXPECT_EQ(clock.NowTicks() - base::TimeDelta::FromSeconds(20),
info.last_notification_timestamp);
EXPECT_TRUE(message_center->FindVisibleNotificationById(kTestBatteryPath) ==
EXPECT_TRUE(message_center_->FindVisibleNotificationById(kTestBatteryId) ==
nullptr);
}
......@@ -120,25 +190,25 @@ TEST_F(PeripheralBatteryNotifierTest, InvalidBatteryInfo) {
const std::string invalid_path1 = "invalid-path";
const std::string invalid_path2 = "/sys/class/power_supply/hid-battery";
observer_->PeripheralBatteryStatusReceived(invalid_path1, kTestDeviceName,
10);
EXPECT_TRUE(observer_->batteries_.empty());
battery_notifier_->PeripheralBatteryStatusReceived(invalid_path1,
kTestDeviceName, 10);
EXPECT_TRUE(battery_notifier_->batteries_.empty());
observer_->PeripheralBatteryStatusReceived(invalid_path2, kTestDeviceName,
10);
EXPECT_TRUE(observer_->batteries_.empty());
battery_notifier_->PeripheralBatteryStatusReceived(invalid_path2,
kTestDeviceName, 10);
EXPECT_TRUE(battery_notifier_->batteries_.empty());
observer_->PeripheralBatteryStatusReceived(kTestBatteryPath, kTestDeviceName,
-2);
EXPECT_TRUE(observer_->batteries_.empty());
battery_notifier_->PeripheralBatteryStatusReceived(kTestBatteryPath,
kTestDeviceName, -2);
EXPECT_TRUE(battery_notifier_->batteries_.empty());
observer_->PeripheralBatteryStatusReceived(kTestBatteryPath, kTestDeviceName,
101);
EXPECT_TRUE(observer_->batteries_.empty());
battery_notifier_->PeripheralBatteryStatusReceived(kTestBatteryPath,
kTestDeviceName, 101);
EXPECT_TRUE(battery_notifier_->batteries_.empty());
observer_->PeripheralBatteryStatusReceived(kTestBatteryPath, kTestDeviceName,
-1);
EXPECT_TRUE(observer_->batteries_.empty());
battery_notifier_->PeripheralBatteryStatusReceived(kTestBatteryPath,
kTestDeviceName, -1);
EXPECT_TRUE(battery_notifier_->batteries_.empty());
}
// Verify that for Bluetooth devices, the correct address gets stored in the
......@@ -148,37 +218,36 @@ TEST_F(PeripheralBatteryNotifierTest, ExtractBluetoothAddress) {
const std::string bluetooth_path =
"/sys/class/power_supply/hid-A0:b1:C2:d3:E4:f5-battery";
const std::string expected_bluetooth_address = "f5:e4:d3:c2:b1:a0";
const std::string expected_bluetooth_id =
"battery_notification_bluetooth-f5:e4:d3:c2:b1:a0";
const std::string non_bluetooth_path =
"/sys/class/power_supply/hid-notbluetooth-battery";
observer_->PeripheralBatteryStatusReceived(bluetooth_path, kTestDeviceName,
10);
observer_->PeripheralBatteryStatusReceived(non_bluetooth_path,
kTestDeviceName, 10);
EXPECT_EQ(2u, observer_->batteries_.size());
battery_notifier_->PeripheralBatteryStatusReceived(bluetooth_path,
kTestDeviceName, 10);
battery_notifier_->PeripheralBatteryStatusReceived(non_bluetooth_path,
kTestDeviceName, 10);
EXPECT_EQ(2u, battery_notifier_->batteries_.size());
const PeripheralBatteryNotifier::BatteryInfo& bluetooth_device_info =
observer_->batteries_[bluetooth_path];
battery_notifier_->batteries_[expected_bluetooth_id];
EXPECT_EQ(expected_bluetooth_address,
bluetooth_device_info.bluetooth_address);
const PeripheralBatteryNotifier::BatteryInfo& non_bluetooth_device_info =
observer_->batteries_[non_bluetooth_path];
battery_notifier_->batteries_[non_bluetooth_path];
EXPECT_TRUE(non_bluetooth_device_info.bluetooth_address.empty());
}
// TODO(crbug.com/765794): Flaky on ash_unittests with mus.
TEST_F(PeripheralBatteryNotifierTest, DISABLED_DeviceRemove) {
message_center::MessageCenter* message_center =
message_center::MessageCenter::Get();
observer_->PeripheralBatteryStatusReceived(kTestBatteryPath, kTestDeviceName,
5);
EXPECT_EQ(1u, observer_->batteries_.count(kTestBatteryPath));
EXPECT_TRUE(message_center->FindVisibleNotificationById(kTestBatteryPath) !=
battery_notifier_->PeripheralBatteryStatusReceived(kTestBatteryPath,
kTestDeviceName, 5);
EXPECT_EQ(1u, battery_notifier_->batteries_.count(kTestBatteryId));
EXPECT_TRUE(message_center_->FindVisibleNotificationById(kTestBatteryId) !=
nullptr);
observer_->RemoveBluetoothBattery(kTestBatteryAddress);
EXPECT_TRUE(message_center->FindVisibleNotificationById(kTestBatteryPath) ==
battery_notifier_->RemoveBluetoothBattery(kTestBatteryAddress);
EXPECT_TRUE(message_center_->FindVisibleNotificationById(kTestBatteryId) ==
nullptr);
}
......@@ -196,32 +265,228 @@ TEST_F(PeripheralBatteryNotifierTest, DISABLED_StylusNotification) {
ui::DeviceDataManagerTestApi().SetTouchscreenDevices({stylus});
message_center::MessageCenter* message_center =
message_center::MessageCenter::Get();
// Verify that when the battery level is 50, no stylus low battery
// notification is shown.
observer_->PeripheralBatteryStatusReceived(kTestStylusBatteryPath,
kTestStylusName, 50);
EXPECT_TRUE(message_center->FindVisibleNotificationById(
battery_notifier_->PeripheralBatteryStatusReceived(kTestStylusBatteryPath,
kTestStylusName, 50);
EXPECT_TRUE(message_center_->FindVisibleNotificationById(
PeripheralBatteryNotifier::kStylusNotificationId) == nullptr);
// Verify that when the battery level is 5, a stylus low battery notification
// is shown. Also check that a non stylus device low battery notification will
// not show up.
observer_->PeripheralBatteryStatusReceived(kTestStylusBatteryPath,
kTestStylusName, 5);
EXPECT_TRUE(message_center->FindVisibleNotificationById(
battery_notifier_->PeripheralBatteryStatusReceived(kTestStylusBatteryPath,
kTestStylusName, 5);
EXPECT_TRUE(message_center_->FindVisibleNotificationById(
PeripheralBatteryNotifier::kStylusNotificationId) != nullptr);
EXPECT_TRUE(message_center->FindVisibleNotificationById(
EXPECT_TRUE(message_center_->FindVisibleNotificationById(
kTestBatteryAddress) == nullptr);
// Verify that when the battery level is -1, the previous stylus low battery
// notification is cancelled.
observer_->PeripheralBatteryStatusReceived(kTestStylusBatteryPath,
kTestStylusName, -1);
EXPECT_TRUE(message_center->FindVisibleNotificationById(
battery_notifier_->PeripheralBatteryStatusReceived(kTestStylusBatteryPath,
kTestStylusName, -1);
EXPECT_TRUE(message_center_->FindVisibleNotificationById(
PeripheralBatteryNotifier::kStylusNotificationId) == nullptr);
}
TEST_F(PeripheralBatteryNotifierTest,
Bluetooth_OnlyShowNotificationForLowBatteryLevels) {
// Should not create a notification for battery changes above the threshold.
battery_notifier_->DeviceBatteryChanged(mock_adapter_.get(),
mock_device_1_.get(),
80 /* new_battery_percentage */);
EXPECT_EQ(0u, message_center_->NotificationCount());
battery_notifier_->DeviceBatteryChanged(mock_adapter_.get(),
mock_device_1_.get(),
100 /* new_battery_percentage */);
EXPECT_EQ(0u, message_center_->NotificationCount());
// Should trigger notificaiton.
battery_notifier_->DeviceBatteryChanged(mock_adapter_.get(),
mock_device_1_.get(),
10 /* new_battery_percentage */);
EXPECT_EQ(1u, message_center_->NotificationCount());
message_center::Notification* notification =
message_center_->FindVisibleNotificationById(kBluetoothDeviceId1);
EXPECT_EQ(mock_device_1_->GetNameForDisplay(), notification->title());
EXPECT_EQ(10, ExtractBatteryPercentage(notification));
}
TEST_F(PeripheralBatteryNotifierTest,
Bluetooth_CreatesANotificationForEachDevice) {
battery_notifier_->DeviceBatteryChanged(mock_adapter_.get(),
mock_device_1_.get(),
5 /* new_battery_percentage */);
battery_notifier_->DeviceBatteryChanged(mock_adapter_.get(),
mock_device_2_.get(),
0 /* new_battery_percentage */);
// Verify 2 notifications were posted with the correct values.
EXPECT_EQ(2u, message_center_->NotificationCount());
message_center::Notification* notification_1 =
message_center_->FindVisibleNotificationById(kBluetoothDeviceId1);
message_center::Notification* notification_2 =
message_center_->FindVisibleNotificationById(kBluetoothDeviceId2);
EXPECT_EQ(mock_device_1_->GetNameForDisplay(), notification_1->title());
EXPECT_EQ(5, ExtractBatteryPercentage(notification_1));
EXPECT_EQ(mock_device_2_->GetNameForDisplay(), notification_2->title());
EXPECT_EQ(0, ExtractBatteryPercentage(notification_2));
}
TEST_F(PeripheralBatteryNotifierTest,
Bluetooth_RemovesNotificationForDisconnectedDevices) {
battery_notifier_->DeviceBatteryChanged(mock_adapter_.get(),
mock_device_1_.get(),
5 /* new_battery_percentage */);
battery_notifier_->DeviceBatteryChanged(mock_adapter_.get(),
mock_device_2_.get(),
0 /* new_battery_percentage */);
// Verify 2 notifications were posted.
EXPECT_EQ(2u, message_center_->NotificationCount());
// Verify only the notification for device 1 gets removed.
battery_notifier_->DeviceConnectedStateChanged(mock_adapter_.get(),
mock_device_1_.get(), false);
EXPECT_EQ(1u, message_center_->NotificationCount());
EXPECT_TRUE(
message_center_->FindVisibleNotificationById(kBluetoothDeviceId2));
// Remove the second notification.
battery_notifier_->DeviceRemoved(mock_adapter_.get(), mock_device_2_.get());
EXPECT_EQ(0u, message_center_->NotificationCount());
}
TEST_F(PeripheralBatteryNotifierTest,
Bluetooth_CancelNotificationForInvalidBatteryLevel) {
battery_notifier_->DeviceBatteryChanged(mock_adapter_.get(),
mock_device_1_.get(),
1 /* new_battery_percentage */);
EXPECT_TRUE(
message_center_->FindVisibleNotificationById(kBluetoothDeviceId1));
// The notification should get canceled.
battery_notifier_->DeviceBatteryChanged(
mock_adapter_.get(), mock_device_1_.get(),
base::nullopt /* new_battery_percentage */);
EXPECT_FALSE(
message_center_->FindVisibleNotificationById(kBluetoothDeviceId1));
}
// Don't post a notification if the battery level drops again under the
// threshold before kNotificationInterval is completed.
TEST_F(PeripheralBatteryNotifierTest,
DontShowSecondNotificationWithinASmallTimeInterval) {
base::SimpleTestTickClock clock;
SetTestingClock(&clock);
clock.Advance(base::TimeDelta::FromSeconds(100));
// Post a notification.
battery_notifier_->DeviceBatteryChanged(mock_adapter_.get(),
mock_device_1_.get(),
1 /* new_battery_percentage */);
EXPECT_TRUE(
message_center_->FindVisibleNotificationById(kBluetoothDeviceId1));
// Cancel the notification.
clock.Advance(base::TimeDelta::FromSeconds(1));
battery_notifier_->DeviceBatteryChanged(
mock_adapter_.get(), mock_device_1_.get(),
base::nullopt /* new_battery_percentage */);
EXPECT_FALSE(
message_center_->FindVisibleNotificationById(kBluetoothDeviceId1));
// The battery level falls below the threshold after a short time period. No
// notification should get posted.
clock.Advance(base::TimeDelta::FromSeconds(1));
battery_notifier_->DeviceBatteryChanged(mock_adapter_.get(),
mock_device_1_.get(),
1 /* new_battery_percentage */);
EXPECT_FALSE(
message_center_->FindVisibleNotificationById(kBluetoothDeviceId1));
}
// Post a notification if the battery is under threshold, then unknown level and
// then is again under the threshold after kNotificationInterval is completed.
TEST_F(PeripheralBatteryNotifierTest,
PostNotificationIfBatteryGoesFromUnknownLevelToBelowThreshold) {
base::SimpleTestTickClock clock;
SetTestingClock(&clock);
clock.Advance(base::TimeDelta::FromSeconds(100));
// Post a notification.
battery_notifier_->DeviceBatteryChanged(mock_adapter_.get(),
mock_device_1_.get(),
1) /* new_battery_percentage */;
EXPECT_TRUE(
message_center_->FindVisibleNotificationById(kBluetoothDeviceId1));
// Cancel the notification.
clock.Advance(base::TimeDelta::FromSeconds(1));
battery_notifier_->DeviceBatteryChanged(
mock_adapter_.get(), mock_device_1_.get(),
base::nullopt /* new_battery_percentage */);
EXPECT_FALSE(
message_center_->FindVisibleNotificationById(kBluetoothDeviceId1));
// Post notification if we are out of the kNotificationInterval.
clock.Advance(base::TimeDelta::FromSeconds(100));
battery_notifier_->DeviceBatteryChanged(mock_adapter_.get(),
mock_device_1_.get(),
1 /* new_battery_percentage */);
EXPECT_TRUE(
message_center_->FindVisibleNotificationById(kBluetoothDeviceId1));
}
// Don't Post another notification if the battery level keeps low and the user
// dismissed the previous notification.
TEST_F(PeripheralBatteryNotifierTest,
DontRepostNotificationIfUserDismissedPreviousOne) {
base::SimpleTestTickClock clock;
SetTestingClock(&clock);
clock.Advance(base::TimeDelta::FromSeconds(100));
battery_notifier_->DeviceBatteryChanged(mock_adapter_.get(),
mock_device_1_.get(),
5 /* new_battery_percentage */);
EXPECT_EQ(1u, message_center_->NotificationCount());
// Simulate the user clears the notification.
message_center_->RemoveAllNotifications(
true /* by_user */, message_center::MessageCenter::RemoveType::ALL);
// The battery level remains low, but shouldn't post a notificaiton.
clock.Advance(base::TimeDelta::FromSeconds(100));
battery_notifier_->DeviceBatteryChanged(mock_adapter_.get(),
mock_device_1_.get(),
3 /* new_battery_percentage */);
EXPECT_EQ(0u, message_center_->NotificationCount());
}
// If there is an existing notificaiton and the battery level remains low,
// update its content.
TEST_F(PeripheralBatteryNotifierTest, UpdateNotificationIfVisible) {
base::SimpleTestTickClock clock;
SetTestingClock(&clock);
clock.Advance(base::TimeDelta::FromSeconds(100));
battery_notifier_->DeviceBatteryChanged(mock_adapter_.get(),
mock_device_1_.get(),
5 /* new_battery_percentage */);
EXPECT_EQ(1u, message_center_->NotificationCount());
// The battery level remains low, should update the notification.
clock.Advance(base::TimeDelta::FromSeconds(100));
battery_notifier_->DeviceBatteryChanged(mock_adapter_.get(),
mock_device_1_.get(),
3 /* new_battery_percentage */);
message_center::Notification* notification =
message_center_->FindVisibleNotificationById(kBluetoothDeviceId1);
EXPECT_EQ(mock_device_1_->GetNameForDisplay(), notification->title());
EXPECT_EQ(3, ExtractBatteryPercentage(notification));
}
} // namespace ash
......@@ -230,6 +230,15 @@ void BluetoothAdapter::NotifyDevicePairedChanged(BluetoothDevice* device,
}
#endif
#if defined(OS_CHROMEOS)
void BluetoothAdapter::NotifyDeviceBatteryChanged(BluetoothDevice* device) {
DCHECK_EQ(device->GetAdapter(), this);
for (auto& observer : observers_) {
observer.DeviceBatteryChanged(this, device, device->battery_percentage());
}
}
#endif
void BluetoothAdapter::NotifyGattServiceAdded(
BluetoothRemoteGattService* service) {
DCHECK_EQ(service->GetDevice()->GetAdapter(), this);
......
......@@ -170,6 +170,14 @@ class DEVICE_BLUETOOTH_EXPORT BluetoothAdapter
bool is_now_connected) {}
#endif
#if defined(OS_CHROMEOS)
// Called when the battery level of the device has been updated.
virtual void DeviceBatteryChanged(
BluetoothAdapter* adapter,
BluetoothDevice* device,
base::Optional<uint8_t> new_battery_percentage) {}
#endif
// Called when the device |device| is removed from the adapter |adapter|,
// either as a result of a discovered device being lost between discovering
// phases or pairing information deleted. |device| should not be
......@@ -582,6 +590,11 @@ class DEVICE_BLUETOOTH_EXPORT BluetoothAdapter
void NotifyDevicePairedChanged(BluetoothDevice* device,
bool new_paired_status);
#endif
#if defined(OS_CHROMEOS)
void NotifyDeviceBatteryChanged(BluetoothDevice* device);
#endif
void NotifyGattServiceAdded(BluetoothRemoteGattService* service);
void NotifyGattServiceRemoved(BluetoothRemoteGattService* service);
void NotifyGattServiceChanged(BluetoothRemoteGattService* service);
......
......@@ -465,6 +465,20 @@ BluetoothDevice::GetPrimaryServicesByUUID(const BluetoothUUID& service_uuid) {
return services;
}
#if defined(OS_CHROMEOS)
void BluetoothDevice::SetBatteryPercentage(
base::Optional<uint8_t> battery_percentage) {
if (battery_percentage)
DCHECK_LE(battery_percentage.value(), 100);
if (battery_percentage_ == battery_percentage)
return;
battery_percentage_ = battery_percentage;
GetAdapter()->NotifyDeviceBatteryChanged(this);
}
#endif
void BluetoothDevice::DidConnectGatt() {
for (const auto& callback : create_gatt_connection_success_callbacks_) {
callback.Run(
......
......@@ -579,7 +579,6 @@ class DEVICE_BLUETOOTH_EXPORT BluetoothDevice {
// Aborts all the previous prepare writes in a reliable write session.
virtual void AbortWrite(const base::Closure& callback,
const AbortWriteErrorCallback& error_callback) = 0;
#endif
// Set the remaining battery of the device to show in the UI. This value must
// be between 0 and 100, inclusive.
......@@ -587,13 +586,7 @@ class DEVICE_BLUETOOTH_EXPORT BluetoothDevice {
// ash::GattBatteryPoller and used only by Chrome OS. In the future, when
// there is a unified Mojo service, this logic will be moved to
// BluetoothDeviceInfo.
void set_battery_percentage(base::Optional<uint8_t> battery_percentage) {
if (battery_percentage) {
DCHECK(battery_percentage.value() >= 0 &&
battery_percentage.value() <= 100);
}
battery_percentage_ = battery_percentage;
}
void SetBatteryPercentage(base::Optional<uint8_t> battery_percentage);
// Returns the remaining battery for the device.
// TODO(https://crbug.com/973237): Battery percentage is populated by
......@@ -603,6 +596,7 @@ class DEVICE_BLUETOOTH_EXPORT BluetoothDevice {
const base::Optional<uint8_t>& battery_percentage() const {
return battery_percentage_;
}
#endif
protected:
// BluetoothGattConnection is a friend to call Add/RemoveGattConnection.
......@@ -737,13 +731,13 @@ class DEVICE_BLUETOOTH_EXPORT BluetoothDevice {
// a device type for display when |name_| is empty.
base::string16 GetAddressWithLocalizedDeviceTypeName() const;
#if defined(OS_CHROMEOS)
// Remaining battery level of the device.
// TODO(https://crbug.com/973237): This field is populated by
// ash::GattBatteryPoller and used only by Chrome OS. This field is different
// from others because it is not filled by the platform. In the future, when
// there is a unified Mojo service, this field will be moved to
// BluetoothDeviceInfo.
// TODO(https://crbug.com/973237): This field is different from others because
// it is not filled by the platform. In the future, when there is a unified
// Mojo service, this field will be moved to BluetoothDeviceInfo.
base::Optional<uint8_t> battery_percentage_;
#endif
DISALLOW_COPY_AND_ASSIGN(BluetoothDevice);
};
......
......@@ -159,10 +159,12 @@ void BluetoothDeviceToApiDevice(const device::BluetoothDevice& device,
else
out->inquiry_tx_power.reset();
#if defined(OS_CHROMEOS)
if (device.battery_percentage())
out->battery_percentage.reset(new int(device.battery_percentage().value()));
else
out->battery_percentage.reset();
#endif
#if defined(OS_LINUX)
ConvertTransportToApi(device.GetType(), &(out->transport));
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment