diff options
author | armansito@chromium.org <armansito@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2014-06-28 01:43:48 +0000 |
---|---|---|
committer | armansito@chromium.org <armansito@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2014-06-28 01:43:48 +0000 |
commit | 184a2e7847020fca1bd038bbbc65ed26d9f2609f (patch) | |
tree | d5d3a35390e91be6c40cdad7c72993f893f343ca | |
parent | aa22ab042e9586fd22dd142e7262751931ebed7e (diff) | |
download | chromium_src-184a2e7847020fca1bd038bbbc65ed26d9f2609f.zip chromium_src-184a2e7847020fca1bd038bbbc65ed26d9f2609f.tar.gz chromium_src-184a2e7847020fca1bd038bbbc65ed26d9f2609f.tar.bz2 |
bluetoothLowEnergy: Add functions to start/stop characteristic notifications.
This CL adds the startCharacteristicNotifications and
stopCharacteristicNotifications functions to the chrome.bluetoothLowEnergy API.
These changes the API behavior from automatically getting characteristic value
notifications to explicitly having to enable them by calling the new API method.
Internally an API resource is created for each app on a per-characteristic
basis, managing the life-time of a device::BluetoothGattNotifySession
object. Notifications are physically enabled/disabled and shared among apps
using session objects.
BUG=387989
TEST=browser_tests --gtest_filter=BluetoothLowEnergyApiTest.*
Review URL: https://codereview.chromium.org/351123002
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@280477 0039d316-1c4b-4281-b951-d872f2087c98
14 files changed, 838 insertions, 27 deletions
diff --git a/chrome/browser/extensions/api/bluetooth_low_energy/bluetooth_low_energy_api.cc b/chrome/browser/extensions/api/bluetooth_low_energy/bluetooth_low_energy_api.cc index b335227..c10703a 100644 --- a/chrome/browser/extensions/api/bluetooth_low_energy/bluetooth_low_energy_api.cc +++ b/chrome/browser/extensions/api/bluetooth_low_energy/bluetooth_low_energy_api.cc @@ -26,8 +26,10 @@ namespace { const char kErrorAdapterNotInitialized[] = "Could not initialize Bluetooth adapter"; const char kErrorAlreadyConnected[] = "Already connected"; +const char kErrorAlreadyNotifying[] = "Already notifying"; const char kErrorInProgress[] = "In progress"; const char kErrorNotConnected[] = "Not connected"; +const char kErrorNotNotifying[] = "Not notifying"; const char kErrorNotFound[] = "Instance not found"; const char kErrorOperationFailed[] = "Operation failed"; const char kErrorPermissionDenied[] = "Permission denied"; @@ -45,8 +47,12 @@ std::string StatusToString(BluetoothLowEnergyEventRouter::Status status) { return kErrorNotFound; case BluetoothLowEnergyEventRouter::kStatusErrorAlreadyConnected: return kErrorAlreadyConnected; + case BluetoothLowEnergyEventRouter::kStatusErrorAlreadyNotifying: + return kErrorAlreadyNotifying; case BluetoothLowEnergyEventRouter::kStatusErrorNotConnected: return kErrorNotConnected; + case BluetoothLowEnergyEventRouter::kStatusErrorNotNotifying: + return kErrorNotNotifying; case BluetoothLowEnergyEventRouter::kStatusErrorInProgress: return kErrorInProgress; case BluetoothLowEnergyEventRouter::kStatusSuccess: @@ -573,6 +579,96 @@ void BluetoothLowEnergyWriteCharacteristicValueFunction::ErrorCallback( SendResponse(false); } +bool BluetoothLowEnergyStartCharacteristicNotificationsFunction::DoWork() { + DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); + + BluetoothLowEnergyEventRouter* event_router = + GetEventRouter(browser_context()); + + // The adapter must be initialized at this point, but return an error instead + // of asserting. + if (!event_router->HasAdapter()) { + SetError(kErrorAdapterNotInitialized); + SendResponse(false); + return false; + } + + scoped_ptr<apibtle::StartCharacteristicNotifications::Params> params( + apibtle::StartCharacteristicNotifications::Params::Create(*args_)); + EXTENSION_FUNCTION_VALIDATE(params.get() != NULL); + + bool persistent = false; // Not persistent by default. + apibtle::NotificationProperties* properties = params.get()->properties.get(); + if (properties) + persistent = properties->persistent; + + event_router->StartCharacteristicNotifications( + persistent, + GetExtension(), + params->characteristic_id, + base::Bind(&BluetoothLowEnergyStartCharacteristicNotificationsFunction:: + SuccessCallback, + this), + base::Bind(&BluetoothLowEnergyStartCharacteristicNotificationsFunction:: + ErrorCallback, + this)); + + return true; +} + +void +BluetoothLowEnergyStartCharacteristicNotificationsFunction::SuccessCallback() { + SendResponse(true); +} + +void BluetoothLowEnergyStartCharacteristicNotificationsFunction::ErrorCallback( + BluetoothLowEnergyEventRouter::Status status) { + SetError(StatusToString(status)); + SendResponse(false); +} + +bool BluetoothLowEnergyStopCharacteristicNotificationsFunction::DoWork() { + DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); + + BluetoothLowEnergyEventRouter* event_router = + GetEventRouter(browser_context()); + + // The adapter must be initialized at this point, but return an error instead + // of asserting. + if (!event_router->HasAdapter()) { + SetError(kErrorAdapterNotInitialized); + SendResponse(false); + return false; + } + + scoped_ptr<apibtle::StopCharacteristicNotifications::Params> params( + apibtle::StopCharacteristicNotifications::Params::Create(*args_)); + EXTENSION_FUNCTION_VALIDATE(params.get() != NULL); + + event_router->StopCharacteristicNotifications( + GetExtension(), + params->characteristic_id, + base::Bind(&BluetoothLowEnergyStopCharacteristicNotificationsFunction:: + SuccessCallback, + this), + base::Bind(&BluetoothLowEnergyStopCharacteristicNotificationsFunction:: + ErrorCallback, + this)); + + return true; +} + +void +BluetoothLowEnergyStopCharacteristicNotificationsFunction::SuccessCallback() { + SendResponse(true); +} + +void BluetoothLowEnergyStopCharacteristicNotificationsFunction::ErrorCallback( + BluetoothLowEnergyEventRouter::Status status) { + SetError(StatusToString(status)); + SendResponse(false); +} + bool BluetoothLowEnergyReadDescriptorValueFunction::DoWork() { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); diff --git a/chrome/browser/extensions/api/bluetooth_low_energy/bluetooth_low_energy_api.h b/chrome/browser/extensions/api/bluetooth_low_energy/bluetooth_low_energy_api.h index 3fd57ed..4165169 100644 --- a/chrome/browser/extensions/api/bluetooth_low_energy/bluetooth_low_energy_api.h +++ b/chrome/browser/extensions/api/bluetooth_low_energy/bluetooth_low_energy_api.h @@ -247,6 +247,46 @@ class BluetoothLowEnergyWriteCharacteristicValueFunction std::string instance_id_; }; +class BluetoothLowEnergyStartCharacteristicNotificationsFunction + : public BluetoothLowEnergyExtensionFunction { + public: + DECLARE_EXTENSION_FUNCTION( + "bluetoothLowEnergy.startCharacteristicNotifications", + BLUETOOTHLOWENERGY_STARTCHARACTERISTICNOTIFICATIONS); + + protected: + virtual ~BluetoothLowEnergyStartCharacteristicNotificationsFunction() {} + + // BluetoothLowEnergyExtensionFunction override. + virtual bool DoWork() OVERRIDE; + + private: + // Success and error callbacks, called by + // BluetoothLowEnergyEventRouter::StartCharacteristicNotifications. + void SuccessCallback(); + void ErrorCallback(BluetoothLowEnergyEventRouter::Status status); +}; + +class BluetoothLowEnergyStopCharacteristicNotificationsFunction + : public BluetoothLowEnergyExtensionFunction { + public: + DECLARE_EXTENSION_FUNCTION( + "bluetoothLowEnergy.stopCharacteristicNotifications", + BLUETOOTHLOWENERGY_STOPCHARACTERISTICNOTIFICATIONS); + + protected: + virtual ~BluetoothLowEnergyStopCharacteristicNotificationsFunction() {} + + // BluetoothLowEnergyExtensionFunction override. + virtual bool DoWork() OVERRIDE; + + private: + // Success and error callbacks, called by + // BluetoothLowEnergyEventRouter::StopCharacteristicNotifications. + void SuccessCallback(); + void ErrorCallback(BluetoothLowEnergyEventRouter::Status status); +}; + class BluetoothLowEnergyReadDescriptorValueFunction : public BluetoothLowEnergyExtensionFunction { public: diff --git a/chrome/browser/extensions/api/bluetooth_low_energy/bluetooth_low_energy_apitest.cc b/chrome/browser/extensions/api/bluetooth_low_energy/bluetooth_low_energy_apitest.cc index 416c107..78fd1ff 100644 --- a/chrome/browser/extensions/api/bluetooth_low_energy/bluetooth_low_energy_apitest.cc +++ b/chrome/browser/extensions/api/bluetooth_low_energy/bluetooth_low_energy_apitest.cc @@ -13,6 +13,7 @@ #include "device/bluetooth/test/mock_bluetooth_gatt_characteristic.h" #include "device/bluetooth/test/mock_bluetooth_gatt_connection.h" #include "device/bluetooth/test/mock_bluetooth_gatt_descriptor.h" +#include "device/bluetooth/test/mock_bluetooth_gatt_notify_session.h" #include "device/bluetooth/test/mock_bluetooth_gatt_service.h" #include "testing/gmock/include/gmock/gmock.h" @@ -23,12 +24,14 @@ using device::BluetoothGattCharacteristic; using device::BluetoothGattConnection; using device::BluetoothGattDescriptor; using device::BluetoothGattService; +using device::BluetoothGattNotifySession; using device::MockBluetoothAdapter; using device::MockBluetoothDevice; using device::MockBluetoothGattCharacteristic; using device::MockBluetoothGattConnection; using device::MockBluetoothGattDescriptor; using device::MockBluetoothGattService; +using device::MockBluetoothGattNotifySession; using extensions::BluetoothLowEnergyEventRouter; using testing::Invoke; using testing::Return; @@ -605,11 +608,6 @@ IN_PROC_BROWSER_TEST_F(BluetoothLowEnergyApiTest, CharacteristicValueChanged) { ResultCatcher catcher; catcher.RestrictToProfile(browser()->profile()); - // Load the extension and let it set up. - ExtensionTestMessageListener listener("ready", true); - ASSERT_TRUE(LoadExtension(test_data_dir_.AppendASCII( - "bluetooth_low_energy/characteristic_value_changed"))); - // Cause events to be sent to the extension. event_router()->DeviceAdded(mock_adapter_, device0_.get()); event_router()->GattServiceAdded(device0_.get(), service0_.get()); @@ -617,13 +615,52 @@ IN_PROC_BROWSER_TEST_F(BluetoothLowEnergyApiTest, CharacteristicValueChanged) { event_router()->GattCharacteristicAdded(service0_.get(), chrc0_.get()); event_router()->GattCharacteristicAdded(service1_.get(), chrc2_.get()); + EXPECT_CALL(*mock_adapter_, GetDevice(_)) + .Times(2) + .WillRepeatedly(Return(device0_.get())); + EXPECT_CALL(*device0_, GetGattService(kTestServiceId0)) + .Times(1) + .WillOnce(Return(service0_.get())); + EXPECT_CALL(*device0_, GetGattService(kTestServiceId1)) + .Times(1) + .WillOnce(Return(service1_.get())); + EXPECT_CALL(*service0_, GetCharacteristic(kTestCharacteristicId0)) + .Times(1) + .WillOnce(Return(chrc0_.get())); + EXPECT_CALL(*service1_, GetCharacteristic(kTestCharacteristicId2)) + .Times(1) + .WillOnce(Return(chrc2_.get())); + + BluetoothGattNotifySession* session0 = + new testing::NiceMock<MockBluetoothGattNotifySession>( + kTestCharacteristicId0); + BluetoothGattNotifySession* session1 = + new testing::NiceMock<MockBluetoothGattNotifySession>( + kTestCharacteristicId2); + + EXPECT_CALL(*chrc0_, StartNotifySession(_, _)) + .Times(1) + .WillOnce( + InvokeCallbackWithScopedPtrArg<0, BluetoothGattNotifySession>( + session0)); + EXPECT_CALL(*chrc2_, StartNotifySession(_, _)) + .Times(1) + .WillOnce( + InvokeCallbackWithScopedPtrArg<0, BluetoothGattNotifySession>( + session1)); + + ExtensionTestMessageListener listener("ready", true); + ASSERT_TRUE(LoadExtension(test_data_dir_.AppendASCII( + "bluetooth_low_energy/characteristic_value_changed"))); + + EXPECT_TRUE(listener.WaitUntilSatisfied()); + std::vector<uint8> value; event_router()->GattCharacteristicValueChanged( service0_.get(), chrc0_.get(), value); event_router()->GattCharacteristicValueChanged( service1_.get(), chrc2_.get(), value); - EXPECT_TRUE(listener.WaitUntilSatisfied()); listener.Reply("go"); EXPECT_TRUE(catcher.GetNextResult()) << catcher.message(); @@ -1157,4 +1194,79 @@ IN_PROC_BROWSER_TEST_F(BluetoothLowEnergyApiTest, ConnectInProgress) { event_router()->DeviceRemoved(mock_adapter_, device0_.get()); } +IN_PROC_BROWSER_TEST_F(BluetoothLowEnergyApiTest, StartStopNotifications) { + ResultCatcher catcher; + catcher.RestrictToProfile(browser()->profile()); + + event_router()->DeviceAdded(mock_adapter_, device0_.get()); + event_router()->GattServiceAdded(device0_.get(), service0_.get()); + event_router()->GattServiceAdded(device0_.get(), service1_.get()); + event_router()->GattCharacteristicAdded(service0_.get(), chrc0_.get()); + event_router()->GattCharacteristicAdded(service0_.get(), chrc1_.get()); + event_router()->GattCharacteristicAdded(service1_.get(), chrc2_.get()); + + EXPECT_CALL(*mock_adapter_, GetDevice(_)) + .WillRepeatedly(Return(device0_.get())); + EXPECT_CALL(*device0_, GetGattService(kTestServiceId0)) + .WillRepeatedly(Return(service0_.get())); + EXPECT_CALL(*device0_, GetGattService(kTestServiceId1)) + .WillRepeatedly(Return(service1_.get())); + EXPECT_CALL(*service1_, GetCharacteristic(kTestCharacteristicId2)) + .Times(1) + .WillOnce(Return(chrc2_.get())); + EXPECT_CALL(*service0_, GetCharacteristic(kTestCharacteristicId0)) + .Times(2) + .WillRepeatedly(Return(chrc0_.get())); + EXPECT_CALL(*service0_, GetCharacteristic(kTestCharacteristicId1)) + .Times(1) + .WillOnce(Return(chrc1_.get())); + + BluetoothGattNotifySession* session0 = + new testing::NiceMock<MockBluetoothGattNotifySession>( + kTestCharacteristicId0); + MockBluetoothGattNotifySession* session1 = + new testing::NiceMock<MockBluetoothGattNotifySession>( + kTestCharacteristicId1); + + EXPECT_CALL(*session1, Stop(_)) + .Times(1) + .WillOnce(InvokeCallbackArgument<0>()); + + EXPECT_CALL(*chrc0_, StartNotifySession(_, _)) + .Times(2) + .WillOnce(InvokeCallbackArgument<1>()) + .WillOnce( + InvokeCallbackWithScopedPtrArg<0, BluetoothGattNotifySession>( + session0)); + EXPECT_CALL(*chrc1_, StartNotifySession(_, _)) + .Times(1) + .WillOnce( + InvokeCallbackWithScopedPtrArg<0, BluetoothGattNotifySession>( + session1)); + + ExtensionTestMessageListener listener("ready", true); + ASSERT_TRUE(LoadExtension(test_data_dir_.AppendASCII( + "bluetooth_low_energy/start_stop_notifications"))); + + EXPECT_TRUE(listener.WaitUntilSatisfied()); + + std::vector<uint8> value; + event_router()->GattCharacteristicValueChanged( + service0_.get(), chrc0_.get(), value); + event_router()->GattCharacteristicValueChanged( + service0_.get(), chrc1_.get(), value); + event_router()->GattCharacteristicValueChanged( + service1_.get(), chrc2_.get(), value); + + listener.Reply("go"); + + EXPECT_TRUE(catcher.GetNextResult()) << catcher.message(); + event_router()->GattCharacteristicRemoved(service1_.get(), chrc2_.get()); + event_router()->GattCharacteristicRemoved(service0_.get(), chrc1_.get()); + event_router()->GattCharacteristicRemoved(service0_.get(), chrc0_.get()); + event_router()->GattServiceRemoved(device0_.get(), service1_.get()); + event_router()->GattServiceRemoved(device0_.get(), service0_.get()); + event_router()->DeviceRemoved(mock_adapter_, device0_.get()); +} + } // namespace diff --git a/chrome/browser/extensions/api/bluetooth_low_energy/bluetooth_low_energy_event_router.cc b/chrome/browser/extensions/api/bluetooth_low_energy/bluetooth_low_energy_event_router.cc index 8374e5d..b5f5afd 100644 --- a/chrome/browser/extensions/api/bluetooth_low_energy/bluetooth_low_energy_event_router.cc +++ b/chrome/browser/extensions/api/bluetooth_low_energy/bluetooth_low_energy_event_router.cc @@ -8,6 +8,7 @@ #include "base/logging.h" #include "base/values.h" #include "chrome/browser/extensions/api/bluetooth_low_energy/bluetooth_low_energy_connection.h" +#include "chrome/browser/extensions/api/bluetooth_low_energy/bluetooth_low_energy_notify_session.h" #include "chrome/browser/extensions/api/bluetooth_low_energy/utils.h" #include "chrome/common/extensions/api/bluetooth/bluetooth_manifest_data.h" #include "content/public/browser/browser_thread.h" @@ -136,6 +137,20 @@ ConnectionResourceManager* GetConnectionResourceManager( return manager; } +typedef extensions::ApiResourceManager< + extensions::BluetoothLowEnergyNotifySession> NotifySessionResourceManager; +NotifySessionResourceManager* GetNotifySessionResourceManager( + content::BrowserContext* context) { + NotifySessionResourceManager* manager = + NotifySessionResourceManager::Get(context); + DCHECK(manager) + << "There is no Bluetooth low energy value update session manager." + "If this assertion is failing during a test, then it is likely that " + "TestExtensionSystem is failing to provide an instance of " + "ApiResourceManager<BluetoothLowEnergyNotifySession>."; + return manager; +} + } // namespace namespace extensions { @@ -627,6 +642,103 @@ void BluetoothLowEnergyEventRouter::WriteCharacteristicValue( error_callback)); } +void BluetoothLowEnergyEventRouter::StartCharacteristicNotifications( + bool persistent, + const Extension* extension, + const std::string& instance_id, + const base::Closure& callback, + const ErrorCallback& error_callback) { + DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); + if (!adapter_) { + VLOG(1) << "BluetoothAdapter not ready."; + error_callback.Run(kStatusErrorFailed); + return; + } + + const std::string extension_id = extension->id(); + const std::string session_id = extension_id + instance_id; + + if (pending_session_calls_.count(session_id) != 0) { + error_callback.Run(kStatusErrorInProgress); + return; + } + + BluetoothLowEnergyNotifySession* session = + FindNotifySession(extension_id, instance_id); + if (session) { + if (session->GetSession()->IsActive()) { + VLOG(1) << "Application has already enabled notifications from " + << "characteristic: " << instance_id; + error_callback.Run(kStatusErrorAlreadyNotifying); + return; + } + + RemoveNotifySession(extension_id, instance_id); + } + + BluetoothGattCharacteristic* characteristic = + FindCharacteristicById(instance_id); + if (!characteristic) { + VLOG(1) << "Characteristic not found: " << instance_id; + error_callback.Run(kStatusErrorNotFound); + return; + } + + BluetoothPermissionRequest request( + characteristic->GetService()->GetUUID().value()); + if (!BluetoothManifestData::CheckRequest(extension, request)) { + VLOG(1) << "App has no permission to access this characteristic: " + << instance_id; + error_callback.Run(kStatusErrorPermissionDenied); + return; + } + + pending_session_calls_.insert(session_id); + characteristic->StartNotifySession( + base::Bind(&BluetoothLowEnergyEventRouter::OnStartNotifySession, + weak_ptr_factory_.GetWeakPtr(), + persistent, + extension_id, + instance_id, + callback), + base::Bind(&BluetoothLowEnergyEventRouter::OnStartNotifySessionError, + weak_ptr_factory_.GetWeakPtr(), + extension_id, + instance_id, + error_callback)); +} + +void BluetoothLowEnergyEventRouter::StopCharacteristicNotifications( + const Extension* extension, + const std::string& instance_id, + const base::Closure& callback, + const ErrorCallback& error_callback) { + DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); + if (!adapter_) { + VLOG(1) << "BluetoothAdapter not ready."; + error_callback.Run(kStatusErrorFailed); + return; + } + + const std::string extension_id = extension->id(); + + BluetoothLowEnergyNotifySession* session = + FindNotifySession(extension_id, instance_id); + if (!session || !session->GetSession()->IsActive()) { + VLOG(1) << "Application has not enabled notifications from " + << "characteristic: " << instance_id; + error_callback.Run(kStatusErrorNotNotifying); + return; + } + + session->GetSession()->Stop( + base::Bind(&BluetoothLowEnergyEventRouter::OnStopNotifySession, + weak_ptr_factory_.GetWeakPtr(), + extension_id, + instance_id, + callback)); +} + void BluetoothLowEnergyEventRouter::ReadDescriptorValue( const Extension* extension, const std::string& instance_id, @@ -801,6 +913,7 @@ void BluetoothLowEnergyEventRouter::GattServiceChanged( DispatchEventToExtensionsWithPermission( apibtle::OnServiceChanged::kEventName, service->GetUUID(), + "" /* characteristic_id */, apibtle::OnServiceChanged::Create(api_service)); } @@ -890,6 +1003,7 @@ void BluetoothLowEnergyEventRouter::GattCharacteristicValueChanged( DispatchEventToExtensionsWithPermission( apibtle::OnCharacteristicValueChanged::kEventName, service->GetUUID(), + characteristic->GetIdentifier(), args.Pass()); } @@ -916,6 +1030,7 @@ void BluetoothLowEnergyEventRouter::GattDescriptorValueChanged( DispatchEventToExtensionsWithPermission( apibtle::OnDescriptorValueChanged::kEventName, characteristic->GetService()->GetUUID(), + "" /* characteristic_id */, args.Pass()); } @@ -990,9 +1105,10 @@ void BluetoothLowEnergyEventRouter::InitializeIdentifierMappings() { } void BluetoothLowEnergyEventRouter::DispatchEventToExtensionsWithPermission( - const std::string& event_name, - const device::BluetoothUUID& uuid, - scoped_ptr<base::ListValue> args) { + const std::string& event_name, + const device::BluetoothUUID& uuid, + const std::string& characteristic_id, + scoped_ptr<base::ListValue> args) { // Obtain the listeners of |event_name|. The list can contain multiple // entries for the same extension, so we keep track of the extensions that we // already sent the event to, since we want the send an event to an extension @@ -1023,6 +1139,14 @@ void BluetoothLowEnergyEventRouter::DispatchEventToExtensionsWithPermission( !BluetoothManifestData::CheckLowEnergyPermitted(extension)) continue; + // If |event_name| is "onCharacteristicValueChanged", then send the + // event only if the extension has requested notifications from the + // related characteristic. + if (event_name == apibtle::OnCharacteristicValueChanged::kEventName && + !characteristic_id.empty() && + !FindNotifySession(extension_id, characteristic_id)) + continue; + // Send the event. scoped_ptr<base::ListValue> args_copy(args->DeepCopy()); scoped_ptr<Event> event(new Event(event_name, args_copy.Pass())); @@ -1181,6 +1305,62 @@ void BluetoothLowEnergyEventRouter::OnConnectError( error_callback.Run(kStatusErrorFailed); } +void BluetoothLowEnergyEventRouter::OnStartNotifySession( + bool persistent, + const std::string& extension_id, + const std::string& characteristic_id, + const base::Closure& callback, + scoped_ptr<device::BluetoothGattNotifySession> session) { + VLOG(2) << "Value update session created for characteristic: " + << characteristic_id; + DCHECK(session.get()); + DCHECK(!FindNotifySession(extension_id, characteristic_id)); + DCHECK_EQ(characteristic_id, session->GetCharacteristicIdentifier()); + + const std::string session_id = extension_id + characteristic_id; + DCHECK_NE(0U, pending_session_calls_.count(session_id)); + + BluetoothLowEnergyNotifySession* resource = + new BluetoothLowEnergyNotifySession( + persistent, extension_id, session.Pass()); + + NotifySessionResourceManager* manager = + GetNotifySessionResourceManager(browser_context_); + manager->Add(resource); + + pending_session_calls_.erase(session_id); + callback.Run(); +} + +void BluetoothLowEnergyEventRouter::OnStartNotifySessionError( + const std::string& extension_id, + const std::string& characteristic_id, + const ErrorCallback& error_callback) { + VLOG(2) << "Failed to create value update session for characteristic: " + << characteristic_id; + + const std::string session_id = extension_id + characteristic_id; + DCHECK_NE(0U, pending_session_calls_.count(session_id)); + + pending_session_calls_.erase(session_id); + error_callback.Run(kStatusErrorFailed); +} + +void BluetoothLowEnergyEventRouter::OnStopNotifySession( + const std::string& extension_id, + const std::string& characteristic_id, + const base::Closure& callback) { + VLOG(2) << "Value update session terminated."; + + if (!RemoveNotifySession(extension_id, characteristic_id)) { + VLOG(1) << "The value update session was removed before Stop completed, " + << "id: " << extension_id + << ", characteristic: " << characteristic_id; + } + + callback.Run(); +} + BluetoothLowEnergyConnection* BluetoothLowEnergyEventRouter::FindConnection( const std::string& extension_id, const std::string& device_address) { @@ -1231,4 +1411,58 @@ bool BluetoothLowEnergyEventRouter::RemoveConnection( return false; } +BluetoothLowEnergyNotifySession* +BluetoothLowEnergyEventRouter::FindNotifySession( + const std::string& extension_id, + const std::string& characteristic_id) { + NotifySessionResourceManager* manager = + GetNotifySessionResourceManager(browser_context_); + + base::hash_set<int>* ids = manager->GetResourceIds(extension_id); + if (!ids) + return NULL; + + for (base::hash_set<int>::const_iterator iter = ids->begin(); + iter != ids->end(); + ++iter) { + BluetoothLowEnergyNotifySession* session = + manager->Get(extension_id, *iter); + if (!session) + continue; + + if (session->GetSession()->GetCharacteristicIdentifier() == + characteristic_id) + return session; + } + + return NULL; +} + +bool BluetoothLowEnergyEventRouter::RemoveNotifySession( + const std::string& extension_id, + const std::string& characteristic_id) { + NotifySessionResourceManager* manager = + GetNotifySessionResourceManager(browser_context_); + + base::hash_set<int>* ids = manager->GetResourceIds(extension_id); + if (!ids) + return false; + + for (base::hash_set<int>::const_iterator iter = ids->begin(); + iter != ids->end(); + ++iter) { + BluetoothLowEnergyNotifySession* session = + manager->Get(extension_id, *iter); + if (!session || + session->GetSession()->GetCharacteristicIdentifier() != + characteristic_id) + continue; + + manager->Remove(extension_id, *iter); + return true; + } + + return false; +} + } // namespace extensions diff --git a/chrome/browser/extensions/api/bluetooth_low_energy/bluetooth_low_energy_event_router.h b/chrome/browser/extensions/api/bluetooth_low_energy/bluetooth_low_energy_event_router.h index 1f1f1f4..6421159 100644 --- a/chrome/browser/extensions/api/bluetooth_low_energy/bluetooth_low_energy_event_router.h +++ b/chrome/browser/extensions/api/bluetooth_low_energy/bluetooth_low_energy_event_router.h @@ -31,9 +31,16 @@ class BrowserContext; } // namespace content +namespace device { + +class BluetoothGattNotifySession; + +} // namespace device + namespace extensions { class BluetoothLowEnergyConnection; +class BluetoothLowEnergyNotifySession; class Extension; // The BluetoothLowEnergyEventRouter is used by the bluetoothLowEnergy API to @@ -52,7 +59,9 @@ class BluetoothLowEnergyEventRouter kStatusErrorPermissionDenied, kStatusErrorNotFound, kStatusErrorAlreadyConnected, + kStatusErrorAlreadyNotifying, kStatusErrorNotConnected, + kStatusErrorNotNotifying, kStatusErrorInProgress, kStatusErrorFailed }; @@ -180,6 +189,24 @@ class BluetoothLowEnergyEventRouter const base::Closure& callback, const ErrorCallback& error_callback); + // Sends a request to start characteristic notifications from characteristic + // with instance ID |instance_id|, for extension |extension|. Invokes + // |callback| on success and |error_callback| on failure. If |persistent| is + // true, then the allocated connection resource is persistent across unloads. + void StartCharacteristicNotifications(bool persistent, + const Extension* extension, + const std::string& instance_id, + const base::Closure& callback, + const ErrorCallback& error_callback); + + // Sends a request to stop characteristic notifications from characteristic + // with instance ID |instance_id|, for extension |extension|. Invokes + // |callback| on success and |error_callback| on failure. + void StopCharacteristicNotifications(const Extension* extension, + const std::string& instance_id, + const base::Closure& callback, + const ErrorCallback& error_callback); + // Sends a request to read the value of the descriptor with instance ID // |instance_id|. Invokes |callback| on success and |error_callback| on // failure. |extension| is the extension that made the call. @@ -249,10 +276,13 @@ class BluetoothLowEnergyEventRouter // Sends the event named |event_name| to all listeners of that event that // have the Bluetooth UUID manifest permission for UUID |uuid| and the // "low_energy" manifest permission, with |args| as the argument to that - // event. + // event. If the event involves a characteristic, then |characteristic_id| + // should be the instance ID of the involved characteristic. Otherwise, an + // empty string should be passed. void DispatchEventToExtensionsWithPermission( const std::string& event_name, const device::BluetoothUUID& uuid, + const std::string& characteristic_id, scoped_ptr<base::ListValue> args); // Returns a BluetoothGattService by its instance ID |instance_id|. Returns @@ -298,6 +328,26 @@ class BluetoothLowEnergyEventRouter const ErrorCallback& error_callback, device::BluetoothDevice::ConnectErrorCode error_code); + // Called by BluetoothGattCharacteristic in response to a call to + // StartNotifySession. + void OnStartNotifySession( + bool persistent, + const std::string& extension_id, + const std::string& characteristic_id, + const base::Closure& callback, + scoped_ptr<device::BluetoothGattNotifySession> session); + + // Called by BluetoothGattCharacteristic in response to a call to + // StartNotifySession. + void OnStartNotifySessionError(const std::string& extension_id, + const std::string& characteristic_id, + const ErrorCallback& error_callback); + + // Called by BluetoothGattNotifySession in response to a call to Stop. + void OnStopNotifySession(const std::string& extension_id, + const std::string& characteristic_id, + const base::Closure& callback); + // Finds and returns a BluetoothLowEnergyConnection to device with address // |device_address| from the managed API resources for extension with ID // |extension_id|. @@ -311,6 +361,20 @@ class BluetoothLowEnergyEventRouter bool RemoveConnection(const std::string& extension_id, const std::string& device_address); + // Finds and returns a BluetoothLowEnergyNotifySession associated with + // characteristic with instance ID |characteristic_id| from the managed API + // API resources for extension with ID |extension_id|. + BluetoothLowEnergyNotifySession* FindNotifySession( + const std::string& extension_id, + const std::string& characteristic_id); + + // Removes the notify session associated with characteristic with + // instance ID |characteristic_id| from the managed API resources for + // extension with ID |extension_id|. Returns false, if the session could + // not be found. + bool RemoveNotifySession(const std::string& extension_id, + const std::string& characteristic_id); + // Mapping from instance ids to identifiers of owning instances. The keys are // used to identify individual instances of GATT objects and are used by // bluetoothLowEnergy API functions to obtain the correct GATT object to @@ -343,6 +407,10 @@ class BluetoothLowEnergyEventRouter std::set<std::string> connecting_devices_; std::set<std::string> disconnecting_devices_; + // Set of extension ID + characteristic ID to which a request to start a + // notify session is currently pending. + std::set<std::string> pending_session_calls_; + // BrowserContext passed during initialization. content::BrowserContext* browser_context_; diff --git a/chrome/browser/extensions/api/bluetooth_low_energy/bluetooth_low_energy_notify_session.cc b/chrome/browser/extensions/api/bluetooth_low_energy/bluetooth_low_energy_notify_session.cc new file mode 100644 index 0000000..281c7cc --- /dev/null +++ b/chrome/browser/extensions/api/bluetooth_low_energy/bluetooth_low_energy_notify_session.cc @@ -0,0 +1,41 @@ +// Copyright 2014 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 "chrome/browser/extensions/api/bluetooth_low_energy/bluetooth_low_energy_notify_session.h" + +namespace extensions { + +static base::LazyInstance<BrowserContextKeyedAPIFactory< + ApiResourceManager<BluetoothLowEnergyNotifySession> > > g_factory = + LAZY_INSTANCE_INITIALIZER; + +template <> +BrowserContextKeyedAPIFactory< + ApiResourceManager<BluetoothLowEnergyNotifySession> >* +ApiResourceManager<BluetoothLowEnergyNotifySession>::GetFactoryInstance() { + return g_factory.Pointer(); +} + +BluetoothLowEnergyNotifySession::BluetoothLowEnergyNotifySession( + bool persistent, + const std::string& owner_extension_id, + scoped_ptr<device::BluetoothGattNotifySession> session) + : ApiResource(owner_extension_id), + persistent_(persistent), + session_(session.release()) { +} + +BluetoothLowEnergyNotifySession::~BluetoothLowEnergyNotifySession() { +} + +device::BluetoothGattNotifySession* +BluetoothLowEnergyNotifySession::GetSession() const { + return session_.get(); +} + +bool BluetoothLowEnergyNotifySession::IsPersistent() const { + return persistent_; +} + +} // namespace extensions diff --git a/chrome/browser/extensions/api/bluetooth_low_energy/bluetooth_low_energy_notify_session.h b/chrome/browser/extensions/api/bluetooth_low_energy/bluetooth_low_energy_notify_session.h new file mode 100644 index 0000000..8cac5d9 --- /dev/null +++ b/chrome/browser/extensions/api/bluetooth_low_energy/bluetooth_low_energy_notify_session.h @@ -0,0 +1,54 @@ +// Copyright 2014 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 CHROME_BROWSER_EXTENSIONS_API_BLUETOOTH_LOW_ENERGY_BLUETOOTH_LOW_ENERGY_NOTIFY_SESSION_H_ +#define CHROME_BROWSER_EXTENSIONS_API_BLUETOOTH_LOW_ENERGY_BLUETOOTH_LOW_ENERGY_NOTIFY_SESSION_H_ + +#include <string> + +#include "base/memory/scoped_ptr.h" +#include "device/bluetooth/bluetooth_gatt_notify_session.h" +#include "extensions/browser/api/api_resource.h" +#include "extensions/browser/api/api_resource_manager.h" + +namespace extensions { + +// An ApiResource wrapper for a device::BluetoothGattNotifySession +class BluetoothLowEnergyNotifySession : public ApiResource { + public: + explicit BluetoothLowEnergyNotifySession( + bool persistent, + const std::string& owner_extension_id, + scoped_ptr<device::BluetoothGattNotifySession> session); + virtual ~BluetoothLowEnergyNotifySession(); + + // Returns a pointer to the underlying session object. + device::BluetoothGattNotifySession* GetSession() const; + + // ApiResource override. + virtual bool IsPersistent() const OVERRIDE; + + // This resource should be managed on the UI thread. + static const content::BrowserThread::ID kThreadId = + content::BrowserThread::UI; + + private: + friend class ApiResourceManager<BluetoothLowEnergyNotifySession>; + static const char* service_name() { + return "BluetoothLowEnergyNotifySessionManager"; + } + + // True, if this resource should be persistent across suspends. + bool persistent_; + + // The session is owned by this instance and will automatically stop when + // deleted. + scoped_ptr<device::BluetoothGattNotifySession> session_; + + DISALLOW_COPY_AND_ASSIGN(BluetoothLowEnergyNotifySession); +}; + +} // namespace extensions + +#endif // CHROME_BROWSER_EXTENSIONS_API_BLUETOOTH_LOW_ENERGY_BLUETOOTH_LOW_ENERGY_NOTIFY_SESSION_H_ diff --git a/chrome/chrome_browser_extensions.gypi b/chrome/chrome_browser_extensions.gypi index 3fc3065..85497de 100644 --- a/chrome/chrome_browser_extensions.gypi +++ b/chrome/chrome_browser_extensions.gypi @@ -177,6 +177,8 @@ 'browser/extensions/api/bluetooth_low_energy/bluetooth_low_energy_connection.h', 'browser/extensions/api/bluetooth_low_energy/bluetooth_low_energy_event_router.cc', 'browser/extensions/api/bluetooth_low_energy/bluetooth_low_energy_event_router.h', + 'browser/extensions/api/bluetooth_low_energy/bluetooth_low_energy_notify_session.cc', + 'browser/extensions/api/bluetooth_low_energy/bluetooth_low_energy_notify_session.h', 'browser/extensions/api/bluetooth_low_energy/utils.cc', 'browser/extensions/api/bluetooth_low_energy/utils.h', 'browser/extensions/api/bluetooth_socket/bluetooth_api_socket.h', diff --git a/chrome/common/extensions/api/bluetooth_low_energy.idl b/chrome/common/extensions/api/bluetooth_low_energy.idl index 64d537b..517a813 100644 --- a/chrome/common/extensions/api/bluetooth_low_energy.idl +++ b/chrome/common/extensions/api/bluetooth_low_energy.idl @@ -64,9 +64,8 @@ namespace bluetoothLowEnergy { DOMString? instanceId; // The currently cached characteristic value. This value gets updated when - // the value of the characteristic is read, written, or updated via a - // notification or indication. For local characteristics, this is the value - // that will be returned upon requests from remote peripherals by default. + // the value of the characteristic is read or updated via a notification + // or indication. ArrayBuffer? value; }; @@ -92,13 +91,11 @@ namespace bluetoothLowEnergy { DOMString? instanceId; // The currently cached descriptor value. This value gets updated when - // the value of the descriptor is read or written. For local descriptors, - // this is the value that will be returned upon requests from remote - // peripherals by default. + // the value of the descriptor is read. ArrayBuffer? value; }; - // The connection properties specified during a call to $ref:connect. + // The connection properties specified during a call to $(ref:connect). dictionary ConnectProperties { // Flag indicating whether a connection to the device is left open when the // event page of the application is unloaded (see <a @@ -107,6 +104,16 @@ namespace bluetoothLowEnergy { boolean persistent; }; + // Optional characteristic notification session properties specified during a + // call to $(ref:startCharacteristicNotifications). + dictionary NotificationProperties { + // Flag indicating whether the app should receive notifications when the + // event page of the application is unloaded (see <a + // href="http://developer.chrome.com/apps/app_lifecycle.html">Manage App + // Lifecycle</a>). The default value is <code>false</code>. + boolean persistent; + }; + callback CharacteristicCallback = void(Characteristic result); callback CharacteristicsCallback = void(Characteristic[] result); callback DescriptorCallback = void(Descriptor result); @@ -194,7 +201,7 @@ namespace bluetoothLowEnergy { DescriptorsCallback callback); // Retrieve the value of a specified characteristic from a remote - // peripheral. This function will fail if the characteristic is local. + // peripheral. // |characteristicId| : The instance ID of the GATT characteristic whose // value should be read from the remote device. // |callback| : Called with the Characteristic object whose value was @@ -204,7 +211,6 @@ namespace bluetoothLowEnergy { CharacteristicCallback callback); // Write the value of a specified characteristic from a remote peripheral. - // This function will fail if the characteristic is local. // |characteristicId| : The instance ID of the GATT characteristic whose // value should be written to. // |value| : The value that should be sent to the remote characteristic as @@ -214,8 +220,30 @@ namespace bluetoothLowEnergy { ArrayBuffer value, ResultCallback callback); + // Enable value notifications/indications from the specified characteristic. + // Once enabled, an application can listen to notifications using the + // $(ref:onCharacteristicValueChanged) event. + // |characteristicId| : The instance ID of the GATT characteristic that + // notifications should be enabled on. + // |properties| : Notification session properties (optional). + // |callback| : Called when the request has completed. + static void startCharacteristicNotifications( + DOMString characteristicId, + optional NotificationProperties properties, + ResultCallback callback); + + // Disable value notifications/indications from the specified + // characteristic. After a successful call, the application will stop + // receiving notifications/indications from this characteristic. + // |characteristicId| : The instance ID of the GATT characteristic on which + // this app's notification session should be stopped. + // |callback| : Called when the request has completed (optional). + static void stopCharacteristicNotifications( + DOMString characteristicId, + optional ResultCallback callback); + // Retrieve the value of a specified characteristic descriptor from a remote - // peripheral. This function will fail if the descriptor is local. + // peripheral. // |descriptorId| : The instance ID of the GATT characteristic descriptor // whose value should be read from the remote device. // |callback| : Called with the Descriptor object whose value was requested. @@ -225,7 +253,7 @@ namespace bluetoothLowEnergy { DescriptorCallback callback); // Write the value of a specified characteristic descriptor from a remote - // peripheral. This function will fail if the descriptor is local. + // peripheral. // |descriptorId| : The instance ID of the GATT characteristic descriptor // whose value should be written to. // |value| : The value that should be sent to the remote descriptor as part @@ -254,13 +282,16 @@ namespace bluetoothLowEnergy { static void onServiceRemoved(Service service); // Fired when the value of a remote GATT characteristic changes, either as - // a result of a read or write request, or a value change notification or - // indication. + // a result of a read request, or a value change notification/indication + // This event will only be sent if the app has enabled notifications by + // calling $(ref:startCharacteristicNotifications). // |characteristic| : The GATT characteristic whose value has changed. static void onCharacteristicValueChanged(Characteristic characteristic); // Fired when the value of a remote GATT characteristic descriptor changes, - // usually as a result of a read or write request. + // usually as a result of a read request. This event exists + // mostly for convenience and will always be sent after a successful + // call to $(ref:readDescriptorValue). // |descriptor| : The GATT characteristic descriptor whose value has // changed. static void onDescriptorValueChanged(Descriptor descriptor); diff --git a/chrome/test/data/extensions/api_test/bluetooth_low_energy/characteristic_value_changed/runtest.js b/chrome/test/data/extensions/api_test/bluetooth_low_energy/characteristic_value_changed/runtest.js index 86fdb22..254c517 100644 --- a/chrome/test/data/extensions/api_test/bluetooth_low_energy/characteristic_value_changed/runtest.js +++ b/chrome/test/data/extensions/api_test/bluetooth_low_energy/characteristic_value_changed/runtest.js @@ -3,6 +3,11 @@ // found in the LICENSE file. function testCharacteristicValueChanged() { + if (error) { + chrome.test.fail('Unexpected error: ' + error.message); + return; + } + chrome.test.assertEq(2, Object.keys(changedChrcs).length); chrome.test.assertEq(charId0, changedChrcs[charId0].instanceId); @@ -13,14 +18,35 @@ function testCharacteristicValueChanged() { var charId0 = 'char_id0'; var charId2 = 'char_id2'; +var error; + +var changedChrcs = {}; -var changedChrcs = {} +function sendReady() { + chrome.test.sendMessage('ready', function (message) { + chrome.test.runTests([testCharacteristicValueChanged]); + }); +} chrome.bluetoothLowEnergy.onCharacteristicValueChanged.addListener( function (chrc) { changedChrcs[chrc.instanceId] = chrc; }); -chrome.test.sendMessage('ready', function (message) { - chrome.test.runTests([testCharacteristicValueChanged]); +chrome.bluetoothLowEnergy.startCharacteristicNotifications( + charId0, + function () { + if (chrome.runtime.lastError) { + error = chrome.runtime.lastError; + sendReady(); + return; + } + + chrome.bluetoothLowEnergy.startCharacteristicNotifications( + charId2, + function () { + if (chrome.runtime.lastError) + error = chrome.runtime.lastError; + sendReady(); + }); }); diff --git a/chrome/test/data/extensions/api_test/bluetooth_low_energy/start_stop_notifications/manifest.json b/chrome/test/data/extensions/api_test/bluetooth_low_energy/start_stop_notifications/manifest.json new file mode 100644 index 0000000..fda6583 --- /dev/null +++ b/chrome/test/data/extensions/api_test/bluetooth_low_energy/start_stop_notifications/manifest.json @@ -0,0 +1,14 @@ +{ + "manifest_version": 2, + "name": "Test the Bluetooth LE API methods to start and stop notifications", + "version": "1.0", + "app": { + "background": { + "scripts": ["runtest.js"] + } + }, + "bluetooth": { + "low_energy": true, + "uuids": ["1234"] + } +} diff --git a/chrome/test/data/extensions/api_test/bluetooth_low_energy/start_stop_notifications/runtest.js b/chrome/test/data/extensions/api_test/bluetooth_low_energy/start_stop_notifications/runtest.js new file mode 100644 index 0000000..fad86d8 --- /dev/null +++ b/chrome/test/data/extensions/api_test/bluetooth_low_energy/start_stop_notifications/runtest.js @@ -0,0 +1,89 @@ +// Copyright 2014 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. + +function testStartStopNotifications() { + chrome.test.assertEq(1, Object.keys(changedChrcs).length); + chrome.test.assertEq(charId0, changedChrcs[charId0].instanceId); + chrome.test.succeed(); +} + +var errorAlreadyNotifying = 'Already notifying'; +var errorNotFound = 'Instance not found'; +var errorNotNotifying = 'Not notifying'; +var errorOperationFailed = 'Operation failed'; +var errorPermissionDenied = 'Permission denied'; + +var charId0 = 'char_id0'; +var charId1 = 'char_id1'; +var charId2 = 'char_id2'; + +var changedChrcs = {}; +var ble = chrome.bluetoothLowEnergy; +var start = ble.startCharacteristicNotifications; +var stop = ble.stopCharacteristicNotifications; + +function sendReady(errorMessage) { + chrome.test.sendMessage('ready', function (message) { + if (errorMessage) { + chrome.test.fail(errorMessage); + return; + } + + chrome.test.runTests([testStartStopNotifications]); + }); +} + +function expectError(expectedMessage) { + if (!chrome.runtime.lastError) { + sendReady('Expected error: ' + expectedMessage); + return; + } + + if (chrome.runtime.lastError.message != expectedMessage) { + sendReady('Expected error: ' + expectedMessage + ', got error: ' + + expectedMessage); + } +} + +function expectSuccess() { + if (chrome.runtime.lastError) { + sendReady('Unexpected error: ' + chrome.runtime.lastError.message); + } +} + +ble.onCharacteristicValueChanged.addListener(function (chrc) { + changedChrcs[chrc.instanceId] = chrc; +}); + +start('foo', function () { + expectError(errorNotFound); + start(charId2, function () { + expectError(errorPermissionDenied); + stop(charId0, function () { + expectError(errorNotNotifying); + start(charId0, function () { + expectError(errorOperationFailed); + start(charId0, function () { + expectSuccess(); + start(charId0, function () { + expectError(errorAlreadyNotifying); + start(charId1, function () { + expectSuccess(); + stop(charId1, function () { + expectSuccess(); + stop(charId1, function () { + expectError(errorNotNotifying); + stop(charId2, function () { + expectError(errorNotNotifying); + sendReady(undefined); + }); + }); + }); + }); + }); + }); + }); + }); + }); +}); diff --git a/extensions/browser/extension_function_histogram_value.h b/extensions/browser/extension_function_histogram_value.h index bb15481..2d87e95 100644 --- a/extensions/browser/extension_function_histogram_value.h +++ b/extensions/browser/extension_function_histogram_value.h @@ -879,6 +879,8 @@ enum HistogramValue { WEBVIEWINTERNAL_STOP, WEBVIEWINTERNAL_STOPFINDING, WEBVIEWINTERNAL_TERMINATE, + BLUETOOTHLOWENERGY_STARTCHARACTERISTICNOTIFICATIONS, + BLUETOOTHLOWENERGY_STOPCHARACTERISTICNOTIFICATIONS, // Last entry: Add new entries above and ensure to update // tools/metrics/histograms/histograms/histograms.xml. ENUM_BOUNDARY diff --git a/tools/metrics/histograms/histograms.xml b/tools/metrics/histograms/histograms.xml index e89ba70..31dbc37 100644 --- a/tools/metrics/histograms/histograms.xml +++ b/tools/metrics/histograms/histograms.xml @@ -38132,6 +38132,8 @@ Therefore, the affected-histogram name has to have at least one dot in it. <int value="818" label="WEBVIEWINTERNAL_STOP"/> <int value="819" label="WEBVIEWINTERNAL_STOPFINDING"/> <int value="820" label="WEBVIEWINTERNAL_TERMINATE"/> + <int value="821" label="BLUETOOTHLOWENERGY_STARTCHARACTERISTICNOTIFICATIONS"/> + <int value="822" label="BLUETOOTHLOWENERGY_STOPCHARACTERISTICNOTIFICATIONS"/> </enum> <enum name="ExtensionInstallCause" type="int"> |