summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--system/binder/Android.bp2
-rw-r--r--system/binder/android/bluetooth/IBluetoothVolumeControl.aidl39
-rw-r--r--system/bta/Android.bp45
-rw-r--r--system/bta/include/bta_vc_api.h40
-rw-r--r--system/bta/test/common/bta_gatt_api_mock.cc86
-rw-r--r--system/bta/test/common/bta_gatt_api_mock.h95
-rw-r--r--system/bta/test/common/bta_gatt_queue_mock.cc46
-rw-r--r--system/bta/test/common/bta_gatt_queue_mock.h49
-rw-r--r--system/bta/test/common/btm_api_mock.cc40
-rw-r--r--system/bta/test/common/btm_api_mock.h60
-rw-r--r--system/bta/vc/device.cc245
-rw-r--r--system/bta/vc/devices.h190
-rw-r--r--system/bta/vc/devices_test.cc470
-rw-r--r--system/bta/vc/types.h47
-rw-r--r--system/bta/vc/vc.cc573
-rw-r--r--system/bta/vc/vc_test.cc643
-rwxr-xr-xsystem/btif/Android.bp1
-rw-r--r--system/btif/BUILD.gn1
-rw-r--r--system/btif/src/bluetooth.cc7
-rw-r--r--system/btif/src/btif_dm.cc4
-rw-r--r--system/btif/src/btif_vc.cc125
-rw-r--r--system/include/hardware/bluetooth.h1
-rw-r--r--system/include/hardware/bt_vc.h76
23 files changed, 2884 insertions, 1 deletions
diff --git a/system/binder/Android.bp b/system/binder/Android.bp
index 2534bdecdd..ca53ff3540 100644
--- a/system/binder/Android.bp
+++ b/system/binder/Android.bp
@@ -28,6 +28,7 @@ cc_library_shared {
"android/bluetooth/IBluetoothProfileServiceConnection.aidl",
"android/bluetooth/IBluetoothHeadset.aidl",
"android/bluetooth/IBluetoothHearingAid.aidl",
+ "android/bluetooth/IBluetoothVolumeControl.aidl",
"android/bluetooth/IBluetoothHidHost.aidl",
"android/bluetooth/IBluetoothPan.aidl",
"android/bluetooth/IBluetoothManager.aidl",
@@ -102,6 +103,7 @@ filegroup {
"android/bluetooth/IBluetoothProfileServiceConnection.aidl",
"android/bluetooth/IBluetoothHeadset.aidl",
"android/bluetooth/IBluetoothHearingAid.aidl",
+ "android/bluetooth/IBluetoothVolumeControl.aidl",
"android/bluetooth/IBluetoothHidHost.aidl",
"android/bluetooth/IBluetoothLeAudio.aidl",
"android/bluetooth/IBluetoothPan.aidl",
diff --git a/system/binder/android/bluetooth/IBluetoothVolumeControl.aidl b/system/binder/android/bluetooth/IBluetoothVolumeControl.aidl
new file mode 100644
index 0000000000..66d3396563
--- /dev/null
+++ b/system/binder/android/bluetooth/IBluetoothVolumeControl.aidl
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2021 HIMSA II K/S - www.himsa.com.
+ * Represented by EHIMA - www.ehima.com
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.bluetooth;
+
+import android.bluetooth.BluetoothDevice;
+
+/**
+ * APIs for Bluetooth Volume Control service
+ *
+ * @hide
+ */
+interface IBluetoothVolumeControl {
+ /* Public API */
+ boolean connect(in BluetoothDevice device);
+ boolean disconnect(in BluetoothDevice device);
+ List<BluetoothDevice> getConnectedDevices();
+ List<BluetoothDevice> getDevicesMatchingConnectionStates(in int[] states);
+ int getConnectionState(in BluetoothDevice device);
+ boolean setConnectionPolicy(in BluetoothDevice device, int connectionPolicy);
+ int getConnectionPolicy(in BluetoothDevice device);
+
+ void setVolume(in BluetoothDevice device, int volume);
+ void setVolumeGroup(int group_id, int volume);
+}
diff --git a/system/bta/Android.bp b/system/bta/Android.bp
index bb06c7c37b..a11e63b296 100644
--- a/system/bta/Android.bp
+++ b/system/bta/Android.bp
@@ -80,6 +80,8 @@ cc_library_static {
"gatt/bta_gatts_utils.cc",
"gatt/database.cc",
"gatt/database_builder.cc",
+ "vc/device.cc",
+ "vc/vc.cc",
"hearing_aid/hearing_aid.cc",
"hearing_aid/hearing_aid_audio_source.cc",
"hf_client/bta_hf_client_act.cc",
@@ -255,3 +257,46 @@ cc_test {
],
cflags: ["-DBUILDCFG"],
}
+
+
+// bta unit tests for host
+// ========================================================
+cc_test {
+ name: "bluetooth_vc_test",
+ test_suites: ["device-tests"],
+ defaults: [
+ "fluoride_bta_defaults",
+ "clang_coverage_bin",
+ ],
+ host_supported: true,
+ include_dirs: [
+ "packages/modules/Bluetooth/system",
+ "packages/modules/Bluetooth/system/bta/include",
+ "packages/modules/Bluetooth/system/bta/test/common",
+ "packages/modules/Bluetooth/system/stack/include",
+ ],
+ srcs : [
+ "gatt/database.cc",
+ "gatt/database_builder.cc",
+ "test/common/bta_gatt_api_mock.cc",
+ "test/common/bta_gatt_queue_mock.cc",
+ "test/common/btm_api_mock.cc",
+ "vc/devices_test.cc",
+ "vc/device.cc",
+ "vc/vc.cc",
+ "vc/vc_test.cc",
+ ],
+ shared_libs: [
+ "libprotobuf-cpp-lite",
+ "libcrypto",
+ ],
+ static_libs : [
+ "crypto_toolbox_for_tests",
+ "libgmock",
+ "libbt-common",
+ "libbt-protos-lite",
+ ],
+ sanitize: {
+ cfi: false,
+ },
+}
diff --git a/system/bta/include/bta_vc_api.h b/system/bta/include/bta_vc_api.h
new file mode 100644
index 0000000000..66a47cd113
--- /dev/null
+++ b/system/bta/include/bta_vc_api.h
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2021 HIMSA II K/S - www.himsa.com.
+ * Represented by EHIMA - www.ehima.com
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <hardware/bt_vc.h>
+
+class VolumeControl {
+ public:
+ virtual ~VolumeControl() = default;
+
+ static void Initialize(bluetooth::vc::VolumeControlCallbacks* callbacks);
+ static void CleanUp();
+ static VolumeControl* Get();
+ static void DebugDump(int fd);
+
+ static void AddFromStorage(const RawAddress& address, bool auto_connect);
+
+ static bool IsVolumeControlRunning();
+
+ /* Volume Control Server (VCS) */
+ virtual void Connect(const RawAddress& address) = 0;
+ virtual void Disconnect(const RawAddress& address) = 0;
+ virtual void SetVolume(std::variant<RawAddress, int> addr_or_group_id,
+ uint8_t volume) = 0;
+};
diff --git a/system/bta/test/common/bta_gatt_api_mock.cc b/system/bta/test/common/bta_gatt_api_mock.cc
new file mode 100644
index 0000000000..628c4d51c9
--- /dev/null
+++ b/system/bta/test/common/bta_gatt_api_mock.cc
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2021 HIMSA II K/S - www.himsa.com.
+ * Represented by EHIMA - www.ehima.com
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "bta_gatt_api_mock.h"
+
+static gatt::MockBtaGattInterface* gatt_interface = nullptr;
+
+void gatt::SetMockBtaGattInterface(
+ MockBtaGattInterface* mock_bta_gatt_interface) {
+ gatt_interface = mock_bta_gatt_interface;
+}
+
+void BTA_GATTC_AppRegister(tBTA_GATTC_CBACK* p_client_cb,
+ BtaAppRegisterCallback cb, bool eatt_support) {
+ gatt_interface->AppRegister(p_client_cb, cb, eatt_support);
+}
+
+void BTA_GATTC_AppDeregister(tGATT_IF client_if) {
+ gatt_interface->AppDeregister(client_if);
+}
+
+void BTA_GATTC_Open(tGATT_IF client_if, const RawAddress& remote_bda,
+ bool is_direct, tBT_TRANSPORT transport,
+ bool opportunistic) {
+ gatt_interface->Open(client_if, remote_bda, is_direct, transport,
+ opportunistic);
+}
+
+void BTA_GATTC_Open(tGATT_IF client_if, const RawAddress& remote_bda,
+ bool is_direct, bool opportunistic) {
+ gatt_interface->Open(client_if, remote_bda, is_direct, opportunistic);
+}
+
+void BTA_GATTC_CancelOpen(tGATT_IF client_if, const RawAddress& remote_bda,
+ bool is_direct) {
+ gatt_interface->CancelOpen(client_if, remote_bda, is_direct);
+}
+
+void BTA_GATTC_Close(uint16_t conn_id) { gatt_interface->Close(conn_id); }
+
+void BTA_GATTC_ServiceSearchRequest(uint16_t conn_id,
+ const bluetooth::Uuid* p_srvc_uuid) {
+ gatt_interface->ServiceSearchRequest(conn_id, p_srvc_uuid);
+}
+
+const std::list<gatt::Service>* BTA_GATTC_GetServices(uint16_t conn_id) {
+ return gatt_interface->GetServices(conn_id);
+}
+
+const gatt::Characteristic* BTA_GATTC_GetCharacteristic(uint16_t conn_id,
+ uint16_t handle) {
+ return gatt_interface->GetCharacteristic(conn_id, handle);
+}
+
+const gatt::Service* BTA_GATTC_GetOwningService(uint16_t conn_id,
+ uint16_t handle) {
+ return gatt_interface->GetOwningService(conn_id, handle);
+}
+
+tGATT_STATUS BTA_GATTC_RegisterForNotifications(tGATT_IF client_if,
+ const RawAddress& remote_bda,
+ uint16_t handle) {
+ return gatt_interface->RegisterForNotifications(client_if, remote_bda,
+ handle);
+}
+
+tGATT_STATUS BTA_GATTC_DeregisterForNotifications(tGATT_IF client_if,
+ const RawAddress& remote_bda,
+ uint16_t handle) {
+ return gatt_interface->DeregisterForNotifications(client_if, remote_bda,
+ handle);
+}
diff --git a/system/bta/test/common/bta_gatt_api_mock.h b/system/bta/test/common/bta_gatt_api_mock.h
new file mode 100644
index 0000000000..3ddd6da76c
--- /dev/null
+++ b/system/bta/test/common/bta_gatt_api_mock.h
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2021 HIMSA II K/S - www.himsa.com.
+ * Represented by EHIMA - www.ehima.com
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#pragma once
+
+#include <base/callback.h>
+#include <gmock/gmock.h>
+
+#include "bta_gatt_api.h"
+
+namespace gatt {
+
+class BtaGattInterface {
+ public:
+ virtual void AppRegister(tBTA_GATTC_CBACK* p_client_cb,
+ BtaAppRegisterCallback cb, bool eatt_support) = 0;
+ virtual void AppDeregister(tGATT_IF client_if) = 0;
+ virtual void Open(tGATT_IF client_if, const RawAddress& remote_bda,
+ bool is_direct, tBT_TRANSPORT transport,
+ bool opportunistic) = 0;
+ virtual void Open(tGATT_IF client_if, const RawAddress& remote_bda,
+ bool is_direct, bool opportunistic) = 0;
+ virtual void CancelOpen(tGATT_IF client_if, const RawAddress& remote_bda,
+ bool is_direct) = 0;
+ virtual void Close(uint16_t conn_id) = 0;
+ virtual void ServiceSearchRequest(uint16_t conn_id,
+ const bluetooth::Uuid* p_srvc_uuid) = 0;
+ virtual const std::list<Service>* GetServices(uint16_t conn_id) = 0;
+ virtual const Characteristic* GetCharacteristic(uint16_t conn_id,
+ uint16_t handle) = 0;
+ virtual const Service* GetOwningService(uint16_t conn_id,
+ uint16_t handle) = 0;
+ virtual tGATT_STATUS RegisterForNotifications(tGATT_IF client_if,
+ const RawAddress& remote_bda,
+ uint16_t handle) = 0;
+ virtual tGATT_STATUS DeregisterForNotifications(tGATT_IF client_if,
+ const RawAddress& remote_bda,
+ uint16_t handle) = 0;
+ virtual ~BtaGattInterface() = default;
+};
+
+class MockBtaGattInterface : public BtaGattInterface {
+ public:
+ MOCK_METHOD((void), AppRegister,
+ (tBTA_GATTC_CBACK * p_client_cb, BtaAppRegisterCallback cb,
+ bool eatt_support),
+ (override));
+ MOCK_METHOD((void), AppDeregister, (tGATT_IF client_if), (override));
+ MOCK_METHOD((void), Open,
+ (tGATT_IF client_if, const RawAddress& remote_bda, bool is_direct,
+ tBT_TRANSPORT transport, bool opportunistic));
+ MOCK_METHOD((void), Open,
+ (tGATT_IF client_if, const RawAddress& remote_bda, bool is_direct,
+ bool opportunistic));
+ MOCK_METHOD((void), CancelOpen,
+ (tGATT_IF client_if, const RawAddress& remote_bda,
+ bool is_direct));
+ MOCK_METHOD((void), Close, (uint16_t conn_id));
+ MOCK_METHOD((void), ServiceSearchRequest,
+ (uint16_t conn_id, const bluetooth::Uuid* p_srvc_uuid));
+ MOCK_METHOD((std::list<Service>*), GetServices, (uint16_t conn_id));
+ MOCK_METHOD((const Characteristic*), GetCharacteristic,
+ (uint16_t conn_id, uint16_t handle));
+ MOCK_METHOD((const Service*), GetOwningService,
+ (uint16_t conn_id, uint16_t handle));
+ MOCK_METHOD((tGATT_STATUS), RegisterForNotifications,
+ (tGATT_IF client_if, const RawAddress& remote_bda,
+ uint16_t handle));
+ MOCK_METHOD((tGATT_STATUS), DeregisterForNotifications,
+ (tGATT_IF client_if, const RawAddress& remote_bda,
+ uint16_t handle));
+};
+
+/**
+ * Set the {@link MockBtaGattInterface} for testing
+ *
+ * @param mock_bta_gatt_interface pointer to mock bta gatt interface,
+ * could be null
+ */
+void SetMockBtaGattInterface(MockBtaGattInterface* mock_bta_gatt_interface);
+
+} // namespace gatt
diff --git a/system/bta/test/common/bta_gatt_queue_mock.cc b/system/bta/test/common/bta_gatt_queue_mock.cc
new file mode 100644
index 0000000000..737b341c3b
--- /dev/null
+++ b/system/bta/test/common/bta_gatt_queue_mock.cc
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2021 HIMSA II K/S - www.himsa.com.
+ * Represented by EHIMA - www.ehima.com
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "bta_gatt_queue_mock.h"
+
+static gatt::MockBtaGattQueue* gatt_queue = nullptr;
+
+void gatt::SetMockBtaGattQueue(MockBtaGattQueue* mock_bta_gatt_queue) {
+ gatt_queue = mock_bta_gatt_queue;
+}
+
+void BtaGattQueue::Clean(uint16_t conn_id) { gatt_queue->Clean(conn_id); }
+
+void BtaGattQueue::ReadCharacteristic(uint16_t conn_id, uint16_t handle,
+ GATT_READ_OP_CB cb, void* cb_data) {
+ gatt_queue->ReadCharacteristic(conn_id, handle, cb, cb_data);
+}
+
+void BtaGattQueue::WriteCharacteristic(uint16_t conn_id, uint16_t handle,
+ std::vector<uint8_t> value,
+ tGATT_WRITE_TYPE write_type,
+ GATT_WRITE_OP_CB cb, void* cb_data) {
+ gatt_queue->WriteCharacteristic(conn_id, handle, value, write_type, cb,
+ cb_data);
+}
+
+void BtaGattQueue::WriteDescriptor(uint16_t conn_id, uint16_t handle,
+ std::vector<uint8_t> value,
+ tGATT_WRITE_TYPE write_type,
+ GATT_WRITE_OP_CB cb, void* cb_data) {
+ gatt_queue->WriteDescriptor(conn_id, handle, value, write_type, cb, cb_data);
+}
diff --git a/system/bta/test/common/bta_gatt_queue_mock.h b/system/bta/test/common/bta_gatt_queue_mock.h
new file mode 100644
index 0000000000..127bdc8b73
--- /dev/null
+++ b/system/bta/test/common/bta_gatt_queue_mock.h
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2021 HIMSA II K/S - www.himsa.com.
+ * Represented by EHIMA - www.ehima.com
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <gmock/gmock.h>
+
+#include "bta_gatt_queue.h"
+
+namespace gatt {
+
+class MockBtaGattQueue {
+ public:
+ MOCK_METHOD((void), Clean, (uint16_t conn_id));
+ MOCK_METHOD((void), ReadCharacteristic,
+ (uint16_t conn_id, uint16_t handle, GATT_READ_OP_CB cb,
+ void* cb_data));
+ MOCK_METHOD((void), WriteCharacteristic,
+ (uint16_t conn_id, uint16_t handle, std::vector<uint8_t> value,
+ tGATT_WRITE_TYPE write_type, GATT_WRITE_OP_CB cb,
+ void* cb_data));
+ MOCK_METHOD((void), WriteDescriptor,
+ (uint16_t conn_id, uint16_t handle, std::vector<uint8_t> value,
+ tGATT_WRITE_TYPE write_type, GATT_WRITE_OP_CB cb,
+ void* cb_data));
+};
+
+/**
+ * Set the {@link MockBtaGattQueue} for testing
+ *
+ * @param mock_bta_gatt_queue pointer to mock bta gatt queue, could be null
+ */
+void SetMockBtaGattQueue(MockBtaGattQueue* mock_bta_gatt_queue);
+
+} // namespace gatt
diff --git a/system/bta/test/common/btm_api_mock.cc b/system/bta/test/common/btm_api_mock.cc
new file mode 100644
index 0000000000..88d511a2de
--- /dev/null
+++ b/system/bta/test/common/btm_api_mock.cc
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2021 HIMSA II K/S - www.himsa.com.
+ * Represented by EHIMA - www.ehima.com
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "btm_api_mock.h"
+
+static bluetooth::manager::MockBtmInterface* btm_interface = nullptr;
+
+void bluetooth::manager::SetMockBtmInterface(
+ MockBtmInterface* mock_btm_interface) {
+ btm_interface = mock_btm_interface;
+}
+
+bool BTM_GetSecurityFlagsByTransport(const RawAddress& bd_addr,
+ uint8_t* p_sec_flags,
+ tBT_TRANSPORT transport) {
+ return btm_interface->GetSecurityFlagsByTransport(bd_addr, p_sec_flags,
+ transport);
+}
+
+tBTM_STATUS BTM_SetEncryption(const RawAddress& bd_addr,
+ tBT_TRANSPORT transport,
+ tBTM_SEC_CALLBACK* p_callback, void* p_ref_data,
+ tBTM_BLE_SEC_ACT sec_act) {
+ return btm_interface->SetEncryption(bd_addr, transport, p_callback,
+ p_ref_data, sec_act);
+}
diff --git a/system/bta/test/common/btm_api_mock.h b/system/bta/test/common/btm_api_mock.h
new file mode 100644
index 0000000000..d337b85f50
--- /dev/null
+++ b/system/bta/test/common/btm_api_mock.h
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2021 HIMSA II K/S - www.himsa.com.
+ * Represented by EHIMA - www.ehima.com
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#pragma once
+
+#include <gmock/gmock.h>
+
+#include "btm_api.h"
+
+namespace bluetooth {
+namespace manager {
+
+class BtmInterface {
+ public:
+ virtual bool GetSecurityFlagsByTransport(const RawAddress& bd_addr,
+ uint8_t* p_sec_flags,
+ tBT_TRANSPORT transport) = 0;
+ virtual tBTM_STATUS SetEncryption(const RawAddress& bd_addr,
+ tBT_TRANSPORT transport,
+ tBTM_SEC_CALLBACK* p_callback,
+ void* p_ref_data,
+ tBTM_BLE_SEC_ACT sec_act) = 0;
+ virtual ~BtmInterface() = default;
+};
+
+class MockBtmInterface : public BtmInterface {
+ public:
+ MOCK_METHOD((bool), GetSecurityFlagsByTransport,
+ (const RawAddress& bd_addr, uint8_t* p_sec_flags,
+ tBT_TRANSPORT transport),
+ (override));
+ MOCK_METHOD((tBTM_STATUS), SetEncryption,
+ (const RawAddress& bd_addr, tBT_TRANSPORT transport,
+ tBTM_SEC_CALLBACK* p_callback, void* p_ref_data,
+ tBTM_BLE_SEC_ACT sec_act),
+ (override));
+};
+
+/**
+ * Set the {@link MockBtmInterface} for testing
+ *
+ * @param mock_btm_interface pointer to mock btm interface, could be null
+ */
+void SetMockBtmInterface(MockBtmInterface* mock_btm_interface);
+
+} // namespace manager
+} // namespace bluetooth
diff --git a/system/bta/vc/device.cc b/system/bta/vc/device.cc
new file mode 100644
index 0000000000..9e059e7819
--- /dev/null
+++ b/system/bta/vc/device.cc
@@ -0,0 +1,245 @@
+/*
+ * Copyright 2020 HIMSA II K/S - www.himsa.com.
+ * Represented by EHIMA - www.ehima.com
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <map>
+#include <vector>
+
+#include "bta_gatt_api.h"
+#include "bta_gatt_queue.h"
+#include "devices.h"
+#include "gatt_api.h"
+#include "stack/btm/btm_sec.h"
+
+using namespace bluetooth::vc::internal;
+
+void VolumeControlDevice::Disconnect(tGATT_IF gatt_if) {
+ LOG(INFO) << __func__ << ": " << this->ToString();
+
+ if (IsConnected()) {
+ if (volume_state_handle != 0)
+ BTA_GATTC_DeregisterForNotifications(gatt_if, address,
+ volume_state_handle);
+
+ if (volume_flags_handle != 0)
+ BTA_GATTC_DeregisterForNotifications(gatt_if, address,
+ volume_flags_handle);
+
+ BtaGattQueue::Clean(connection_id);
+ BTA_GATTC_Close(connection_id);
+ connection_id = GATT_INVALID_CONN_ID;
+ } else {
+ BTA_GATTC_CancelOpen(gatt_if, address, false);
+ }
+
+ device_ready = false;
+ handles_pending.clear();
+}
+
+/*
+ * Find the handle for the client characteristics configuration of a given
+ * characteristics
+ */
+uint16_t VolumeControlDevice::find_ccc_handle(uint16_t chrc_handle) {
+ const gatt::Characteristic* p_char =
+ BTA_GATTC_GetCharacteristic(connection_id, chrc_handle);
+ if (!p_char) {
+ LOG(WARNING) << __func__ << ": no such handle=" << loghex(chrc_handle);
+ return 0;
+ }
+
+ for (const gatt::Descriptor& desc : p_char->descriptors) {
+ if (desc.uuid == Uuid::From16Bit(GATT_UUID_CHAR_CLIENT_CONFIG))
+ return desc.handle;
+ }
+
+ return 0;
+}
+
+bool VolumeControlDevice::set_volume_control_service_handles(
+ const gatt::Service& service) {
+ uint16_t state_handle = 0, state_ccc_handle = 0, control_point_handle = 0,
+ flags_handle = 0, flags_ccc_handle = 0;
+
+ for (const gatt::Characteristic& chrc : service.characteristics) {
+ if (chrc.uuid == kVolumeControlStateUuid) {
+ state_handle = chrc.value_handle;
+ state_ccc_handle = find_ccc_handle(chrc.value_handle);
+ } else if (chrc.uuid == kVolumeControlPointUuid) {
+ control_point_handle = chrc.value_handle;
+ } else if (chrc.uuid == kVolumeFlagsUuid) {
+ flags_handle = chrc.value_handle;
+ flags_ccc_handle = find_ccc_handle(chrc.value_handle);
+ } else {
+ LOG(WARNING) << __func__ << ": unknown characteristic=" << chrc.uuid;
+ }
+ }
+
+ // Validate service handles
+ if (GATT_HANDLE_IS_VALID(state_handle) &&
+ GATT_HANDLE_IS_VALID(state_ccc_handle) &&
+ GATT_HANDLE_IS_VALID(control_point_handle) &&
+ GATT_HANDLE_IS_VALID(flags_handle)
+ /* volume_flags_ccc_handle is optional */) {
+ volume_state_handle = state_handle;
+ volume_state_ccc_handle = state_ccc_handle;
+ volume_control_point_handle = control_point_handle;
+ volume_flags_handle = flags_handle;
+ volume_flags_ccc_handle = flags_ccc_handle;
+ return true;
+ }
+
+ return false;
+}
+
+bool VolumeControlDevice::UpdateHandles(void) {
+ ResetHandles();
+
+ bool vcs_found = false;
+ const std::list<gatt::Service>* services =
+ BTA_GATTC_GetServices(connection_id);
+ if (services == nullptr) {
+ LOG(ERROR) << "No services found";
+ return false;
+ }
+
+ for (auto const& service : *services) {
+ if (service.uuid == kVolumeControlUuid) {
+ LOG(INFO) << "Found VCS, handle=" << loghex(service.handle);
+ vcs_found = set_volume_control_service_handles(service);
+ if (!vcs_found) break;
+ }
+ }
+
+ return vcs_found;
+}
+
+void VolumeControlDevice::ResetHandles(void) {
+ device_ready = false;
+
+ // the handles are not valid, so discard pending GATT operations
+ BtaGattQueue::Clean(connection_id);
+
+ volume_state_handle = 0;
+ volume_state_ccc_handle = 0;
+ volume_control_point_handle = 0;
+ volume_flags_handle = 0;
+ volume_flags_ccc_handle = 0;
+}
+
+void VolumeControlDevice::ControlPointOperation(uint8_t opcode,
+ const std::vector<uint8_t>* arg,
+ GATT_WRITE_OP_CB cb,
+ void* cb_data) {
+ std::vector<uint8_t> set_value({opcode, change_counter});
+ if (arg != nullptr)
+ set_value.insert(set_value.end(), (*arg).begin(), (*arg).end());
+
+ BtaGattQueue::WriteCharacteristic(connection_id, volume_control_point_handle,
+ set_value, GATT_WRITE, cb, cb_data);
+}
+
+bool VolumeControlDevice::subscribe_for_notifications(tGATT_IF gatt_if,
+ uint16_t handle,
+ uint16_t ccc_handle,
+ GATT_WRITE_OP_CB cb) {
+ tGATT_STATUS status =
+ BTA_GATTC_RegisterForNotifications(gatt_if, address, handle);
+ if (status != GATT_SUCCESS) {
+ LOG(ERROR) << __func__ << ": failed, status=" << loghex(+status);
+ return false;
+ }
+
+ std::vector<uint8_t> value(2);
+ uint8_t* ptr = value.data();
+ UINT16_TO_STREAM(ptr, GATT_CHAR_CLIENT_CONFIG_NOTIFICATION);
+ BtaGattQueue::WriteDescriptor(connection_id, ccc_handle, std::move(value),
+ GATT_WRITE, cb, nullptr);
+
+ return true;
+}
+
+/**
+ * Enqueue GATT requests that are required by the Volume Control to be
+ * functional. This includes State characteristics read and subscription.
+ * Those characteristics contain the change counter needed to send any request
+ * via Control Point. Once completed successfully, the device can be stored
+ * and reported as connected. In each case we subscribe first to be sure we do
+ * not miss any value change.
+ */
+bool VolumeControlDevice::EnqueueInitialRequests(
+ tGATT_IF gatt_if, GATT_READ_OP_CB chrc_read_cb,
+ GATT_WRITE_OP_CB cccd_write_cb) {
+ handles_pending.clear();
+ handles_pending.insert(volume_state_handle);
+ handles_pending.insert(volume_state_ccc_handle);
+ if (!subscribe_for_notifications(gatt_if, volume_state_handle,
+ volume_state_ccc_handle, cccd_write_cb)) {
+ return false;
+ }
+
+ BtaGattQueue::ReadCharacteristic(connection_id, volume_state_handle,
+ chrc_read_cb, nullptr);
+
+ return true;
+}
+
+/**
+ * Enqueue the remaining requests. Those are not so crucial and can be done
+ * once Volume Control instance indicates it's readiness to profile.
+ * This includes characteristics read and subscription.
+ * In each case we subscribe first to be sure we do not miss any value change.
+ */
+void VolumeControlDevice::EnqueueRemainingRequests(
+ tGATT_IF gatt_if, GATT_READ_OP_CB chrc_read_cb,
+ GATT_WRITE_OP_CB cccd_write_cb) {
+ std::map<uint16_t, uint16_t> handle_pairs{
+ {volume_flags_handle, volume_flags_ccc_handle},
+ };
+
+ for (auto const& handles : handle_pairs) {
+ if (GATT_HANDLE_IS_VALID(handles.second)) {
+ subscribe_for_notifications(gatt_if, handles.first, handles.second,
+ cccd_write_cb);
+ }
+
+ BtaGattQueue::ReadCharacteristic(connection_id, handles.first, chrc_read_cb,
+ nullptr);
+ }
+}
+
+bool VolumeControlDevice::VerifyReady(uint16_t handle) {
+ handles_pending.erase(handle);
+ device_ready = handles_pending.size() == 0;
+ return device_ready;
+}
+
+bool VolumeControlDevice::IsEncryptionEnabled() {
+ uint8_t sec_flag = 0;
+ bool device_found =
+ BTM_GetSecurityFlagsByTransport(address, &sec_flag, BT_TRANSPORT_LE);
+ LOG(INFO) << __func__ << ": found=" << static_cast<int>(device_found)
+ << " sec_flag=" << loghex(sec_flag);
+ return device_found && (sec_flag & BTM_SEC_FLAG_ENCRYPTED);
+}
+
+bool VolumeControlDevice::EnableEncryption(tBTM_SEC_CALLBACK* callback) {
+ int result = BTM_SetEncryption(address, BT_TRANSPORT_LE, callback, nullptr,
+ BTM_BLE_SEC_ENCRYPT);
+ LOG(INFO) << __func__ << ": result=" << +result;
+ // TODO: should we care about the result??
+ return true;
+}
diff --git a/system/bta/vc/devices.h b/system/bta/vc/devices.h
new file mode 100644
index 0000000000..7a496ad8f1
--- /dev/null
+++ b/system/bta/vc/devices.h
@@ -0,0 +1,190 @@
+/*
+ * Copyright 2021 HIMSA II K/S - www.himsa.com.
+ * Represented by EHIMA - www.ehima.com
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <hardware/bt_vc.h>
+
+#include <unordered_set>
+#include <vector>
+
+#include "bta_gatt_api.h"
+#include "types.h"
+
+namespace bluetooth {
+namespace vc {
+namespace internal {
+
+class VolumeControlDevice {
+ public:
+ RawAddress address;
+ /* This is true only during first connection to profile, until we store the
+ * device
+ */
+ bool first_connection;
+
+ /* we are making active attempt to connect to this device, 'direct connect'.
+ * This is true only during initial phase of first connection. */
+ bool connecting_actively;
+
+ bool service_changed_rcvd;
+
+ uint8_t volume;
+ uint8_t change_counter;
+ bool mute;
+ uint8_t flags;
+
+ uint16_t connection_id;
+
+ /* Volume Control Service */
+ uint16_t volume_state_handle;
+ uint16_t volume_state_ccc_handle;
+ uint16_t volume_control_point_handle;
+ uint16_t volume_flags_handle;
+ uint16_t volume_flags_ccc_handle;
+
+ bool device_ready; /* Set when device read server status and registgered for
+ notifications */
+
+ VolumeControlDevice(const RawAddress& address, bool first_connection)
+ : address(address),
+ first_connection(first_connection),
+ connecting_actively(first_connection),
+ service_changed_rcvd(false),
+ volume(0),
+ change_counter(0),
+ mute(false),
+ connection_id(GATT_INVALID_CONN_ID),
+ volume_state_handle(0),
+ volume_state_ccc_handle(0),
+ volume_control_point_handle(0),
+ volume_flags_handle(0),
+ volume_flags_ccc_handle(0),
+ device_ready(false) {}
+
+ ~VolumeControlDevice() = default;
+
+ inline std::string ToString() { return address.ToString(); }
+
+ void DebugDump(int fd) { dprintf(fd, "%s\n", this->ToString().c_str()); }
+
+ bool IsConnected() { return connection_id != GATT_INVALID_CONN_ID; }
+
+ void Disconnect(tGATT_IF gatt_if);
+
+ bool UpdateHandles(void);
+
+ void ResetHandles(void);
+
+ bool HasHandles(void) { return GATT_HANDLE_IS_VALID(volume_state_handle); }
+
+ void ControlPointOperation(uint8_t opcode, const std::vector<uint8_t>* arg,
+ GATT_WRITE_OP_CB cb, void* cb_data);
+ bool IsEncryptionEnabled();
+
+ bool EnableEncryption(tBTM_SEC_CALLBACK* callback);
+
+ bool EnqueueInitialRequests(tGATT_IF gatt_if, GATT_READ_OP_CB chrc_read_cb,
+ GATT_WRITE_OP_CB cccd_write_cb);
+ void EnqueueRemainingRequests(tGATT_IF gatt_if, GATT_READ_OP_CB chrc_read_cb,
+ GATT_WRITE_OP_CB cccd_write_cb);
+ bool VerifyReady(uint16_t handle);
+
+ private:
+ /*
+ * This is used to track the pending GATT operation handles. Once the list is
+ * empty the device is assumed ready and connected. We are doing it because we
+ * want to make sure all the required characteristics and descritors are
+ * available on server side.
+ */
+ std::unordered_set<uint16_t> handles_pending;
+
+ uint16_t find_ccc_handle(uint16_t chrc_handle);
+ bool set_volume_control_service_handles(const gatt::Service& service);
+ bool subscribe_for_notifications(tGATT_IF gatt_if, uint16_t handle,
+ uint16_t ccc_handle, GATT_WRITE_OP_CB cb);
+};
+
+class VolumeControlDevices {
+ public:
+ void Add(const RawAddress& address, bool first_connection) {
+ if (FindByAddress(address) != nullptr) return;
+
+ devices_.emplace_back(address, first_connection);
+ }
+
+ void Remove(const RawAddress& address) {
+ for (auto it = devices_.begin(); it != devices_.end(); it++) {
+ if (it->address == address) {
+ it = devices_.erase(it);
+ break;
+ }
+ }
+ }
+
+ VolumeControlDevice* FindByAddress(const RawAddress& address) {
+ auto iter = std::find_if(devices_.begin(), devices_.end(),
+ [&address](const VolumeControlDevice& device) {
+ return device.address == address;
+ });
+
+ return (iter == devices_.end()) ? nullptr : &(*iter);
+ }
+
+ VolumeControlDevice* FindByConnId(uint16_t connection_id) {
+ auto iter =
+ std::find_if(devices_.begin(), devices_.end(),
+ [&connection_id](const VolumeControlDevice& device) {
+ return device.connection_id == connection_id;
+ });
+
+ return (iter == devices_.end()) ? nullptr : &(*iter);
+ }
+
+ size_t Size() { return (devices_.size()); }
+
+ void Clear() { devices_.clear(); }
+
+ void DebugDump(int fd) {
+ for (auto& device : devices_) {
+ device.DebugDump(fd);
+ }
+ }
+
+ void Disconnect(tGATT_IF gatt_if) {
+ for (auto& device : devices_) {
+ device.Disconnect(gatt_if);
+ }
+ }
+
+ void ControlPointOperation(std::vector<RawAddress>& devices, uint8_t opcode,
+ const std::vector<uint8_t>* arg,
+ GATT_WRITE_OP_CB cb, void* cb_data) {
+ for (auto& addr : devices) {
+ VolumeControlDevice* device = FindByAddress(addr);
+ if (device && device->IsConnected())
+ device->ControlPointOperation(opcode, arg, cb, cb_data);
+ }
+ }
+
+ private:
+ std::vector<VolumeControlDevice> devices_;
+};
+
+} // namespace internal
+} // namespace vc
+} // namespace bluetooth
diff --git a/system/bta/vc/devices_test.cc b/system/bta/vc/devices_test.cc
new file mode 100644
index 0000000000..e1fb3d4ec7
--- /dev/null
+++ b/system/bta/vc/devices_test.cc
@@ -0,0 +1,470 @@
+/*
+ * Copyright 2021 HIMSA II K/S - www.himsa.com.
+ * Represented by EHIMA - www.ehima.com
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "devices.h"
+
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+#include <map>
+
+#include "bta_gatt_api_mock.h"
+#include "bta_gatt_queue_mock.h"
+#include "btm_api_mock.h"
+#include "gatt/database_builder.h"
+
+namespace bluetooth {
+namespace vc {
+namespace internal {
+
+using ::testing::_;
+using ::testing::DoAll;
+using ::testing::Invoke;
+using ::testing::Mock;
+using ::testing::Return;
+using ::testing::SetArgPointee;
+using ::testing::Test;
+
+RawAddress GetTestAddress(int index) {
+ CHECK_LT(index, UINT8_MAX);
+ RawAddress result = {
+ {0xC0, 0xDE, 0xC0, 0xDE, 0x00, static_cast<uint8_t>(index)}};
+ return result;
+}
+
+class VolumeControlDevicesTest : public ::testing::Test {
+ protected:
+ void SetUp() override {
+ devices_ = new VolumeControlDevices();
+ gatt::SetMockBtaGattInterface(&gatt_interface);
+ gatt::SetMockBtaGattQueue(&gatt_queue);
+ }
+
+ void TearDown() override {
+ gatt::SetMockBtaGattQueue(nullptr);
+ gatt::SetMockBtaGattInterface(nullptr);
+ delete devices_;
+ }
+
+ VolumeControlDevices* devices_ = nullptr;
+ gatt::MockBtaGattInterface gatt_interface;
+ gatt::MockBtaGattQueue gatt_queue;
+};
+
+TEST_F(VolumeControlDevicesTest, test_add) {
+ RawAddress test_address_0 = GetTestAddress(0);
+ ASSERT_EQ((size_t)0, devices_->Size());
+ devices_->Add(test_address_0, true);
+ ASSERT_EQ((size_t)1, devices_->Size());
+}
+
+TEST_F(VolumeControlDevicesTest, test_add_twice) {
+ RawAddress test_address_0 = GetTestAddress(0);
+ ASSERT_EQ((size_t)0, devices_->Size());
+ devices_->Add(test_address_0, true);
+ devices_->Add(test_address_0, true);
+ ASSERT_EQ((size_t)1, devices_->Size());
+}
+
+TEST_F(VolumeControlDevicesTest, test_remove) {
+ RawAddress test_address_0 = GetTestAddress(0);
+ RawAddress test_address_1 = GetTestAddress(1);
+ devices_->Add(test_address_0, true);
+ devices_->Add(test_address_1, true);
+ ASSERT_EQ((size_t)2, devices_->Size());
+ devices_->Remove(test_address_0);
+ ASSERT_EQ((size_t)1, devices_->Size());
+}
+
+TEST_F(VolumeControlDevicesTest, test_clear) {
+ RawAddress test_address_0 = GetTestAddress(0);
+ ASSERT_EQ((size_t)0, devices_->Size());
+ devices_->Add(test_address_0, true);
+ ASSERT_EQ((size_t)1, devices_->Size());
+ devices_->Clear();
+ ASSERT_EQ((size_t)0, devices_->Size());
+}
+
+TEST_F(VolumeControlDevicesTest, test_find_by_address) {
+ RawAddress test_address_0 = GetTestAddress(0);
+ RawAddress test_address_1 = GetTestAddress(1);
+ RawAddress test_address_2 = GetTestAddress(2);
+ devices_->Add(test_address_0, true);
+ devices_->Add(test_address_1, false);
+ devices_->Add(test_address_2, true);
+ VolumeControlDevice* device = devices_->FindByAddress(test_address_1);
+ ASSERT_NE(nullptr, device);
+ ASSERT_EQ(test_address_1, device->address);
+}
+
+TEST_F(VolumeControlDevicesTest, test_find_by_conn_id) {
+ RawAddress test_address_0 = GetTestAddress(0);
+ devices_->Add(test_address_0, true);
+ VolumeControlDevice* test_device = devices_->FindByAddress(test_address_0);
+ test_device->connection_id = 0x0005;
+ ASSERT_NE(nullptr, devices_->FindByConnId(test_device->connection_id));
+}
+
+TEST_F(VolumeControlDevicesTest, test_disconnect) {
+ RawAddress test_address_0 = GetTestAddress(0);
+ RawAddress test_address_1 = GetTestAddress(1);
+ devices_->Add(test_address_0, true);
+ devices_->Add(test_address_1, true);
+ VolumeControlDevice* test_device_0 = devices_->FindByAddress(test_address_0);
+ test_device_0->connection_id = 0x0005;
+ tGATT_IF gatt_if = 8;
+ EXPECT_CALL(gatt_interface, Close(test_device_0->connection_id));
+ EXPECT_CALL(gatt_interface, CancelOpen(gatt_if, test_address_1, _));
+ devices_->Disconnect(gatt_if);
+}
+
+TEST_F(VolumeControlDevicesTest, test_control_point_operation) {
+ uint8_t opcode = 50;
+ std::vector<RawAddress> devices;
+
+ for (int i = 5; i > 0; i--) {
+ RawAddress test_address = GetTestAddress(i);
+ devices.push_back(test_address);
+ uint8_t change_counter = 10 * i;
+ uint16_t control_point_handle = 0x0020 + i;
+ uint16_t connection_id = i;
+ devices_->Add(test_address, true);
+ VolumeControlDevice* device = devices_->FindByAddress(test_address);
+ device->connection_id = connection_id;
+ device->change_counter = change_counter;
+ device->volume_control_point_handle = control_point_handle;
+ std::vector<uint8_t> data_expected({opcode, change_counter});
+
+ EXPECT_CALL(gatt_queue,
+ WriteCharacteristic(connection_id, control_point_handle,
+ data_expected, GATT_WRITE, _, _));
+ }
+
+ const std::vector<uint8_t>* arg = nullptr;
+ GATT_WRITE_OP_CB cb = nullptr;
+ void* cb_data = nullptr;
+ devices_->ControlPointOperation(devices, opcode, arg, cb, cb_data);
+}
+
+TEST_F(VolumeControlDevicesTest, test_control_point_operation_args) {
+ uint8_t opcode = 60;
+ uint8_t arg_1 = 0x02;
+ uint8_t arg_2 = 0x05;
+ std::vector<RawAddress> devices;
+
+ for (int i = 5; i > 0; i--) {
+ RawAddress test_address = GetTestAddress(i);
+ devices.push_back(test_address);
+ uint8_t change_counter = 10 * i;
+ uint16_t control_point_handle = 0x0020 + i;
+ uint16_t connection_id = i;
+ devices_->Add(test_address, true);
+ VolumeControlDevice* device = devices_->FindByAddress(test_address);
+ device->connection_id = connection_id;
+ device->change_counter = change_counter;
+ device->volume_control_point_handle = control_point_handle;
+ std::vector<uint8_t> data_expected({opcode, change_counter, arg_1, arg_2});
+
+ EXPECT_CALL(gatt_queue,
+ WriteCharacteristic(connection_id, control_point_handle,
+ data_expected, GATT_WRITE, _, _));
+ }
+
+ std::vector<uint8_t> arg({arg_1, arg_2});
+ GATT_WRITE_OP_CB cb = nullptr;
+ void* cb_data = nullptr;
+ devices_->ControlPointOperation(devices, opcode, &arg, cb, cb_data);
+}
+
+TEST_F(VolumeControlDevicesTest, test_control_point_skip_not_connected) {
+ RawAddress test_address = GetTestAddress(1);
+ devices_->Add(test_address, true);
+ VolumeControlDevice* device = devices_->FindByAddress(test_address);
+ device->connection_id = GATT_INVALID_CONN_ID;
+ uint16_t control_point_handle = 0x0020;
+ device->volume_control_point_handle = control_point_handle;
+
+ EXPECT_CALL(gatt_queue,
+ WriteCharacteristic(_, control_point_handle, _, _, _, _))
+ .Times(0);
+
+ uint8_t opcode = 5;
+ std::vector<RawAddress> devices = {test_address};
+ const std::vector<uint8_t>* arg = nullptr;
+ GATT_WRITE_OP_CB cb = nullptr;
+ void* cb_data = nullptr;
+ devices_->ControlPointOperation(devices, opcode, arg, cb, cb_data);
+}
+
+class VolumeControlDeviceTest : public ::testing::Test {
+ protected:
+ void SetUp() override {
+ device = new VolumeControlDevice(GetTestAddress(1), true);
+ gatt::SetMockBtaGattInterface(&gatt_interface);
+ gatt::SetMockBtaGattQueue(&gatt_queue);
+ bluetooth::manager::SetMockBtmInterface(&btm_interface);
+
+ ON_CALL(gatt_interface, GetCharacteristic(_, _))
+ .WillByDefault(
+ Invoke([&](uint16_t conn_id,
+ uint16_t handle) -> const gatt::Characteristic* {
+ for (auto const& service : services) {
+ for (auto const& characteristic : service.characteristics) {
+ if (characteristic.value_handle == handle) {
+ return &characteristic;
+ }
+ }
+ }
+
+ return nullptr;
+ }));
+
+ ON_CALL(gatt_interface, GetOwningService(_, _))
+ .WillByDefault(Invoke(
+ [&](uint16_t conn_id, uint16_t handle) -> const gatt::Service* {
+ for (auto const& service : services) {
+ if (service.handle <= handle && service.end_handle >= handle) {
+ return &service;
+ }
+ }
+
+ return nullptr;
+ }));
+
+ ON_CALL(gatt_interface, GetServices(_)).WillByDefault(Return(&services));
+ }
+
+ void TearDown() override {
+ bluetooth::manager::SetMockBtmInterface(nullptr);
+ gatt::SetMockBtaGattQueue(nullptr);
+ gatt::SetMockBtaGattInterface(nullptr);
+ delete device;
+ }
+
+ /* sample database 1xVCS, 2xAICS, 2xVOCS */
+ void SetSampleDatabase1(void) {
+ gatt::DatabaseBuilder builder;
+ builder.AddService(0x0001, 0x0016, kVolumeControlUuid, true);
+ builder.AddCharacteristic(
+ 0x0010, 0x0011, kVolumeControlStateUuid,
+ GATT_CHAR_PROP_BIT_READ | GATT_CHAR_PROP_BIT_NOTIFY);
+ builder.AddDescriptor(0x0012,
+ Uuid::From16Bit(GATT_UUID_CHAR_CLIENT_CONFIG));
+ builder.AddCharacteristic(0x0013, 0x0014, kVolumeControlPointUuid,
+ GATT_CHAR_PROP_BIT_WRITE);
+ builder.AddCharacteristic(0x0015, 0x0016, kVolumeFlagsUuid,
+ GATT_CHAR_PROP_BIT_READ);
+ builder.AddService(0x00a0, 0x00a3,
+ Uuid::From16Bit(UUID_SERVCLASS_GATT_SERVER), true);
+ builder.AddCharacteristic(0x00a1, 0x00a2,
+ Uuid::From16Bit(GATT_UUID_GATT_SRV_CHGD),
+ GATT_CHAR_PROP_BIT_NOTIFY);
+ builder.AddDescriptor(0x00a3,
+ Uuid::From16Bit(GATT_UUID_CHAR_CLIENT_CONFIG));
+ services = builder.Build().Services();
+ ASSERT_EQ(true, device->UpdateHandles());
+ }
+
+ /* sample database no VCS */
+ void SetSampleDatabase2(void) {
+ gatt::DatabaseBuilder builder;
+ builder.AddService(0x0001, 0x0003, Uuid::From16Bit(0x1800), true);
+ builder.AddCharacteristic(0x0002, 0x0003, Uuid::From16Bit(0x2a00),
+ GATT_CHAR_PROP_BIT_READ);
+ services = builder.Build().Services();
+ ASSERT_EQ(false, device->UpdateHandles());
+ }
+
+ VolumeControlDevice* device = nullptr;
+ gatt::MockBtaGattInterface gatt_interface;
+ gatt::MockBtaGattQueue gatt_queue;
+ bluetooth::manager::MockBtmInterface btm_interface;
+ std::list<gatt::Service> services;
+};
+
+TEST_F(VolumeControlDeviceTest, test_service_volume_control_not_found) {
+ SetSampleDatabase2();
+ ASSERT_EQ(false, device->HasHandles());
+}
+
+TEST_F(VolumeControlDeviceTest, test_service_volume_control_incomplete) {
+ gatt::DatabaseBuilder builder;
+ builder.AddService(0x0001, 0x0006, kVolumeControlUuid, true);
+ builder.AddCharacteristic(
+ 0x0002, 0x0003, kVolumeControlStateUuid,
+ GATT_CHAR_PROP_BIT_READ | GATT_CHAR_PROP_BIT_NOTIFY);
+ builder.AddDescriptor(0x0004, Uuid::From16Bit(GATT_UUID_CHAR_CLIENT_CONFIG));
+ builder.AddCharacteristic(0x0005, 0x0006, kVolumeControlPointUuid,
+ GATT_CHAR_PROP_BIT_WRITE);
+ /* no Volume Control Flags characteristic */
+ services = builder.Build().Services();
+ ASSERT_EQ(false, device->UpdateHandles());
+ ASSERT_EQ(0x0000, device->volume_state_handle);
+ ASSERT_EQ(0x0000, device->volume_state_ccc_handle);
+ ASSERT_EQ(0x0000, device->volume_control_point_handle);
+ ASSERT_EQ(0x0000, device->volume_flags_handle);
+ ASSERT_EQ(0x0000, device->volume_flags_ccc_handle);
+ ASSERT_EQ(false, device->HasHandles());
+}
+
+TEST_F(VolumeControlDeviceTest, test_services_changed) {
+ SetSampleDatabase1();
+ ASSERT_NE(0, device->volume_state_handle);
+ ASSERT_NE(0, device->volume_control_point_handle);
+ ASSERT_NE(0, device->volume_flags_handle);
+ ASSERT_EQ(true, device->HasHandles());
+ SetSampleDatabase2();
+ ASSERT_EQ(0, device->volume_state_handle);
+ ASSERT_EQ(0, device->volume_control_point_handle);
+ ASSERT_EQ(0, device->volume_flags_handle);
+ ASSERT_EQ(false, device->HasHandles());
+}
+
+TEST_F(VolumeControlDeviceTest, test_enqueue_initial_requests) {
+ SetSampleDatabase1();
+
+ tGATT_IF gatt_if = 0x0001;
+ std::vector<uint8_t> register_for_notification_data({0x01, 0x00});
+
+ std::map<uint16_t, uint16_t> expected_to_read_write{
+ {0x0011, 0x0012} /* volume control state */};
+
+ for (auto const& handle_pair : expected_to_read_write) {
+ EXPECT_CALL(gatt_queue, ReadCharacteristic(_, handle_pair.first, _, _));
+ EXPECT_CALL(gatt_queue, WriteDescriptor(_, handle_pair.second,
+ register_for_notification_data,
+ GATT_WRITE, _, _));
+ EXPECT_CALL(gatt_interface,
+ RegisterForNotifications(gatt_if, _, handle_pair.first));
+ }
+
+ auto chrc_read_cb = [](uint16_t conn_id, tGATT_STATUS status, uint16_t handle,
+ uint16_t len, uint8_t* value, void* data) {};
+ auto cccd_write_cb = [](uint16_t conn_id, tGATT_STATUS status,
+ uint16_t handle, void* data) {};
+ ASSERT_EQ(true, device->EnqueueInitialRequests(gatt_if, chrc_read_cb,
+ cccd_write_cb));
+};
+
+TEST_F(VolumeControlDeviceTest, test_device_ready) {
+ SetSampleDatabase1();
+
+ // grab all the handles requested
+ std::vector<uint16_t> requested_handles;
+ ON_CALL(gatt_queue, WriteDescriptor(_, _, _, _, _, _))
+ .WillByDefault(Invoke(
+ [&requested_handles](
+ uint16_t conn_id, uint16_t handle, std::vector<uint8_t> value,
+ tGATT_WRITE_TYPE write_type, GATT_WRITE_OP_CB cb,
+ void* cb_data) -> void { requested_handles.push_back(handle); }));
+ ON_CALL(gatt_queue, ReadCharacteristic(_, _, _, _))
+ .WillByDefault(Invoke(
+ [&requested_handles](uint16_t conn_id, uint16_t handle,
+ GATT_READ_OP_CB cb, void* cb_data) -> void {
+ requested_handles.push_back(handle);
+ }));
+
+ auto chrc_read_cb = [](uint16_t conn_id, tGATT_STATUS status, uint16_t handle,
+ uint16_t len, uint8_t* value, void* data) {};
+ auto cccd_write_cb = [](uint16_t conn_id, tGATT_STATUS status,
+ uint16_t handle, void* data) {};
+ ASSERT_EQ(true, device->EnqueueInitialRequests(0x0001, chrc_read_cb,
+ cccd_write_cb));
+ ASSERT_NE((size_t)0, requested_handles.size());
+
+ // indicate non-pending requests
+ ASSERT_EQ(false, device->device_ready);
+ device->VerifyReady(0xffff);
+
+ for (uint16_t handle : requested_handles) {
+ ASSERT_EQ(false, device->device_ready);
+ device->VerifyReady(handle);
+ }
+
+ ASSERT_EQ(true, device->device_ready);
+}
+
+TEST_F(VolumeControlDeviceTest, test_enqueue_remaining_requests) {
+ SetSampleDatabase1();
+
+ tGATT_IF gatt_if = 0x0001;
+ std::vector<uint8_t> register_for_notification_data({0x01, 0x00});
+
+ std::vector<uint16_t> expected_to_read{0x0016 /* volume flags */};
+
+ std::map<uint16_t, uint16_t> expected_to_write_value_ccc_handle_map{};
+
+ for (uint16_t handle : expected_to_read) {
+ EXPECT_CALL(gatt_queue, ReadCharacteristic(_, handle, _, _));
+ }
+
+ for (auto const& handle_pair : expected_to_write_value_ccc_handle_map) {
+ EXPECT_CALL(gatt_queue, WriteDescriptor(_, handle_pair.second,
+ register_for_notification_data,
+ GATT_WRITE, _, _));
+ EXPECT_CALL(gatt_interface,
+ RegisterForNotifications(gatt_if, _, handle_pair.first));
+ }
+
+ auto chrc_read_cb = [](uint16_t conn_id, tGATT_STATUS status, uint16_t handle,
+ uint16_t len, uint8_t* value, void* data) {};
+ auto cccd_write_cb = [](uint16_t conn_id, tGATT_STATUS status,
+ uint16_t handle, void* data) {};
+ device->EnqueueRemainingRequests(gatt_if, chrc_read_cb, cccd_write_cb);
+}
+
+TEST_F(VolumeControlDeviceTest, test_check_link_encrypted) {
+ ON_CALL(btm_interface, GetSecurityFlagsByTransport(_, _, _))
+ .WillByDefault(
+ DoAll(SetArgPointee<1>(BTM_SEC_FLAG_ENCRYPTED), Return(true)));
+ ASSERT_EQ(true, device->IsEncryptionEnabled());
+
+ ON_CALL(btm_interface, GetSecurityFlagsByTransport(_, _, _))
+ .WillByDefault(DoAll(SetArgPointee<1>(0), Return(false)));
+ ASSERT_NE(true, device->IsEncryptionEnabled());
+
+ ON_CALL(btm_interface, GetSecurityFlagsByTransport(_, _, _))
+ .WillByDefault(DoAll(SetArgPointee<1>(0), Return(true)));
+ ASSERT_NE(true, device->IsEncryptionEnabled());
+}
+
+TEST_F(VolumeControlDeviceTest, test_control_point_operation) {
+ GATT_WRITE_OP_CB write_cb = [](uint16_t conn_id, tGATT_STATUS status,
+ uint16_t handle, void* data) {};
+ SetSampleDatabase1();
+ device->change_counter = 0x01;
+ std::vector<uint8_t> expected_data({0x03, 0x01});
+ EXPECT_CALL(gatt_queue, WriteCharacteristic(_, 0x0014, expected_data,
+ GATT_WRITE, write_cb, nullptr));
+ device->ControlPointOperation(0x03, nullptr, write_cb, nullptr);
+}
+
+TEST_F(VolumeControlDeviceTest, test_control_point_operation_arg) {
+ GATT_WRITE_OP_CB write_cb = [](uint16_t conn_id, tGATT_STATUS status,
+ uint16_t handle, void* data) {};
+ SetSampleDatabase1();
+ device->change_counter = 0x55;
+ std::vector<uint8_t> expected_data({0x01, 0x55, 0x02, 0x03});
+ EXPECT_CALL(gatt_queue, WriteCharacteristic(_, 0x0014, expected_data,
+ GATT_WRITE, write_cb, nullptr));
+ std::vector<uint8_t> arg({0x02, 0x03});
+ device->ControlPointOperation(0x01, &arg, write_cb, nullptr);
+}
+
+} // namespace internal
+} // namespace vc
+} // namespace bluetooth
diff --git a/system/bta/vc/types.h b/system/bta/vc/types.h
new file mode 100644
index 0000000000..808df9ffac
--- /dev/null
+++ b/system/bta/vc/types.h
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2020 HIMSA II K/S - www.himsa.com.
+ * Represented by EHIMA - www.ehima.com
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <queue>
+#include <vector>
+
+#include "raw_address.h"
+
+namespace bluetooth {
+namespace vc {
+namespace internal {
+
+/* clang-format off */
+/* Volume control point opcodes */
+static constexpr uint8_t kControlPointOpcodeVolumeDown = 0x00;
+static constexpr uint8_t kControlPointOpcodeVolumeUp = 0x01;
+static constexpr uint8_t kControlPointOpcodeUnmuteVolumeDown = 0x02;
+static constexpr uint8_t kControlPointOpcodeUnmuteVolumeUp = 0x03;
+static constexpr uint8_t kControlPointOpcodeSetAbsoluteVolume = 0x04;
+static constexpr uint8_t kControlPointOpcodeUnmute = 0x05;
+static constexpr uint8_t kControlPointOpcodeMute = 0x06;
+
+static const Uuid kVolumeControlUuid = Uuid::From16Bit(0x1844);
+static const Uuid kVolumeControlStateUuid = Uuid::From16Bit(0x2B7D);
+static const Uuid kVolumeControlPointUuid = Uuid::From16Bit(0x2B7E);
+static const Uuid kVolumeFlagsUuid = Uuid::From16Bit(0x2B7F);
+/* clang-format on */
+
+} // namespace internal
+} // namespace vc
+} // namespace bluetooth
diff --git a/system/bta/vc/vc.cc b/system/bta/vc/vc.cc
new file mode 100644
index 0000000000..97b8266e90
--- /dev/null
+++ b/system/bta/vc/vc.cc
@@ -0,0 +1,573 @@
+/*
+ * Copyright 2021 HIMSA II K/S - www.himsa.com.
+ * Represented by EHIMA - www.ehima.com
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <base/bind.h>
+#include <base/bind_helpers.h>
+#include <base/logging.h>
+#include <base/strings/string_number_conversions.h>
+#include <hardware/bt_vc.h>
+
+#include <string>
+#include <vector>
+
+#include "bta_gatt_api.h"
+#include "bta_gatt_queue.h"
+#include "bta_vc_api.h"
+#include "btif_storage.h"
+#include "devices.h"
+
+using base::Closure;
+using bluetooth::Uuid;
+using bluetooth::vc::ConnectionState;
+using namespace bluetooth::vc::internal;
+
+namespace {
+class VolumeControlImpl;
+VolumeControlImpl* instance;
+
+/**
+ * Overview:
+ *
+ * This is Volume Control Implementation class which realize Volume Control
+ * Profile (VCP)
+ *
+ * Each connected peer device supporting Volume Control Service (VCS) is on the
+ * list of devices (volume_control_devices_).
+ *
+ * Once all the mandatory characteristis for all the services are discovered,
+ * Fluoride calls ON_CONNECTED callback.
+ *
+ * It is assumed that whenever application changes general audio options in this
+ * profile e.g. Volume up/down, mute/unmute etc, profile configures all the
+ * devices which are active Le Audio devices.
+ *
+ *
+ */
+class VolumeControlImpl : public VolumeControl {
+ public:
+ ~VolumeControlImpl() override = default;
+
+ VolumeControlImpl(bluetooth::vc::VolumeControlCallbacks* callbacks)
+ : gatt_if_(0), callbacks_(callbacks) {
+ BTA_GATTC_AppRegister(
+ gattc_callback_static,
+ base::Bind([](uint8_t client_id, uint8_t status) {
+ if (status != GATT_SUCCESS) {
+ LOG(ERROR) << "Can't start Volume Control profile - no gatt "
+ "clients left!";
+ return;
+ }
+ instance->gatt_if_ = client_id;
+ }),
+ true);
+ }
+
+ void Connect(const RawAddress& address) override {
+ LOG(INFO) << __func__ << " " << address;
+
+ auto device = volume_control_devices_.FindByAddress(address);
+ if (!device) {
+ volume_control_devices_.Add(address, true);
+ } else {
+ device->connecting_actively = true;
+ }
+
+ BTA_GATTC_Open(gatt_if_, address, true, false);
+ }
+
+ void AddFromStorage(const RawAddress& address, bool auto_connect) {
+ LOG(INFO) << __func__ << " " << address
+ << ", auto_connect=" << auto_connect;
+
+ if (auto_connect) {
+ volume_control_devices_.Add(address, false);
+
+ /* Add device into BG connection to accept remote initiated connection */
+ BTA_GATTC_Open(gatt_if_, address, false, false);
+ }
+ }
+
+ void OnGattConnected(tGATT_STATUS status, uint16_t connection_id,
+ tGATT_IF /*client_if*/, RawAddress address,
+ tBT_TRANSPORT /*transport*/, uint16_t /*mtu*/) {
+ LOG(INFO) << __func__ << ": address=" << address
+ << ", connection_id=" << connection_id;
+
+ VolumeControlDevice* device =
+ volume_control_devices_.FindByAddress(address);
+ if (!device) {
+ LOG(ERROR) << __func__ << "Skipping unknown device, address=" << address;
+ return;
+ }
+
+ if (status != GATT_SUCCESS) {
+ LOG(INFO) << "Failed to connect to Volume Control device";
+ device_cleanup_helper(device, device->connecting_actively);
+ return;
+ }
+
+ device->connection_id = connection_id;
+
+ if (device->IsEncryptionEnabled()) {
+ OnEncryptionComplete(address, BTM_SUCCESS);
+ return;
+ }
+
+ if (!device->EnableEncryption(enc_callback_static)) {
+ device_cleanup_helper(device, device->connecting_actively);
+ }
+ }
+
+ void OnEncryptionComplete(const RawAddress& address, uint8_t success) {
+ VolumeControlDevice* device =
+ volume_control_devices_.FindByAddress(address);
+ if (!device) {
+ LOG(ERROR) << __func__ << "Skipping unknown device" << address;
+ return;
+ }
+
+ if (success != BTM_SUCCESS) {
+ LOG(ERROR) << "encryption failed "
+ << "status: " << int{success};
+ // If the encryption failed, do not remove the device.
+ // Disconnect only, since the Android will try to re-enable encryption
+ // after disconnection
+ device->Disconnect(gatt_if_);
+ if (device->connecting_actively)
+ callbacks_->OnConnectionState(ConnectionState::DISCONNECTED,
+ device->address);
+ return;
+ }
+
+ LOG(INFO) << __func__ << " " << address << "status: " << success;
+
+ if (device->HasHandles()) {
+ device->EnqueueInitialRequests(gatt_if_, chrc_read_callback_static,
+ OnGattWriteCccStatic);
+
+ } else {
+ device->first_connection = true;
+ BTA_GATTC_ServiceSearchRequest(device->connection_id,
+ &kVolumeControlUuid);
+ }
+ }
+
+ void OnServiceChangeEvent(const RawAddress& address) {
+ VolumeControlDevice* device =
+ volume_control_devices_.FindByAddress(address);
+ if (!device) {
+ LOG(ERROR) << __func__ << "Skipping unknown device" << address;
+ return;
+ }
+ LOG(INFO) << __func__ << ": address=" << address;
+ device->first_connection = true;
+ device->service_changed_rcvd = true;
+ BtaGattQueue::Clean(device->connection_id);
+ }
+
+ void OnServiceDiscDoneEvent(const RawAddress& address) {
+ VolumeControlDevice* device =
+ volume_control_devices_.FindByAddress(address);
+ if (!device) {
+ LOG(ERROR) << __func__ << "Skipping unknown device" << address;
+ return;
+ }
+
+ if (device->service_changed_rcvd)
+ BTA_GATTC_ServiceSearchRequest(device->connection_id,
+ &kVolumeControlUuid);
+ }
+
+ void OnServiceSearchComplete(uint16_t connection_id, tGATT_STATUS status) {
+ VolumeControlDevice* device =
+ volume_control_devices_.FindByConnId(connection_id);
+ if (!device) {
+ LOG(ERROR) << __func__ << "Skipping unknown device, connection_id="
+ << loghex(connection_id);
+ return;
+ }
+
+ /* Known device, nothing to do */
+ if (!device->first_connection) return;
+
+ if (status != GATT_SUCCESS) {
+ /* close connection and report service discovery complete with error */
+ LOG(ERROR) << "Service discovery failed";
+ device_cleanup_helper(device, device->first_connection);
+ return;
+ }
+
+ bool success = device->UpdateHandles();
+ if (!success) {
+ LOG(ERROR) << "Incomplete service database";
+ device_cleanup_helper(device, true);
+ return;
+ }
+
+ device->EnqueueInitialRequests(gatt_if_, chrc_read_callback_static,
+ OnGattWriteCccStatic);
+ }
+
+ void OnCharacteristicValueChanged(uint16_t conn_id, tGATT_STATUS status,
+ uint16_t handle, uint16_t len,
+ uint8_t* value, void* /* data */) {
+ VolumeControlDevice* device = volume_control_devices_.FindByConnId(conn_id);
+ if (!device) {
+ LOG(INFO) << __func__ << ": unknown conn_id=" << loghex(conn_id);
+ return;
+ }
+
+ if (status != GATT_SUCCESS) {
+ LOG(INFO) << __func__ << ": status=" << static_cast<int>(status);
+ return;
+ }
+
+ if (handle == device->volume_state_handle) {
+ OnVolumeControlStateChanged(device, len, value);
+ verify_device_ready(device, handle);
+ return;
+ }
+ if (handle == device->volume_flags_handle) {
+ OnVolumeControlFlagsChanged(device, len, value);
+ verify_device_ready(device, handle);
+ return;
+ }
+
+ LOG(ERROR) << __func__ << ": unknown handle=" << loghex(handle);
+ }
+
+ void OnNotificationEvent(uint16_t conn_id, uint16_t handle, uint16_t len,
+ uint8_t* value) {
+ LOG(INFO) << __func__ << ": handle=" << loghex(handle);
+ OnCharacteristicValueChanged(conn_id, GATT_SUCCESS, handle, len, value,
+ nullptr);
+ }
+
+ void VolumeControlReadCommon(uint16_t conn_id, uint16_t handle) {
+ BtaGattQueue::ReadCharacteristic(conn_id, handle, chrc_read_callback_static,
+ nullptr);
+ }
+
+ void OnVolumeControlStateChanged(VolumeControlDevice* device, uint16_t len,
+ uint8_t* value) {
+ if (len != 3) {
+ LOG(INFO) << __func__ << ": malformed len=" << loghex(len);
+ return;
+ }
+
+ uint8_t* pp = value;
+ STREAM_TO_UINT8(device->volume, pp);
+ STREAM_TO_UINT8(device->mute, pp);
+ STREAM_TO_UINT8(device->change_counter, pp);
+
+ LOG(INFO) << __func__ << " " << base::HexEncode(value, len);
+ LOG(INFO) << __func__ << "volume " << loghex(device->volume) << "mute"
+ << loghex(device->mute) << "change_counter"
+ << loghex(device->change_counter);
+
+ if (!device->device_ready) return;
+
+ callbacks_->OnVolumeStateChanged(device->address, device->volume,
+ device->mute);
+ }
+
+ void OnVolumeControlFlagsChanged(VolumeControlDevice* device, uint16_t len,
+ uint8_t* value) {
+ device->flags = *value;
+
+ LOG(INFO) << __func__ << " " << base::HexEncode(value, len);
+ LOG(INFO) << __func__ << "flags " << loghex(device->flags);
+ }
+
+ void OnGattWriteCcc(uint16_t connection_id, tGATT_STATUS status,
+ uint16_t handle, void* /*data*/) {
+ VolumeControlDevice* device =
+ volume_control_devices_.FindByConnId(connection_id);
+ if (!device) {
+ LOG(INFO) << __func__
+ << "unknown connection_id=" << loghex(connection_id);
+ BtaGattQueue::Clean(connection_id);
+ return;
+ }
+
+ if (status != GATT_SUCCESS) {
+ LOG(ERROR) << __func__
+ << "Failed to register for notification: " << loghex(handle)
+ << " status: " << status;
+ device_cleanup_helper(device, true);
+ return;
+ }
+
+ LOG(INFO) << __func__
+ << "Successfully register for indications: " << loghex(handle);
+
+ verify_device_ready(device, handle);
+ }
+
+ static void OnGattWriteCccStatic(uint16_t connection_id, tGATT_STATUS status,
+ uint16_t handle, void* data) {
+ if (!instance) {
+ LOG(ERROR) << __func__ << "No instance=" << handle;
+ return;
+ }
+
+ instance->OnGattWriteCcc(connection_id, status, handle, data);
+ }
+
+ void Dump(int fd) { volume_control_devices_.DebugDump(fd); }
+
+ void Disconnect(const RawAddress& address) override {
+ VolumeControlDevice* device =
+ volume_control_devices_.FindByAddress(address);
+ if (!device) {
+ LOG(INFO) << "Device not connected to profile" << address;
+ return;
+ }
+
+ LOG(INFO) << __func__ << ": " << address;
+ LOG(INFO) << "GAP_EVT_CONN_CLOSED: " << device->address;
+ device_cleanup_helper(device, true);
+ }
+
+ void OnGattDisconnected(uint16_t connection_id, tGATT_IF /*client_if*/,
+ RawAddress remote_bda, tBTA_GATT_REASON /*reason*/) {
+ VolumeControlDevice* device =
+ volume_control_devices_.FindByConnId(connection_id);
+ if (!device) {
+ LOG(ERROR) << __func__
+ << " Skipping unknown device disconnect, connection_id="
+ << loghex(connection_id);
+ return;
+ }
+
+ // If we get here, it means, device has not been exlicitly disconnected.
+ bool device_ready = device->device_ready;
+
+ device_cleanup_helper(device, device->connecting_actively);
+
+ if (device_ready) {
+ volume_control_devices_.Add(remote_bda, true);
+
+ /* Add device into BG connection to accept remote initiated connection */
+ BTA_GATTC_Open(gatt_if_, remote_bda, false, false);
+ }
+ }
+
+ void OnWriteControlResponse(uint16_t connection_id, tGATT_STATUS status,
+ uint16_t handle, void* /*data*/) {
+ VolumeControlDevice* device =
+ volume_control_devices_.FindByConnId(connection_id);
+ if (!device) {
+ LOG(ERROR) << __func__
+ << "Skipping unknown device disconnect, connection_id="
+ << loghex(connection_id);
+ return;
+ }
+
+ LOG(INFO) << "Write response handle: " << loghex(handle)
+ << " status: " << loghex((int)(status));
+ }
+
+ void SetVolume(std::variant<RawAddress, int> addr_or_group_id,
+ uint8_t volume) override {
+ LOG(INFO) << __func__ << "vol: " << +volume;
+
+ if (std::holds_alternative<RawAddress>(addr_or_group_id)) {
+ std::vector<RawAddress> devices = {
+ std::get<RawAddress>(addr_or_group_id)};
+ std::vector<uint8_t> arg({volume});
+ devices_control_point_helper(devices,
+ kControlPointOpcodeSetAbsoluteVolume, &arg);
+ return;
+ }
+
+ /* TODO implement handling group request */
+ }
+
+ void CleanUp() {
+ LOG(INFO) << __func__;
+ volume_control_devices_.Disconnect(gatt_if_);
+ volume_control_devices_.Clear();
+ BTA_GATTC_AppDeregister(gatt_if_);
+ }
+
+ private:
+ tGATT_IF gatt_if_;
+ bluetooth::vc::VolumeControlCallbacks* callbacks_;
+ VolumeControlDevices volume_control_devices_;
+
+ void verify_device_ready(VolumeControlDevice* device, uint16_t handle) {
+ if (device->device_ready) return;
+
+ // VerifyReady sets the device_ready flag if all remaining GATT operations
+ // are completed
+ if (device->VerifyReady(handle)) {
+ LOG(INFO) << __func__ << "Outstanding reads completed ";
+
+ callbacks_->OnConnectionState(ConnectionState::CONNECTED,
+ device->address);
+
+ device->connecting_actively = true;
+
+ device->first_connection = false;
+
+ // once profile connected we can notify current states
+ callbacks_->OnVolumeStateChanged(device->address, device->volume,
+ device->mute);
+
+ device->EnqueueRemainingRequests(gatt_if_, chrc_read_callback_static,
+ OnGattWriteCccStatic);
+ }
+ }
+
+ void device_cleanup_helper(VolumeControlDevice* device, bool notify) {
+ device->Disconnect(gatt_if_);
+ if (notify)
+ callbacks_->OnConnectionState(ConnectionState::DISCONNECTED,
+ device->address);
+ volume_control_devices_.Remove(device->address);
+ }
+
+ void devices_control_point_helper(std::vector<RawAddress>& devices,
+ uint8_t opcode,
+ const std::vector<uint8_t>* arg) {
+ volume_control_devices_.ControlPointOperation(
+ devices, opcode, arg,
+ [](uint16_t connection_id, tGATT_STATUS status, uint16_t handle,
+ void* data) {
+ if (instance)
+ instance->OnWriteControlResponse(connection_id, status, handle,
+ data);
+ },
+ nullptr);
+ }
+
+ void gattc_callback(tBTA_GATTC_EVT event, tBTA_GATTC* p_data) {
+ LOG(INFO) << __func__ << " event = " << static_cast<int>(event);
+
+ if (p_data == nullptr) return;
+
+ switch (event) {
+ case BTA_GATTC_OPEN_EVT: {
+ tBTA_GATTC_OPEN& o = p_data->open;
+ OnGattConnected(o.status, o.conn_id, o.client_if, o.remote_bda,
+ o.transport, o.mtu);
+
+ } break;
+
+ case BTA_GATTC_CLOSE_EVT: {
+ tBTA_GATTC_CLOSE& c = p_data->close;
+ OnGattDisconnected(c.conn_id, c.client_if, c.remote_bda, c.reason);
+ } break;
+
+ case BTA_GATTC_SEARCH_CMPL_EVT:
+ OnServiceSearchComplete(p_data->search_cmpl.conn_id,
+ p_data->search_cmpl.status);
+ break;
+
+ case BTA_GATTC_NOTIF_EVT: {
+ tBTA_GATTC_NOTIFY& n = p_data->notify;
+ if (!n.is_notify || n.len > GATT_MAX_ATTR_LEN) {
+ LOG(ERROR) << __func__ << ": rejected BTA_GATTC_NOTIF_EVT. is_notify="
+ << n.is_notify << ", len=" << static_cast<int>(n.len);
+ break;
+ }
+ OnNotificationEvent(n.conn_id, n.handle, n.len, n.value);
+ } break;
+
+ case BTA_GATTC_ENC_CMPL_CB_EVT:
+ OnEncryptionComplete(p_data->enc_cmpl.remote_bda, true);
+ break;
+
+ case BTA_GATTC_SRVC_CHG_EVT:
+ OnServiceChangeEvent(p_data->remote_bda);
+ break;
+
+ case BTA_GATTC_SRVC_DISC_DONE_EVT:
+ OnServiceDiscDoneEvent(p_data->remote_bda);
+ break;
+
+ default:
+ break;
+ }
+ }
+
+ static void gattc_callback_static(tBTA_GATTC_EVT event, tBTA_GATTC* p_data) {
+ if (instance) instance->gattc_callback(event, p_data);
+ }
+
+ static void enc_callback_static(const RawAddress* address, tBT_TRANSPORT,
+ void*, tBTM_STATUS status) {
+ if (instance) instance->OnEncryptionComplete(*address, status);
+ }
+
+ static void chrc_read_callback_static(uint16_t conn_id, tGATT_STATUS status,
+ uint16_t handle, uint16_t len,
+ uint8_t* value, void* data) {
+ if (instance)
+ instance->OnCharacteristicValueChanged(conn_id, status, handle, len,
+ value, data);
+ }
+};
+} // namespace
+
+void VolumeControl::Initialize(
+ bluetooth::vc::VolumeControlCallbacks* callbacks) {
+ if (instance) {
+ LOG(ERROR) << "Already initialized!";
+ return;
+ }
+
+ instance = new VolumeControlImpl(callbacks);
+}
+
+bool VolumeControl::IsVolumeControlRunning() { return instance; }
+
+VolumeControl* VolumeControl::Get(void) {
+ CHECK(instance);
+ return instance;
+};
+
+void VolumeControl::AddFromStorage(const RawAddress& address,
+ bool auto_connect) {
+ if (!instance) {
+ LOG(ERROR) << "Not initialized yet";
+ return;
+ }
+
+ instance->AddFromStorage(address, auto_connect);
+};
+
+void VolumeControl::CleanUp() {
+ if (!instance) {
+ LOG(ERROR) << "not initialized!";
+ return;
+ }
+
+ VolumeControlImpl* ptr = instance;
+ instance = nullptr;
+
+ ptr->CleanUp();
+
+ delete ptr;
+};
+
+void VolumeControl::DebugDump(int fd) {
+ dprintf(fd, "Volume Control Manager:\n");
+ if (instance) instance->Dump(fd);
+ dprintf(fd, "\n");
+}
diff --git a/system/bta/vc/vc_test.cc b/system/bta/vc/vc_test.cc
new file mode 100644
index 0000000000..30eef87fa1
--- /dev/null
+++ b/system/bta/vc/vc_test.cc
@@ -0,0 +1,643 @@
+/*
+ * Copyright 2021 HIMSA II K/S - www.himsa.com.
+ * Represented by EHIMA - www.ehima.com
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <base/bind.h>
+#include <base/bind_helpers.h>
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+#include "bta_gatt_api_mock.h"
+#include "bta_gatt_queue_mock.h"
+#include "bta_vc_api.h"
+#include "btm_api_mock.h"
+#include "gatt/database_builder.h"
+#include "hardware/bt_gatt_types.h"
+#include "types.h"
+
+void btif_storage_add_volume_control(const RawAddress& addr, bool auto_conn) {}
+
+namespace bluetooth {
+namespace vc {
+namespace internal {
+namespace {
+
+using base::Bind;
+using base::Unretained;
+
+using bluetooth::vc::ConnectionState;
+using bluetooth::vc::VolumeControlCallbacks;
+
+using testing::_;
+using testing::DoAll;
+using testing::DoDefault;
+using testing::Invoke;
+using testing::Mock;
+using testing::NotNull;
+using testing::Return;
+using testing::SaveArg;
+using testing::SetArgPointee;
+using testing::WithArg;
+
+RawAddress GetTestAddress(int index) {
+ CHECK_LT(index, UINT8_MAX);
+ RawAddress result = {
+ {0xC0, 0xDE, 0xC0, 0xDE, 0x00, static_cast<uint8_t>(index)}};
+ return result;
+}
+
+class MockVolumeControlCallbacks : public VolumeControlCallbacks {
+ public:
+ MockVolumeControlCallbacks() = default;
+ ~MockVolumeControlCallbacks() override = default;
+
+ MOCK_METHOD((void), OnConnectionState,
+ (ConnectionState state, const RawAddress& address), (override));
+ MOCK_METHOD((void), OnVolumeStateChanged,
+ (const RawAddress& address, uint8_t volume, bool mute),
+ (override));
+ MOCK_METHOD((void), OnGroupVolumeStateChanged,
+ (int group_id, uint8_t volume, bool mute), (override));
+
+ private:
+ DISALLOW_COPY_AND_ASSIGN(MockVolumeControlCallbacks);
+};
+
+class VolumeControlTest : public ::testing::Test {
+ private:
+ void set_sample_database(uint16_t conn_id, bool vcs, bool vcs_broken,
+ bool aics, bool aics_broken, bool vocs,
+ bool vocs_broken) {
+ gatt::DatabaseBuilder builder;
+ builder.AddService(0x0001, 0x0003, Uuid::From16Bit(0x1800), true);
+ builder.AddCharacteristic(0x0002, 0x0003, Uuid::From16Bit(0x2a00),
+ GATT_CHAR_PROP_BIT_READ);
+ /* 0x0004-0x000f RFU */
+ if (vcs) {
+ /* VCS */
+ builder.AddService(0x0010, 0x0026, kVolumeControlUuid, true);
+ if (aics) {
+ /* TODO Place holder */
+ }
+ if (vocs) {
+ /* TODO Place holder */
+ }
+ /* 0x0015-0x001f RFU */
+ builder.AddCharacteristic(
+ 0x0020, 0x0021, kVolumeControlStateUuid,
+ GATT_CHAR_PROP_BIT_READ | GATT_CHAR_PROP_BIT_NOTIFY);
+ builder.AddDescriptor(0x0022,
+ Uuid::From16Bit(GATT_UUID_CHAR_CLIENT_CONFIG));
+ if (!vcs_broken) {
+ builder.AddCharacteristic(0x0023, 0x0024, kVolumeControlPointUuid,
+ GATT_CHAR_PROP_BIT_WRITE);
+ }
+ builder.AddCharacteristic(0x0025, 0x0026, kVolumeFlagsUuid,
+ GATT_CHAR_PROP_BIT_READ);
+ /* 0x0027-0x002f RFU */
+ if (aics) {
+ /* TODO Place holder for AICS */
+ }
+ if (vocs) {
+ /* TODO Place holder for VOCS */
+ }
+ }
+ /* 0x008c-0x008f RFU */
+
+ /* GATTS */
+ builder.AddService(0x0090, 0x0093,
+ Uuid::From16Bit(UUID_SERVCLASS_GATT_SERVER), true);
+ builder.AddCharacteristic(0x0091, 0x0092,
+ Uuid::From16Bit(GATT_UUID_GATT_SRV_CHGD),
+ GATT_CHAR_PROP_BIT_NOTIFY);
+ builder.AddDescriptor(0x0093,
+ Uuid::From16Bit(GATT_UUID_CHAR_CLIENT_CONFIG));
+ services_map[conn_id] = builder.Build().Services();
+
+ ON_CALL(gatt_queue, ReadCharacteristic(conn_id, _, _, _))
+ .WillByDefault(Invoke([&](uint16_t conn_id, uint16_t handle,
+ GATT_READ_OP_CB cb, void* cb_data) -> void {
+ std::vector<uint8_t> value;
+
+ switch (handle) {
+ case 0x0003:
+ /* device name */
+ value.resize(20);
+ break;
+
+ case 0x0021:
+ /* volume state */
+ value.resize(3);
+ break;
+
+ case 0x0026:
+ /* volume flags */
+ value.resize(1);
+ break;
+
+ default:
+ ASSERT_TRUE(false);
+ return;
+ }
+
+ cb(conn_id, GATT_SUCCESS, handle, value.size(), value.data(),
+ cb_data);
+ }));
+ }
+
+ protected:
+ void SetUp(void) override {
+ bluetooth::manager::SetMockBtmInterface(&btm_interface);
+ gatt::SetMockBtaGattInterface(&gatt_interface);
+ gatt::SetMockBtaGattQueue(&gatt_queue);
+ callbacks.reset(new MockVolumeControlCallbacks());
+
+ // default action for GetCharacteristic function call
+ ON_CALL(gatt_interface, GetCharacteristic(_, _))
+ .WillByDefault(
+ Invoke([&](uint16_t conn_id,
+ uint16_t handle) -> const gatt::Characteristic* {
+ std::list<gatt::Service>& services = services_map[conn_id];
+ for (auto const& service : services) {
+ for (auto const& characteristic : service.characteristics) {
+ if (characteristic.value_handle == handle) {
+ return &characteristic;
+ }
+ }
+ }
+
+ return nullptr;
+ }));
+
+ // default action for GetOwningService function call
+ ON_CALL(gatt_interface, GetOwningService(_, _))
+ .WillByDefault(Invoke(
+ [&](uint16_t conn_id, uint16_t handle) -> const gatt::Service* {
+ std::list<gatt::Service>& services = services_map[conn_id];
+ for (auto const& service : services) {
+ if (service.handle <= handle && service.end_handle >= handle) {
+ return &service;
+ }
+ }
+
+ return nullptr;
+ }));
+
+ // default action for GetServices function call
+ ON_CALL(gatt_interface, GetServices(_))
+ .WillByDefault(WithArg<0>(
+ Invoke([&](uint16_t conn_id) -> std::list<gatt::Service>* {
+ return &services_map[conn_id];
+ })));
+
+ // default action for RegisterForNotifications function call
+ ON_CALL(gatt_interface, RegisterForNotifications(gatt_if, _, _))
+ .WillByDefault(Return(GATT_SUCCESS));
+
+ // default action for DeregisterForNotifications function call
+ ON_CALL(gatt_interface, DeregisterForNotifications(gatt_if, _, _))
+ .WillByDefault(Return(GATT_SUCCESS));
+
+ // default action for WriteDescriptor function call
+ ON_CALL(gatt_queue, WriteDescriptor(_, _, _, _, _, _))
+ .WillByDefault(
+ Invoke([](uint16_t conn_id, uint16_t handle,
+ std::vector<uint8_t> value, tGATT_WRITE_TYPE write_type,
+ GATT_WRITE_OP_CB cb, void* cb_data) -> void {
+ if (cb) cb(conn_id, GATT_SUCCESS, handle, cb_data);
+ }));
+ }
+
+ void TearDown(void) override {
+ services_map.clear();
+ callbacks.reset();
+ gatt::SetMockBtaGattQueue(nullptr);
+ gatt::SetMockBtaGattInterface(nullptr);
+ bluetooth::manager::SetMockBtmInterface(nullptr);
+ }
+
+ void TestAppRegister(void) {
+ BtaAppRegisterCallback app_register_callback;
+ EXPECT_CALL(gatt_interface, AppRegister(_, _, _))
+ .WillOnce(DoAll(SaveArg<0>(&gatt_callback),
+ SaveArg<1>(&app_register_callback)));
+ VolumeControl::Initialize(callbacks.get());
+ ASSERT_TRUE(gatt_callback);
+ ASSERT_TRUE(app_register_callback);
+ app_register_callback.Run(gatt_if, GATT_SUCCESS);
+ ASSERT_TRUE(VolumeControl::IsVolumeControlRunning());
+ }
+
+ void TestAppUnregister(void) {
+ EXPECT_CALL(gatt_interface, AppDeregister(gatt_if));
+ VolumeControl::CleanUp();
+ ASSERT_FALSE(VolumeControl::IsVolumeControlRunning());
+ gatt_callback = nullptr;
+ }
+
+ void TestConnect(const RawAddress& address) {
+ // by default indicate link as encrypted
+ ON_CALL(btm_interface, GetSecurityFlagsByTransport(address, NotNull(), _))
+ .WillByDefault(
+ DoAll(SetArgPointee<1>(BTM_SEC_FLAG_ENCRYPTED), Return(true)));
+
+ EXPECT_CALL(gatt_interface, Open(gatt_if, address, true, _));
+ VolumeControl::Get()->Connect(address);
+ }
+
+ void TestDisconnect(const RawAddress& address, uint16_t conn_id) {
+ if (conn_id) {
+ EXPECT_CALL(gatt_interface, Close(conn_id));
+ } else {
+ EXPECT_CALL(gatt_interface, CancelOpen(gatt_if, address, _));
+ }
+ VolumeControl::Get()->Disconnect(address);
+ }
+
+ void TestAddFromStorage(const RawAddress& address, bool auto_connect) {
+ // by default indicate link as encrypted
+ ON_CALL(btm_interface, GetSecurityFlagsByTransport(address, NotNull(), _))
+ .WillByDefault(
+ DoAll(SetArgPointee<1>(BTM_SEC_FLAG_ENCRYPTED), Return(true)));
+
+ if (auto_connect) {
+ EXPECT_CALL(gatt_interface, Open(gatt_if, address, false, _));
+ } else {
+ EXPECT_CALL(gatt_interface, Open(gatt_if, address, _, _)).Times(0);
+ }
+ VolumeControl::Get()->AddFromStorage(address, auto_connect);
+ }
+
+ void TestSubscribeNotifications(const RawAddress& address, uint16_t conn_id,
+ std::map<uint16_t, uint16_t>& handle_pairs) {
+ SetSampleDatabase(conn_id);
+ TestAppRegister();
+ TestConnect(address);
+ GetConnectedEvent(address, conn_id);
+
+ EXPECT_CALL(gatt_queue, WriteDescriptor(_, _, _, _, _, _))
+ .WillRepeatedly(DoDefault());
+ EXPECT_CALL(gatt_interface, RegisterForNotifications(_, _, _))
+ .WillRepeatedly(DoDefault());
+
+ std::vector<uint8_t> notify_value({0x01, 0x00});
+ for (auto const& handles : handle_pairs) {
+ EXPECT_CALL(gatt_queue, WriteDescriptor(conn_id, handles.second,
+ notify_value, GATT_WRITE, _, _))
+ .WillOnce(DoDefault());
+ EXPECT_CALL(gatt_interface,
+ RegisterForNotifications(gatt_if, address, handles.first))
+ .WillOnce(DoDefault());
+ }
+
+ GetSearchCompleteEvent(conn_id);
+ TestAppUnregister();
+ }
+
+ void TestReadCharacteristic(const RawAddress& address, uint16_t conn_id,
+ std::vector<uint16_t> handles) {
+ SetSampleDatabase(conn_id);
+ TestAppRegister();
+ TestConnect(address);
+ GetConnectedEvent(address, conn_id);
+
+ EXPECT_CALL(gatt_queue, ReadCharacteristic(conn_id, _, _, _))
+ .WillRepeatedly(DoDefault());
+ for (auto const& handle : handles) {
+ EXPECT_CALL(gatt_queue, ReadCharacteristic(conn_id, handle, _, _))
+ .WillOnce(DoDefault());
+ }
+
+ GetSearchCompleteEvent(conn_id);
+ TestAppUnregister();
+ }
+
+ void GetConnectedEvent(const RawAddress& address, uint16_t conn_id) {
+ tBTA_GATTC_OPEN event_data = {
+ .status = GATT_SUCCESS,
+ .conn_id = conn_id,
+ .client_if = gatt_if,
+ .remote_bda = address,
+ .transport = GATT_TRANSPORT_LE,
+ .mtu = 240,
+ };
+
+ gatt_callback(BTA_GATTC_OPEN_EVT, (tBTA_GATTC*)&event_data);
+ }
+
+ void GetDisconnectedEvent(const RawAddress& address, uint16_t conn_id) {
+ tBTA_GATTC_CLOSE event_data = {
+ .status = GATT_SUCCESS,
+ .conn_id = conn_id,
+ .client_if = gatt_if,
+ .remote_bda = address,
+ .reason = HCI_ERR_PEER_USER,
+ };
+
+ gatt_callback(BTA_GATTC_CLOSE_EVT, (tBTA_GATTC*)&event_data);
+ }
+
+ void GetSearchCompleteEvent(uint16_t conn_id) {
+ tBTA_GATTC_SEARCH_CMPL event_data = {
+ .status = GATT_SUCCESS,
+ .conn_id = conn_id,
+ };
+
+ gatt_callback(BTA_GATTC_SEARCH_CMPL_EVT, (tBTA_GATTC*)&event_data);
+ }
+
+ void SetEncryptionResult(const RawAddress& address, bool success) {
+ ON_CALL(btm_interface, GetSecurityFlagsByTransport(address, NotNull(), _))
+ .WillByDefault(DoAll(SetArgPointee<1>(0), Return(true)));
+ EXPECT_CALL(btm_interface,
+ SetEncryption(address, _, NotNull(), _, BTM_BLE_SEC_ENCRYPT))
+ .WillOnce(Invoke(
+ [&success](const RawAddress& bd_addr, tBT_TRANSPORT transport,
+ tBTM_SEC_CALLBACK* p_callback, void* p_ref_data,
+ tBTM_BLE_SEC_ACT sec_act) -> tBTM_STATUS {
+ p_callback(&bd_addr, transport, p_ref_data,
+ success ? BTM_SUCCESS : BTM_FAILED_ON_SECURITY);
+ return BTM_SUCCESS;
+ }));
+ }
+
+ void SetSampleDatabaseVCS(uint16_t conn_id) {
+ set_sample_database(conn_id, true, false, false, false, false, false);
+ }
+
+ void SetSampleDatabaseNoVCS(uint16_t conn_id) {
+ set_sample_database(conn_id, false, false, true, false, true, false);
+ }
+
+ void SetSampleDatabaseVCSBroken(uint16_t conn_id) {
+ set_sample_database(conn_id, true, true, true, false, true, false);
+ }
+
+ void SetSampleDatabase(uint16_t conn_id) {
+ set_sample_database(conn_id, true, false, true, false, true, false);
+ }
+
+ std::unique_ptr<MockVolumeControlCallbacks> callbacks;
+ bluetooth::manager::MockBtmInterface btm_interface;
+ gatt::MockBtaGattInterface gatt_interface;
+ gatt::MockBtaGattQueue gatt_queue;
+ tBTA_GATTC_CBACK* gatt_callback;
+ const uint8_t gatt_if = 0xff;
+ std::map<uint16_t, std::list<gatt::Service>> services_map;
+};
+
+TEST_F(VolumeControlTest, test_get_uninitialized) {
+ ASSERT_DEATH(VolumeControl::Get(), "");
+}
+
+TEST_F(VolumeControlTest, test_initialize) {
+ VolumeControl::Initialize(callbacks.get());
+ ASSERT_TRUE(VolumeControl::IsVolumeControlRunning());
+ VolumeControl::CleanUp();
+}
+
+TEST_F(VolumeControlTest, test_initialize_twice) {
+ VolumeControl::Initialize(callbacks.get());
+ VolumeControl* volume_control_p = VolumeControl::Get();
+ VolumeControl::Initialize(callbacks.get());
+ ASSERT_EQ(volume_control_p, VolumeControl::Get());
+ VolumeControl::CleanUp();
+}
+
+TEST_F(VolumeControlTest, test_cleanup_initialized) {
+ VolumeControl::Initialize(callbacks.get());
+ VolumeControl::CleanUp();
+ ASSERT_FALSE(VolumeControl::IsVolumeControlRunning());
+}
+
+TEST_F(VolumeControlTest, test_cleanup_uninitialized) {
+ VolumeControl::CleanUp();
+ ASSERT_FALSE(VolumeControl::IsVolumeControlRunning());
+}
+
+TEST_F(VolumeControlTest, test_app_registration) {
+ TestAppRegister();
+ TestAppUnregister();
+}
+
+TEST_F(VolumeControlTest, test_connect) {
+ TestAppRegister();
+ TestConnect(GetTestAddress(0));
+ TestAppUnregister();
+}
+
+TEST_F(VolumeControlTest, test_add_from_storage) {
+ TestAppRegister();
+ TestAddFromStorage(GetTestAddress(0), true);
+ TestAddFromStorage(GetTestAddress(1), false);
+ TestAppUnregister();
+}
+
+TEST_F(VolumeControlTest, test_disconnect_non_connected) {
+ const RawAddress test_address = GetTestAddress(0);
+ TestAppRegister();
+ TestConnect(test_address);
+ EXPECT_CALL(*callbacks,
+ OnConnectionState(ConnectionState::DISCONNECTED, test_address));
+ TestDisconnect(test_address, 0);
+ TestAppUnregister();
+}
+
+TEST_F(VolumeControlTest, test_disconnect_connected) {
+ const RawAddress test_address = GetTestAddress(0);
+ TestAppRegister();
+ TestConnect(test_address);
+ GetConnectedEvent(test_address, 1);
+ EXPECT_CALL(*callbacks,
+ OnConnectionState(ConnectionState::DISCONNECTED, test_address));
+ TestDisconnect(test_address, 1);
+ TestAppUnregister();
+}
+
+TEST_F(VolumeControlTest, test_disconnected) {
+ const RawAddress test_address = GetTestAddress(0);
+ TestAppRegister();
+ TestConnect(test_address);
+ GetConnectedEvent(test_address, 1);
+ EXPECT_CALL(*callbacks,
+ OnConnectionState(ConnectionState::DISCONNECTED, test_address));
+ GetDisconnectedEvent(test_address, 1);
+ TestAppUnregister();
+}
+
+TEST_F(VolumeControlTest, test_disconnected_while_autoconnect) {
+ const RawAddress test_address = GetTestAddress(0);
+ TestAppRegister();
+ TestAddFromStorage(test_address, true);
+ GetConnectedEvent(test_address, 1);
+ // autoconnect - don't indicate disconnection
+ EXPECT_CALL(*callbacks,
+ OnConnectionState(ConnectionState::DISCONNECTED, test_address))
+ .Times(0);
+ GetDisconnectedEvent(test_address, 1);
+ TestAppUnregister();
+}
+
+TEST_F(VolumeControlTest, test_reconnect_after_encryption_failed) {
+ const RawAddress test_address = GetTestAddress(0);
+ TestAppRegister();
+ TestAddFromStorage(test_address, true);
+ SetEncryptionResult(test_address, false);
+ // autoconnect - don't indicate disconnection
+ EXPECT_CALL(*callbacks,
+ OnConnectionState(ConnectionState::DISCONNECTED, test_address))
+ .Times(0);
+ GetConnectedEvent(test_address, 1);
+ Mock::VerifyAndClearExpectations(&btm_interface);
+ SetEncryptionResult(test_address, true);
+ GetConnectedEvent(test_address, 1);
+ TestAppUnregister();
+}
+
+TEST_F(VolumeControlTest, test_discovery_vcs_found) {
+ const RawAddress test_address = GetTestAddress(0);
+ SetSampleDatabaseVCS(1);
+ TestAppRegister();
+ TestConnect(test_address);
+ EXPECT_CALL(*callbacks,
+ OnConnectionState(ConnectionState::CONNECTED, test_address));
+ GetConnectedEvent(test_address, 1);
+ GetSearchCompleteEvent(1);
+ Mock::VerifyAndClearExpectations(callbacks.get());
+ TestAppUnregister();
+}
+
+TEST_F(VolumeControlTest, test_discovery_vcs_not_found) {
+ const RawAddress test_address = GetTestAddress(0);
+ SetSampleDatabaseNoVCS(1);
+ TestAppRegister();
+ TestConnect(test_address);
+ EXPECT_CALL(*callbacks,
+ OnConnectionState(ConnectionState::DISCONNECTED, test_address));
+ GetConnectedEvent(test_address, 1);
+
+ GetSearchCompleteEvent(1);
+ Mock::VerifyAndClearExpectations(callbacks.get());
+ TestAppUnregister();
+}
+
+TEST_F(VolumeControlTest, test_discovery_vcs_broken) {
+ const RawAddress test_address = GetTestAddress(0);
+ SetSampleDatabaseVCSBroken(1);
+ TestAppRegister();
+ TestConnect(test_address);
+ EXPECT_CALL(*callbacks,
+ OnConnectionState(ConnectionState::DISCONNECTED, test_address));
+ GetConnectedEvent(test_address, 1);
+ GetSearchCompleteEvent(1);
+ Mock::VerifyAndClearExpectations(callbacks.get());
+ TestAppUnregister();
+}
+
+TEST_F(VolumeControlTest, test_subscribe_vcs_volume_state) {
+ std::map<uint16_t, uint16_t> handles({{0x0021, 0x0022}});
+ TestSubscribeNotifications(GetTestAddress(0), 1, handles);
+}
+
+TEST_F(VolumeControlTest, test_read_vcs_volume_state) {
+ const RawAddress test_address = GetTestAddress(0);
+ EXPECT_CALL(*callbacks, OnVolumeStateChanged(test_address, _, _));
+ std::vector<uint16_t> handles({0x0021});
+ TestReadCharacteristic(test_address, 1, handles);
+}
+
+TEST_F(VolumeControlTest, test_read_vcs_volume_flags) {
+ std::vector<uint16_t> handles({0x0026});
+ TestReadCharacteristic(GetTestAddress(0), 1, handles);
+}
+
+class VolumeControlCallbackTest : public VolumeControlTest {
+ protected:
+ const RawAddress test_address = GetTestAddress(0);
+ uint16_t conn_id = 22;
+
+ void SetUp(void) override {
+ VolumeControlTest::SetUp();
+ SetSampleDatabase(conn_id);
+ TestAppRegister();
+ TestConnect(test_address);
+ GetConnectedEvent(test_address, conn_id);
+ GetSearchCompleteEvent(conn_id);
+ }
+
+ void TearDown(void) override {
+ TestAppUnregister();
+ VolumeControlTest::TearDown();
+ }
+
+ void GetNotificationEvent(uint16_t handle, std::vector<uint8_t>& value) {
+ tBTA_GATTC_NOTIFY event_data = {
+ .conn_id = conn_id,
+ .bda = test_address,
+ .handle = handle,
+ .len = (uint8_t)value.size(),
+ .is_notify = true,
+ };
+
+ std::copy(value.begin(), value.end(), event_data.value);
+ gatt_callback(BTA_GATTC_NOTIF_EVT, (tBTA_GATTC*)&event_data);
+ }
+};
+
+TEST_F(VolumeControlCallbackTest, test_volume_state_changed) {
+ std::vector<uint8_t> value({0x03, 0x01, 0x02});
+ EXPECT_CALL(*callbacks, OnVolumeStateChanged(test_address, 0x03, true));
+ GetNotificationEvent(0x0021, value);
+}
+
+TEST_F(VolumeControlCallbackTest, test_volume_state_changed_malformed) {
+ EXPECT_CALL(*callbacks, OnVolumeStateChanged(test_address, _, _)).Times(0);
+ std::vector<uint8_t> too_short({0x03, 0x01});
+ GetNotificationEvent(0x0021, too_short);
+ std::vector<uint8_t> too_long({0x03, 0x01, 0x02, 0x03});
+ GetNotificationEvent(0x0021, too_long);
+}
+
+class VolumeControlValueSetTest : public VolumeControlTest {
+ protected:
+ const RawAddress test_address = GetTestAddress(0);
+ uint16_t conn_id = 22;
+
+ void SetUp(void) override {
+ VolumeControlTest::SetUp();
+ SetSampleDatabase(conn_id);
+ TestAppRegister();
+ TestConnect(test_address);
+ GetConnectedEvent(test_address, conn_id);
+ GetSearchCompleteEvent(conn_id);
+ }
+
+ void TearDown(void) override {
+ TestAppUnregister();
+ VolumeControlTest::TearDown();
+ }
+};
+
+TEST_F(VolumeControlValueSetTest, test_set_volume) {
+ std::vector<uint8_t> expected_data({0x04, 0x00, 0x10});
+ EXPECT_CALL(gatt_queue, WriteCharacteristic(conn_id, 0x0024, expected_data,
+ GATT_WRITE, _, _));
+ VolumeControl::Get()->SetVolume(test_address, 0x10);
+}
+} // namespace
+} // namespace internal
+} // namespace vc
+} // namespace bluetooth
diff --git a/system/btif/Android.bp b/system/btif/Android.bp
index e7a3986c3d..ab557277f7 100755
--- a/system/btif/Android.bp
+++ b/system/btif/Android.bp
@@ -76,6 +76,7 @@ cc_library_static {
"src/btif_gatt_server.cc",
"src/btif_gatt_test.cc",
"src/btif_gatt_util.cc",
+ "src/btif_vc.cc",
"src/btif_hearing_aid.cc",
"src/btif_hf.cc",
"src/btif_hf_client.cc",
diff --git a/system/btif/BUILD.gn b/system/btif/BUILD.gn
index 7c30b224be..df326e5d17 100644
--- a/system/btif/BUILD.gn
+++ b/system/btif/BUILD.gn
@@ -64,6 +64,7 @@ static_library("btif") {
"src/btif_gatt_test.cc",
"src/btif_gatt_util.cc",
"src/btif_hd.cc",
+ "src/btif_vc.cc",
"src/btif_hearing_aid.cc",
"src/btif_hf.cc",
"src/btif_hf_client.cc",
diff --git a/system/btif/src/bluetooth.cc b/system/btif/src/bluetooth.cc
index 92029cb41d..fc6ea3f48f 100644
--- a/system/btif/src/bluetooth.cc
+++ b/system/btif/src/bluetooth.cc
@@ -40,6 +40,7 @@
#include <hardware/bt_rc.h>
#include <hardware/bt_sdp.h>
#include <hardware/bt_sock.h>
+#include <hardware/bt_vc.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@@ -84,6 +85,7 @@
using bluetooth::hearing_aid::HearingAidInterface;
using bluetooth::le_audio::LeAudioClientInterface;
+using bluetooth::vc::VolumeControlInterface;
/*******************************************************************************
* Static variables
@@ -127,6 +129,8 @@ extern const btsdp_interface_t* btif_sdp_get_interface();
extern HearingAidInterface* btif_hearing_aid_get_interface();
/* LeAudio testi client */
extern LeAudioClientInterface* btif_le_audio_get_interface();
+/* Volume Control client */
+extern VolumeControlInterface* btif_volume_control_get_interface();
/*******************************************************************************
* Functions
@@ -454,6 +458,9 @@ static const void* get_profile_interface(const char* profile_id) {
if (is_profile(profile_id, BT_PROFILE_LE_AUDIO_ID))
return btif_le_audio_get_interface();
+ if (is_profile(profile_id, BT_PROFILE_VC_ID))
+ return btif_volume_control_get_interface();
+
return NULL;
}
diff --git a/system/btif/src/btif_dm.cc b/system/btif/src/btif_dm.cc
index 365aeffb6a..8d093d744f 100644
--- a/system/btif/src/btif_dm.cc
+++ b/system/btif/src/btif_dm.cc
@@ -81,6 +81,7 @@ using bluetooth::Uuid;
*****************************************************************************/
const Uuid UUID_HEARING_AID = Uuid::FromString("FDF0");
+const Uuid UUID_VC = Uuid::FromString("1844");
#define COD_MASK 0x07FF
@@ -1239,7 +1240,8 @@ static void btif_dm_search_devices_evt(tBTA_DM_SEARCH_EVT event,
/* Returns true if |uuid| should be passed as device property */
static bool btif_is_interesting_le_service(bluetooth::Uuid uuid) {
- return uuid.As16Bit() == UUID_SERVCLASS_LE_HID || uuid == UUID_HEARING_AID;
+ return (uuid.As16Bit() == UUID_SERVCLASS_LE_HID || uuid == UUID_HEARING_AID ||
+ uuid == UUID_VC);
}
/*******************************************************************************
diff --git a/system/btif/src/btif_vc.cc b/system/btif/src/btif_vc.cc
new file mode 100644
index 0000000000..bf51eb5913
--- /dev/null
+++ b/system/btif/src/btif_vc.cc
@@ -0,0 +1,125 @@
+/*
+ * Copyright 2021 HIMSA II K/S - www.himsa.com.
+ * Represented by EHIMA - www.ehima.com
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/* Volume Control Interface */
+
+#include <hardware/bluetooth.h>
+#include <hardware/bt_vc.h>
+
+#include <base/bind.h>
+#include <base/location.h>
+#include <base/logging.h>
+
+#include "bta_vc_api.h"
+#include "btif_common.h"
+#include "stack/include/btu.h"
+
+using base::Bind;
+using base::Unretained;
+using bluetooth::vc::ConnectionState;
+using bluetooth::vc::VolumeControlCallbacks;
+using bluetooth::vc::VolumeControlInterface;
+
+namespace {
+std::unique_ptr<VolumeControlInterface> vc_instance;
+
+class VolumeControlInterfaceImpl : public VolumeControlInterface,
+ public VolumeControlCallbacks {
+ ~VolumeControlInterfaceImpl() override = default;
+
+ void Init(VolumeControlCallbacks* callbacks) override {
+ DVLOG(2) << __func__;
+ this->callbacks_ = callbacks;
+ do_in_main_thread(FROM_HERE, Bind(&VolumeControl::Initialize, this));
+ }
+
+ void OnConnectionState(ConnectionState state,
+ const RawAddress& address) override {
+ DVLOG(2) << __func__ << " address: " << address;
+ do_in_jni_thread(FROM_HERE, Bind(&VolumeControlCallbacks::OnConnectionState,
+ Unretained(callbacks_), state, address));
+ }
+
+ void OnVolumeStateChanged(const RawAddress& address, uint8_t volume,
+ bool mute) override {
+ DVLOG(2) << __func__ << " address: " << address << "volume: " << volume
+ << "mute: " << mute;
+ do_in_jni_thread(FROM_HERE,
+ Bind(&VolumeControlCallbacks::OnVolumeStateChanged,
+ Unretained(callbacks_), address, volume, mute));
+ }
+
+ void OnGroupVolumeStateChanged(int group_id, uint8_t volume,
+ bool mute) override {
+ DVLOG(2) << __func__ << "group_id: " << group_id << "volume: " << volume
+ << "mute: " << mute;
+ do_in_jni_thread(FROM_HERE,
+ Bind(&VolumeControlCallbacks::OnGroupVolumeStateChanged,
+ Unretained(callbacks_), group_id, volume, mute));
+ }
+
+ void Connect(const RawAddress& address) override {
+ DVLOG(2) << __func__ << " address: " << address;
+ do_in_main_thread(FROM_HERE,
+ Bind(&VolumeControl::Connect,
+ Unretained(VolumeControl::Get()), address));
+ }
+
+ void Disconnect(const RawAddress& address) override {
+ DVLOG(2) << __func__ << " address: " << address;
+ do_in_main_thread(FROM_HERE,
+ Bind(&VolumeControl::Disconnect,
+ Unretained(VolumeControl::Get()), address));
+ }
+
+ void SetVolume(std::variant<RawAddress, int> addr_or_group_id,
+ uint8_t volume) override {
+ DVLOG(2) << __func__ << " volume: " << volume;
+ do_in_main_thread(FROM_HERE, Bind(&VolumeControl::SetVolume,
+ Unretained(VolumeControl::Get()),
+ std::move(addr_or_group_id), volume));
+ }
+
+ void RemoveDevice(const RawAddress& address) override {
+ DVLOG(2) << __func__ << " address: " << address;
+
+ /* RemoveDevice can be called on devices that don't have HA enabled */
+ if (VolumeControl::IsVolumeControlRunning()) {
+ do_in_main_thread(FROM_HERE,
+ Bind(&VolumeControl::Disconnect,
+ Unretained(VolumeControl::Get()), address));
+ }
+
+ /* Placeholder: Remove things from storage here */
+ }
+
+ void Cleanup(void) override {
+ DVLOG(2) << __func__;
+ do_in_main_thread(FROM_HERE, Bind(&VolumeControl::CleanUp));
+ }
+
+ private:
+ VolumeControlCallbacks* callbacks_;
+};
+
+} /* namespace */
+
+VolumeControlInterface* btif_volume_control_get_interface(void) {
+ if (!vc_instance) vc_instance.reset(new VolumeControlInterfaceImpl());
+
+ return vc_instance.get();
+}
diff --git a/system/include/hardware/bluetooth.h b/system/include/hardware/bluetooth.h
index cd7259529b..e0b9b08dd5 100644
--- a/system/include/hardware/bluetooth.h
+++ b/system/include/hardware/bluetooth.h
@@ -51,6 +51,7 @@
#define BT_PROFILE_LE_AUDIO_ID "le_audio"
#define BT_KEYSTORE_ID "bluetooth_keystore"
#define BT_ACTIVITY_ATTRIBUTION_ID "activity_attribution"
+#define BT_PROFILE_VC_ID "volume_control"
/** Bluetooth Device Name */
typedef struct { uint8_t name[249]; } __attribute__((packed)) bt_bdname_t;
diff --git a/system/include/hardware/bt_vc.h b/system/include/hardware/bt_vc.h
new file mode 100644
index 0000000000..f1a63d3dca
--- /dev/null
+++ b/system/include/hardware/bt_vc.h
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2021 HIMSA II K/S - www.himsa.com.
+ * Represented by EHIMA - www.ehima.com
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <hardware/bluetooth.h>
+
+#include <variant>
+
+namespace bluetooth {
+namespace vc {
+
+enum class ConnectionState {
+ DISCONNECTED = 0,
+ CONNECTING,
+ CONNECTED,
+ DISCONNECTING
+};
+
+class VolumeControlCallbacks {
+ public:
+ virtual ~VolumeControlCallbacks() = default;
+
+ /** Callback for profile connection state change */
+ virtual void OnConnectionState(ConnectionState state,
+ const RawAddress& address) = 0;
+
+ /* Callback for the volume change changed on the device */
+ virtual void OnVolumeStateChanged(const RawAddress& address, uint8_t volume,
+ bool mute) = 0;
+
+ /* Callback for the volume change changed on the group*/
+ virtual void OnGroupVolumeStateChanged(int group_id, uint8_t volume,
+ bool mute) = 0;
+};
+
+class VolumeControlInterface {
+ public:
+ virtual ~VolumeControlInterface() = default;
+
+ /** Register the Volume Control callbacks */
+ virtual void Init(VolumeControlCallbacks* callbacks) = 0;
+
+ /** Closes the interface */
+ virtual void Cleanup(void) = 0;
+
+ /** Connect to Volume Control */
+ virtual void Connect(const RawAddress& address) = 0;
+
+ /** Disconnect from Volume Control */
+ virtual void Disconnect(const RawAddress& address) = 0;
+
+ /** Called when Volume control devices is unbonded */
+ virtual void RemoveDevice(const RawAddress& address) = 0;
+
+ /** Set the volume */
+ virtual void SetVolume(std::variant<RawAddress, int> addr_or_group_id,
+ uint8_t volume) = 0;
+};
+
+} /* namespace vc */
+} /* namespace bluetooth */