Commit 97134d9c authored by Alberto Herrera's avatar Alberto Herrera Committed by Commit Bot

[Bluetooth] Create GattBatteryPoller

Class that gets the battery level of a connected Bluetooth device
through the standardized GATT Battery Service. Polling is done
periodically and updates the corresponding device::BluetoothDevice.

Instances for this class are expected to be created and owned by the
GattBatteryController class (to be implemented in a separate CL), for
each connected BLE device.

Design doc at go/cros-bt-battery.

Bug: 785758
Change-Id: Ic0de3e9b3d813537610378480cac6daaf8e25204
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1674716
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 avatarKyle Horimoto <khorimoto@chromium.org>
Reviewed-by: default avatarRyan Hansberry <hansberry@chromium.org>
Cr-Commit-Position: refs/heads/master@{#678369}
parent c8590efc
......@@ -506,8 +506,12 @@ component("ash") {
"multi_user/user_switch_animator.h",
"policy/policy_recommendation_restorer.cc",
"policy/policy_recommendation_restorer.h",
"power/fake_gatt_battery_percentage_fetcher.cc",
"power/fake_gatt_battery_percentage_fetcher.h",
"power/gatt_battery_percentage_fetcher.cc",
"power/gatt_battery_percentage_fetcher.h",
"power/gatt_battery_poller.cc",
"power/gatt_battery_poller.h",
"root_window_controller.cc",
"root_window_settings.cc",
"root_window_settings.h",
......@@ -1712,6 +1716,7 @@ test("ash_unittests") {
"multi_device_setup/multi_device_notification_presenter_unittest.cc",
"policy/policy_recommendation_restorer_unittest.cc",
"power/gatt_battery_percentage_fetcher_unittest.cc",
"power/gatt_battery_poller_unittest.cc",
"root_window_controller_unittest.cc",
"rotator/screen_rotation_animation_unittest.cc",
"rotator/screen_rotation_animator_unittest.cc",
......
// Copyright 2019 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/power/fake_gatt_battery_percentage_fetcher.h"
namespace ash {
FakeGattBatteryPercentageFetcher::FakeGattBatteryPercentageFetcher(
const std::string& device_address,
BatteryPercentageCallback callback)
: GattBatteryPercentageFetcher(device_address, std::move(callback)) {}
FakeGattBatteryPercentageFetcher::~FakeGattBatteryPercentageFetcher() = default;
} // namespace ash
// Copyright 2019 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.
#ifndef ASH_POWER_FAKE_GATT_BATTERY_PERCENTAGE_FETCHER_H_
#define ASH_POWER_FAKE_GATT_BATTERY_PERCENTAGE_FETCHER_H_
#include <string>
#include "ash/ash_export.h"
#include "ash/power/gatt_battery_percentage_fetcher.h"
#include "base/macros.h"
namespace ash {
// Fake implementation of GattBatteryPercentageFetcher to use in tests.
class ASH_EXPORT FakeGattBatteryPercentageFetcher
: public GattBatteryPercentageFetcher {
public:
FakeGattBatteryPercentageFetcher(const std::string& device_address,
BatteryPercentageCallback callback);
~FakeGattBatteryPercentageFetcher() override;
using GattBatteryPercentageFetcher::InvokeCallbackWithFailedFetch;
using GattBatteryPercentageFetcher::InvokeCallbackWithSuccessfulFetch;
private:
DISALLOW_COPY_AND_ASSIGN(FakeGattBatteryPercentageFetcher);
};
} // namespace ash
#endif // ASH_POWER_FAKE_GATT_BATTERY_PERCENTAGE_FETCHER_H_
// Copyright 2019 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/power/gatt_battery_poller.h"
#include "ash/power/gatt_battery_percentage_fetcher.h"
#include "base/bind.h"
#include "base/memory/ptr_util.h"
#include "base/time/time.h"
#include "base/timer/timer.h"
#include "device/bluetooth/bluetooth_adapter.h"
namespace ash {
namespace {
// Maximum number of consecutive attempts to try reading the battery status.
// The class stops polling if |current_retry_count_| exceeds this value.
const int kMaxRetryCount = 3;
// Default interval for polling the device battery value.
constexpr base::TimeDelta kDefaultPollInterval =
base::TimeDelta::FromMinutes(10);
GattBatteryPoller::Factory* g_test_factory_instance = nullptr;
} // namespace
// static
void GattBatteryPoller::Factory::SetFactoryForTesting(Factory* factory) {
g_test_factory_instance = factory;
}
// static
std::unique_ptr<GattBatteryPoller> GattBatteryPoller::Factory::NewInstance(
scoped_refptr<device::BluetoothAdapter> adapter,
const std::string& device_address,
std::unique_ptr<base::OneShotTimer> poll_timer) {
if (g_test_factory_instance) {
return g_test_factory_instance->BuildInstance(adapter, device_address,
std::move(poll_timer));
}
auto instance = base::WrapUnique(new GattBatteryPoller(device_address));
instance->StartFetching(adapter, std::move(poll_timer));
return instance;
}
GattBatteryPoller::GattBatteryPoller(const std::string& device_address)
: device_address_(device_address) {}
GattBatteryPoller::~GattBatteryPoller() = default;
void GattBatteryPoller::StartFetching(
scoped_refptr<device::BluetoothAdapter> adapter,
std::unique_ptr<base::OneShotTimer> poll_timer) {
adapter_ = adapter;
poll_timer_ = std::move(poll_timer);
CreateBatteryFetcher();
}
void GattBatteryPoller::CreateBatteryFetcher() {
DCHECK(!fetcher_);
// Creating the fetcher implicitly begins the process of fetching the battery
// status.
fetcher_ = GattBatteryPercentageFetcher::Factory::NewInstance(
adapter_, device_address_,
base::BindOnce(&GattBatteryPoller::OnBatteryPercentageFetched,
weak_ptr_factory_.GetWeakPtr()));
}
void GattBatteryPoller::OnBatteryPercentageFetched(
base::Optional<uint8_t> battery_percentage) {
fetcher_.reset();
if (battery_percentage) {
device::BluetoothDevice* device = adapter_->GetDevice(device_address_);
if (device)
device->set_battery_percentage(*battery_percentage);
}
ScheduleNextAttempt(battery_percentage.has_value());
}
void GattBatteryPoller::ScheduleNextAttempt(bool was_last_attempt_successful) {
device::BluetoothDevice* device = adapter_->GetDevice(device_address_);
// If the device is not present now, it won't be present in the future. Give
// up retrying.
if (!device)
return;
if (was_last_attempt_successful) {
current_retry_count_ = 0;
StartNextAttemptTimer();
return;
}
++current_retry_count_;
if (current_retry_count_ <= kMaxRetryCount) {
StartNextAttemptTimer();
} else {
// Reset battery field after exceeding the retry count.
device->set_battery_percentage(base::nullopt);
}
}
void GattBatteryPoller::StartNextAttemptTimer() {
poll_timer_->Start(FROM_HERE, kDefaultPollInterval,
base::BindOnce(&GattBatteryPoller::CreateBatteryFetcher,
weak_ptr_factory_.GetWeakPtr()));
}
} // namespace ash
// Copyright 2019 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.
#ifndef ASH_POWER_GATT_BATTERY_POLLER_H_
#define ASH_POWER_GATT_BATTERY_POLLER_H_
#include <cstdint>
#include <memory>
#include <string>
#include "ash/ash_export.h"
#include "base/macros.h"
#include "base/memory/scoped_refptr.h"
#include "base/memory/weak_ptr.h"
#include "base/optional.h"
namespace base {
class OneShotTimer;
} // namespace base
namespace device {
class BluetoothAdapter;
} // namespace device
namespace ash {
class GattBatteryPercentageFetcher;
// Gets the battery level of a connected Bluetooth device through the
// standardized GATT Battery Service. Polling is done periodically and updates
// the device::BluetoothDevice with the specified |device_address_|.
class ASH_EXPORT GattBatteryPoller {
public:
class Factory {
public:
virtual ~Factory() = default;
static void SetFactoryForTesting(Factory* factory);
static std::unique_ptr<GattBatteryPoller> NewInstance(
scoped_refptr<device::BluetoothAdapter> adapter,
const std::string& device_address,
std::unique_ptr<base::OneShotTimer> poll_timer);
virtual std::unique_ptr<GattBatteryPoller> BuildInstance(
scoped_refptr<device::BluetoothAdapter> adapter,
const std::string& device_address,
std::unique_ptr<base::OneShotTimer> poll_timer) = 0;
};
virtual ~GattBatteryPoller();
protected:
GattBatteryPoller(const std::string& device_address);
// Calling this function starts the fetching process. This allows tests to
// to create instances of this class without running the whole mechanism.
void StartFetching(scoped_refptr<device::BluetoothAdapter> adapter,
std::unique_ptr<base::OneShotTimer> poll_timer);
private:
friend class GattBatteryPollerTest;
// A GattBatteryPercentageFetcher object is created every time it is needed to
// read the battery level.
void CreateBatteryFetcher();
// Callback function to run after the fetcher completes getting the battery.
void OnBatteryPercentageFetched(base::Optional<uint8_t> battery_percentage);
// Schedules the next attempt for reading the battery level. Will stop
// scheduling after several consecutive unsuccessful attempts or if the device
// is no longer found in the adapter.
void ScheduleNextAttempt(bool was_last_attempt_successful);
// Starts a timer. When time's up, tries to fetch the battery level again.
void StartNextAttemptTimer();
scoped_refptr<device::BluetoothAdapter> adapter_;
const std::string device_address_;
std::unique_ptr<base::OneShotTimer> poll_timer_;
std::unique_ptr<GattBatteryPercentageFetcher> fetcher_;
// The number of consecutive attempts we have tried to read the battery status
// and failed. Resets when the battery is read successfully.
int current_retry_count_ = 0;
base::WeakPtrFactory<GattBatteryPoller> weak_ptr_factory_{this};
DISALLOW_COPY_AND_ASSIGN(GattBatteryPoller);
};
} // namespace ash
#endif // ASH_POWER_GATT_BATTERY_POLLER_H_
// Copyright 2019 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/power/gatt_battery_poller.h"
#include "ash/power/fake_gatt_battery_percentage_fetcher.h"
#include "ash/power/gatt_battery_percentage_fetcher.h"
#include "base/macros.h"
#include "base/timer/mock_timer.h"
#include "device/bluetooth/bluetooth_adapter_factory.h"
#include "device/bluetooth/test/mock_bluetooth_adapter.h"
#include "device/bluetooth/test/mock_bluetooth_device.h"
#include "testing/gtest/include/gtest/gtest.h"
using device::BluetoothDevice;
using testing::NiceMock;
using testing::Return;
namespace ash {
namespace {
constexpr char kDeviceAddress[] = "AA:BB:CC:DD:EE:FF";
const uint8_t kBatteryPercentage = 100;
class FakeGattBatteryFetcherFactory
: public GattBatteryPercentageFetcher::Factory {
public:
FakeGattBatteryFetcherFactory() = default;
~FakeGattBatteryFetcherFactory() override = default;
FakeGattBatteryPercentageFetcher* last_fake_fetcher() {
return last_fake_fetcher_;
}
int fetchers_created_count() { return fetchers_created_count_; }
private:
std::unique_ptr<GattBatteryPercentageFetcher> BuildInstance(
scoped_refptr<device::BluetoothAdapter> adapter,
const std::string& device_address,
GattBatteryPercentageFetcher::BatteryPercentageCallback callback)
override {
++fetchers_created_count_;
auto instance = std::make_unique<FakeGattBatteryPercentageFetcher>(
device_address, std::move(callback));
last_fake_fetcher_ = instance.get();
return std::move(instance);
}
FakeGattBatteryPercentageFetcher* last_fake_fetcher_ = nullptr;
int fetchers_created_count_ = 0;
DISALLOW_COPY_AND_ASSIGN(FakeGattBatteryFetcherFactory);
};
} // namespace
class GattBatteryPollerTest : public testing::Test {
public:
GattBatteryPollerTest() = default;
~GattBatteryPollerTest() override = default;
void SetUp() override {
mock_device_ = std::make_unique<NiceMock<device::MockBluetoothDevice>>(
mock_adapter_.get(), 0 /* bluetooth_class */, "device_name",
kDeviceAddress, true /* paired */, true /* connected */);
ASSERT_FALSE(mock_device_->battery_percentage());
mock_adapter_ =
base::MakeRefCounted<NiceMock<device::MockBluetoothAdapter>>();
ON_CALL(*mock_adapter_, GetDevice(kDeviceAddress))
.WillByDefault(Return(mock_device_.get()));
device::BluetoothAdapterFactory::SetAdapterForTesting(mock_adapter_);
fake_gatt_battery_fetcher_factory_ =
std::make_unique<FakeGattBatteryFetcherFactory>();
GattBatteryPercentageFetcher::Factory::SetFactoryForTesting(
fake_gatt_battery_fetcher_factory_.get());
}
void TearDown() override {
GattBatteryPercentageFetcher::Factory::SetFactoryForTesting(nullptr);
}
void CreateGattBatteryPoller() {
auto mock_timer = std::make_unique<base::MockOneShotTimer>();
mock_timer_ = mock_timer.get();
poller_ = GattBatteryPoller::Factory::NewInstance(
mock_adapter_, kDeviceAddress, std::move(mock_timer));
}
FakeGattBatteryPercentageFetcher* last_fake_fetcher() {
return fake_gatt_battery_fetcher_factory_->last_fake_fetcher();
}
int fetchers_created_count() {
return fake_gatt_battery_fetcher_factory_->fetchers_created_count();
}
protected:
scoped_refptr<NiceMock<device::MockBluetoothAdapter>> mock_adapter_;
std::unique_ptr<device::MockBluetoothDevice> mock_device_;
base::MockOneShotTimer* mock_timer_ = nullptr;
std::unique_ptr<FakeGattBatteryFetcherFactory>
fake_gatt_battery_fetcher_factory_;
std::unique_ptr<GattBatteryPoller> poller_;
private:
DISALLOW_COPY_AND_ASSIGN(GattBatteryPollerTest);
};
TEST_F(GattBatteryPollerTest, PollsTheBatteryAndUpdatesTheBluetoothDevice) {
CreateGattBatteryPoller();
// The poller should have created a fetcher.
EXPECT_EQ(1, fetchers_created_count());
last_fake_fetcher()->InvokeCallbackWithSuccessfulFetch(kBatteryPercentage);
// Expect the returned battery value is set in the device.
EXPECT_EQ(kBatteryPercentage, mock_device_->battery_percentage());
}
TEST_F(GattBatteryPollerTest, PollsBatteryAgainAfterSuccess) {
CreateGattBatteryPoller();
EXPECT_EQ(1, fetchers_created_count());
last_fake_fetcher()->InvokeCallbackWithSuccessfulFetch(kBatteryPercentage);
// Expect the returned battery value is set in the device.
EXPECT_EQ(kBatteryPercentage, mock_device_->battery_percentage());
// The poller should be waiting for the timer timeout.
ASSERT_TRUE(mock_timer_->IsRunning());
mock_timer_->Fire();
// A new fetcher should have been created.
EXPECT_EQ(2, fetchers_created_count());
const uint8_t kNewBatteryPercentage = 0;
last_fake_fetcher()->InvokeCallbackWithSuccessfulFetch(kNewBatteryPercentage);
EXPECT_EQ(kNewBatteryPercentage, mock_device_->battery_percentage());
EXPECT_TRUE(mock_timer_->IsRunning());
}
TEST_F(GattBatteryPollerTest, RetryPollingAfterAnError) {
CreateGattBatteryPoller();
EXPECT_EQ(1, fetchers_created_count());
// Simulate the battery level was not fetched.
last_fake_fetcher()->InvokeCallbackWithFailedFetch();
// Battery should not have been set.
EXPECT_FALSE(mock_device_->battery_percentage());
// Retry logic should schedule a new attempt to read the battery level.
ASSERT_TRUE(mock_timer_->IsRunning());
mock_timer_->Fire();
// A new fetcher should have been created.
EXPECT_EQ(2, fetchers_created_count());
// Battery should not have been set.
EXPECT_FALSE(mock_device_->battery_percentage());
}
TEST_F(GattBatteryPollerTest, DoesNotModifyBatteryValueAfterAnError) {
mock_device_->set_battery_percentage(kBatteryPercentage);
CreateGattBatteryPoller();
EXPECT_EQ(1, fetchers_created_count());
last_fake_fetcher()->InvokeCallbackWithFailedFetch();
// Check retry logic is running.
EXPECT_TRUE(mock_timer_->IsRunning());
// Battery should not have changed.
EXPECT_EQ(kBatteryPercentage, mock_device_->battery_percentage());
}
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);
CreateGattBatteryPoller();
const int kMaxRetryCount = 3;
for (int i = 1; i <= kMaxRetryCount; ++i) {
EXPECT_EQ(i, fetchers_created_count());
last_fake_fetcher()->InvokeCallbackWithFailedFetch();
// Battery should not change.
EXPECT_EQ(kBatteryPercentage, mock_device_->battery_percentage());
ASSERT_TRUE(mock_timer_->IsRunning());
mock_timer_->Fire();
}
EXPECT_EQ(4, fetchers_created_count());
last_fake_fetcher()->InvokeCallbackWithFailedFetch();
// Check retry logic is not running.
EXPECT_FALSE(mock_timer_->IsRunning());
// Battery should reset.
EXPECT_FALSE(mock_device_->battery_percentage());
}
} // namespace ash
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