summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJakub Tyszkowski <jakub.tyszkowski@codecoup.pl>2021-04-08 15:08:35 +0000
committerJack He <siyuanh@google.com>2022-02-02 15:18:56 -0800
commitb87fc995f2e21c0cbcac9b0f9374203f90c9ece8 (patch)
treecf5329f7cf17cf6ea7ac8052357bc7df92f50fc5
parenta014314c9490a18fd4b900302d873206df09a3e0 (diff)
hap: Add initial implementation
Implements Hearing Access Profile client. Bug: 150670922 Tag: #feature Test: atest BluetoothInstrumentationTests bluetooth_has_test bluetooth_has_test Sponsor: jpawlowski@ Change-Id: If3cc9f2146d08326800eb922d7e2f795cf110e7d Merged-In: If3cc9f2146d08326800eb922d7e2f795cf110e7d (cherry picked from commit 8e723645df2f56f0edc346302c9c1e5a39f82795)
-rw-r--r--android/app/jni/com_android_bluetooth.h2
-rw-r--r--android/app/jni/com_android_bluetooth_btservice_AdapterService.cpp7
-rw-r--r--android/app/jni/com_android_bluetooth_hap_client.cpp647
-rw-r--r--android/app/src/com/android/bluetooth/btservice/AdapterService.java29
-rw-r--r--android/app/src/com/android/bluetooth/btservice/storage/Metadata.java5
-rw-r--r--android/app/src/com/android/bluetooth/btservice/storage/MetadataDatabase.java21
-rw-r--r--android/app/src/com/android/bluetooth/btservice/storage/ProfilePrioritiesEntity.java3
-rw-r--r--android/app/src/com/android/bluetooth/gatt/GattService.java2
-rw-r--r--android/app/src/com/android/bluetooth/hap/HapClientNativeInterface.java424
-rw-r--r--android/app/src/com/android/bluetooth/hap/HapClientService.java964
-rw-r--r--android/app/src/com/android/bluetooth/hap/HapClientStackEvent.java291
-rw-r--r--android/app/src/com/android/bluetooth/hap/HapClientStateMachine.java588
-rw-r--r--android/app/tests/unit/src/com/android/bluetooth/btservice/storage/DatabaseManagerTest.java23
-rw-r--r--android/app/tests/unit/src/com/android/bluetooth/btservice/storage/schemas/com.android.bluetooth.btservice.storage.MetadataDatabase/110.json310
-rw-r--r--android/app/tests/unit/src/com/android/bluetooth/hap/HapClientStateMachineTest.java267
-rw-r--r--android/app/tests/unit/src/com/android/bluetooth/hap/HapClientTest.java883
-rw-r--r--framework/api/current.txt9
-rw-r--r--framework/api/system-current.txt18
-rw-r--r--framework/java/android/bluetooth/BluetoothAdapter.java7
-rw-r--r--framework/java/android/bluetooth/BluetoothHapClient.java1030
-rw-r--r--framework/java/android/bluetooth/BluetoothHapPresetInfo.java192
-rw-r--r--system/bta/Android.bp57
-rw-r--r--system/bta/has/has_client.cc2046
-rw-r--r--system/bta/has/has_client_test.cc3118
-rw-r--r--system/bta/has/has_ctp.cc285
-rw-r--r--system/bta/has/has_ctp.h257
-rw-r--r--system/bta/has/has_journal.cc56
-rw-r--r--system/bta/has/has_journal.h113
-rw-r--r--system/bta/has/has_preset.cc116
-rw-r--r--system/bta/has/has_preset.h114
-rw-r--r--system/bta/has/has_types.cc30
-rw-r--r--system/bta/has/has_types.h417
-rw-r--r--system/bta/include/bta_has_api.h54
-rw-r--r--system/bta/include/bta_le_audio_uuids.h20
-rw-r--r--system/bta/le_audio/le_audio_types.h3
-rw-r--r--system/bta/test/common/btif_storage_mock.cc43
-rw-r--r--system/bta/test/common/btif_storage_mock.h32
-rw-r--r--system/btif/Android.bp2
-rw-r--r--system/btif/include/btif_storage.h10
-rw-r--r--system/btif/src/bluetooth.cc9
-rw-r--r--system/btif/src/btif_dm.cc6
-rw-r--r--system/btif/src/btif_has_client.cc244
-rw-r--r--system/btif/src/btif_storage.cc137
-rw-r--r--system/include/hardware/bluetooth.h1
-rw-r--r--system/include/hardware/bt_has.h155
-rw-r--r--system/osi/test/alarm_mock.h7
-rw-r--r--system/test/Android.bp8
-rw-r--r--system/test/mock/mock_bta_has.cc55
-rw-r--r--system/test/stub/osi.cc2
49 files changed, 13114 insertions, 5 deletions
diff --git a/android/app/jni/com_android_bluetooth.h b/android/app/jni/com_android_bluetooth.h
index 5c16f042f2..6214e807e9 100644
--- a/android/app/jni/com_android_bluetooth.h
+++ b/android/app/jni/com_android_bluetooth.h
@@ -154,6 +154,8 @@ int register_com_android_bluetooth_sdp(JNIEnv* env);
int register_com_android_bluetooth_hearing_aid(JNIEnv* env);
+int register_com_android_bluetooth_hap_client(JNIEnv* env);
+
int register_com_android_bluetooth_btservice_BluetoothKeystore(JNIEnv* env);
int register_com_android_bluetooth_btservice_activity_attribution(JNIEnv* env);
diff --git a/android/app/jni/com_android_bluetooth_btservice_AdapterService.cpp b/android/app/jni/com_android_bluetooth_btservice_AdapterService.cpp
index e8e2643fec..db1f299f3f 100644
--- a/android/app/jni/com_android_bluetooth_btservice_AdapterService.cpp
+++ b/android/app/jni/com_android_bluetooth_btservice_AdapterService.cpp
@@ -1920,6 +1920,13 @@ jint JNI_OnLoad(JavaVM* jvm, void* reserved) {
return JNI_ERR;
}
+ status = android::register_com_android_bluetooth_hap_client(e);
+ if (status < 0) {
+ ALOGE("jni le audio hearing access client registration failure: %d",
+ status);
+ return JNI_ERR;
+ }
+
status = android::register_com_android_bluetooth_le_audio(e);
if (status < 0) {
ALOGE("jni le_audio registration failure: %d", status);
diff --git a/android/app/jni/com_android_bluetooth_hap_client.cpp b/android/app/jni/com_android_bluetooth_hap_client.cpp
new file mode 100644
index 0000000000..e94cb59f76
--- /dev/null
+++ b/android/app/jni/com_android_bluetooth_hap_client.cpp
@@ -0,0 +1,647 @@
+/*
+ * 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.
+ */
+
+#define LOG_TAG "BluetoothHapClientJni"
+
+#define LOG_NDEBUG 0
+
+#include <string.h>
+
+#include <shared_mutex>
+
+#include "base/logging.h"
+#include "com_android_bluetooth.h"
+#include "hardware/bt_has.h"
+
+using bluetooth::has::ConnectionState;
+using bluetooth::has::ErrorCode;
+using bluetooth::has::HasClientCallbacks;
+using bluetooth::has::HasClientInterface;
+using bluetooth::has::PresetInfo;
+using bluetooth::has::PresetInfoReason;
+
+namespace android {
+static jmethodID method_onConnectionStateChanged;
+static jmethodID method_onDeviceAvailable;
+static jmethodID method_onFeaturesUpdate;
+static jmethodID method_onActivePresetSelected;
+static jmethodID method_onGroupActivePresetSelected;
+static jmethodID method_onActivePresetSelectError;
+static jmethodID method_onGroupActivePresetSelectError;
+static jmethodID method_onPresetInfo;
+static jmethodID method_onGroupPresetInfo;
+static jmethodID method_onPresetInfoError;
+static jmethodID method_onGroupPresetInfoError;
+static jmethodID method_onPresetNameSetError;
+static jmethodID method_onGroupPresetNameSetError;
+
+static HasClientInterface* sHasClientInterface = nullptr;
+static std::shared_timed_mutex interface_mutex;
+
+static jobject mCallbacksObj = nullptr;
+static std::shared_timed_mutex callbacks_mutex;
+
+static struct {
+ jclass clazz;
+ jmethodID constructor;
+ jmethodID getCodecType;
+ jmethodID getCodecPriority;
+ jmethodID getSampleRate;
+ jmethodID getBitsPerSample;
+ jmethodID getChannelMode;
+ jmethodID getCodecSpecific1;
+ jmethodID getCodecSpecific2;
+ jmethodID getCodecSpecific3;
+ jmethodID getCodecSpecific4;
+} android_bluetooth_BluetoothHapPresetInfo;
+
+class HasClientCallbacksImpl : public HasClientCallbacks {
+ public:
+ ~HasClientCallbacksImpl() = default;
+
+ void OnConnectionState(ConnectionState state,
+ const RawAddress& bd_addr) override {
+ LOG(INFO) << __func__;
+
+ std::shared_lock<std::shared_timed_mutex> lock(callbacks_mutex);
+ CallbackEnv sCallbackEnv(__func__);
+ if (!sCallbackEnv.valid() || mCallbacksObj == nullptr) return;
+
+ ScopedLocalRef<jbyteArray> addr(
+ sCallbackEnv.get(), sCallbackEnv->NewByteArray(sizeof(RawAddress)));
+ if (!addr.get()) {
+ LOG(ERROR) << "Failed to new bd addr jbyteArray for connection state";
+ return;
+ }
+
+ sCallbackEnv->SetByteArrayRegion(addr.get(), 0, sizeof(RawAddress),
+ (jbyte*)&bd_addr);
+ sCallbackEnv->CallVoidMethod(mCallbacksObj, method_onConnectionStateChanged,
+ (jint)state, addr.get());
+ }
+
+ void OnDeviceAvailable(const RawAddress& bd_addr, uint8_t features) override {
+ std::shared_lock<std::shared_timed_mutex> lock(callbacks_mutex);
+ CallbackEnv sCallbackEnv(__func__);
+ if (!sCallbackEnv.valid() || mCallbacksObj == nullptr) return;
+
+ ScopedLocalRef<jbyteArray> addr(
+ sCallbackEnv.get(), sCallbackEnv->NewByteArray(sizeof(RawAddress)));
+ if (!addr.get()) {
+ LOG(ERROR) << "Failed to new bd addr jbyteArray for device available";
+ return;
+ }
+ sCallbackEnv->SetByteArrayRegion(addr.get(), 0, sizeof(RawAddress),
+ (jbyte*)&bd_addr);
+
+ sCallbackEnv->CallVoidMethod(mCallbacksObj, method_onDeviceAvailable,
+ addr.get(), (jint)features);
+ }
+
+ void OnFeaturesUpdate(const RawAddress& bd_addr, uint8_t features) override {
+ std::shared_lock<std::shared_timed_mutex> lock(callbacks_mutex);
+ CallbackEnv sCallbackEnv(__func__);
+ if (!sCallbackEnv.valid() || mCallbacksObj == nullptr) return;
+
+ ScopedLocalRef<jbyteArray> addr(
+ sCallbackEnv.get(), sCallbackEnv->NewByteArray(sizeof(RawAddress)));
+ if (!addr.get()) {
+ LOG(ERROR) << "Failed to new bd addr jbyteArray for device available";
+ return;
+ }
+ sCallbackEnv->SetByteArrayRegion(addr.get(), 0, sizeof(RawAddress),
+ (jbyte*)&bd_addr);
+
+ sCallbackEnv->CallVoidMethod(mCallbacksObj, method_onFeaturesUpdate,
+ addr.get(), (jint)features);
+ }
+
+ void OnActivePresetSelected(std::variant<RawAddress, int> addr_or_group_id,
+ uint8_t preset_index) override {
+ std::shared_lock<std::shared_timed_mutex> lock(callbacks_mutex);
+ CallbackEnv sCallbackEnv(__func__);
+ if (!sCallbackEnv.valid() || mCallbacksObj == nullptr) return;
+
+ if (std::holds_alternative<RawAddress>(addr_or_group_id)) {
+ ScopedLocalRef<jbyteArray> addr(
+ sCallbackEnv.get(), sCallbackEnv->NewByteArray(sizeof(RawAddress)));
+ if (!addr.get()) {
+ LOG(ERROR) << "Failed to new bd addr jbyteArray for preset selected";
+ return;
+ }
+ sCallbackEnv->SetByteArrayRegion(
+ addr.get(), 0, sizeof(RawAddress),
+ (jbyte*)&std::get<RawAddress>(addr_or_group_id));
+
+ sCallbackEnv->CallVoidMethod(mCallbacksObj, method_onActivePresetSelected,
+ addr.get(), (jint)preset_index);
+ } else {
+ sCallbackEnv->CallVoidMethod(
+ mCallbacksObj, method_onGroupActivePresetSelected,
+ std::get<int>(addr_or_group_id), (jint)preset_index);
+ }
+ }
+
+ void OnActivePresetSelectError(std::variant<RawAddress, int> addr_or_group_id,
+ ErrorCode error_code) override {
+ std::shared_lock<std::shared_timed_mutex> lock(callbacks_mutex);
+ CallbackEnv sCallbackEnv(__func__);
+ if (!sCallbackEnv.valid() || mCallbacksObj == nullptr) return;
+
+ if (std::holds_alternative<RawAddress>(addr_or_group_id)) {
+ ScopedLocalRef<jbyteArray> addr(
+ sCallbackEnv.get(), sCallbackEnv->NewByteArray(sizeof(RawAddress)));
+ if (!addr.get()) {
+ LOG(ERROR)
+ << "Failed to new bd addr jbyteArray for preset select error";
+ return;
+ }
+ sCallbackEnv->SetByteArrayRegion(
+ addr.get(), 0, sizeof(RawAddress),
+ (jbyte*)&std::get<RawAddress>(addr_or_group_id));
+
+ sCallbackEnv->CallVoidMethod(mCallbacksObj,
+ method_onActivePresetSelectError, addr.get(),
+ (jint)error_code);
+ } else {
+ sCallbackEnv->CallVoidMethod(
+ mCallbacksObj, method_onGroupActivePresetSelectError,
+ std::get<int>(addr_or_group_id), (jint)error_code);
+ }
+ }
+
+ void OnPresetInfo(std::variant<RawAddress, int> addr_or_group_id,
+ PresetInfoReason info_reason,
+ std::vector<PresetInfo> detail_records) override {
+ std::shared_lock<std::shared_timed_mutex> lock(callbacks_mutex);
+ CallbackEnv sCallbackEnv(__func__);
+ if (!sCallbackEnv.valid() || mCallbacksObj == nullptr) return;
+
+ jsize i = 0;
+ jobjectArray presets_array = sCallbackEnv->NewObjectArray(
+ (jsize)detail_records.size(),
+ android_bluetooth_BluetoothHapPresetInfo.clazz, nullptr);
+
+ const char null_str[] = "";
+ for (auto const& info : detail_records) {
+ const char* name = info.preset_name.c_str();
+ if (!sCallbackEnv.isValidUtf(name)) {
+ ALOGE("%s: name is not a valid UTF string.", __func__);
+ name = null_str;
+ }
+
+ ScopedLocalRef<jstring> name_str(sCallbackEnv.get(),
+ sCallbackEnv->NewStringUTF(name));
+ if (!name_str.get()) {
+ LOG(ERROR) << "Failed to new preset name String for preset name";
+ return;
+ }
+
+ jobject infoObj = sCallbackEnv->NewObject(
+ android_bluetooth_BluetoothHapPresetInfo.clazz,
+ android_bluetooth_BluetoothHapPresetInfo.constructor,
+ (jint)info.preset_index, name_str.get(), (jboolean)info.writable,
+ (jboolean)info.available);
+ sCallbackEnv->SetObjectArrayElement(presets_array, i++, infoObj);
+ sCallbackEnv->DeleteLocalRef(infoObj);
+ }
+
+ if (std::holds_alternative<RawAddress>(addr_or_group_id)) {
+ ScopedLocalRef<jbyteArray> addr(
+ sCallbackEnv.get(), sCallbackEnv->NewByteArray(sizeof(RawAddress)));
+ if (!addr.get()) {
+ LOG(ERROR) << "Failed to new bd addr jbyteArray for preset name";
+ return;
+ }
+ sCallbackEnv->SetByteArrayRegion(
+ addr.get(), 0, sizeof(RawAddress),
+ (jbyte*)&std::get<RawAddress>(addr_or_group_id));
+
+ sCallbackEnv->CallVoidMethod(mCallbacksObj, method_onPresetInfo,
+ addr.get(), (jint)info_reason,
+ presets_array);
+ } else {
+ sCallbackEnv->CallVoidMethod(mCallbacksObj, method_onGroupPresetInfo,
+ std::get<int>(addr_or_group_id),
+ (jint)info_reason, presets_array);
+ }
+ }
+
+ virtual void OnPresetInfoError(std::variant<RawAddress, int> addr_or_group_id,
+ uint8_t preset_index,
+ ErrorCode error_code) override {
+ std::shared_lock<std::shared_timed_mutex> lock(callbacks_mutex);
+ CallbackEnv sCallbackEnv(__func__);
+ if (!sCallbackEnv.valid() || mCallbacksObj == nullptr) return;
+
+ if (std::holds_alternative<RawAddress>(addr_or_group_id)) {
+ ScopedLocalRef<jbyteArray> addr(
+ sCallbackEnv.get(), sCallbackEnv->NewByteArray(sizeof(RawAddress)));
+ if (!addr.get()) {
+ LOG(ERROR)
+ << "Failed to new bd addr jbyteArray for preset name get error";
+ return;
+ }
+ sCallbackEnv->SetByteArrayRegion(
+ addr.get(), 0, sizeof(RawAddress),
+ (jbyte*)&std::get<RawAddress>(addr_or_group_id));
+
+ sCallbackEnv->CallVoidMethod(mCallbacksObj, method_onPresetInfoError,
+ addr.get(), (jint)preset_index,
+ (jint)error_code);
+ } else {
+ sCallbackEnv->CallVoidMethod(mCallbacksObj, method_onGroupPresetInfoError,
+ std::get<int>(addr_or_group_id),
+ (jint)preset_index, (jint)error_code);
+ }
+ }
+
+ void OnSetPresetNameError(std::variant<RawAddress, int> addr_or_group_id,
+ uint8_t preset_index,
+ ErrorCode error_code) override {
+ std::shared_lock<std::shared_timed_mutex> lock(callbacks_mutex);
+ CallbackEnv sCallbackEnv(__func__);
+ if (!sCallbackEnv.valid() || mCallbacksObj == nullptr) return;
+
+ if (std::holds_alternative<RawAddress>(addr_or_group_id)) {
+ ScopedLocalRef<jbyteArray> addr(
+ sCallbackEnv.get(), sCallbackEnv->NewByteArray(sizeof(RawAddress)));
+ if (!addr.get()) {
+ LOG(ERROR)
+ << "Failed to new bd addr jbyteArray for preset name set error";
+ return;
+ }
+ sCallbackEnv->SetByteArrayRegion(
+ addr.get(), 0, sizeof(RawAddress),
+ (jbyte*)&std::get<RawAddress>(addr_or_group_id));
+
+ sCallbackEnv->CallVoidMethod(mCallbacksObj, method_onPresetNameSetError,
+ addr.get(), (jint)preset_index,
+ (jint)error_code);
+ } else {
+ sCallbackEnv->CallVoidMethod(mCallbacksObj,
+ method_onGroupPresetNameSetError,
+ std::get<int>(addr_or_group_id),
+ (jint)preset_index, (jint)error_code);
+ }
+ }
+};
+
+static HasClientCallbacksImpl sHasClientCallbacks;
+
+static void classInitNative(JNIEnv* env, jclass clazz) {
+ jclass jniBluetoothBluetoothHapPresetInfoClass =
+ env->FindClass("android/bluetooth/BluetoothHapPresetInfo");
+ CHECK(jniBluetoothBluetoothHapPresetInfoClass != NULL);
+
+ android_bluetooth_BluetoothHapPresetInfo.constructor =
+ env->GetMethodID(jniBluetoothBluetoothHapPresetInfoClass, "<init>",
+ "(ILjava/lang/String;ZZ)V");
+
+ method_onConnectionStateChanged =
+ env->GetMethodID(clazz, "onConnectionStateChanged", "(I[B)V");
+
+ method_onDeviceAvailable =
+ env->GetMethodID(clazz, "onDeviceAvailable", "([BI)V");
+
+ method_onFeaturesUpdate =
+ env->GetMethodID(clazz, "onFeaturesUpdate", "([BI)V");
+
+ method_onActivePresetSelected =
+ env->GetMethodID(clazz, "onActivePresetSelected", "([BI)V");
+
+ method_onGroupActivePresetSelected =
+ env->GetMethodID(clazz, "onActivePresetGroupSelected", "(II)V");
+
+ method_onActivePresetSelectError =
+ env->GetMethodID(clazz, "onActivePresetSelectError", "([BI)V");
+
+ method_onGroupActivePresetSelectError =
+ env->GetMethodID(clazz, "onActivePresetGroupSelectError", "(II)V");
+
+ method_onPresetInfo =
+ env->GetMethodID(clazz, "onPresetInfo",
+ "([BI[Landroid/bluetooth/BluetoothHapPresetInfo;)V");
+
+ method_onGroupPresetInfo =
+ env->GetMethodID(clazz, "onGroupPresetInfo",
+ "(II[Landroid/bluetooth/BluetoothHapPresetInfo;)V");
+
+ method_onPresetNameSetError =
+ env->GetMethodID(clazz, "onPresetNameSetError", "([BII)V");
+
+ method_onGroupPresetNameSetError =
+ env->GetMethodID(clazz, "onGroupPresetNameSetError", "(III)V");
+
+ method_onPresetInfoError =
+ env->GetMethodID(clazz, "onPresetInfoError", "([BII)V");
+
+ method_onGroupPresetInfoError =
+ env->GetMethodID(clazz, "onGroupPresetInfoError", "(III)V");
+
+ LOG(INFO) << __func__ << ": succeeds";
+}
+
+static void initNative(JNIEnv* env, jobject object) {
+ std::unique_lock<std::shared_timed_mutex> interface_lock(interface_mutex);
+ std::unique_lock<std::shared_timed_mutex> callbacks_lock(callbacks_mutex);
+
+ const bt_interface_t* btInf = getBluetoothInterface();
+ if (btInf == nullptr) {
+ LOG(ERROR) << "Bluetooth module is not loaded";
+ return;
+ }
+
+ if (sHasClientInterface != nullptr) {
+ LOG(INFO) << "Cleaning up HearingAid Interface before initializing...";
+ sHasClientInterface->Cleanup();
+ sHasClientInterface = nullptr;
+ }
+
+ if (mCallbacksObj != nullptr) {
+ LOG(INFO) << "Cleaning up HearingAid callback object";
+ env->DeleteGlobalRef(mCallbacksObj);
+ mCallbacksObj = nullptr;
+ }
+
+ if ((mCallbacksObj = env->NewGlobalRef(object)) == nullptr) {
+ LOG(ERROR) << "Failed to allocate Global Ref for Hearing Access Callbacks";
+ return;
+ }
+
+ android_bluetooth_BluetoothHapPresetInfo.clazz = (jclass)env->NewGlobalRef(
+ env->FindClass("android/bluetooth/BluetoothHapPresetInfo"));
+ if (android_bluetooth_BluetoothHapPresetInfo.clazz == nullptr) {
+ ALOGE("%s: Failed to allocate Global Ref for BluetoothHapPresetInfo class",
+ __func__);
+ return;
+ }
+
+ sHasClientInterface = (HasClientInterface*)btInf->get_profile_interface(
+ BT_PROFILE_HAP_CLIENT_ID);
+ if (sHasClientInterface == nullptr) {
+ LOG(ERROR)
+ << "Failed to get Bluetooth Hearing Access Service Client Interface";
+ return;
+ }
+
+ sHasClientInterface->Init(&sHasClientCallbacks);
+}
+
+static void cleanupNative(JNIEnv* env, jobject object) {
+ std::unique_lock<std::shared_timed_mutex> interface_lock(interface_mutex);
+ std::unique_lock<std::shared_timed_mutex> callbacks_lock(callbacks_mutex);
+
+ const bt_interface_t* btInf = getBluetoothInterface();
+ if (btInf == nullptr) {
+ LOG(ERROR) << "Bluetooth module is not loaded";
+ return;
+ }
+
+ if (sHasClientInterface != nullptr) {
+ sHasClientInterface->Cleanup();
+ sHasClientInterface = nullptr;
+ }
+
+ if (mCallbacksObj != nullptr) {
+ env->DeleteGlobalRef(mCallbacksObj);
+ mCallbacksObj = nullptr;
+ }
+}
+
+static jboolean connectHapClientNative(JNIEnv* env, jobject object,
+ jbyteArray address) {
+ std::shared_lock<std::shared_timed_mutex> lock(interface_mutex);
+ if (!sHasClientInterface) {
+ LOG(ERROR) << __func__ << ": Failed to get the Bluetooth HAP Interface";
+ return JNI_FALSE;
+ }
+
+ jbyte* addr = env->GetByteArrayElements(address, nullptr);
+ if (!addr) {
+ jniThrowIOException(env, EINVAL);
+ return JNI_FALSE;
+ }
+
+ RawAddress* tmpraw = (RawAddress*)addr;
+ sHasClientInterface->Connect(*tmpraw);
+ env->ReleaseByteArrayElements(address, addr, 0);
+ return JNI_TRUE;
+}
+
+static jboolean disconnectHapClientNative(JNIEnv* env, jobject object,
+ jbyteArray address) {
+ std::shared_lock<std::shared_timed_mutex> lock(interface_mutex);
+ if (!sHasClientInterface) {
+ LOG(ERROR) << __func__ << ": Failed to get the Bluetooth HAP Interface";
+ return JNI_FALSE;
+ }
+
+ jbyte* addr = env->GetByteArrayElements(address, nullptr);
+ if (!addr) {
+ jniThrowIOException(env, EINVAL);
+ return JNI_FALSE;
+ }
+
+ RawAddress* tmpraw = (RawAddress*)addr;
+ sHasClientInterface->Disconnect(*tmpraw);
+ env->ReleaseByteArrayElements(address, addr, 0);
+ return JNI_TRUE;
+}
+
+static void selectActivePresetNative(JNIEnv* env, jobject object,
+ jbyteArray address, jint preset_index) {
+ std::shared_lock<std::shared_timed_mutex> lock(interface_mutex);
+ if (!sHasClientInterface) {
+ LOG(ERROR) << __func__ << ": Failed to get the Bluetooth HAP Interface";
+ return;
+ }
+
+ jbyte* addr = env->GetByteArrayElements(address, nullptr);
+ if (!addr) {
+ jniThrowIOException(env, EINVAL);
+ return;
+ }
+
+ RawAddress* tmpraw = (RawAddress*)addr;
+ sHasClientInterface->SelectActivePreset(*tmpraw, preset_index);
+ env->ReleaseByteArrayElements(address, addr, 0);
+}
+
+static void groupSelectActivePresetNative(JNIEnv* env, jobject object,
+ jint group_id, jint preset_index) {
+ std::shared_lock<std::shared_timed_mutex> lock(interface_mutex);
+ if (!sHasClientInterface) {
+ LOG(ERROR) << __func__ << ": Failed to get the Bluetooth HAP Interface";
+ return;
+ }
+
+ sHasClientInterface->SelectActivePreset(group_id, preset_index);
+}
+
+static void nextActivePresetNative(JNIEnv* env, jobject object,
+ jbyteArray address) {
+ std::shared_lock<std::shared_timed_mutex> lock(interface_mutex);
+ if (!sHasClientInterface) {
+ LOG(ERROR) << __func__ << ": Failed to get the Bluetooth HAP Interface";
+ return;
+ }
+
+ jbyte* addr = env->GetByteArrayElements(address, nullptr);
+ if (!addr) {
+ jniThrowIOException(env, EINVAL);
+ return;
+ }
+
+ RawAddress* tmpraw = (RawAddress*)addr;
+ sHasClientInterface->NextActivePreset(*tmpraw);
+ env->ReleaseByteArrayElements(address, addr, 0);
+}
+
+static void groupNextActivePresetNative(JNIEnv* env, jobject object,
+ jint group_id) {
+ std::shared_lock<std::shared_timed_mutex> lock(interface_mutex);
+ if (!sHasClientInterface) {
+ LOG(ERROR) << __func__ << ": Failed to get the Bluetooth HAP Interface";
+ return;
+ }
+
+ sHasClientInterface->NextActivePreset(group_id);
+}
+
+static void previousActivePresetNative(JNIEnv* env, jobject object,
+ jbyteArray address) {
+ std::shared_lock<std::shared_timed_mutex> lock(interface_mutex);
+ if (!sHasClientInterface) {
+ LOG(ERROR) << __func__ << ": Failed to get the Bluetooth HAP Interface";
+ return;
+ }
+
+ jbyte* addr = env->GetByteArrayElements(address, nullptr);
+ if (!addr) {
+ jniThrowIOException(env, EINVAL);
+ return;
+ }
+
+ RawAddress* tmpraw = (RawAddress*)addr;
+ sHasClientInterface->PreviousActivePreset(*tmpraw);
+ env->ReleaseByteArrayElements(address, addr, 0);
+}
+
+static void groupPreviousActivePresetNative(JNIEnv* env, jobject object,
+ jint group_id) {
+ std::shared_lock<std::shared_timed_mutex> lock(interface_mutex);
+ if (!sHasClientInterface) {
+ LOG(ERROR) << __func__ << ": Failed to get the Bluetooth HAP Interface";
+ return;
+ }
+
+ sHasClientInterface->PreviousActivePreset(group_id);
+}
+
+static void getPresetInfoNative(JNIEnv* env, jobject object, jbyteArray address,
+ jint preset_index) {
+ std::shared_lock<std::shared_timed_mutex> lock(interface_mutex);
+ if (!sHasClientInterface) {
+ LOG(ERROR) << __func__ << ": Failed to get the Bluetooth HAP Interface";
+ return;
+ }
+
+ jbyte* addr = env->GetByteArrayElements(address, nullptr);
+ if (!addr) {
+ jniThrowIOException(env, EINVAL);
+ return;
+ }
+
+ RawAddress* tmpraw = (RawAddress*)addr;
+ sHasClientInterface->GetPresetInfo(*tmpraw, preset_index);
+ env->ReleaseByteArrayElements(address, addr, 0);
+}
+
+static void setPresetNameNative(JNIEnv* env, jobject object, jbyteArray address,
+ jint preset_index, jstring name) {
+ std::shared_lock<std::shared_timed_mutex> lock(interface_mutex);
+ if (!sHasClientInterface) {
+ LOG(ERROR) << __func__ << ": Failed to get the Bluetooth HAP Interface";
+ return;
+ }
+
+ jbyte* addr = env->GetByteArrayElements(address, nullptr);
+ if (!addr) {
+ jniThrowIOException(env, EINVAL);
+ return;
+ }
+
+ std::string name_str;
+ if (name != nullptr) {
+ const char* value = env->GetStringUTFChars(name, nullptr);
+ name_str = std::string(value);
+ env->ReleaseStringUTFChars(name, value);
+ }
+
+ RawAddress* tmpraw = (RawAddress*)addr;
+ sHasClientInterface->SetPresetName(*tmpraw, preset_index,
+ std::move(name_str));
+ env->ReleaseByteArrayElements(address, addr, 0);
+}
+
+static void groupSetPresetNameNative(JNIEnv* env, jobject object, jint group_id,
+ jint preset_index, jstring name) {
+ std::shared_lock<std::shared_timed_mutex> lock(interface_mutex);
+ if (!sHasClientInterface) {
+ LOG(ERROR) << __func__ << ": Failed to get the Bluetooth HAP Interface";
+ return;
+ }
+
+ std::string name_str;
+ if (name != nullptr) {
+ const char* value = env->GetStringUTFChars(name, nullptr);
+ name_str = std::string(value);
+ env->ReleaseStringUTFChars(name, value);
+ }
+
+ sHasClientInterface->SetPresetName(group_id, preset_index,
+ std::move(name_str));
+}
+
+static JNINativeMethod sMethods[] = {
+ {"classInitNative", "()V", (void*)classInitNative},
+ {"initNative", "()V", (void*)initNative},
+ {"cleanupNative", "()V", (void*)cleanupNative},
+ {"connectHapClientNative", "([B)Z", (void*)connectHapClientNative},
+ {"disconnectHapClientNative", "([B)Z", (void*)disconnectHapClientNative},
+ {"selectActivePresetNative", "([BI)V", (void*)selectActivePresetNative},
+ {"groupSelectActivePresetNative", "(II)V",
+ (void*)groupSelectActivePresetNative},
+ {"nextActivePresetNative", "([B)V", (void*)nextActivePresetNative},
+ {"groupNextActivePresetNative", "(I)V", (void*)groupNextActivePresetNative},
+ {"previousActivePresetNative", "([B)V", (void*)previousActivePresetNative},
+ {"groupPreviousActivePresetNative", "(I)V",
+ (void*)groupPreviousActivePresetNative},
+ {"getPresetInfoNative", "([BI)V", (void*)getPresetInfoNative},
+ {"setPresetNameNative", "([BILjava/lang/String;)V",
+ (void*)setPresetNameNative},
+ {"groupSetPresetNameNative", "(IILjava/lang/String;)V",
+ (void*)groupSetPresetNameNative},
+};
+
+int register_com_android_bluetooth_hap_client(JNIEnv* env) {
+ return jniRegisterNativeMethods(
+ env, "com/android/bluetooth/hap/HapClientNativeInterface", sMethods,
+ NELEM(sMethods));
+}
+} // namespace android
diff --git a/android/app/src/com/android/bluetooth/btservice/AdapterService.java b/android/app/src/com/android/bluetooth/btservice/AdapterService.java
index 548a321a18..9d6f76d015 100644
--- a/android/app/src/com/android/bluetooth/btservice/AdapterService.java
+++ b/android/app/src/com/android/bluetooth/btservice/AdapterService.java
@@ -110,6 +110,7 @@ import com.android.bluetooth.btservice.storage.DatabaseManager;
import com.android.bluetooth.btservice.storage.MetadataDatabase;
import com.android.bluetooth.csip.CsipSetCoordinatorService;
import com.android.bluetooth.gatt.GattService;
+import com.android.bluetooth.hap.HapClientService;
import com.android.bluetooth.hearingaid.HearingAidService;
import com.android.bluetooth.hfp.HeadsetService;
import com.android.bluetooth.hfpclient.HeadsetClientService;
@@ -319,6 +320,7 @@ public class AdapterService extends Service {
private BluetoothPbapService mPbapService;
private PbapClientService mPbapClientService;
private HearingAidService mHearingAidService;
+ private HapClientService mHapClientService;
private SapService mSapService;
private VolumeControlService mVolumeControlService;
private CsipSetCoordinatorService mCsipSetCoordinatorService;
@@ -1074,6 +1076,9 @@ public class AdapterService extends Service {
if (profile == BluetoothProfile.LE_AUDIO) {
return Utils.arrayContains(remoteDeviceUuids, BluetoothUuid.LE_AUDIO);
}
+ if (profile == BluetoothProfile.HAP_CLIENT) {
+ return Utils.arrayContains(remoteDeviceUuids, BluetoothUuid.HAS);
+ }
Log.e(TAG, "isSupported: Unexpected profile passed in to function: " + profile);
return false;
@@ -1124,6 +1129,10 @@ public class AdapterService extends Service {
> BluetoothProfile.CONNECTION_POLICY_FORBIDDEN) {
return true;
}
+ if (mHapClientService != null && mHapClientService.getConnectionPolicy(device)
+ > BluetoothProfile.CONNECTION_POLICY_FORBIDDEN) {
+ return true;
+ }
if (mVolumeControlService != null && mVolumeControlService.getConnectionPolicy(device)
> BluetoothProfile.CONNECTION_POLICY_FORBIDDEN) {
return true;
@@ -1213,6 +1222,13 @@ public class AdapterService extends Service {
Log.i(TAG, "connectEnabledProfiles: Connecting Hearing Aid Profile");
mHearingAidService.connect(device);
}
+ if (mHapClientService != null && isSupported(localDeviceUuids, remoteDeviceUuids,
+ BluetoothProfile.HAP_CLIENT, device)
+ && mHapClientService.getConnectionPolicy(device)
+ > BluetoothProfile.CONNECTION_POLICY_FORBIDDEN) {
+ Log.i(TAG, "connectEnabledProfiles: Connecting HAS Profile");
+ mHapClientService.connect(device);
+ }
if (mVolumeControlService != null && isSupported(localDeviceUuids, remoteDeviceUuids,
BluetoothProfile.VOLUME_CONTROL, device)
&& mVolumeControlService.getConnectionPolicy(device)
@@ -1270,6 +1286,7 @@ public class AdapterService extends Service {
mPbapService = BluetoothPbapService.getBluetoothPbapService();
mPbapClientService = PbapClientService.getPbapClientService();
mHearingAidService = HearingAidService.getHearingAidService();
+ mHapClientService = HapClientService.getHapClientService();
mSapService = SapService.getSapService();
mVolumeControlService = VolumeControlService.getVolumeControlService();
mCsipSetCoordinatorService = CsipSetCoordinatorService.getCsipSetCoordinatorService();
@@ -4169,6 +4186,13 @@ public class AdapterService extends Service {
BluetoothProfile.CONNECTION_POLICY_ALLOWED);
numProfilesConnected++;
}
+ if (mHapClientService != null && isSupported(localDeviceUuids, remoteDeviceUuids,
+ BluetoothProfile.HAP_CLIENT, device)) {
+ Log.i(TAG, "connectAllEnabledProfiles: Connecting Hearing Access Client Profile");
+ mHapClientService.setConnectionPolicy(device,
+ BluetoothProfile.CONNECTION_POLICY_ALLOWED);
+ numProfilesConnected++;
+ }
if (mVolumeControlService != null && isSupported(localDeviceUuids, remoteDeviceUuids,
BluetoothProfile.VOLUME_CONTROL, device)) {
Log.i(TAG, "connectAllEnabledProfiles: Connecting Volume Control Profile");
@@ -4272,6 +4296,11 @@ public class AdapterService extends Service {
Log.i(TAG, "disconnectAllEnabledProfiles: Disconnecting Hearing Aid Profile");
mHearingAidService.disconnect(device);
}
+ if (mHapClientService != null && mHapClientService.getConnectionState(device)
+ == BluetoothProfile.STATE_CONNECTED) {
+ Log.i(TAG, "disconnectAllEnabledProfiles: Disconnecting Hearing Access Profile Client");
+ mHapClientService.disconnect(device);
+ }
if (mVolumeControlService != null && mVolumeControlService.getConnectionState(device)
== BluetoothProfile.STATE_CONNECTED) {
Log.i(TAG, "disconnectAllEnabledProfiles: Disconnecting Volume Control Profile");
diff --git a/android/app/src/com/android/bluetooth/btservice/storage/Metadata.java b/android/app/src/com/android/bluetooth/btservice/storage/Metadata.java
index 8ac06d0edf..0de7e6b2c9 100644
--- a/android/app/src/com/android/bluetooth/btservice/storage/Metadata.java
+++ b/android/app/src/com/android/bluetooth/btservice/storage/Metadata.java
@@ -110,6 +110,9 @@ class Metadata {
case BluetoothProfile.HEARING_AID:
profileConnectionPolicies.hearing_aid_connection_policy = connectionPolicy;
break;
+ case BluetoothProfile.HAP_CLIENT:
+ profileConnectionPolicies.hap_client_connection_policy = connectionPolicy;
+ break;
case BluetoothProfile.LE_AUDIO:
profileConnectionPolicies.le_audio_connection_policy = connectionPolicy;
break;
@@ -153,6 +156,8 @@ class Metadata {
return profileConnectionPolicies.sap_connection_policy;
case BluetoothProfile.HEARING_AID:
return profileConnectionPolicies.hearing_aid_connection_policy;
+ case BluetoothProfile.HAP_CLIENT:
+ return profileConnectionPolicies.hap_client_connection_policy;
case BluetoothProfile.LE_AUDIO:
return profileConnectionPolicies.le_audio_connection_policy;
case BluetoothProfile.VOLUME_CONTROL:
diff --git a/android/app/src/com/android/bluetooth/btservice/storage/MetadataDatabase.java b/android/app/src/com/android/bluetooth/btservice/storage/MetadataDatabase.java
index 3a1201536d..2b1ca5e6de 100644
--- a/android/app/src/com/android/bluetooth/btservice/storage/MetadataDatabase.java
+++ b/android/app/src/com/android/bluetooth/btservice/storage/MetadataDatabase.java
@@ -33,7 +33,7 @@ import java.util.List;
/**
* MetadataDatabase is a Room database stores Bluetooth persistence data
*/
-@Database(entities = {Metadata.class}, version = 109)
+@Database(entities = {Metadata.class}, version = 110)
public abstract class MetadataDatabase extends RoomDatabase {
/**
* The metadata database file name
@@ -62,6 +62,7 @@ public abstract class MetadataDatabase extends RoomDatabase {
.addMigrations(MIGRATION_106_107)
.addMigrations(MIGRATION_107_108)
.addMigrations(MIGRATION_108_109)
+ .addMigrations(MIGRATION_109_110)
.allowMainThreadQueries()
.build();
}
@@ -407,4 +408,22 @@ public abstract class MetadataDatabase extends RoomDatabase {
}
}
};
+
+ @VisibleForTesting
+ static final Migration MIGRATION_109_110 = new Migration(109, 110) {
+ @Override
+ public void migrate(SupportSQLiteDatabase database) {
+ try {
+ database.execSQL(
+ "ALTER TABLE metadata ADD COLUMN `hap_client_connection_policy` "
+ + "INTEGER DEFAULT 100");
+ } catch (SQLException ex) {
+ // Check if user has new schema, but is just missing the version update
+ Cursor cursor = database.query("SELECT * FROM metadata");
+ if (cursor == null || cursor.getColumnIndex("hap_client_connection_policy") == -1) {
+ throw ex;
+ }
+ }
+ }
+ };
}
diff --git a/android/app/src/com/android/bluetooth/btservice/storage/ProfilePrioritiesEntity.java b/android/app/src/com/android/bluetooth/btservice/storage/ProfilePrioritiesEntity.java
index a9a75c77b4..18f9db0f6b 100644
--- a/android/app/src/com/android/bluetooth/btservice/storage/ProfilePrioritiesEntity.java
+++ b/android/app/src/com/android/bluetooth/btservice/storage/ProfilePrioritiesEntity.java
@@ -34,6 +34,7 @@ class ProfilePrioritiesEntity {
public int map_connection_policy;
public int sap_connection_policy;
public int hearing_aid_connection_policy;
+ public int hap_client_connection_policy;
public int map_client_connection_policy;
public int le_audio_connection_policy;
public int volume_control_connection_policy;
@@ -52,6 +53,7 @@ class ProfilePrioritiesEntity {
map_connection_policy = BluetoothProfile.CONNECTION_POLICY_UNKNOWN;
sap_connection_policy = BluetoothProfile.CONNECTION_POLICY_UNKNOWN;
hearing_aid_connection_policy = BluetoothProfile.CONNECTION_POLICY_UNKNOWN;
+ hap_client_connection_policy = BluetoothProfile.CONNECTION_POLICY_UNKNOWN;
map_client_connection_policy = BluetoothProfile.CONNECTION_POLICY_UNKNOWN;
le_audio_connection_policy = BluetoothProfile.CONNECTION_POLICY_UNKNOWN;
volume_control_connection_policy = BluetoothProfile.CONNECTION_POLICY_UNKNOWN;
@@ -73,6 +75,7 @@ class ProfilePrioritiesEntity {
.append("|MAP=").append(map_connection_policy)
.append("|MAP_CLIENT=").append(map_client_connection_policy)
.append("|SAP=").append(sap_connection_policy)
+ .append("|HAP=").append(hap_client_connection_policy)
.append("|HEARING_AID=").append(hearing_aid_connection_policy)
.append("|LE_AUDIO=").append(le_audio_connection_policy)
.append("|VOLUME_CONTROL=").append(volume_control_connection_policy)
diff --git a/android/app/src/com/android/bluetooth/gatt/GattService.java b/android/app/src/com/android/bluetooth/gatt/GattService.java
index deab382e02..9a3ed27fac 100644
--- a/android/app/src/com/android/bluetooth/gatt/GattService.java
+++ b/android/app/src/com/android/bluetooth/gatt/GattService.java
@@ -168,6 +168,8 @@ public class GattService extends ProfileService {
UUID.fromString("00001850-0000-1000-8000-00805F9B34FB"), // PACS
UUID.fromString("0000184E-0000-1000-8000-00805F9B34FB"), // ASCS
UUID.fromString("0000184F-0000-1000-8000-00805F9B34FB"), // BASS
+ /* FIXME: Not known yet, using a placeholder instead. */
+ UUID.fromString("EEEEEEEE-EEEE-EEEE-EEEE-EEEEEEEEEEEE"), // HAP
};
/**
diff --git a/android/app/src/com/android/bluetooth/hap/HapClientNativeInterface.java b/android/app/src/com/android/bluetooth/hap/HapClientNativeInterface.java
new file mode 100644
index 0000000000..69e10d36e0
--- /dev/null
+++ b/android/app/src/com/android/bluetooth/hap/HapClientNativeInterface.java
@@ -0,0 +1,424 @@
+/*
+ * 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 com.android.bluetooth.hap;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothHapPresetInfo;
+import android.util.Log;
+
+import com.android.bluetooth.Utils;
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+
+/**
+ * Hearing Access Profile Client Native Interface to/from JNI.
+ */
+public class HapClientNativeInterface {
+ private static final String TAG = "HapClientNativeInterface";
+ private static final boolean DBG = true;
+ private final BluetoothAdapter mAdapter;
+
+ @GuardedBy("INSTANCE_LOCK")
+ private static HapClientNativeInterface sInstance;
+ private static final Object INSTANCE_LOCK = new Object();
+
+ static {
+ classInitNative();
+ }
+
+ private HapClientNativeInterface() {
+ mAdapter = BluetoothAdapter.getDefaultAdapter();
+ if (mAdapter == null) {
+ Log.wtf(TAG, "No Bluetooth Adapter Available");
+ }
+ }
+
+ /**
+ * Get singleton instance.
+ */
+ public static HapClientNativeInterface getInstance() {
+ synchronized (INSTANCE_LOCK) {
+ if (sInstance == null) {
+ sInstance = new HapClientNativeInterface();
+ }
+ return sInstance;
+ }
+ }
+
+ /**
+ * Initiates HapClientService connection to a remote device.
+ *
+ * @param device the remote device
+ * @return true on success, otherwise false.
+ */
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+ public boolean connectHapClient(BluetoothDevice device) {
+ return connectHapClientNative(getByteAddress(device));
+ }
+
+ /**
+ * Disconnects HapClientService from a remote device.
+ *
+ * @param device the remote device
+ * @return true on success, otherwise false.
+ */
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+ public boolean disconnectHapClient(BluetoothDevice device) {
+ return disconnectHapClientNative(getByteAddress(device));
+ }
+
+ /**
+ * Gets a HapClientService device
+ *
+ * @param address the remote device address
+ * @return Bluetooth Device.
+ */
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+ public BluetoothDevice getDevice(byte[] address) {
+ return mAdapter.getRemoteDevice(address);
+ }
+
+ private byte[] getByteAddress(BluetoothDevice device) {
+ if (device == null) {
+ return Utils.getBytesFromAddress("00:00:00:00:00:00");
+ }
+ return Utils.getBytesFromAddress(device.getAddress());
+ }
+
+ private void sendMessageToService(HapClientStackEvent event) {
+ HapClientService service = HapClientService.getHapClientService();
+ if (service != null) {
+ service.messageFromNative(event);
+ } else {
+ Log.e(TAG, "Event ignored, service not available: " + event);
+ }
+ }
+
+ /**
+ * Initializes the native interface.
+ */
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+ public void init() {
+ initNative();
+ }
+
+ /**
+ * Cleanup the native interface.
+ */
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+ public void cleanup() {
+ cleanupNative();
+ }
+
+ /**
+ * Selects the currently active preset for a HA device
+ *
+ * @param device is the device for which we want to set the active preset
+ * @param presetIndex is an index of one of the available presets
+ */
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+ public void selectActivePreset(BluetoothDevice device, int presetIndex) {
+ selectActivePresetNative(getByteAddress(device), presetIndex);
+ }
+
+ /**
+ * Selects the currently active preset for a HA device group.
+ *
+ * @param groupId is the device group identifier for which want to set the active preset
+ * @param presetIndex is an index of one of the available presets
+ */
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+ public void groupSelectActivePreset(int groupId, int presetIndex) {
+ groupSelectActivePresetNative(groupId, presetIndex);
+ }
+
+ /**
+ * Sets the next preset as a currently active preset for a HA device
+ *
+ * @param device is the device for which we want to set the active preset
+ */
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+ public void nextActivePreset(BluetoothDevice device) {
+ nextActivePresetNative(getByteAddress(device));
+ }
+
+ /**
+ * Sets the next preset as a currently active preset for a HA device group
+ *
+ * @param groupId is the device group identifier for which want to set the active preset
+ */
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+ public void groupNextActivePreset(int groupId) {
+ groupNextActivePresetNative(groupId);
+ }
+
+ /**
+ * Sets the previous preset as a currently active preset for a HA device
+ *
+ * @param device is the device for which we want to set the active preset
+ */
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+ public void previousActivePreset(BluetoothDevice device) {
+ previousActivePresetNative(getByteAddress(device));
+ }
+
+ /**
+ * Sets the previous preset as a currently active preset for a HA device group
+ *
+ * @param groupId is the device group identifier for which want to set the active preset
+ */
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+ public void groupPreviousActivePreset(int groupId) {
+ groupPreviousActivePresetNative(groupId);
+ }
+
+ /**
+ * Requests the preset name
+ *
+ * @param device is the device for which we want to get the preset name
+ * @param presetIndex is an index of one of the available presets
+ */
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+ public void getPresetInfo(BluetoothDevice device, int presetIndex) {
+ getPresetInfoNative(getByteAddress(device), presetIndex);
+ }
+
+ /**
+ * Sets the preset name
+ *
+ * @param device is the device for which we want to get the preset name
+ * @param presetIndex is an index of one of the available presets
+ * @param name is a new name for a preset
+ */
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+ public void setPresetName(BluetoothDevice device, int presetIndex, String name) {
+ setPresetNameNative(getByteAddress(device), presetIndex, name);
+ }
+
+ /**
+ * Sets the preset name
+ *
+ * @param groupId is the device group
+ * @param presetIndex is an index of one of the available presets
+ * @param name is a new name for a preset
+ */
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+ public void groupSetPresetName(int groupId, int presetIndex, String name) {
+ groupSetPresetNameNative(groupId, presetIndex, name);
+ }
+
+ // Callbacks from the native stack back into the Java framework.
+ // All callbacks are routed via the Service which will disambiguate which
+ // state machine the message should be routed to.
+
+ @VisibleForTesting
+ void onConnectionStateChanged(int state, byte[] address) {
+ HapClientStackEvent event =
+ new HapClientStackEvent(
+ HapClientStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED);
+ event.device = getDevice(address);
+ event.valueInt1 = state;
+
+ if (DBG) {
+ Log.d(TAG, "onConnectionStateChanged: " + event);
+ }
+ sendMessageToService(event);
+ }
+
+ @VisibleForTesting
+ void onDeviceAvailable(byte[] address, int features) {
+ HapClientStackEvent event = new HapClientStackEvent(
+ HapClientStackEvent.EVENT_TYPE_DEVICE_AVAILABLE);
+ event.device = getDevice(address);
+ event.valueInt1 = features;
+
+ if (DBG) {
+ Log.d(TAG, "onDeviceAvailable: " + event);
+ }
+ sendMessageToService(event);
+ }
+
+ @VisibleForTesting
+ void onFeaturesUpdate(byte[] address, int features) {
+ HapClientStackEvent event = new HapClientStackEvent(
+ HapClientStackEvent.EVENT_TYPE_DEVICE_FEATURES);
+ event.device = getDevice(address);
+ event.valueInt1 = features;
+
+ if (DBG) {
+ Log.d(TAG, "onFeaturesUpdate: " + event);
+ }
+ sendMessageToService(event);
+ }
+
+ @VisibleForTesting
+ void onActivePresetSelected(byte[] address, int presetIndex) {
+ HapClientStackEvent event = new HapClientStackEvent(
+ HapClientStackEvent.EVENT_TYPE_ON_ACTIVE_PRESET_SELECTED);
+ event.device = getDevice(address);
+ event.valueInt1 = presetIndex;
+
+ if (DBG) {
+ Log.d(TAG, "onActivePresetSelected: " + event);
+ }
+ sendMessageToService(event);
+ }
+
+ @VisibleForTesting
+ void onActivePresetGroupSelected(int groupId, int presetIndex) {
+ HapClientStackEvent event = new HapClientStackEvent(
+ HapClientStackEvent.EVENT_TYPE_ON_ACTIVE_PRESET_SELECTED);
+ event.valueInt1 = presetIndex;
+ event.valueInt2 = groupId;
+
+ if (DBG) {
+ Log.d(TAG, "onActivePresetGroupSelected: " + event);
+ }
+ sendMessageToService(event);
+ }
+
+ @VisibleForTesting
+ void onActivePresetSelectError(byte[] address, int resultCode) {
+ HapClientStackEvent event = new HapClientStackEvent(
+ HapClientStackEvent.EVENT_TYPE_ON_ACTIVE_PRESET_SELECT_ERROR);
+ event.device = getDevice(address);
+ event.valueInt1 = resultCode;
+
+ if (DBG) {
+ Log.d(TAG, "onActivePresetSelectError: " + event);
+ }
+ sendMessageToService(event);
+ }
+
+ @VisibleForTesting
+ void onActivePresetGroupSelectError(int groupId, int resultCode) {
+ HapClientStackEvent event = new HapClientStackEvent(
+ HapClientStackEvent.EVENT_TYPE_ON_ACTIVE_PRESET_SELECT_ERROR);
+ event.valueInt1 = resultCode;
+ event.valueInt2 = groupId;
+
+ if (DBG) {
+ Log.d(TAG, "onActivePresetGroupSelectError: " + event);
+ }
+ sendMessageToService(event);
+ }
+
+ @VisibleForTesting
+ void onPresetInfo(byte[] address, int infoReason, BluetoothHapPresetInfo[] presets) {
+ HapClientStackEvent event = new HapClientStackEvent(
+ HapClientStackEvent.EVENT_TYPE_ON_PRESET_INFO);
+ event.device = getDevice(address);
+ event.valueInt2 = infoReason;
+ event.valueList = new ArrayList<>(Arrays.asList(presets));
+
+ if (DBG) {
+ Log.d(TAG, "onPresetInfo: " + event);
+ }
+ sendMessageToService(event);
+ }
+
+ @VisibleForTesting
+ void onGroupPresetInfo(int groupId, int infoReason, BluetoothHapPresetInfo[] presets) {
+ HapClientStackEvent event = new HapClientStackEvent(
+ HapClientStackEvent.EVENT_TYPE_ON_PRESET_INFO);
+ event.valueInt2 = infoReason;
+ event.valueInt3 = groupId;
+ event.valueList = new ArrayList<>(Arrays.asList(presets));
+
+ if (DBG) {
+ Log.d(TAG, "onPresetInfo: " + event);
+ }
+ sendMessageToService(event);
+ }
+
+ @VisibleForTesting
+ void onPresetNameSetError(byte[] address, int presetIndex, int resultCode) {
+ HapClientStackEvent event = new HapClientStackEvent(
+ HapClientStackEvent.EVENT_TYPE_ON_PRESET_NAME_SET_ERROR);
+ event.device = getDevice(address);
+ event.valueInt1 = resultCode;
+ event.valueInt2 = presetIndex;
+
+ if (DBG) {
+ Log.d(TAG, "OnPresetNameSetError: " + event);
+ }
+ sendMessageToService(event);
+ }
+
+ @VisibleForTesting
+ void onGroupPresetNameSetError(int groupId, int presetIndex, int resultCode) {
+ HapClientStackEvent event = new HapClientStackEvent(
+ HapClientStackEvent.EVENT_TYPE_ON_PRESET_NAME_SET_ERROR);
+ event.valueInt1 = resultCode;
+ event.valueInt2 = presetIndex;
+ event.valueInt3 = groupId;
+
+ if (DBG) {
+ Log.d(TAG, "OnPresetNameSetError: " + event);
+ }
+ sendMessageToService(event);
+ }
+
+ @VisibleForTesting
+ void onPresetInfoError(byte[] address, int presetIndex, int resultCode) {
+ HapClientStackEvent event = new HapClientStackEvent(
+ HapClientStackEvent.EVENT_TYPE_ON_PRESET_INFO_ERROR);
+ event.device = getDevice(address);
+ event.valueInt1 = resultCode;
+ event.valueInt2 = presetIndex;
+
+ if (DBG) {
+ Log.d(TAG, "onPresetInfoError: " + event);
+ }
+ sendMessageToService(event);
+ }
+
+ @VisibleForTesting
+ void onGroupPresetInfoError(int groupId, int presetIndex, int resultCode) {
+ HapClientStackEvent event = new HapClientStackEvent(
+ HapClientStackEvent.EVENT_TYPE_ON_PRESET_INFO_ERROR);
+ event.valueInt1 = resultCode;
+ event.valueInt2 = presetIndex;
+ event.valueInt3 = groupId;
+
+ if (DBG) {
+ Log.d(TAG, "onPresetInfoError: " + event);
+ }
+ sendMessageToService(event);
+ }
+
+ // Native methods that call into the JNI interface
+ private static native void classInitNative();
+ private native void initNative();
+ private native void cleanupNative();
+ private native boolean connectHapClientNative(byte[] address);
+ private native boolean disconnectHapClientNative(byte[] address);
+ private native void selectActivePresetNative(byte[] byteAddress, int presetIndex);
+ private native void groupSelectActivePresetNative(int groupId, int presetIndex);
+ private native void nextActivePresetNative(byte[] byteAddress);
+ private native void groupNextActivePresetNative(int groupId);
+ private native void previousActivePresetNative(byte[] byteAddress);
+ private native void groupPreviousActivePresetNative(int groupId);
+ private native void getPresetInfoNative(byte[] byteAddress, int presetIndex);
+ private native void setPresetNameNative(byte[] byteAddress, int presetIndex, String name);
+ private native void groupSetPresetNameNative(int groupId, int presetIndex, String name);
+}
diff --git a/android/app/src/com/android/bluetooth/hap/HapClientService.java b/android/app/src/com/android/bluetooth/hap/HapClientService.java
index e6e5f0c58e..b8aaab841b 100644
--- a/android/app/src/com/android/bluetooth/hap/HapClientService.java
+++ b/android/app/src/com/android/bluetooth/hap/HapClientService.java
@@ -17,20 +17,42 @@
package com.android.bluetooth.hap;
+import static android.Manifest.permission.BLUETOOTH_CONNECT;
+import static android.Manifest.permission.BLUETOOTH_PRIVILEGED;
+
import android.bluetooth.BluetoothCsipSetCoordinator;
import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothHapClient;
+import android.bluetooth.BluetoothHapPresetInfo;
+import android.bluetooth.BluetoothLeAudio;
import android.bluetooth.BluetoothProfile;
+import android.bluetooth.BluetoothUuid;
import android.bluetooth.IBluetoothHapClient;
import android.content.AttributionSource;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.HandlerThread;
+import android.os.ParcelUuid;
import android.util.Log;
import com.android.bluetooth.Utils;
+import com.android.bluetooth.btservice.AdapterService;
import com.android.bluetooth.btservice.ProfileService;
+import com.android.bluetooth.btservice.ServiceFactory;
+import com.android.bluetooth.csip.CsipSetCoordinatorService;
+import com.android.bluetooth.le_audio.LeAudioService;
import com.android.internal.annotations.VisibleForTesting;
import com.android.modules.utils.SynchronousResultReceiver;
+import java.math.BigInteger;
import java.util.ArrayList;
+import java.util.HashMap;
import java.util.List;
+import java.util.ListIterator;
+import java.util.Map;
+import java.util.Objects;
/**
* Provides Bluetooth Hearing Access profile, as a service.
@@ -40,7 +62,25 @@ public class HapClientService extends ProfileService {
private static final boolean DBG = true;
private static final String TAG = "HapClientService";
+ // Upper limit of all HearingAccess devices: Bonded or Connected
+ private static final int MAX_HEARING_ACCESS_STATE_MACHINES = 10;
private static HapClientService sHapClient;
+ private final Map<BluetoothDevice, HapClientStateMachine> mStateMachines =
+ new HashMap<>();
+ @VisibleForTesting
+ HapClientNativeInterface mHapClientNativeInterface;
+ private AdapterService mAdapterService;
+ private HandlerThread mStateMachinesThread;
+ private BroadcastReceiver mBondStateChangedReceiver;
+ private BroadcastReceiver mConnectionStateChangedReceiver;
+
+ private final Map<BluetoothDevice, Integer> mDeviceCurrentPresetMap = new HashMap<>();
+ private final Map<BluetoothDevice, Integer> mDeviceFeaturesMap = new HashMap<>();
+ private final Map<BluetoothDevice, ArrayList<BluetoothHapPresetInfo>> mPresetsMap =
+ new HashMap<>();
+
+ @VisibleForTesting
+ ServiceFactory mFactory = new ServiceFactory();
private static synchronized void setHapClient(HapClientService instance) {
if (DBG) {
@@ -95,9 +135,39 @@ public class HapClientService extends ProfileService {
throw new IllegalStateException("start() called twice");
}
+ // Get AdapterService, HapClientNativeInterface, AudioManager.
+ // None of them can be null.
+ mAdapterService = Objects.requireNonNull(AdapterService.getAdapterService(),
+ "AdapterService cannot be null when HapClientService starts");
+ mHapClientNativeInterface = Objects.requireNonNull(
+ HapClientNativeInterface.getInstance(),
+ "HapClientNativeInterface cannot be null when HapClientService starts");
+
+ // Start handler thread for state machines
+ mStateMachines.clear();
+ mStateMachinesThread = new HandlerThread("HapClientService.StateMachines");
+ mStateMachinesThread.start();
+
+ mDeviceCurrentPresetMap.clear();
+ mDeviceFeaturesMap.clear();
+ mPresetsMap.clear();
+
+ // Setup broadcast receivers
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
+ mBondStateChangedReceiver = new BondStateChangedReceiver();
+ registerReceiver(mBondStateChangedReceiver, filter);
+ filter = new IntentFilter();
+ filter.addAction(BluetoothHapClient.ACTION_HAP_CONNECTION_STATE_CHANGED);
+ mConnectionStateChangedReceiver = new ConnectionStateChangedReceiver();
+ registerReceiver(mConnectionStateChangedReceiver, filter);
+
// Mark service as started
setHapClient(this);
+ // Initialize native interface
+ mHapClientNativeInterface.init();
+
return true;
}
@@ -111,15 +181,800 @@ public class HapClientService extends ProfileService {
return true;
}
+ // Cleanup GATT interface
+ mHapClientNativeInterface.cleanup();
+ mHapClientNativeInterface = null;
+
// Marks service as stopped
setHapClient(null);
+ // Unregister broadcast receivers
+ unregisterReceiver(mBondStateChangedReceiver);
+ mBondStateChangedReceiver = null;
+ unregisterReceiver(mConnectionStateChangedReceiver);
+ mConnectionStateChangedReceiver = null;
+
+ // Destroy state machines and stop handler thread
+ synchronized (mStateMachines) {
+ for (HapClientStateMachine sm : mStateMachines.values()) {
+ sm.doQuit();
+ sm.cleanup();
+ }
+ mStateMachines.clear();
+ }
+
+ mDeviceCurrentPresetMap.clear();
+ mDeviceFeaturesMap.clear();
+ mPresetsMap.clear();
+
+ if (mStateMachinesThread != null) {
+ mStateMachinesThread.quitSafely();
+ mStateMachinesThread = null;
+ }
+
+ // Clear AdapterService
+ mAdapterService = null;
+
+ return true;
+ }
+
+ @VisibleForTesting
+ void bondStateChanged(BluetoothDevice device, int bondState) {
+ if (DBG) {
+ Log.d(TAG, "Bond state changed for device: " + device + " state: " + bondState);
+ }
+
+ // Remove state machine if the bonding for a device is removed
+ if (bondState != BluetoothDevice.BOND_NONE) {
+ return;
+ }
+
+ mDeviceCurrentPresetMap.remove(device);
+ mDeviceFeaturesMap.remove(device);
+ mPresetsMap.remove(device);
+
+ synchronized (mStateMachines) {
+ HapClientStateMachine sm = mStateMachines.get(device);
+ if (sm == null) {
+ return;
+ }
+ if (sm.getConnectionState() != BluetoothProfile.STATE_DISCONNECTED) {
+ return;
+ }
+ removeStateMachine(device);
+ }
+ }
+
+ private void removeStateMachine(BluetoothDevice device) {
+ synchronized (mStateMachines) {
+ HapClientStateMachine sm = mStateMachines.get(device);
+ if (sm == null) {
+ Log.w(TAG, "removeStateMachine: device " + device
+ + " does not have a state machine");
+ return;
+ }
+ Log.i(TAG, "removeStateMachine: removing state machine for device: " + device);
+ sm.doQuit();
+ sm.cleanup();
+ mStateMachines.remove(device);
+ }
+ }
+
+ List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
+ enforceCallingOrSelfPermission(BLUETOOTH_CONNECT, "Need BLUETOOTH_CONNECT permission");
+ ArrayList<BluetoothDevice> devices = new ArrayList<>();
+ if (states == null) {
+ return devices;
+ }
+ final BluetoothDevice[] bondedDevices = mAdapterService.getBondedDevices();
+ if (bondedDevices == null) {
+ return devices;
+ }
+ synchronized (mStateMachines) {
+ for (BluetoothDevice device : bondedDevices) {
+ final ParcelUuid[] featureUuids = device.getUuids();
+ if (!Utils.arrayContains(featureUuids, BluetoothUuid.HAS)) {
+ continue;
+ }
+ int connectionState = BluetoothProfile.STATE_DISCONNECTED;
+ HapClientStateMachine sm = mStateMachines.get(device);
+ if (sm != null) {
+ connectionState = sm.getConnectionState();
+ }
+ for (int state : states) {
+ if (connectionState == state) {
+ devices.add(device);
+ break;
+ }
+ }
+ }
+ return devices;
+ }
+ }
+
+ List<BluetoothDevice> getConnectedDevices() {
+ synchronized (mStateMachines) {
+ List<BluetoothDevice> devices = new ArrayList<>();
+ for (HapClientStateMachine sm : mStateMachines.values()) {
+ if (sm.isConnected()) {
+ devices.add(sm.getDevice());
+ }
+ }
+ return devices;
+ }
+ }
+
+ /**
+ * Get the current connection state of the profile
+ *
+ * @param device is the remote bluetooth device
+ * @return {@link BluetoothProfile#STATE_DISCONNECTED} if this profile is disconnected,
+ * {@link BluetoothProfile#STATE_CONNECTING} if this profile is being connected,
+ * {@link BluetoothProfile#STATE_CONNECTED} if this profile is connected, or
+ * {@link BluetoothProfile#STATE_DISCONNECTING} if this profile is being disconnected
+ */
+ public int getConnectionState(BluetoothDevice device) {
+ enforceCallingOrSelfPermission(BLUETOOTH_CONNECT, "Need BLUETOOTH_CONNECT permission");
+ synchronized (mStateMachines) {
+ HapClientStateMachine sm = mStateMachines.get(device);
+ if (sm == null) {
+ return BluetoothProfile.STATE_DISCONNECTED;
+ }
+ return sm.getConnectionState();
+ }
+ }
+
+ /**
+ * Set connection policy of the profile and connects it if connectionPolicy is
+ * {@link BluetoothProfile#CONNECTION_POLICY_ALLOWED} or disconnects if connectionPolicy is
+ * {@link BluetoothProfile#CONNECTION_POLICY_FORBIDDEN}
+ *
+ * <p> The device should already be paired.
+ * Connection policy can be one of:
+ * {@link BluetoothProfile#CONNECTION_POLICY_ALLOWED},
+ * {@link BluetoothProfile#CONNECTION_POLICY_FORBIDDEN},
+ * {@link BluetoothProfile#CONNECTION_POLICY_UNKNOWN}
+ *
+ * @param device the remote device
+ * @param connectionPolicy is the connection policy to set to for this profile
+ * @return true on success, otherwise false
+ */
+ public boolean setConnectionPolicy(BluetoothDevice device, int connectionPolicy) {
+ enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED,
+ "Need BLUETOOTH_PRIVILEGED permission");
+ if (DBG) {
+ Log.d(TAG, "Saved connectionPolicy " + device + " = " + connectionPolicy);
+ }
+ mAdapterService.getDatabase()
+ .setProfileConnectionPolicy(device, BluetoothProfile.HAP_CLIENT,
+ connectionPolicy);
+ if (connectionPolicy == BluetoothProfile.CONNECTION_POLICY_ALLOWED) {
+ connect(device);
+ } else if (connectionPolicy == BluetoothProfile.CONNECTION_POLICY_FORBIDDEN) {
+ disconnect(device);
+ }
+ return true;
+ }
+
+ /**
+ * Get the connection policy of the profile.
+ *
+ * <p> The connection policy can be any of:
+ * {@link BluetoothProfile#CONNECTION_POLICY_ALLOWED},
+ * {@link BluetoothProfile#CONNECTION_POLICY_FORBIDDEN},
+ * {@link BluetoothProfile#CONNECTION_POLICY_UNKNOWN}
+ *
+ * @param device Bluetooth device
+ * @return connection policy of the device
+ * @hide
+ */
+ public int getConnectionPolicy(BluetoothDevice device) {
+ return mAdapterService.getDatabase()
+ .getProfileConnectionPolicy(device, BluetoothProfile.HAP_CLIENT);
+ }
+
+ /**
+ * Check whether can connect to a peer device.
+ * The check considers a number of factors during the evaluation.
+ *
+ * @param device the peer device to connect to
+ * @return true if connection is allowed, otherwise false
+ */
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+ public boolean okToConnect(BluetoothDevice device) {
+ // Check if this is an incoming connection in Quiet mode.
+ if (mAdapterService.isQuietModeEnabled()) {
+ Log.e(TAG, "okToConnect: cannot connect to " + device + " : quiet mode enabled");
+ return false;
+ }
+ // Check connection policy and accept or reject the connection.
+ int connectionPolicy = getConnectionPolicy(device);
+ int bondState = mAdapterService.getBondState(device);
+ // Allow this connection only if the device is bonded. Any attempt to connect while
+ // bonding would potentially lead to an unauthorized connection.
+ if (bondState != BluetoothDevice.BOND_BONDED) {
+ Log.w(TAG, "okToConnect: return false, bondState=" + bondState);
+ return false;
+ } else if (connectionPolicy != BluetoothProfile.CONNECTION_POLICY_UNKNOWN
+ && connectionPolicy != BluetoothProfile.CONNECTION_POLICY_ALLOWED) {
+ // Otherwise, reject the connection if connectionPolicy is not valid.
+ Log.w(TAG, "okToConnect: return false, connectionPolicy=" + connectionPolicy);
+ return false;
+ }
+ return true;
+ }
+
+ @VisibleForTesting
+ synchronized void connectionStateChanged(BluetoothDevice device, int fromState,
+ int toState) {
+ if ((device == null) || (fromState == toState)) {
+ Log.e(TAG, "connectionStateChanged: unexpected invocation. device=" + device
+ + " fromState=" + fromState + " toState=" + toState);
+ return;
+ }
+
+ // Check if the device is disconnected - if unbond, remove the state machine
+ if (toState == BluetoothProfile.STATE_DISCONNECTED) {
+ int bondState = mAdapterService.getBondState(device);
+ if (bondState == BluetoothDevice.BOND_NONE) {
+ if (DBG) {
+ Log.d(TAG, device + " is unbond. Remove state machine");
+ }
+ removeStateMachine(device);
+ }
+ }
+ }
+
+ /**
+ * Connects the hearing access service client to the passed in device
+ *
+ * @param device is the device with which we will connect the hearing access service client
+ * @return true if hearing access service client successfully connected, false otherwise
+ */
+ public boolean connect(BluetoothDevice device) {
+ enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED,
+ "Need BLUETOOTH_PRIVILEGED permission");
+ if (DBG) {
+ Log.d(TAG, "connect(): " + device);
+ }
+ if (device == null) {
+ return false;
+ }
+
+ if (getConnectionPolicy(device) == BluetoothProfile.CONNECTION_POLICY_FORBIDDEN) {
+ return false;
+ }
+ ParcelUuid[] featureUuids = mAdapterService.getRemoteUuids(device);
+ if (!Utils.arrayContains(featureUuids, BluetoothUuid.HAS)) {
+ Log.e(TAG, "Cannot connect to " + device
+ + " : Remote does not have Hearing Access Service UUID");
+ return false;
+ }
+ synchronized (mStateMachines) {
+ HapClientStateMachine smConnect = getOrCreateStateMachine(device);
+ if (smConnect == null) {
+ Log.e(TAG, "Cannot connect to " + device + " : no state machine");
+ }
+ smConnect.sendMessage(HapClientStateMachine.CONNECT);
+ }
+
+ return true;
+ }
+
+ /**
+ * Disconnects hearing access service client for the passed in device
+ *
+ * @param device is the device with which we want to disconnect the hearing access service
+ * client
+ * @return true if hearing access service client successfully disconnected, false otherwise
+ */
+ public boolean disconnect(BluetoothDevice device) {
+ enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED,
+ "Need BLUETOOTH_PRIVILEGED permission");
+ if (DBG) {
+ Log.d(TAG, "disconnect(): " + device);
+ }
+ if (device == null) {
+ return false;
+ }
+ synchronized (mStateMachines) {
+ HapClientStateMachine sm = mStateMachines.get(device);
+ if (sm != null) {
+ sm.sendMessage(HapClientStateMachine.DISCONNECT);
+ }
+ }
+
+ return true;
+ }
+
+ private HapClientStateMachine getOrCreateStateMachine(BluetoothDevice device) {
+ if (device == null) {
+ Log.e(TAG, "getOrCreateStateMachine failed: device cannot be null");
+ return null;
+ }
+ synchronized (mStateMachines) {
+ HapClientStateMachine sm = mStateMachines.get(device);
+ if (sm != null) {
+ return sm;
+ }
+ // Limit the maximum number of state machines to avoid DoS attack
+ if (mStateMachines.size() >= MAX_HEARING_ACCESS_STATE_MACHINES) {
+ Log.e(TAG, "Maximum number of HearingAccess state machines reached: "
+ + MAX_HEARING_ACCESS_STATE_MACHINES);
+ return null;
+ }
+ if (DBG) {
+ Log.d(TAG, "Creating a new state machine for " + device);
+ }
+ sm = HapClientStateMachine.make(device, this,
+ mHapClientNativeInterface, mStateMachinesThread.getLooper());
+ mStateMachines.put(device, sm);
+ return sm;
+ }
+ }
+
+ /**
+ * Gets the hearing access device group of the passed device
+ *
+ * @param device is the device with which we want to get the group identifier for
+ * @return group ID if device is part of the coordinated group, 0 otherwise
+ */
+ public int getHapGroup(BluetoothDevice device) {
+ CsipSetCoordinatorService csipClient = mFactory.getCsipSetCoordinatorService();
+
+ if (csipClient != null) {
+ Map<Integer, ParcelUuid> groups = csipClient.getGroupUuidMapByDevice(device);
+ for (Map.Entry<Integer, ParcelUuid> entry : groups.entrySet()) {
+ if (entry.getValue().equals(BluetoothUuid.CAP)) {
+ return entry.getKey();
+ }
+ }
+ }
+ return BluetoothCsipSetCoordinator.GROUP_ID_INVALID;
+ }
+
+ /**
+ * Gets the currently active preset index for a HA device
+ *
+ * @param device is the device for which we want to get the currently active preset
+ * @return true if valid request was sent, false otherwise
+ */
+ public boolean getActivePresetIndex(BluetoothDevice device) {
+ notifyActivePresetIndex(device, mDeviceCurrentPresetMap.getOrDefault(device,
+ BluetoothHapClient.PRESET_INDEX_UNAVAILABLE));
+ return true;
+ }
+
+ /**
+ * Selects the currently active preset for a HA device
+ *
+ * @param device is the device for which we want to set the active preset
+ * @param presetIndex is an index of one of the available presets
+ * @return true if valid request was sent, false otherwise
+ */
+ public boolean selectActivePreset(BluetoothDevice device, int presetIndex) {
+ if (presetIndex == BluetoothHapClient.PRESET_INDEX_UNAVAILABLE) return false;
+ mHapClientNativeInterface.selectActivePreset(device, presetIndex);
+ return true;
+ }
+
+ /**
+ * Selects the currently active preset for a HA device group.
+ *
+ * @param groupId is the device group identifier for which want to set the active preset
+ * @param presetIndex is an index of one of the available presets
+ * @return true if valid group request was sent, false otherwise
+ */
+ public boolean groupSelectActivePreset(int groupId, int presetIndex) {
+ if (presetIndex == BluetoothHapClient.PRESET_INDEX_UNAVAILABLE
+ || groupId == BluetoothCsipSetCoordinator.GROUP_ID_INVALID) {
+ return false;
+ }
+
+ mHapClientNativeInterface.groupSelectActivePreset(groupId, presetIndex);
+ return true;
+ }
+
+ /**
+ * Sets the next preset as a currently active preset for a HA device
+ *
+ * @param device is the device for which we want to set the active preset
+ * @return true if valid request was sent, false otherwise
+ */
+ public boolean nextActivePreset(BluetoothDevice device) {
+ mHapClientNativeInterface.nextActivePreset(device);
+ return true;
+ }
+
+ /**
+ * Sets the next preset as a currently active preset for a HA device group
+ *
+ * @param groupId is the device group identifier for which want to set the active preset
+ * @return true if valid group request was sent, false otherwise
+ */
+ public boolean groupNextActivePreset(int groupId) {
+ if (groupId == BluetoothCsipSetCoordinator.GROUP_ID_INVALID) return false;
+
+ mHapClientNativeInterface.groupNextActivePreset(groupId);
+ return true;
+ }
+
+ /**
+ * Sets the previous preset as a currently active preset for a HA device
+ *
+ * @param device is the device for which we want to set the active preset
+ * @return true if valid request was sent, false otherwise
+ */
+ public boolean previousActivePreset(BluetoothDevice device) {
+ mHapClientNativeInterface.previousActivePreset(device);
+ return true;
+ }
+
+ /**
+ * Sets the previous preset as a currently active preset for a HA device group
+ *
+ * @param groupId is the device group identifier for which want to set the active preset
+ * @return true if valid group request was sent, false otherwise
+ */
+ public boolean groupPreviousActivePreset(int groupId) {
+ if (groupId == BluetoothCsipSetCoordinator.GROUP_ID_INVALID) return false;
+
+ mHapClientNativeInterface.groupPreviousActivePreset(groupId);
+ return true;
+ }
+
+ /**
+ * Requests the preset name
+ *
+ * @param device is the device for which we want to get the preset name
+ * @param presetIndex is an index of one of the available presets
+ * @return true if valid request was sent, false otherwise
+ */
+ public boolean getPresetInfo(BluetoothDevice device, int presetIndex) {
+ if (presetIndex == BluetoothHapClient.PRESET_INDEX_UNAVAILABLE) return false;
+ mHapClientNativeInterface.getPresetInfo(device, presetIndex);
+ return true;
+ }
+
+ /**
+ * Requests all presets info
+ *
+ * @param device is the device for which we want to get all presets info
+ * @return true if request was processed, false otherwise
+ */
+ public boolean getAllPresetsInfo(BluetoothDevice device) {
+ if (mPresetsMap.containsKey(device)) {
+ notifyPresets(device, BluetoothHapClient.PRESET_INFO_REASON_ALL_PRESET_INFO,
+ mPresetsMap.get(device));
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Requests features
+ *
+ * @param device is the device for which we want to get features
+ * @return true if request was processed, false otherwise
+ */
+ public boolean getFeatures(BluetoothDevice device) {
+ if (mDeviceFeaturesMap.containsKey(device)) {
+ notifyFeatures(device, mDeviceFeaturesMap.get(device));
+ return true;
+ }
+ return false;
+ }
+
+ private void notifyPresets(BluetoothDevice device, int infoReason,
+ ArrayList<BluetoothHapPresetInfo> presets) {
+ Intent intent = null;
+
+ intent = new Intent(BluetoothHapClient.ACTION_HAP_ON_PRESET_INFO);
+ if (intent != null) {
+ intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
+ intent.putExtra(BluetoothHapClient.EXTRA_HAP_PRESET_INFO_REASON, infoReason);
+ intent.putParcelableArrayListExtra(BluetoothHapClient.EXTRA_HAP_PRESET_INFO, presets);
+ sendBroadcast(intent, BLUETOOTH_PRIVILEGED);
+ }
+ }
+
+ private void notifyPresets(int groupId, int infoReason,
+ ArrayList<BluetoothHapPresetInfo> presets) {
+ Intent intent = null;
+
+ intent = new Intent(BluetoothHapClient.ACTION_HAP_ON_PRESET_INFO);
+ if (intent != null) {
+ intent.putExtra(BluetoothHapClient.EXTRA_HAP_GROUP_ID, groupId);
+ intent.putExtra(BluetoothHapClient.EXTRA_HAP_PRESET_INFO_REASON, infoReason);
+ intent.putParcelableArrayListExtra(BluetoothHapClient.EXTRA_HAP_PRESET_INFO, presets);
+ sendBroadcast(intent, BLUETOOTH_PRIVILEGED);
+ }
+ }
+
+ private void notifyFeatures(BluetoothDevice device, int features) {
+ Intent intent = null;
+
+ intent = new Intent(BluetoothHapClient.ACTION_HAP_ON_DEVICE_FEATURES);
+ if (intent != null) {
+ intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
+ intent.putExtra(BluetoothHapClient.EXTRA_HAP_FEATURES, features);
+ sendBroadcast(intent, BLUETOOTH_PRIVILEGED);
+ }
+ }
+
+ /**
+ * Sets the preset name
+ *
+ * @param device is the device for which we want to get the preset name
+ * @param presetIndex is an index of one of the available presets
+ * @param name is a new name for a preset
+ * @return true if valid request was sent, false otherwise
+ */
+ public boolean setPresetName(BluetoothDevice device, int presetIndex, String name) {
+ if (presetIndex == BluetoothHapClient.PRESET_INDEX_UNAVAILABLE) return false;
+ mHapClientNativeInterface.setPresetName(device, presetIndex, name);
+ return true;
+ }
+
+ /**
+ * Sets the preset name
+ *
+ * @param groupId is the device group identifier
+ * @param presetIndex is an index of one of the available presets
+ * @param name is a new name for a preset
+ * @return true if valid request was sent, false otherwise
+ */
+ public boolean groupSetPresetName(int groupId, int presetIndex, String name) {
+ if (groupId == BluetoothCsipSetCoordinator.GROUP_ID_INVALID) return false;
+ if (presetIndex == BluetoothHapClient.PRESET_INDEX_UNAVAILABLE) return false;
+ mHapClientNativeInterface.groupSetPresetName(groupId, presetIndex, name);
return true;
}
@Override
public void dump(StringBuilder sb) {
super.dump(sb);
+ for (HapClientStateMachine sm : mStateMachines.values()) {
+ sm.dump(sb);
+ }
+ }
+
+ private boolean isPresetCoordinationSupported(BluetoothDevice device) {
+ Integer features = mDeviceFeaturesMap.getOrDefault(device, 0x00);
+ return BigInteger.valueOf(features).testBit(
+ HapClientStackEvent.FEATURE_BIT_NUM_SYNCHRONIZATED_PRESETS);
+ }
+
+ void notifyActivePresetIndex(BluetoothDevice device, int presetIndex) {
+ Intent intent = new Intent(BluetoothHapClient.ACTION_HAP_ON_ACTIVE_PRESET);
+ intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
+ intent.putExtra(BluetoothHapClient.EXTRA_HAP_PRESET_INDEX, presetIndex);
+ sendBroadcast(intent, BLUETOOTH_PRIVILEGED);
+ }
+
+ void notifyActivePresetIndex(int groupId, int presetIndex) {
+ Intent intent = new Intent(BluetoothHapClient.ACTION_HAP_ON_ACTIVE_PRESET);
+ intent.putExtra(BluetoothHapClient.EXTRA_HAP_GROUP_ID, groupId);
+ intent.putExtra(BluetoothHapClient.EXTRA_HAP_PRESET_INDEX, presetIndex);
+ sendBroadcast(intent, BLUETOOTH_PRIVILEGED);
+ }
+
+ void updateDevicePresetsCache(BluetoothDevice device, int infoReason,
+ ArrayList<BluetoothHapPresetInfo> presets) {
+ switch (infoReason) {
+ case BluetoothHapClient.PRESET_INFO_REASON_ALL_PRESET_INFO:
+ mPresetsMap.put(device, presets);
+ break;
+ case BluetoothHapClient.PRESET_INFO_REASON_PRESET_INFO_UPDATE:
+ case BluetoothHapClient.PRESET_INFO_REASON_PRESET_AVAILABILITY_CHANGED:
+ case BluetoothHapClient.PRESET_INFO_REASON_PRESET_INFO_REQUEST_RESPONSE: {
+ ArrayList current_presets = mPresetsMap.get(device);
+ if (current_presets != null) {
+ ListIterator<BluetoothHapPresetInfo> iter = current_presets.listIterator();
+ for (BluetoothHapPresetInfo new_preset : presets) {
+ while (iter.hasNext()) {
+ if (iter.next().getIndex() == new_preset.getIndex()) {
+ iter.remove();
+ }
+ }
+ }
+ current_presets.addAll(presets);
+ mPresetsMap.put(device, current_presets);
+ } else {
+ mPresetsMap.put(device, presets);
+ }
+ }
+ break;
+
+ case BluetoothHapClient.PRESET_INFO_REASON_PRESET_DELETED: {
+ ArrayList current_presets = mPresetsMap.get(device);
+ if (current_presets != null) {
+ ListIterator<BluetoothHapPresetInfo> iter = current_presets.listIterator();
+ for (BluetoothHapPresetInfo new_preset : presets) {
+ while (iter.hasNext()) {
+ if (iter.next().getIndex() == new_preset.getIndex()) {
+ iter.remove();
+ }
+ }
+ }
+ mPresetsMap.put(device, current_presets);
+ }
+ }
+ break;
+
+ default:
+ break;
+ }
+ }
+
+ /**
+ * Handle messages from native (JNI) to Java
+ *
+ * @param stackEvent the event that need to be handled
+ */
+ public void messageFromNative(HapClientStackEvent stackEvent) {
+ // Decide which event should be sent to the state machine
+ if (stackEvent.type == HapClientStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED) {
+ resendToStateMachine(stackEvent);
+ return;
+ }
+
+ Intent intent = null;
+ BluetoothDevice device = stackEvent.device;
+
+ switch (stackEvent.type) {
+ case (HapClientStackEvent.EVENT_TYPE_DEVICE_AVAILABLE): {
+ int features = stackEvent.valueInt1;
+
+ if (device != null) {
+ mDeviceFeaturesMap.put(device, features);
+
+ intent = new Intent(BluetoothHapClient.ACTION_HAP_DEVICE_AVAILABLE);
+ intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
+ intent.putExtra(BluetoothHapClient.EXTRA_HAP_FEATURES, features);
+ }
+ } break;
+
+ case (HapClientStackEvent.EVENT_TYPE_DEVICE_FEATURES): {
+ int features = stackEvent.valueInt1;
+
+ if (device != null) {
+ mDeviceFeaturesMap.put(device, features);
+ notifyFeatures(device, features);
+ }
+ } return;
+
+ case (HapClientStackEvent.EVENT_TYPE_ON_ACTIVE_PRESET_SELECTED): {
+ int currentPresetIndex = stackEvent.valueInt1;
+ int groupId = stackEvent.valueInt2;
+
+ if (device != null) {
+ mDeviceCurrentPresetMap.put(device, currentPresetIndex);
+ notifyActivePresetIndex(device, currentPresetIndex);
+
+ } else if (groupId != BluetoothCsipSetCoordinator.GROUP_ID_INVALID) {
+ // TODO: Fix missing CSIS service API to decouple from LeAudioService
+ LeAudioService le_audio_service = mFactory.getLeAudioService();
+ if (le_audio_service != null) {
+ int group_id = le_audio_service.getGroupId(device);
+ if (group_id != BluetoothLeAudio.GROUP_ID_INVALID) {
+ List<BluetoothDevice> all_group_devices =
+ le_audio_service.getGroupDevices(group_id);
+ for (BluetoothDevice dev : all_group_devices) {
+ mDeviceCurrentPresetMap.put(dev, currentPresetIndex);
+ }
+ }
+ }
+ notifyActivePresetIndex(groupId, currentPresetIndex);
+ }
+ } return;
+
+ case (HapClientStackEvent.EVENT_TYPE_ON_ACTIVE_PRESET_SELECT_ERROR): {
+ int statusCode = stackEvent.valueInt1;
+ int groupId = stackEvent.valueInt2;
+
+ intent = new Intent(BluetoothHapClient.ACTION_HAP_ON_ACTIVE_PRESET_SELECT_ERROR);
+ intent.putExtra(BluetoothHapClient.EXTRA_HAP_STATUS_CODE, statusCode);
+
+ if (device != null) {
+ intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
+ } else if (groupId != BluetoothCsipSetCoordinator.GROUP_ID_INVALID) {
+ intent.putExtra(BluetoothHapClient.EXTRA_HAP_GROUP_ID, groupId);
+ }
+ } break;
+
+ case (HapClientStackEvent.EVENT_TYPE_ON_PRESET_INFO): {
+ int presetIndex = stackEvent.valueInt1;
+ int infoReason = stackEvent.valueInt2;
+ int groupId = stackEvent.valueInt3;
+ ArrayList presets = stackEvent.valueList;
+
+ if (device != null) {
+ updateDevicePresetsCache(device, infoReason, presets);
+ notifyPresets(device, infoReason, presets);
+
+ } else if (groupId != BluetoothCsipSetCoordinator.GROUP_ID_INVALID) {
+ // TODO: Fix missing CSIS service API to decouple from LeAudioService
+ LeAudioService le_audio_service = mFactory.getLeAudioService();
+ if (le_audio_service != null) {
+ int group_id = le_audio_service.getGroupId(device);
+ if (group_id != BluetoothLeAudio.GROUP_ID_INVALID) {
+ List<BluetoothDevice> all_group_devices =
+ le_audio_service.getGroupDevices(group_id);
+ for (BluetoothDevice dev : all_group_devices) {
+ updateDevicePresetsCache(dev, infoReason, presets);
+ }
+ }
+ }
+ notifyPresets(groupId, infoReason, presets);
+ }
+
+ } return;
+
+ case (HapClientStackEvent.EVENT_TYPE_ON_PRESET_NAME_SET_ERROR): {
+ int statusCode = stackEvent.valueInt1;
+ int presetIndex = stackEvent.valueInt2;
+
+ intent = new Intent(BluetoothHapClient.ACTION_HAP_ON_PRESET_NAME_SET_ERROR);
+ intent.putExtra(BluetoothHapClient.EXTRA_HAP_PRESET_INDEX, presetIndex);
+ intent.putExtra(BluetoothHapClient.EXTRA_HAP_STATUS_CODE, statusCode);
+
+ if (device != null) {
+ intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
+ } else {
+ int groupId = stackEvent.valueInt3;
+ intent.putExtra(BluetoothHapClient.EXTRA_HAP_GROUP_ID, groupId);
+ }
+ } break;
+
+ case (HapClientStackEvent.EVENT_TYPE_ON_PRESET_INFO_ERROR): {
+ int statusCode = stackEvent.valueInt1;
+ int presetIndex = stackEvent.valueInt2;
+
+ intent = new Intent(BluetoothHapClient.ACTION_HAP_ON_PRESET_INFO_GET_ERROR);
+ intent.putExtra(BluetoothHapClient.EXTRA_HAP_PRESET_INDEX, presetIndex);
+ intent.putExtra(BluetoothHapClient.EXTRA_HAP_STATUS_CODE, statusCode);
+
+ if (device != null) {
+ intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
+ } else {
+ int groupId = stackEvent.valueInt3;
+ intent.putExtra(BluetoothHapClient.EXTRA_HAP_GROUP_ID, groupId);
+ }
+ } break;
+
+ default:
+ return;
+ }
+
+ if (intent != null) {
+ sendBroadcast(intent, BLUETOOTH_PRIVILEGED);
+ }
+ }
+
+ private void resendToStateMachine(HapClientStackEvent stackEvent) {
+ synchronized (mStateMachines) {
+ BluetoothDevice device = stackEvent.device;
+ HapClientStateMachine sm = mStateMachines.get(device);
+
+ if (sm == null) {
+ if (stackEvent.type == HapClientStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED) {
+ switch (stackEvent.valueInt1) {
+ case HapClientStackEvent.CONNECTION_STATE_CONNECTED:
+ case HapClientStackEvent.CONNECTION_STATE_CONNECTING:
+ sm = getOrCreateStateMachine(device);
+ break;
+ default:
+ break;
+ }
+ }
+ }
+ if (sm == null) {
+ Log.e(TAG, "Cannot process stack event: no state machine: " + stackEvent);
+ return;
+ }
+ sm.sendMessage(HapClientStateMachine.STACK_EVENT, stackEvent);
+ }
}
/**
@@ -158,6 +1013,10 @@ public class HapClientService extends ProfileService {
SynchronousResultReceiver receiver) {
try {
boolean defaultValue = false;
+ HapClientService service = getService(source);
+ if (service != null) {
+ defaultValue = service.connect(device);
+ }
receiver.send(defaultValue);
} catch (RuntimeException e) {
receiver.propagateException(e);
@@ -169,6 +1028,10 @@ public class HapClientService extends ProfileService {
SynchronousResultReceiver receiver) {
try {
boolean defaultValue = false;
+ HapClientService service = getService(source);
+ if (service != null) {
+ defaultValue = service.disconnect(device);
+ }
receiver.send(defaultValue);
} catch (RuntimeException e) {
receiver.propagateException(e);
@@ -180,6 +1043,10 @@ public class HapClientService extends ProfileService {
SynchronousResultReceiver receiver) {
try {
List<BluetoothDevice> defaultValue = new ArrayList<>();
+ HapClientService service = getService(source);
+ if (service != null) {
+ defaultValue = service.getConnectedDevices();
+ }
receiver.send(defaultValue);
} catch (RuntimeException e) {
receiver.propagateException(e);
@@ -191,6 +1058,10 @@ public class HapClientService extends ProfileService {
AttributionSource source, SynchronousResultReceiver receiver) {
try {
List<BluetoothDevice> defaultValue = new ArrayList<>();
+ HapClientService service = getService(source);
+ if (service != null) {
+ defaultValue = service.getDevicesMatchingConnectionStates(states);
+ }
receiver.send(defaultValue);
} catch (RuntimeException e) {
receiver.propagateException(e);
@@ -202,6 +1073,10 @@ public class HapClientService extends ProfileService {
SynchronousResultReceiver receiver) {
try {
int defaultValue = BluetoothProfile.STATE_DISCONNECTED;
+ HapClientService service = getService(source);
+ if (service != null) {
+ defaultValue = service.getConnectionState(device);
+ }
receiver.send(defaultValue);
} catch (RuntimeException e) {
receiver.propagateException(e);
@@ -213,6 +1088,10 @@ public class HapClientService extends ProfileService {
AttributionSource source, SynchronousResultReceiver receiver) {
try {
boolean defaultValue = false;
+ HapClientService service = getService(source);
+ if (service != null) {
+ defaultValue = service.setConnectionPolicy(device, connectionPolicy);
+ }
receiver.send(defaultValue);
} catch (RuntimeException e) {
receiver.propagateException(e);
@@ -224,6 +1103,10 @@ public class HapClientService extends ProfileService {
SynchronousResultReceiver receiver) {
try {
int defaultValue = BluetoothProfile.CONNECTION_POLICY_UNKNOWN;
+ HapClientService service = getService(source);
+ if (service != null) {
+ defaultValue = service.getConnectionPolicy(device);
+ }
receiver.send(defaultValue);
} catch (RuntimeException e) {
receiver.propagateException(e);
@@ -235,6 +1118,10 @@ public class HapClientService extends ProfileService {
SynchronousResultReceiver receiver) {
try {
boolean defaultValue = false;
+ HapClientService service = getService(source);
+ if (service != null) {
+ defaultValue = service.getActivePresetIndex(device);
+ }
receiver.send(defaultValue);
} catch (RuntimeException e) {
receiver.propagateException(e);
@@ -246,6 +1133,10 @@ public class HapClientService extends ProfileService {
SynchronousResultReceiver receiver) {
try {
int defaultValue = BluetoothCsipSetCoordinator.GROUP_ID_INVALID;
+ HapClientService service = getService(source);
+ if (service != null) {
+ defaultValue = service.getHapGroup(device);
+ }
receiver.send(defaultValue);
} catch (RuntimeException e) {
receiver.propagateException(e);
@@ -257,6 +1148,10 @@ public class HapClientService extends ProfileService {
AttributionSource source, SynchronousResultReceiver receiver) {
try {
boolean defaultValue = false;
+ HapClientService service = getService(source);
+ if (service != null) {
+ defaultValue = service.selectActivePreset(device, presetIndex);
+ }
receiver.send(defaultValue);
} catch (RuntimeException e) {
receiver.propagateException(e);
@@ -268,6 +1163,10 @@ public class HapClientService extends ProfileService {
AttributionSource source, SynchronousResultReceiver receiver) {
try {
boolean defaultValue = false;
+ HapClientService service = getService(source);
+ if (service != null) {
+ defaultValue = service.groupSelectActivePreset(groupId, presetIndex);
+ }
receiver.send(defaultValue);
} catch (RuntimeException e) {
receiver.propagateException(e);
@@ -279,6 +1178,10 @@ public class HapClientService extends ProfileService {
SynchronousResultReceiver receiver) {
try {
boolean defaultValue = false;
+ HapClientService service = getService(source);
+ if (service != null) {
+ defaultValue = service.nextActivePreset(device);
+ }
receiver.send(defaultValue);
} catch (RuntimeException e) {
receiver.propagateException(e);
@@ -290,6 +1193,10 @@ public class HapClientService extends ProfileService {
SynchronousResultReceiver receiver) {
try {
boolean defaultValue = false;
+ HapClientService service = getService(source);
+ if (service != null) {
+ defaultValue = service.groupNextActivePreset(groupId);
+ }
receiver.send(defaultValue);
} catch (RuntimeException e) {
receiver.propagateException(e);
@@ -301,6 +1208,10 @@ public class HapClientService extends ProfileService {
SynchronousResultReceiver receiver) {
try {
boolean defaultValue = false;
+ HapClientService service = getService(source);
+ if (service != null) {
+ defaultValue = service.previousActivePreset(device);
+ }
receiver.send(defaultValue);
} catch (RuntimeException e) {
receiver.propagateException(e);
@@ -311,6 +1222,10 @@ public class HapClientService extends ProfileService {
public void groupPreviousActivePreset(int groupId, AttributionSource source, SynchronousResultReceiver receiver) {
try {
boolean defaultValue = false;
+ HapClientService service = getService(source);
+ if (service != null) {
+ defaultValue = service.groupPreviousActivePreset(groupId);
+ }
receiver.send(defaultValue);
} catch (RuntimeException e) {
receiver.propagateException(e);
@@ -322,6 +1237,10 @@ public class HapClientService extends ProfileService {
AttributionSource source, SynchronousResultReceiver receiver) {
try {
boolean defaultValue = false;
+ HapClientService service = getService(source);
+ if (service != null) {
+ defaultValue = service.getPresetInfo(device, presetIndex);
+ }
receiver.send(defaultValue);
} catch (RuntimeException e) {
receiver.propagateException(e);
@@ -332,6 +1251,10 @@ public class HapClientService extends ProfileService {
public void getAllPresetsInfo(BluetoothDevice device, AttributionSource source, SynchronousResultReceiver receiver) {
try {
boolean defaultValue = false;
+ HapClientService service = getService(source);
+ if (service != null) {
+ defaultValue = service.getAllPresetsInfo(device);
+ }
receiver.send(defaultValue);
} catch (RuntimeException e) {
receiver.propagateException(e);
@@ -342,6 +1265,10 @@ public class HapClientService extends ProfileService {
public void getFeatures(BluetoothDevice device, AttributionSource source, SynchronousResultReceiver receiver) {
try {
boolean defaultValue = false;
+ HapClientService service = getService(source);
+ if (service != null) {
+ defaultValue = service.getFeatures(device);
+ }
receiver.send(defaultValue);
} catch (RuntimeException e) {
receiver.propagateException(e);
@@ -353,6 +1280,10 @@ public class HapClientService extends ProfileService {
AttributionSource source, SynchronousResultReceiver receiver) {
try {
boolean defaultValue = false;
+ HapClientService service = getService(source);
+ if (service != null) {
+ defaultValue = service.setPresetName(device, presetIndex, name);
+ }
receiver.send(defaultValue);
} catch (RuntimeException e) {
receiver.propagateException(e);
@@ -364,10 +1295,43 @@ public class HapClientService extends ProfileService {
AttributionSource source, SynchronousResultReceiver receiver) {
try {
boolean defaultValue = false;
+ HapClientService service = getService(source);
+ if (service != null) {
+ defaultValue = service.groupSetPresetName(groupId, presetIndex, name);
+ }
receiver.send(defaultValue);
} catch (RuntimeException e) {
receiver.propagateException(e);
}
}
}
+
+ // Remove state machine if the bonding for a device is removed
+ private class BondStateChangedReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (!BluetoothDevice.ACTION_BOND_STATE_CHANGED.equals(intent.getAction())) {
+ return;
+ }
+ int state = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE,
+ BluetoothDevice.ERROR);
+ BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
+ Objects.requireNonNull(device, "ACTION_BOND_STATE_CHANGED with no EXTRA_DEVICE");
+ bondStateChanged(device, state);
+ }
+ }
+
+ private class ConnectionStateChangedReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (!BluetoothHapClient.ACTION_HAP_CONNECTION_STATE_CHANGED.equals(
+ intent.getAction())) {
+ return;
+ }
+ BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
+ int toState = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1);
+ int fromState = intent.getIntExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, -1);
+ connectionStateChanged(device, fromState, toState);
+ }
+ }
}
diff --git a/android/app/src/com/android/bluetooth/hap/HapClientStackEvent.java b/android/app/src/com/android/bluetooth/hap/HapClientStackEvent.java
new file mode 100644
index 0000000000..278bb1a06f
--- /dev/null
+++ b/android/app/src/com/android/bluetooth/hap/HapClientStackEvent.java
@@ -0,0 +1,291 @@
+/*
+ * 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 com.android.bluetooth.hap;
+
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.IBluetoothHapClient;
+
+import java.math.BigInteger;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Stack event sent via a callback from GATT Service client to main service module, or generated
+ * internally by the Hearing Access Profile State Machine.
+ */
+public class HapClientStackEvent {
+ // Event types for STACK_EVENT message (coming from native HAS client)
+ private static final int EVENT_TYPE_NONE = 0;
+ public static final int EVENT_TYPE_CONNECTION_STATE_CHANGED = 1;
+ public static final int EVENT_TYPE_DEVICE_AVAILABLE = 2;
+ public static final int EVENT_TYPE_DEVICE_FEATURES = 3;
+ public static final int EVENT_TYPE_ON_ACTIVE_PRESET_SELECTED = 4;
+ public static final int EVENT_TYPE_ON_ACTIVE_PRESET_SELECT_ERROR = 5;
+ public static final int EVENT_TYPE_ON_PRESET_INFO = 6;
+ public static final int EVENT_TYPE_ON_PRESET_NAME_SET_ERROR = 7;
+ public static final int EVENT_TYPE_ON_PRESET_INFO_ERROR = 8;
+
+ // Connection state values as defined in bt_has.h
+ static final int CONNECTION_STATE_DISCONNECTED = 0;
+ static final int CONNECTION_STATE_CONNECTING = 1;
+ static final int CONNECTION_STATE_CONNECTED = 2;
+ static final int CONNECTION_STATE_DISCONNECTING = 3;
+
+ // Possible operation results
+ static final int STATUS_SET_NAME_NOT_ALLOWED =
+ IBluetoothHapClient.STATUS_SET_NAME_NOT_ALLOWED;
+ static final int STATUS_OPERATION_NOT_SUPPORTED =
+ IBluetoothHapClient.STATUS_OPERATION_NOT_SUPPORTED;
+ static final int STATUS_OPERATION_NOT_POSSIBLE =
+ IBluetoothHapClient.STATUS_OPERATION_NOT_POSSIBLE;
+ static final int STATUS_INVALID_PRESET_NAME_LENGTH =
+ IBluetoothHapClient.STATUS_INVALID_PRESET_NAME_LENGTH;
+ static final int STATUS_INVALID_PRESET_INDEX =
+ IBluetoothHapClient.STATUS_INVALID_PRESET_INDEX;
+ static final int STATUS_GROUP_OPERATION_NOT_SUPPORTED =
+ IBluetoothHapClient.STATUS_GROUP_OPERATION_NOT_SUPPORTED;
+ static final int STATUS_PROCEDURE_ALREADY_IN_PROGRESS =
+ IBluetoothHapClient.STATUS_PROCEDURE_ALREADY_IN_PROGRESS;
+
+ // Supported features
+ public static final int FEATURE_BIT_NUM_TYPE_MONAURAL =
+ IBluetoothHapClient.FEATURE_BIT_NUM_TYPE_MONAURAL;
+ public static final int FEATURE_BIT_NUM_TYPE_BANDED =
+ IBluetoothHapClient.FEATURE_BIT_NUM_TYPE_BANDED;
+ public static final int FEATURE_BIT_NUM_SYNCHRONIZATED_PRESETS =
+ IBluetoothHapClient.FEATURE_BIT_NUM_SYNCHRONIZATED_PRESETS;
+ public static final int FEATURE_BIT_NUM_INDEPENDENT_PRESETS =
+ IBluetoothHapClient.FEATURE_BIT_NUM_INDEPENDENT_PRESETS;
+ public static final int FEATURE_BIT_NUM_DYNAMIC_PRESETS =
+ IBluetoothHapClient.FEATURE_BIT_NUM_DYNAMIC_PRESETS;
+ public static final int FEATURE_BIT_NUM_WRITABLE_PRESETS =
+ IBluetoothHapClient.FEATURE_BIT_NUM_WRITABLE_PRESETS;
+
+ // Preset Info notification reason
+ public static final int PRESET_INFO_REASON_ALL_PRESET_INFO =
+ IBluetoothHapClient.PRESET_INFO_REASON_ALL_PRESET_INFO;
+ public static final int PRESET_INFO_REASON_PRESET_INFO_UPDATE =
+ IBluetoothHapClient.PRESET_INFO_REASON_PRESET_INFO_UPDATE;
+ public static final int PRESET_INFO_REASON_PRESET_DELETED =
+ IBluetoothHapClient.PRESET_INFO_REASON_PRESET_DELETED;
+ public static final int PRESET_INFO_REASON_PRESET_AVAILABILITY_CHANGED =
+ IBluetoothHapClient.PRESET_INFO_REASON_PRESET_AVAILABILITY_CHANGED;
+ public static final int PRESET_INFO_REASON_PRESET_INFO_REQUEST_RESPONSE =
+ IBluetoothHapClient.PRESET_INFO_REASON_PRESET_INFO_REQUEST_RESPONSE;
+
+ public int type;
+ public BluetoothDevice device;
+ public int valueInt1;
+ public int valueInt2;
+ public int valueInt3;
+ public ArrayList valueList;
+
+ HapClientStackEvent(int type) {
+ this.type = type;
+ }
+
+ @Override
+ public String toString() {
+ // event dump
+ StringBuilder result = new StringBuilder();
+ result.append("HearingAccessStackEvent {type:" + eventTypeToString(type));
+ result.append(", device: " + device);
+ result.append(", value1: " + eventTypeValueInt1ToString(type, valueInt1));
+ result.append(", value2: " + eventTypeValueInt2ToString(type, valueInt2));
+ result.append(", value3: " + eventTypeValueInt3ToString(type, valueInt3));
+ result.append(", list: " + eventTypeValueListToString(type, valueList));
+
+ result.append("}");
+ return result.toString();
+ }
+
+ private String eventTypeValueListToString(int type, List value) {
+ switch (type) {
+ case EVENT_TYPE_ON_PRESET_INFO:
+ return "{presets count: " + value.size() + "}";
+ default:
+ return "{list: empty}";
+ }
+ }
+
+ private String eventTypeValueInt1ToString(int type, int value) {
+ switch (type) {
+ case EVENT_TYPE_CONNECTION_STATE_CHANGED:
+ return "{state: " + connectionStateValueToString(value) + "}";
+ case EVENT_TYPE_DEVICE_AVAILABLE:
+ return "{features: " + featuresToString(value) + "}";
+ case EVENT_TYPE_DEVICE_FEATURES:
+ return "{features: " + featuresToString(value) + "}";
+ case EVENT_TYPE_ON_PRESET_NAME_SET_ERROR:
+ return "{statusCode: " + statusCodeValueToString(value) + "}";
+ case EVENT_TYPE_ON_PRESET_INFO_ERROR:
+ return "{statusCode: " + statusCodeValueToString(value) + "}";
+ case EVENT_TYPE_ON_ACTIVE_PRESET_SELECTED:
+ return "{presetIndex: " + value + "}";
+ case EVENT_TYPE_ON_ACTIVE_PRESET_SELECT_ERROR:
+ return "{statusCode: " + statusCodeValueToString(value) + "}";
+ case EVENT_TYPE_ON_PRESET_INFO:
+ default:
+ return "{unused: " + value + "}";
+ }
+ }
+
+ private String infoReasonToString(int value) {
+ switch (value) {
+ case PRESET_INFO_REASON_ALL_PRESET_INFO:
+ return "PRESET_INFO_REASON_ALL_PRESET_INFO";
+ case PRESET_INFO_REASON_PRESET_INFO_UPDATE:
+ return "PRESET_INFO_REASON_PRESET_INFO_UPDATE";
+ case PRESET_INFO_REASON_PRESET_DELETED:
+ return "PRESET_INFO_REASON_PRESET_DELETED";
+ case PRESET_INFO_REASON_PRESET_AVAILABILITY_CHANGED:
+ return "PRESET_INFO_REASON_PRESET_AVAILABILITY_CHANGED";
+ case PRESET_INFO_REASON_PRESET_INFO_REQUEST_RESPONSE:
+ return "PRESET_INFO_REASON_PRESET_INFO_REQUEST_RESPONSE";
+ default:
+ return "UNKNOWN";
+ }
+ }
+
+ private String eventTypeValueInt2ToString(int type, int value) {
+ switch (type) {
+ case EVENT_TYPE_ON_PRESET_NAME_SET_ERROR:
+ return "{presetIndex: " + value + "}";
+ case EVENT_TYPE_ON_PRESET_INFO:
+ return "{info_reason: " + infoReasonToString(value) + "}";
+ case EVENT_TYPE_ON_PRESET_INFO_ERROR:
+ return "{presetIndex: " + value + "}";
+ case EVENT_TYPE_ON_ACTIVE_PRESET_SELECTED:
+ return "{groupId: " + value + "}";
+ case EVENT_TYPE_ON_ACTIVE_PRESET_SELECT_ERROR:
+ return "{groupId: " + value + "}";
+ default:
+ return "{unused: " + value + "}";
+ }
+ }
+
+ private String eventTypeValueInt3ToString(int type, int value) {
+ switch (type) {
+ case EVENT_TYPE_ON_PRESET_INFO:
+ case EVENT_TYPE_ON_PRESET_INFO_ERROR:
+ case EVENT_TYPE_ON_PRESET_NAME_SET_ERROR:
+ return "{groupId: " + value + "}";
+ default:
+ return "{unused: " + value + "}";
+ }
+ }
+
+ private String connectionStateValueToString(int value) {
+ switch (value) {
+ case CONNECTION_STATE_DISCONNECTED:
+ return "CONNECTION_STATE_DISCONNECTED";
+ case CONNECTION_STATE_CONNECTING:
+ return "CONNECTION_STATE_CONNECTING";
+ case CONNECTION_STATE_CONNECTED:
+ return "CONNECTION_STATE_CONNECTED";
+ case CONNECTION_STATE_DISCONNECTING:
+ return "CONNECTION_STATE_DISCONNECTING";
+ default:
+ return "CONNECTION_STATE_UNKNOWN!";
+ }
+ }
+
+ private String statusCodeValueToString(int value) {
+ switch (value) {
+ case STATUS_SET_NAME_NOT_ALLOWED:
+ return "STATUS_SET_NAME_NOT_ALLOWED";
+ case STATUS_OPERATION_NOT_SUPPORTED:
+ return "STATUS_OPERATION_NOT_SUPPORTED";
+ case STATUS_OPERATION_NOT_POSSIBLE:
+ return "STATUS_OPERATION_NOT_POSSIBLE";
+ case STATUS_INVALID_PRESET_NAME_LENGTH:
+ return "STATUS_INVALID_PRESET_NAME_LENGTH";
+ case STATUS_INVALID_PRESET_INDEX:
+ return "STATUS_INVALID_PRESET_INDEX";
+ case STATUS_GROUP_OPERATION_NOT_SUPPORTED:
+ return "STATUS_GROUP_OPERATION_NOT_SUPPORTED";
+ case STATUS_PROCEDURE_ALREADY_IN_PROGRESS:
+ return "STATUS_PROCEDURE_ALREADY_IN_PROGRESS";
+ default:
+ return "UNKNOWN_STATUS_CODE";
+ }
+ }
+
+ private String featuresToString(int value) {
+ String features_str = "";
+ if (BigInteger.valueOf(value).testBit(FEATURE_BIT_NUM_TYPE_MONAURAL)) {
+ features_str += "TYPE_MONAURAL";
+ } else if (BigInteger.valueOf(value).testBit(FEATURE_BIT_NUM_TYPE_BANDED)) {
+ features_str += "TYPE_BANDED";
+ } else {
+ features_str += "TYPE_BINAURAL";
+ }
+
+ if (BigInteger.valueOf(value).testBit(FEATURE_BIT_NUM_SYNCHRONIZATED_PRESETS)) {
+ features_str += ", SYNCHRONIZATED_PRESETS";
+ }
+ if (BigInteger.valueOf(value).testBit(FEATURE_BIT_NUM_INDEPENDENT_PRESETS)) {
+ features_str += ", INDEPENDENT_PRESETS";
+ }
+ if (BigInteger.valueOf(value).testBit(FEATURE_BIT_NUM_DYNAMIC_PRESETS)) {
+ features_str += ", DYNAMIC_PRESETS";
+ }
+ if (BigInteger.valueOf(value).testBit(FEATURE_BIT_NUM_WRITABLE_PRESETS)) {
+ features_str += ", WRITABLE_PRESETS";
+ }
+
+ return features_str;
+ }
+
+ private String availablePresetsToString(byte[] value) {
+ if (value.length == 0) return "NONE";
+
+ String presets_str = "[";
+ for (int i = 0; i < value.length; i++) {
+ presets_str += (value[i] + ", ");
+ }
+
+ presets_str += "]";
+ return presets_str;
+ }
+
+ private static String eventTypeToString(int type) {
+ switch (type) {
+ case EVENT_TYPE_NONE:
+ return "EVENT_TYPE_NONE";
+ case EVENT_TYPE_CONNECTION_STATE_CHANGED:
+ return "EVENT_TYPE_CONNECTION_STATE_CHANGED";
+ case EVENT_TYPE_DEVICE_AVAILABLE:
+ return "EVENT_TYPE_DEVICE_AVAILABLE";
+ case EVENT_TYPE_DEVICE_FEATURES:
+ return "EVENT_TYPE_DEVICE_FEATURES";
+ case EVENT_TYPE_ON_ACTIVE_PRESET_SELECTED:
+ return "EVENT_TYPE_ON_ACTIVE_PRESET_SELECTED";
+ case EVENT_TYPE_ON_ACTIVE_PRESET_SELECT_ERROR:
+ return "EVENT_TYPE_ON_ACTIVE_PRESET_SELECT_ERROR";
+ case EVENT_TYPE_ON_PRESET_INFO:
+ return "EVENT_TYPE_ON_PRESET_INFO";
+ case EVENT_TYPE_ON_PRESET_NAME_SET_ERROR:
+ return "EVENT_TYPE_ON_PRESET_NAME_SET_ERROR";
+ case EVENT_TYPE_ON_PRESET_INFO_ERROR:
+ return "EVENT_TYPE_ON_PRESET_INFO_ERROR";
+ default:
+ return "EVENT_TYPE_UNKNOWN:" + type;
+ }
+ }
+}
diff --git a/android/app/src/com/android/bluetooth/hap/HapClientStateMachine.java b/android/app/src/com/android/bluetooth/hap/HapClientStateMachine.java
new file mode 100644
index 0000000000..7f10decb19
--- /dev/null
+++ b/android/app/src/com/android/bluetooth/hap/HapClientStateMachine.java
@@ -0,0 +1,588 @@
+/*
+ * 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.
+ */
+
+/**
+ * Bluetooth Hap Client StateMachine. There is one instance per remote device.
+ * - "Disconnected" and "Connected" are steady states.
+ * - "Connecting" and "Disconnecting" are transient states until the
+ * connection / disconnection is completed.
+ *
+ *
+ * (Disconnected)
+ * | ^
+ * CONNECT | | DISCONNECTED
+ * V |
+ * (Connecting)<--->(Disconnecting)
+ * | ^
+ * CONNECTED | | DISCONNECT
+ * V |
+ * (Connected)
+ * NOTES:
+ * - If state machine is in "Connecting" state and the remote device sends
+ * DISCONNECT request, the state machine transitions to "Disconnecting" state.
+ * - Similarly, if the state machine is in "Disconnecting" state and the remote device
+ * sends CONNECT request, the state machine transitions to "Connecting" state.
+ *
+ * DISCONNECT
+ * (Connecting) ---------------> (Disconnecting)
+ * <---------------
+ * CONNECT
+ *
+ */
+
+package com.android.bluetooth.hap;
+
+import static android.Manifest.permission.BLUETOOTH_CONNECT;
+
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothHapClient;
+import android.bluetooth.BluetoothProfile;
+import android.content.Intent;
+import android.os.Looper;
+import android.os.Message;
+import android.util.Log;
+
+import com.android.bluetooth.btservice.ProfileService;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.State;
+import com.android.internal.util.StateMachine;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.Scanner;
+
+final class HapClientStateMachine extends StateMachine {
+ static final int CONNECT = 1;
+ static final int DISCONNECT = 2;
+ @VisibleForTesting
+ static final int STACK_EVENT = 101;
+ private static final boolean DBG = true;
+ private static final String TAG = "HapClientStateMachine";
+ private static final int CONNECT_TIMEOUT = 201;
+
+ // NOTE: the value is not "final" - it is modified in the unit tests
+ @VisibleForTesting
+ static int sConnectTimeoutMs = 30000; // 30s
+
+ private final Disconnected mDisconnected;
+ private final Connecting mConnecting;
+ private final Disconnecting mDisconnecting;
+ private final Connected mConnected;
+ private int mConnectionState = BluetoothProfile.STATE_DISCONNECTED;
+ private int mLastConnectionState = -1;
+
+ private final HapClientService mService;
+ private final HapClientNativeInterface mNativeInterface;
+
+ private final BluetoothDevice mDevice;
+
+ HapClientStateMachine(BluetoothDevice device, HapClientService svc,
+ HapClientNativeInterface gattInterface, Looper looper) {
+ super(TAG, looper);
+ mDevice = device;
+ mService = svc;
+ mNativeInterface = gattInterface;
+
+ mDisconnected = new Disconnected();
+ mConnecting = new Connecting();
+ mDisconnecting = new Disconnecting();
+ mConnected = new Connected();
+
+ addState(mDisconnected);
+ addState(mConnecting);
+ addState(mDisconnecting);
+ addState(mConnected);
+
+ setInitialState(mDisconnected);
+ }
+
+ static HapClientStateMachine make(BluetoothDevice device, HapClientService svc,
+ HapClientNativeInterface gattInterface, Looper looper) {
+ Log.i(TAG, "make for device " + device);
+ HapClientStateMachine hearingAccessSm = new HapClientStateMachine(device, svc,
+ gattInterface, looper);
+ hearingAccessSm.start();
+ return hearingAccessSm;
+ }
+
+ private static String messageWhatToString(int what) {
+ switch (what) {
+ case CONNECT:
+ return "CONNECT";
+ case DISCONNECT:
+ return "DISCONNECT";
+ case STACK_EVENT:
+ return "STACK_EVENT";
+ case CONNECT_TIMEOUT:
+ return "CONNECT_TIMEOUT";
+ default:
+ break;
+ }
+ return Integer.toString(what);
+ }
+
+ private static String profileStateToString(int state) {
+ switch (state) {
+ case BluetoothProfile.STATE_DISCONNECTED:
+ return "DISCONNECTED";
+ case BluetoothProfile.STATE_CONNECTING:
+ return "CONNECTING";
+ case BluetoothProfile.STATE_CONNECTED:
+ return "CONNECTED";
+ case BluetoothProfile.STATE_DISCONNECTING:
+ return "DISCONNECTING";
+ default:
+ break;
+ }
+ return Integer.toString(state);
+ }
+
+ public void doQuit() {
+ log("doQuit for device " + mDevice);
+ quitNow();
+ }
+
+ public void cleanup() {
+ log("cleanup for device " + mDevice);
+ }
+
+ int getConnectionState() {
+ return mConnectionState;
+ }
+
+ BluetoothDevice getDevice() {
+ return mDevice;
+ }
+
+ synchronized boolean isConnected() {
+ return (getConnectionState() == BluetoothProfile.STATE_CONNECTED);
+ }
+
+ // This method does not check for error condition (newState == prevState)
+ private void broadcastConnectionState(int newState, int prevState) {
+ log("Connection state " + mDevice + ": " + profileStateToString(prevState)
+ + "->" + profileStateToString(newState));
+
+ Intent intent = new Intent(BluetoothHapClient.ACTION_HAP_CONNECTION_STATE_CHANGED);
+ intent.putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, prevState);
+ intent.putExtra(BluetoothProfile.EXTRA_STATE, newState);
+ intent.putExtra(BluetoothDevice.EXTRA_DEVICE, mDevice);
+ intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT
+ | Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND);
+ mService.sendBroadcast(intent, BLUETOOTH_CONNECT);
+ }
+
+ public void dump(StringBuilder sb) {
+ ProfileService.println(sb, "mDevice: " + mDevice);
+ ProfileService.println(sb, " StateMachine: " + this);
+ // Dump the state machine logs
+ StringWriter stringWriter = new StringWriter();
+ PrintWriter printWriter = new PrintWriter(stringWriter);
+ super.dump(new FileDescriptor(), printWriter, new String[]{});
+ printWriter.flush();
+ stringWriter.flush();
+ ProfileService.println(sb, " StateMachineLog:");
+ Scanner scanner = new Scanner(stringWriter.toString());
+ while (scanner.hasNextLine()) {
+ String line = scanner.nextLine();
+ ProfileService.println(sb, " " + line);
+ }
+ scanner.close();
+ }
+
+ @Override
+ protected void log(String msg) {
+ if (DBG) {
+ super.log(msg);
+ }
+ }
+
+ @VisibleForTesting
+ class Disconnected extends State {
+ @Override
+ public void enter() {
+ Log.i(TAG, "Enter Disconnected(" + mDevice + "): " + messageWhatToString(
+ getCurrentMessage().what));
+ mConnectionState = BluetoothProfile.STATE_DISCONNECTED;
+
+ removeDeferredMessages(DISCONNECT);
+
+ if (mLastConnectionState != -1) {
+ // Don't broadcast during startup
+ broadcastConnectionState(BluetoothProfile.STATE_DISCONNECTED,
+ mLastConnectionState);
+ }
+ }
+
+ @Override
+ public void exit() {
+ log("Exit Disconnected(" + mDevice + "): " + messageWhatToString(
+ getCurrentMessage().what));
+ mLastConnectionState = BluetoothProfile.STATE_DISCONNECTED;
+ }
+
+ @Override
+ public boolean processMessage(Message message) {
+ log("Disconnected process message(" + mDevice + "): " + messageWhatToString(
+ message.what));
+
+ switch (message.what) {
+ case CONNECT:
+ log("Connecting to " + mDevice);
+ if (!mNativeInterface.connectHapClient(mDevice)) {
+ Log.e(TAG, "Disconnected: error connecting to " + mDevice);
+ break;
+ }
+ if (mService.okToConnect(mDevice)) {
+ transitionTo(mConnecting);
+ } else {
+ // Reject the request and stay in Disconnected state
+ Log.w(TAG, "Outgoing HearingAccess Connecting request rejected: "
+ + mDevice);
+ }
+ break;
+ case DISCONNECT:
+ Log.d(TAG, "Disconnected: DISCONNECT: call native disconnect for " + mDevice);
+ mNativeInterface.disconnectHapClient(mDevice);
+ break;
+ case STACK_EVENT:
+ HapClientStackEvent event = (HapClientStackEvent) message.obj;
+ if (DBG) {
+ Log.d(TAG, "Disconnected: stack event: " + event);
+ }
+ if (!mDevice.equals(event.device)) {
+ Log.wtf(TAG, "Device(" + mDevice + "): event mismatch: " + event);
+ }
+ switch (event.type) {
+ case HapClientStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED:
+ processConnectionEvent(event.valueInt1);
+ break;
+ default:
+ Log.e(TAG, "Disconnected: ignoring stack event: " + event);
+ break;
+ }
+ break;
+ default:
+ return NOT_HANDLED;
+ }
+ return HANDLED;
+ }
+
+ // in Disconnected state
+ private void processConnectionEvent(int state) {
+ switch (state) {
+ case HapClientStackEvent.CONNECTION_STATE_DISCONNECTED:
+ Log.w(TAG, "Ignore HearingAccess DISCONNECTED event: " + mDevice);
+ break;
+ case HapClientStackEvent.CONNECTION_STATE_CONNECTING:
+ if (mService.okToConnect(mDevice)) {
+ Log.i(TAG, "Incoming HearingAccess Connecting request accepted: "
+ + mDevice);
+ transitionTo(mConnecting);
+ } else {
+ // Reject the connection and stay in Disconnected state itself
+ Log.w(TAG, "Incoming HearingAccess Connecting request rejected: "
+ + mDevice);
+ mNativeInterface.disconnectHapClient(mDevice);
+ }
+ break;
+ case HapClientStackEvent.CONNECTION_STATE_CONNECTED:
+ Log.w(TAG, "HearingAccess Connected from Disconnected state: " + mDevice);
+ if (mService.okToConnect(mDevice)) {
+ Log.i(TAG, "Incoming HearingAccess Connected request accepted: " + mDevice);
+ transitionTo(mConnected);
+ } else {
+ // Reject the connection and stay in Disconnected state itself
+ Log.w(TAG, "Incoming HearingAccess Connected request rejected: " + mDevice);
+ mNativeInterface.disconnectHapClient(mDevice);
+ }
+ break;
+ case HapClientStackEvent.CONNECTION_STATE_DISCONNECTING:
+ Log.w(TAG, "Ignore HearingAccess DISCONNECTING event: " + mDevice);
+ break;
+ default:
+ Log.e(TAG, "Incorrect state: " + state + " device: " + mDevice);
+ break;
+ }
+ }
+ }
+
+ @VisibleForTesting
+ class Connecting extends State {
+ @Override
+ public void enter() {
+ Log.i(TAG, "Enter Connecting(" + mDevice + "): "
+ + messageWhatToString(getCurrentMessage().what));
+ sendMessageDelayed(CONNECT_TIMEOUT, sConnectTimeoutMs);
+ mConnectionState = BluetoothProfile.STATE_CONNECTING;
+ broadcastConnectionState(BluetoothProfile.STATE_CONNECTING, mLastConnectionState);
+ }
+
+ @Override
+ public void exit() {
+ log("Exit Connecting(" + mDevice + "): "
+ + messageWhatToString(getCurrentMessage().what));
+ mLastConnectionState = BluetoothProfile.STATE_CONNECTING;
+ removeMessages(CONNECT_TIMEOUT);
+ }
+
+ @Override
+ public boolean processMessage(Message message) {
+ log("Connecting process message(" + mDevice + "): "
+ + messageWhatToString(message.what));
+
+ switch (message.what) {
+ case CONNECT:
+ deferMessage(message);
+ break;
+ case CONNECT_TIMEOUT:
+ Log.w(TAG, "Connecting connection timeout: " + mDevice);
+ mNativeInterface.disconnectHapClient(mDevice);
+ HapClientStackEvent disconnectEvent =
+ new HapClientStackEvent(
+ HapClientStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED);
+ disconnectEvent.device = mDevice;
+ disconnectEvent.valueInt1 = HapClientStackEvent.CONNECTION_STATE_DISCONNECTED;
+ sendMessage(STACK_EVENT, disconnectEvent);
+ break;
+ case DISCONNECT:
+ log("Connecting: connection canceled to " + mDevice);
+ mNativeInterface.disconnectHapClient(mDevice);
+ transitionTo(mDisconnected);
+ break;
+ case STACK_EVENT:
+ HapClientStackEvent event = (HapClientStackEvent) message.obj;
+ log("Connecting: stack event: " + event);
+ if (!mDevice.equals(event.device)) {
+ Log.wtf(TAG, "Device(" + mDevice + "): event mismatch: " + event);
+ }
+ switch (event.type) {
+ case HapClientStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED:
+ processConnectionEvent(event.valueInt1);
+ break;
+ default:
+ Log.e(TAG, "Connecting: ignoring stack event: " + event);
+ break;
+ }
+ break;
+ default:
+ return NOT_HANDLED;
+ }
+ return HANDLED;
+ }
+
+ // in Connecting state
+ private void processConnectionEvent(int state) {
+ switch (state) {
+ case HapClientStackEvent.CONNECTION_STATE_DISCONNECTED:
+ Log.w(TAG, "Connecting device disconnected: " + mDevice);
+ transitionTo(mDisconnected);
+ break;
+ case HapClientStackEvent.CONNECTION_STATE_CONNECTED:
+ transitionTo(mConnected);
+ break;
+ case HapClientStackEvent.CONNECTION_STATE_CONNECTING:
+ break;
+ case HapClientStackEvent.CONNECTION_STATE_DISCONNECTING:
+ Log.w(TAG, "Connecting interrupted: device is disconnecting: " + mDevice);
+ transitionTo(mDisconnecting);
+ break;
+ default:
+ Log.e(TAG, "Incorrect state: " + state);
+ break;
+ }
+ }
+ }
+
+ @VisibleForTesting
+ class Disconnecting extends State {
+ @Override
+ public void enter() {
+ Log.i(TAG, "Enter Disconnecting(" + mDevice + "): "
+ + messageWhatToString(getCurrentMessage().what));
+ sendMessageDelayed(CONNECT_TIMEOUT, sConnectTimeoutMs);
+ mConnectionState = BluetoothProfile.STATE_DISCONNECTING;
+ broadcastConnectionState(BluetoothProfile.STATE_DISCONNECTING, mLastConnectionState);
+ }
+
+ @Override
+ public void exit() {
+ log("Exit Disconnecting(" + mDevice + "): "
+ + messageWhatToString(getCurrentMessage().what));
+ mLastConnectionState = BluetoothProfile.STATE_DISCONNECTING;
+ removeMessages(CONNECT_TIMEOUT);
+ }
+
+ @Override
+ public boolean processMessage(Message message) {
+ log("Disconnecting process message(" + mDevice + "): "
+ + messageWhatToString(message.what));
+
+ switch (message.what) {
+ case CONNECT:
+ deferMessage(message);
+ break;
+ case CONNECT_TIMEOUT: {
+ Log.w(TAG, "Disconnecting connection timeout: " + mDevice);
+ mNativeInterface.disconnectHapClient(mDevice);
+
+ HapClientStackEvent disconnectEvent =
+ new HapClientStackEvent(
+ HapClientStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED);
+ disconnectEvent.device = mDevice;
+ disconnectEvent.valueInt1 = HapClientStackEvent.CONNECTION_STATE_DISCONNECTED;
+ sendMessage(STACK_EVENT, disconnectEvent);
+ break;
+ }
+ case DISCONNECT:
+ deferMessage(message);
+ break;
+ case STACK_EVENT:
+ HapClientStackEvent event = (HapClientStackEvent) message.obj;
+ log("Disconnecting: stack event: " + event);
+ if (!mDevice.equals(event.device)) {
+ Log.wtf(TAG, "Device(" + mDevice + "): event mismatch: " + event);
+ }
+ switch (event.type) {
+ case HapClientStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED:
+ processConnectionEvent(event.valueInt1);
+ break;
+ default:
+ Log.e(TAG, "Disconnecting: ignoring stack event: " + event);
+ break;
+ }
+ break;
+ default:
+ return NOT_HANDLED;
+ }
+ return HANDLED;
+ }
+
+ // in Disconnecting state
+ private void processConnectionEvent(int state) {
+ switch (state) {
+ case HapClientStackEvent.CONNECTION_STATE_DISCONNECTED:
+ Log.i(TAG, "Disconnected: " + mDevice);
+ transitionTo(mDisconnected);
+ break;
+ case HapClientStackEvent.CONNECTION_STATE_CONNECTED:
+ if (mService.okToConnect(mDevice)) {
+ Log.w(TAG, "Disconnecting interrupted: device is connected: " + mDevice);
+ transitionTo(mConnected);
+ } else {
+ // Reject the connection and stay in Disconnecting state
+ Log.w(TAG, "Incoming HearingAccess Connected request rejected: " + mDevice);
+ mNativeInterface.disconnectHapClient(mDevice);
+ }
+ break;
+ case HapClientStackEvent.CONNECTION_STATE_CONNECTING:
+ if (mService.okToConnect(mDevice)) {
+ Log.i(TAG, "Disconnecting interrupted: try to reconnect: " + mDevice);
+ transitionTo(mConnecting);
+ } else {
+ // Reject the connection and stay in Disconnecting state
+ Log.w(TAG, "Incoming HearingAccess Connecting request rejected: "
+ + mDevice);
+ mNativeInterface.disconnectHapClient(mDevice);
+ }
+ break;
+ case HapClientStackEvent.CONNECTION_STATE_DISCONNECTING:
+ break;
+ default:
+ Log.e(TAG, "Incorrect state: " + state);
+ break;
+ }
+ }
+ }
+
+ @VisibleForTesting
+ class Connected extends State {
+ @Override
+ public void enter() {
+ Log.i(TAG, "Enter Connected(" + mDevice + "): "
+ + messageWhatToString(getCurrentMessage().what));
+ mConnectionState = BluetoothProfile.STATE_CONNECTED;
+ removeDeferredMessages(CONNECT);
+ broadcastConnectionState(BluetoothProfile.STATE_CONNECTED, mLastConnectionState);
+ }
+
+ @Override
+ public void exit() {
+ log("Exit Connected(" + mDevice + "): "
+ + messageWhatToString(getCurrentMessage().what));
+ mLastConnectionState = BluetoothProfile.STATE_CONNECTED;
+ }
+
+ @Override
+ public boolean processMessage(Message message) {
+ log("Connected process message(" + mDevice + "): "
+ + messageWhatToString(message.what));
+
+ switch (message.what) {
+ case CONNECT:
+ Log.w(TAG, "Connected: CONNECT ignored: " + mDevice);
+ break;
+ case DISCONNECT:
+ log("Disconnecting from " + mDevice);
+ if (!mNativeInterface.disconnectHapClient(mDevice)) {
+ // If error in the native stack, transition directly to Disconnected state.
+ Log.e(TAG, "Connected: error disconnecting from " + mDevice);
+ transitionTo(mDisconnected);
+ break;
+ }
+ transitionTo(mDisconnecting);
+ break;
+ case STACK_EVENT:
+ HapClientStackEvent event = (HapClientStackEvent) message.obj;
+ log("Connected: stack event: " + event);
+ if (!mDevice.equals(event.device)) {
+ Log.wtf(TAG, "Device(" + mDevice + "): event mismatch: " + event);
+ }
+ switch (event.type) {
+ case HapClientStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED:
+ processConnectionEvent(event.valueInt1);
+ break;
+ default:
+ Log.e(TAG, "Connected: ignoring stack event: " + event);
+ break;
+ }
+ break;
+ default:
+ return NOT_HANDLED;
+ }
+ return HANDLED;
+ }
+
+ // in Connected state
+ private void processConnectionEvent(int state) {
+ switch (state) {
+ case HapClientStackEvent.CONNECTION_STATE_DISCONNECTED:
+ Log.i(TAG, "Disconnected from " + mDevice + " but still in Allowlist");
+ transitionTo(mDisconnected);
+ break;
+ case HapClientStackEvent.CONNECTION_STATE_DISCONNECTING:
+ Log.i(TAG, "Disconnecting from " + mDevice);
+ transitionTo(mDisconnecting);
+ break;
+ default:
+ Log.e(TAG, "Connection State Device: " + mDevice + " bad state: " + state);
+ break;
+ }
+ }
+ }
+}
diff --git a/android/app/tests/unit/src/com/android/bluetooth/btservice/storage/DatabaseManagerTest.java b/android/app/tests/unit/src/com/android/bluetooth/btservice/storage/DatabaseManagerTest.java
index bc6483b3b0..57eed65e5f 100644
--- a/android/app/tests/unit/src/com/android/bluetooth/btservice/storage/DatabaseManagerTest.java
+++ b/android/app/tests/unit/src/com/android/bluetooth/btservice/storage/DatabaseManagerTest.java
@@ -1105,6 +1105,29 @@ public final class DatabaseManagerTest {
}
}
+ @Test
+ public void testDatabaseMigration_109_110() throws IOException {
+ String testString = "TEST STRING";
+ // Create a database with version 109
+ SupportSQLiteDatabase db = testHelper.createDatabase(DB_NAME, 109);
+ // insert a device to the database
+ ContentValues device = new ContentValues();
+ device.put("address", TEST_BT_ADDR);
+ device.put("migrated", false);
+ assertThat(db.insert("metadata", SQLiteDatabase.CONFLICT_IGNORE, device),
+ CoreMatchers.not(-1));
+ // Migrate database from 109 to 110
+ db.close();
+ db = testHelper.runMigrationsAndValidate(DB_NAME, 110, true,
+ MetadataDatabase.MIGRATION_109_110);
+ Cursor cursor = db.query("SELECT * FROM metadata");
+ assertHasColumn(cursor, "hap_client_connection_policy", true);
+ while (cursor.moveToNext()) {
+ // Check the new columns was added with default value
+ assertColumnIntData(cursor, "hap_client_connection_policy", 100);
+ }
+ }
+
/**
* Helper function to check whether the database has the expected column
*/
diff --git a/android/app/tests/unit/src/com/android/bluetooth/btservice/storage/schemas/com.android.bluetooth.btservice.storage.MetadataDatabase/110.json b/android/app/tests/unit/src/com/android/bluetooth/btservice/storage/schemas/com.android.bluetooth.btservice.storage.MetadataDatabase/110.json
new file mode 100644
index 0000000000..7e934727a4
--- /dev/null
+++ b/android/app/tests/unit/src/com/android/bluetooth/btservice/storage/schemas/com.android.bluetooth.btservice.storage.MetadataDatabase/110.json
@@ -0,0 +1,310 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 110,
+ "identityHash": "c7e5587836ae523b01483700aa686a1f",
+ "entities": [
+ {
+ "tableName": "metadata",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`address` TEXT NOT NULL, `migrated` INTEGER NOT NULL, `a2dpSupportsOptionalCodecs` INTEGER NOT NULL, `a2dpOptionalCodecsEnabled` INTEGER NOT NULL, `last_active_time` INTEGER NOT NULL, `is_active_a2dp_device` INTEGER NOT NULL, `a2dp_connection_policy` INTEGER, `a2dp_sink_connection_policy` INTEGER, `hfp_connection_policy` INTEGER, `hfp_client_connection_policy` INTEGER, `hid_host_connection_policy` INTEGER, `pan_connection_policy` INTEGER, `pbap_connection_policy` INTEGER, `pbap_client_connection_policy` INTEGER, `map_connection_policy` INTEGER, `sap_connection_policy` INTEGER, `hearing_aid_connection_policy` INTEGER, `hap_client_connection_policy` INTEGER, `map_client_connection_policy` INTEGER, `le_audio_connection_policy` INTEGER, `volume_control_connection_policy` INTEGER, `csip_set_coordinator_connection_policy` INTEGER, `le_call_control_connection_policy` INTEGER, `manufacturer_name` BLOB, `model_name` BLOB, `software_version` BLOB, `hardware_version` BLOB, `companion_app` BLOB, `main_icon` BLOB, `is_untethered_headset` BLOB, `untethered_left_icon` BLOB, `untethered_right_icon` BLOB, `untethered_case_icon` BLOB, `untethered_left_battery` BLOB, `untethered_right_battery` BLOB, `untethered_case_battery` BLOB, `untethered_left_charging` BLOB, `untethered_right_charging` BLOB, `untethered_case_charging` BLOB, `enhanced_settings_ui_uri` BLOB, `device_type` BLOB, `main_battery` BLOB, `main_charging` BLOB, `main_low_battery_threshold` BLOB, `untethered_left_low_battery_threshold` BLOB, `untethered_right_low_battery_threshold` BLOB, `untethered_case_low_battery_threshold` BLOB, PRIMARY KEY(`address`))",
+ "fields": [
+ {
+ "fieldPath": "address",
+ "columnName": "address",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "migrated",
+ "columnName": "migrated",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "a2dpSupportsOptionalCodecs",
+ "columnName": "a2dpSupportsOptionalCodecs",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "a2dpOptionalCodecsEnabled",
+ "columnName": "a2dpOptionalCodecsEnabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "last_active_time",
+ "columnName": "last_active_time",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "is_active_a2dp_device",
+ "columnName": "is_active_a2dp_device",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "profileConnectionPolicies.a2dp_connection_policy",
+ "columnName": "a2dp_connection_policy",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profileConnectionPolicies.a2dp_sink_connection_policy",
+ "columnName": "a2dp_sink_connection_policy",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profileConnectionPolicies.hfp_connection_policy",
+ "columnName": "hfp_connection_policy",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profileConnectionPolicies.hfp_client_connection_policy",
+ "columnName": "hfp_client_connection_policy",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profileConnectionPolicies.hid_host_connection_policy",
+ "columnName": "hid_host_connection_policy",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profileConnectionPolicies.pan_connection_policy",
+ "columnName": "pan_connection_policy",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profileConnectionPolicies.pbap_connection_policy",
+ "columnName": "pbap_connection_policy",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profileConnectionPolicies.pbap_client_connection_policy",
+ "columnName": "pbap_client_connection_policy",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profileConnectionPolicies.map_connection_policy",
+ "columnName": "map_connection_policy",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profileConnectionPolicies.sap_connection_policy",
+ "columnName": "sap_connection_policy",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profileConnectionPolicies.hearing_aid_connection_policy",
+ "columnName": "hearing_aid_connection_policy",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profileConnectionPolicies.hap_client_connection_policy",
+ "columnName": "hap_client_connection_policy",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profileConnectionPolicies.map_client_connection_policy",
+ "columnName": "map_client_connection_policy",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profileConnectionPolicies.le_audio_connection_policy",
+ "columnName": "le_audio_connection_policy",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profileConnectionPolicies.volume_control_connection_policy",
+ "columnName": "volume_control_connection_policy",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profileConnectionPolicies.csip_set_coordinator_connection_policy",
+ "columnName": "csip_set_coordinator_connection_policy",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "profileConnectionPolicies.le_call_control_connection_policy",
+ "columnName": "le_call_control_connection_policy",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.manufacturer_name",
+ "columnName": "manufacturer_name",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.model_name",
+ "columnName": "model_name",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.software_version",
+ "columnName": "software_version",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.hardware_version",
+ "columnName": "hardware_version",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.companion_app",
+ "columnName": "companion_app",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.main_icon",
+ "columnName": "main_icon",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.is_untethered_headset",
+ "columnName": "is_untethered_headset",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.untethered_left_icon",
+ "columnName": "untethered_left_icon",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.untethered_right_icon",
+ "columnName": "untethered_right_icon",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.untethered_case_icon",
+ "columnName": "untethered_case_icon",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.untethered_left_battery",
+ "columnName": "untethered_left_battery",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.untethered_right_battery",
+ "columnName": "untethered_right_battery",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.untethered_case_battery",
+ "columnName": "untethered_case_battery",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.untethered_left_charging",
+ "columnName": "untethered_left_charging",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.untethered_right_charging",
+ "columnName": "untethered_right_charging",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.untethered_case_charging",
+ "columnName": "untethered_case_charging",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.enhanced_settings_ui_uri",
+ "columnName": "enhanced_settings_ui_uri",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.device_type",
+ "columnName": "device_type",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.main_battery",
+ "columnName": "main_battery",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.main_charging",
+ "columnName": "main_charging",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.main_low_battery_threshold",
+ "columnName": "main_low_battery_threshold",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.untethered_left_low_battery_threshold",
+ "columnName": "untethered_left_low_battery_threshold",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.untethered_right_low_battery_threshold",
+ "columnName": "untethered_right_low_battery_threshold",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "publicMetadata.untethered_case_low_battery_threshold",
+ "columnName": "untethered_case_low_battery_threshold",
+ "affinity": "BLOB",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "address"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'c7e5587836ae523b01483700aa686a1f')"
+ ]
+ }
+} \ No newline at end of file
diff --git a/android/app/tests/unit/src/com/android/bluetooth/hap/HapClientStateMachineTest.java b/android/app/tests/unit/src/com/android/bluetooth/hap/HapClientStateMachineTest.java
new file mode 100644
index 0000000000..7127016dcc
--- /dev/null
+++ b/android/app/tests/unit/src/com/android/bluetooth/hap/HapClientStateMachineTest.java
@@ -0,0 +1,267 @@
+/*
+ * 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 com.android.bluetooth.hap;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.*;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothProfile;
+import android.content.Context;
+import android.content.Intent;
+import android.os.HandlerThread;
+import android.test.suitebuilder.annotation.MediumTest;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.bluetooth.R;
+import com.android.bluetooth.TestUtils;
+import com.android.bluetooth.btservice.AdapterService;
+
+import org.hamcrest.core.IsInstanceOf;
+import org.junit.*;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class HapClientStateMachineTest {
+ private BluetoothAdapter mAdapter;
+ private Context mTargetContext;
+ private HandlerThread mHandlerThread;
+ private HapClientStateMachine mHapClientStateMachine;
+ private BluetoothDevice mTestDevice;
+ private static final int TIMEOUT_MS = 1000;
+
+ @Mock
+ private AdapterService mAdapterService;
+ @Mock
+ private HapClientService mHapClientService;
+ @Mock
+ private HapClientNativeInterface mHearingAccessGattClientInterface;
+
+ @Before
+ public void setUp() throws Exception {
+ mTargetContext = InstrumentationRegistry.getTargetContext();
+ Assume.assumeTrue("Ignore test when HearingAccessClientService is not enabled",
+ mTargetContext.getResources().getBoolean(
+ R.bool.profile_supported_hap_client));
+ // Set up mocks and test assets
+ MockitoAnnotations.initMocks(this);
+ TestUtils.setAdapterService(mAdapterService);
+
+ mAdapter = BluetoothAdapter.getDefaultAdapter();
+
+ // Get a device for testing
+ mTestDevice = mAdapter.getRemoteDevice("00:01:02:03:04:05");
+
+ // Set up thread and looper
+ mHandlerThread = new HandlerThread("HapClientStateMachineTestHandlerThread");
+ mHandlerThread.start();
+ mHapClientStateMachine = new HapClientStateMachine(mTestDevice,
+ mHapClientService, mHearingAccessGattClientInterface, mHandlerThread.getLooper());
+ // Override the timeout value to speed up the test
+ HapClientStateMachine.sConnectTimeoutMs = 1000; // 1s
+ mHapClientStateMachine.start();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ if (!mTargetContext.getResources().getBoolean(
+ R.bool.profile_supported_hap_client)) {
+ return;
+ }
+ mHapClientStateMachine.doQuit();
+ mHandlerThread.quit();
+ TestUtils.clearAdapterService(mAdapterService);
+ }
+
+ /**
+ * Test that default state is disconnected
+ */
+ @Test
+ public void testDefaultDisconnectedState() {
+ Assert.assertEquals(BluetoothProfile.STATE_DISCONNECTED,
+ mHapClientStateMachine.getConnectionState());
+ }
+
+ /**
+ * Allow/disallow connection to any device.
+ *
+ * @param allow if true, connection is allowed
+ */
+ private void allowConnection(boolean allow) {
+ doReturn(allow).when(mHapClientService).okToConnect(any(BluetoothDevice.class));
+ }
+
+ /**
+ * Test that an incoming connection with policy forbidding connection is rejected
+ */
+ @Test
+ public void testIncomingPolicyReject() {
+ allowConnection(false);
+
+ // Inject an event for when incoming connection is requested
+ HapClientStackEvent connStCh =
+ new HapClientStackEvent(
+ HapClientStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED);
+ connStCh.device = mTestDevice;
+ connStCh.valueInt1 = HapClientStackEvent.CONNECTION_STATE_CONNECTED;
+ mHapClientStateMachine.sendMessage(HapClientStateMachine.STACK_EVENT, connStCh);
+
+ // Verify that no connection state broadcast is executed
+ verify(mHapClientService, after(TIMEOUT_MS).never()).sendBroadcast(any(Intent.class),
+ anyString());
+ // Check that we are in Disconnected state
+ Assert.assertThat(mHapClientStateMachine.getCurrentState(),
+ IsInstanceOf.instanceOf(HapClientStateMachine.Disconnected.class));
+ }
+
+ /**
+ * Test that an incoming connection with policy allowing connection is accepted
+ */
+ @Test
+ public void testIncomingPolicyAccept() {
+ allowConnection(true);
+
+ // Inject an event for when incoming connection is requested
+ HapClientStackEvent connStCh =
+ new HapClientStackEvent(
+ HapClientStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED);
+ connStCh.device = mTestDevice;
+ connStCh.valueInt1 = HapClientStackEvent.CONNECTION_STATE_CONNECTING;
+ mHapClientStateMachine.sendMessage(HapClientStateMachine.STACK_EVENT, connStCh);
+
+ // Verify that one connection state broadcast is executed
+ ArgumentCaptor<Intent> intentArgument1 = ArgumentCaptor.forClass(Intent.class);
+ verify(mHapClientService, timeout(TIMEOUT_MS).times(1)).sendBroadcast(
+ intentArgument1.capture(), anyString());
+ Assert.assertEquals(BluetoothProfile.STATE_CONNECTING,
+ intentArgument1.getValue().getIntExtra(BluetoothProfile.EXTRA_STATE, -1));
+
+ // Check that we are in Connecting state
+ Assert.assertThat(mHapClientStateMachine.getCurrentState(),
+ IsInstanceOf.instanceOf(HapClientStateMachine.Connecting.class));
+
+ // Send a message to trigger connection completed
+ HapClientStackEvent connCompletedEvent =
+ new HapClientStackEvent(HapClientStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED);
+ connCompletedEvent.device = mTestDevice;
+ connCompletedEvent.valueInt1 = HapClientStackEvent.CONNECTION_STATE_CONNECTED;
+ mHapClientStateMachine.sendMessage(HapClientStateMachine.STACK_EVENT,
+ connCompletedEvent);
+
+ // Verify that the expected number of broadcasts are executed:
+ // - two calls to broadcastConnectionState(): Disconnected -> Connecting -> Connected
+ ArgumentCaptor<Intent> intentArgument2 = ArgumentCaptor.forClass(Intent.class);
+ verify(mHapClientService, timeout(TIMEOUT_MS).times(2)).sendBroadcast(
+ intentArgument2.capture(), anyString());
+ // Check that we are in Connected state
+ Assert.assertThat(mHapClientStateMachine.getCurrentState(),
+ IsInstanceOf.instanceOf(HapClientStateMachine.Connected.class));
+ }
+
+ /**
+ * Test that an outgoing connection times out
+ */
+ @Test
+ public void testOutgoingTimeout() {
+ allowConnection(true);
+ doReturn(true).when(mHearingAccessGattClientInterface).connectHapClient(any(
+ BluetoothDevice.class));
+ doReturn(true).when(mHearingAccessGattClientInterface).disconnectHapClient(any(
+ BluetoothDevice.class));
+
+ // Send a connect request
+ mHapClientStateMachine.sendMessage(HapClientStateMachine.CONNECT, mTestDevice);
+
+ // Verify that one connection state broadcast is executed
+ ArgumentCaptor<Intent> intentArgument1 = ArgumentCaptor.forClass(Intent.class);
+ verify(mHapClientService, timeout(TIMEOUT_MS).times(1)).sendBroadcast(
+ intentArgument1.capture(),
+ anyString());
+ Assert.assertEquals(BluetoothProfile.STATE_CONNECTING,
+ intentArgument1.getValue().getIntExtra(BluetoothProfile.EXTRA_STATE, -1));
+
+ // Check that we are in Connecting state
+ Assert.assertThat(mHapClientStateMachine.getCurrentState(),
+ IsInstanceOf.instanceOf(HapClientStateMachine.Connecting.class));
+
+ // Verify that one connection state broadcast is executed
+ ArgumentCaptor<Intent> intentArgument2 = ArgumentCaptor.forClass(Intent.class);
+ verify(mHapClientService, timeout(HapClientStateMachine.sConnectTimeoutMs * 2).times(
+ 2)).sendBroadcast(intentArgument2.capture(), anyString());
+ Assert.assertEquals(BluetoothProfile.STATE_DISCONNECTED,
+ intentArgument2.getValue().getIntExtra(BluetoothProfile.EXTRA_STATE, -1));
+
+ // Check that we are in Disconnected state
+ Assert.assertThat(mHapClientStateMachine.getCurrentState(),
+ IsInstanceOf.instanceOf(HapClientStateMachine.Disconnected.class));
+ verify(mHearingAccessGattClientInterface).disconnectHapClient(eq(mTestDevice));
+ }
+
+ /**
+ * Test that an incoming connection times out
+ */
+ @Test
+ public void testIncomingTimeout() {
+ allowConnection(true);
+ doReturn(true).when(mHearingAccessGattClientInterface).connectHapClient(any(
+ BluetoothDevice.class));
+ doReturn(true).when(mHearingAccessGattClientInterface).disconnectHapClient(any(
+ BluetoothDevice.class));
+
+ // Inject an event for when incoming connection is requested
+ HapClientStackEvent connStCh =
+ new HapClientStackEvent(
+ HapClientStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED);
+ connStCh.device = mTestDevice;
+ connStCh.valueInt1 = HapClientStackEvent.CONNECTION_STATE_CONNECTING;
+ mHapClientStateMachine.sendMessage(HapClientStateMachine.STACK_EVENT, connStCh);
+
+ // Verify that one connection state broadcast is executed
+ ArgumentCaptor<Intent> intentArgument1 = ArgumentCaptor.forClass(Intent.class);
+ verify(mHapClientService, timeout(TIMEOUT_MS).times(1)).sendBroadcast(
+ intentArgument1.capture(),
+ anyString());
+ Assert.assertEquals(BluetoothProfile.STATE_CONNECTING,
+ intentArgument1.getValue().getIntExtra(BluetoothProfile.EXTRA_STATE, -1));
+
+ // Check that we are in Connecting state
+ Assert.assertThat(mHapClientStateMachine.getCurrentState(),
+ IsInstanceOf.instanceOf(HapClientStateMachine.Connecting.class));
+
+ // Verify that one connection state broadcast is executed
+ ArgumentCaptor<Intent> intentArgument2 = ArgumentCaptor.forClass(Intent.class);
+ verify(mHapClientService, timeout(HapClientStateMachine.sConnectTimeoutMs * 2).times(
+ 2)).sendBroadcast(intentArgument2.capture(), anyString());
+ Assert.assertEquals(BluetoothProfile.STATE_DISCONNECTED,
+ intentArgument2.getValue().getIntExtra(BluetoothProfile.EXTRA_STATE, -1));
+
+ // Check that we are in Disconnected state
+ Assert.assertThat(mHapClientStateMachine.getCurrentState(),
+ IsInstanceOf.instanceOf(HapClientStateMachine.Disconnected.class));
+ verify(mHearingAccessGattClientInterface).disconnectHapClient(eq(mTestDevice));
+ }
+}
diff --git a/android/app/tests/unit/src/com/android/bluetooth/hap/HapClientTest.java b/android/app/tests/unit/src/com/android/bluetooth/hap/HapClientTest.java
new file mode 100644
index 0000000000..7589ff24ae
--- /dev/null
+++ b/android/app/tests/unit/src/com/android/bluetooth/hap/HapClientTest.java
@@ -0,0 +1,883 @@
+/*
+ * 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 com.android.bluetooth.hap;
+
+import static org.mockito.Mockito.*;
+
+import android.bluetooth.*;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Looper;
+import android.os.ParcelUuid;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.MediumTest;
+import androidx.test.rule.ServiceTestRule;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.bluetooth.R;
+import com.android.bluetooth.TestUtils;
+import com.android.bluetooth.Utils;
+import com.android.bluetooth.btservice.AdapterService;
+import com.android.bluetooth.btservice.ServiceFactory;
+import com.android.bluetooth.btservice.storage.DatabaseManager;
+import com.android.bluetooth.csip.CsipSetCoordinatorService;
+
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Assume;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeoutException;
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class HapClientTest {
+ private static final int TIMEOUT_MS = 1000;
+ @Rule
+ public final ServiceTestRule mServiceRule = new ServiceTestRule();
+ private BluetoothAdapter mAdapter;
+ private BluetoothDevice mDevice;
+ private BluetoothDevice mDevice2;
+ private BluetoothDevice mDevice3;
+ private Context mTargetContext;
+ private HapClientService mService;
+ private IBluetoothHapClient.Stub mServiceBinder;
+ private HasIntentReceiver mHasIntentReceiver;
+ private HashMap<BluetoothDevice, LinkedBlockingQueue<Intent>> mIntentQueue;
+ @Mock
+ private AdapterService mAdapterService;
+ @Mock
+ private DatabaseManager mDatabaseManager;
+ @Mock
+ private HapClientNativeInterface mNativeInterface;
+ @Mock
+ private ServiceFactory mServiceFactory;
+ @Mock
+ private CsipSetCoordinatorService mCsipService;
+
+ @Before
+ public void setUp() throws Exception {
+ mTargetContext = InstrumentationRegistry.getTargetContext();
+ Assume.assumeTrue("Ignore test when HearingAccessClientService is not enabled",
+ mTargetContext.getResources().getBoolean(R.bool.profile_supported_hap_client));
+
+ // Set up mocks and test assets
+ MockitoAnnotations.initMocks(this);
+
+ if (Looper.myLooper() == null) {
+ Looper.prepare();
+ }
+
+ TestUtils.setAdapterService(mAdapterService);
+ doReturn(mDatabaseManager).when(mAdapterService).getDatabase();
+ doReturn(true, false).when(mAdapterService).isStartedProfile(anyString());
+
+ mAdapter = BluetoothAdapter.getDefaultAdapter();
+
+ startService();
+ mService.mHapClientNativeInterface = mNativeInterface;
+ mService.mFactory = mServiceFactory;
+ doReturn(mCsipService).when(mServiceFactory).getCsipSetCoordinatorService();
+ mServiceBinder = (IBluetoothHapClient.Stub) mService.initBinder();
+ Assert.assertNotNull(mServiceBinder);
+
+ // Set up the State Changed receiver
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(BluetoothHapClient.ACTION_HAP_CONNECTION_STATE_CHANGED);
+ filter.addAction(BluetoothHapClient.ACTION_HAP_DEVICE_AVAILABLE);
+ filter.addAction(BluetoothHapClient.ACTION_HAP_ON_ACTIVE_PRESET);
+ filter.addAction(BluetoothHapClient.ACTION_HAP_ON_ACTIVE_PRESET_SELECT_ERROR);
+ filter.addAction(BluetoothHapClient.ACTION_HAP_ON_PRESET_INFO);
+ filter.addAction(BluetoothHapClient.ACTION_HAP_ON_PRESET_NAME_SET_ERROR);
+ filter.addAction(BluetoothHapClient.ACTION_HAP_ON_PRESET_INFO_GET_ERROR);
+
+ mHasIntentReceiver = new HasIntentReceiver();
+ mTargetContext.registerReceiver(mHasIntentReceiver, filter);
+
+ mDevice = TestUtils.getTestDevice(mAdapter, 0);
+ when(mNativeInterface.getDevice(getByteAddress(mDevice))).thenReturn(mDevice);
+ mDevice2 = TestUtils.getTestDevice(mAdapter, 1);
+ when(mNativeInterface.getDevice(getByteAddress(mDevice2))).thenReturn(mDevice2);
+ mDevice3 = TestUtils.getTestDevice(mAdapter, 2);
+ when(mNativeInterface.getDevice(getByteAddress(mDevice3))).thenReturn(mDevice3);
+
+ doReturn(BluetoothDevice.BOND_BONDED).when(mAdapterService)
+ .getBondState(any(BluetoothDevice.class));
+ doReturn(new ParcelUuid[]{BluetoothUuid.HAS}).when(mAdapterService)
+ .getRemoteUuids(any(BluetoothDevice.class));
+ doReturn(mDatabaseManager).when(mAdapterService).getDatabase();
+
+ mIntentQueue = new HashMap<>();
+ mIntentQueue.put(mDevice, new LinkedBlockingQueue<>());
+ mIntentQueue.put(mDevice2, new LinkedBlockingQueue<>());
+ mIntentQueue.put(mDevice3, new LinkedBlockingQueue<>());
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ if (!mTargetContext.getResources().getBoolean(
+ R.bool.profile_supported_hap_client)) {
+ return;
+ }
+ stopService();
+ mTargetContext.unregisterReceiver(mHasIntentReceiver);
+ TestUtils.clearAdapterService(mAdapterService);
+ mIntentQueue.clear();
+ }
+
+ private void startService() throws TimeoutException {
+ TestUtils.startService(mServiceRule, HapClientService.class);
+ mService = HapClientService.getHapClientService();
+ Assert.assertNotNull(mService);
+ }
+
+ private void stopService() throws TimeoutException {
+ TestUtils.stopService(mServiceRule, HapClientService.class);
+ mService = HapClientService.getHapClientService();
+ Assert.assertNull(mService);
+ }
+
+ /**
+ * Test getting HA Service Client
+ */
+ @Test
+ public void testGetHearingAidService() {
+ Assert.assertEquals(mService, HapClientService.getHapClientService());
+ }
+
+ /**
+ * Test stop HA Service Client
+ */
+ @Test
+ public void testStopHearingAidService() {
+ Assert.assertEquals(mService, HapClientService.getHapClientService());
+
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ public void run() {
+ Assert.assertTrue(mService.stop());
+ }
+ });
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ public void run() {
+ Assert.assertTrue(mService.start());
+ }
+ });
+ }
+
+ /**
+ * Test get/set policy for BluetoothDevice
+ */
+ @Test
+ public void testGetSetPolicy() {
+ when(mDatabaseManager
+ .getProfileConnectionPolicy(mDevice, BluetoothProfile.HAP_CLIENT))
+ .thenReturn(BluetoothProfile.CONNECTION_POLICY_UNKNOWN);
+ Assert.assertEquals("Initial device policy",
+ BluetoothProfile.CONNECTION_POLICY_UNKNOWN,
+ mService.getConnectionPolicy(mDevice));
+
+ when(mDatabaseManager
+ .getProfileConnectionPolicy(mDevice, BluetoothProfile.HAP_CLIENT))
+ .thenReturn(BluetoothProfile.CONNECTION_POLICY_FORBIDDEN);
+ Assert.assertEquals("Setting device policy to POLICY_FORBIDDEN",
+ BluetoothProfile.CONNECTION_POLICY_FORBIDDEN,
+ mService.getConnectionPolicy(mDevice));
+
+ when(mDatabaseManager
+ .getProfileConnectionPolicy(mDevice, BluetoothProfile.HAP_CLIENT))
+ .thenReturn(BluetoothProfile.CONNECTION_POLICY_ALLOWED);
+ Assert.assertEquals("Setting device policy to POLICY_ALLOWED",
+ BluetoothProfile.CONNECTION_POLICY_ALLOWED,
+ mService.getConnectionPolicy(mDevice));
+ }
+
+ /**
+ * Test okToConnect method using various test cases
+ */
+ @Test
+ public void testOkToConnect() {
+ int badPolicyValue = 1024;
+ int badBondState = 42;
+ testOkToConnectCase(mDevice,
+ BluetoothDevice.BOND_NONE, BluetoothProfile.CONNECTION_POLICY_UNKNOWN, false);
+ testOkToConnectCase(mDevice,
+ BluetoothDevice.BOND_NONE, BluetoothProfile.CONNECTION_POLICY_FORBIDDEN, false);
+ testOkToConnectCase(mDevice,
+ BluetoothDevice.BOND_NONE, BluetoothProfile.CONNECTION_POLICY_ALLOWED, false);
+ testOkToConnectCase(mDevice,
+ BluetoothDevice.BOND_NONE, badPolicyValue, false);
+ testOkToConnectCase(mDevice,
+ BluetoothDevice.BOND_BONDING, BluetoothProfile.CONNECTION_POLICY_UNKNOWN, false);
+ testOkToConnectCase(mDevice,
+ BluetoothDevice.BOND_BONDING, BluetoothProfile.CONNECTION_POLICY_FORBIDDEN, false);
+ testOkToConnectCase(mDevice,
+ BluetoothDevice.BOND_BONDING, BluetoothProfile.CONNECTION_POLICY_ALLOWED, false);
+ testOkToConnectCase(mDevice,
+ BluetoothDevice.BOND_BONDING, badPolicyValue, false);
+ testOkToConnectCase(mDevice,
+ BluetoothDevice.BOND_BONDED, BluetoothProfile.CONNECTION_POLICY_UNKNOWN, true);
+ testOkToConnectCase(mDevice,
+ BluetoothDevice.BOND_BONDED, BluetoothProfile.CONNECTION_POLICY_FORBIDDEN, false);
+ testOkToConnectCase(mDevice,
+ BluetoothDevice.BOND_BONDED, BluetoothProfile.CONNECTION_POLICY_ALLOWED, true);
+ testOkToConnectCase(mDevice,
+ BluetoothDevice.BOND_BONDED, badPolicyValue, false);
+ testOkToConnectCase(mDevice,
+ badBondState, BluetoothProfile.CONNECTION_POLICY_UNKNOWN, false);
+ testOkToConnectCase(mDevice,
+ badBondState, BluetoothProfile.CONNECTION_POLICY_FORBIDDEN, false);
+ testOkToConnectCase(mDevice,
+ badBondState, BluetoothProfile.CONNECTION_POLICY_ALLOWED, false);
+ testOkToConnectCase(mDevice,
+ badBondState, badPolicyValue, false);
+ }
+
+ /**
+ * Test that an outgoing connection to device that does not have HAS UUID is rejected
+ */
+ @Test
+ public void testOutgoingConnectMissingHasUuid() {
+ // Update the device policy so okToConnect() returns true
+ when(mDatabaseManager
+ .getProfileConnectionPolicy(mDevice, BluetoothProfile.HAP_CLIENT))
+ .thenReturn(BluetoothProfile.CONNECTION_POLICY_ALLOWED);
+ doReturn(true).when(mNativeInterface).connectHapClient(any(BluetoothDevice.class));
+ doReturn(true).when(mNativeInterface).disconnectHapClient(any(BluetoothDevice.class));
+
+ // Return No UUID
+ doReturn(new ParcelUuid[]{}).when(mAdapterService)
+ .getRemoteUuids(any(BluetoothDevice.class));
+
+ // Send a connect request
+ Assert.assertFalse("Connect expected to fail", mService.connect(mDevice));
+ }
+
+ /**
+ * Test that an outgoing connection to device that have HAS UUID is successful
+ */
+ @Test
+ public void testOutgoingConnectExistingHasUuid() {
+ // Update the device policy so okToConnect() returns true
+ when(mDatabaseManager
+ .getProfileConnectionPolicy(mDevice, BluetoothProfile.HAP_CLIENT))
+ .thenReturn(BluetoothProfile.CONNECTION_POLICY_ALLOWED);
+ doReturn(true).when(mNativeInterface).connectHapClient(any(BluetoothDevice.class));
+ doReturn(true).when(mNativeInterface).disconnectHapClient(any(BluetoothDevice.class));
+
+ doReturn(new ParcelUuid[]{BluetoothUuid.HAS}).when(mAdapterService)
+ .getRemoteUuids(any(BluetoothDevice.class));
+
+ // Send a connect request
+ Assert.assertTrue("Connect expected to succeed", mService.connect(mDevice));
+ }
+
+ /**
+ * Test that an outgoing connection to device with POLICY_FORBIDDEN is rejected
+ */
+ @Test
+ public void testOutgoingConnectPolicyForbidden() {
+ doReturn(true).when(mNativeInterface).connectHapClient(any(BluetoothDevice.class));
+ doReturn(true).when(mNativeInterface).disconnectHapClient(any(BluetoothDevice.class));
+
+ // Set the device policy to POLICY_FORBIDDEN so connect() should fail
+ when(mDatabaseManager
+ .getProfileConnectionPolicy(mDevice, BluetoothProfile.HAP_CLIENT))
+ .thenReturn(BluetoothProfile.CONNECTION_POLICY_FORBIDDEN);
+
+ // Send a connect request
+ Assert.assertFalse("Connect expected to fail", mService.connect(mDevice));
+ }
+
+ /**
+ * Test that an outgoing connection times out
+ */
+ @Test
+ public void testOutgoingConnectTimeout() {
+ // Update the device policy so okToConnect() returns true
+ when(mDatabaseManager
+ .getProfileConnectionPolicy(mDevice, BluetoothProfile.HAP_CLIENT))
+ .thenReturn(BluetoothProfile.CONNECTION_POLICY_ALLOWED);
+ doReturn(true).when(mNativeInterface).connectHapClient(any(BluetoothDevice.class));
+ doReturn(true).when(mNativeInterface).disconnectHapClient(any(BluetoothDevice.class));
+
+ // Send a connect request
+ Assert.assertTrue("Connect failed", mService.connect(mDevice));
+
+ // Verify the connection state broadcast, and that we are in Connecting state
+ verifyConnectionStateIntent(TIMEOUT_MS, mDevice, BluetoothProfile.STATE_CONNECTING,
+ BluetoothProfile.STATE_DISCONNECTED);
+ Assert.assertEquals(BluetoothProfile.STATE_CONNECTING,
+ mService.getConnectionState(mDevice));
+
+ // Verify the connection state broadcast, and that we are in Disconnected state
+ verifyConnectionStateIntent(HapClientStateMachine.sConnectTimeoutMs * 2,
+ mDevice, BluetoothProfile.STATE_DISCONNECTED,
+ BluetoothProfile.STATE_CONNECTING);
+ Assert.assertEquals(BluetoothProfile.STATE_DISCONNECTED,
+ mService.getConnectionState(mDevice));
+ }
+
+ /**
+ * Test that an outgoing connection to two device that have HAS UUID is successful
+ */
+ @Test
+ public void testConnectTwo() {
+ doReturn(new ParcelUuid[]{BluetoothUuid.HAS}).when(mAdapterService)
+ .getRemoteUuids(any(BluetoothDevice.class));
+
+ // Send a connect request for the 1st device
+ testConnectingDevice(mDevice);
+
+ // Send a connect request for the 2nd device
+ BluetoothDevice Device2 = TestUtils.getTestDevice(mAdapter, 1);
+ testConnectingDevice(Device2);
+
+ List<BluetoothDevice> devices = mService.getConnectedDevices();
+ Assert.assertTrue(devices.contains(mDevice));
+ Assert.assertTrue(devices.contains(Device2));
+ Assert.assertNotEquals(mDevice, Device2);
+ }
+
+ /**
+ * Test that for the unknown device the API calls are not forwarded down the stack to native.
+ */
+ @Test
+ public void testCallsForNotConnectedDevice() {
+ Assert.assertEquals(true, mService.getActivePresetIndex(mDevice));
+
+ Intent intent = TestUtils.waitForIntent(TIMEOUT_MS, mIntentQueue.get(mDevice));
+ Assert.assertNotNull(intent);
+ Assert.assertEquals(BluetoothHapClient.ACTION_HAP_ON_ACTIVE_PRESET, intent.getAction());
+ Assert.assertEquals(mDevice, intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE));
+ Assert.assertEquals(BluetoothHapClient.PRESET_INDEX_UNAVAILABLE,
+ intent.getIntExtra(BluetoothHapClient.EXTRA_HAP_PRESET_INDEX, -1));
+ }
+
+ /**
+ * Test getting HAS coordinated sets.
+ */
+ @Test
+ public void testGetHapGroupCoordinatedOps() {
+ doReturn(new ParcelUuid[]{BluetoothUuid.HAS}).when(mAdapterService)
+ .getRemoteUuids(any(BluetoothDevice.class));
+ testConnectingDevice(mDevice);
+ testConnectingDevice(mDevice2);
+ testConnectingDevice(mDevice3);
+
+ int flags = 0x04;
+ mNativeInterface.onFeaturesUpdate(getByteAddress(mDevice), flags);
+
+ int flags2 = 0x04;
+ mNativeInterface.onFeaturesUpdate(getByteAddress(mDevice2), flags);
+
+ /* This one has no coordinated operation support but is part of a coordinated set with
+ * mDevice, which supports it, thus mDevice will forward the operation to mDevice2.
+ * This device should also be rocognised as grouped one.
+ */
+ int flags3 = 0;
+ mNativeInterface.onFeaturesUpdate(getByteAddress(mDevice3), flags3);
+
+ /* Prepare CAS groups */
+ int base_group_id = 0x03;
+ Map groups = new HashMap<Integer, ParcelUuid>();
+ groups.put(base_group_id, ParcelUuid.fromString("00001853-0000-1000-8000-00805F9B34FB"));
+
+ Map groups2 = new HashMap<Integer, ParcelUuid>();
+ groups2.put(base_group_id + 1,
+ ParcelUuid.fromString("00001853-0000-1000-8000-00805F9B34FB"));
+
+ doReturn(groups).when(mCsipService).getGroupUuidMapByDevice(mDevice);
+ doReturn(groups).when(mCsipService).getGroupUuidMapByDevice(mDevice3);
+ doReturn(groups2).when(mCsipService).getGroupUuidMapByDevice(mDevice2);
+
+ /* Two devices support coordinated operations thus shell report valid group ID */
+ Assert.assertEquals(base_group_id, mService.getHapGroup(mDevice));
+ Assert.assertEquals(base_group_id + 1, mService.getHapGroup(mDevice2));
+
+ /* Third one has no coordinated operations support but is part of the group */
+ Assert.assertEquals(base_group_id, mService.getHapGroup(mDevice3));
+ }
+
+ /**
+ * Test that selectActivePreset properly calls the native method.
+ */
+ @Test
+ public void testSelectActivePresetNative() {
+ doReturn(new ParcelUuid[]{BluetoothUuid.HAS}).when(mAdapterService)
+ .getRemoteUuids(any(BluetoothDevice.class));
+ testConnectingDevice(mDevice);
+
+ // Verify Native Interface call
+ Assert.assertFalse(mService.selectActivePreset(mDevice, 0x00));
+ verify(mNativeInterface, times(0))
+ .selectActivePreset(eq(mDevice), eq(0x00));
+ Assert.assertTrue(mService.selectActivePreset(mDevice, 0x01));
+ verify(mNativeInterface, times(1))
+ .selectActivePreset(eq(mDevice), eq(0x01));
+ }
+
+ /**
+ * Test that groupSelectActivePreset properly calls the native method.
+ */
+ @Test
+ public void testGroupSelectActivePresetNative() {
+ doReturn(new ParcelUuid[]{BluetoothUuid.HAS}).when(mAdapterService)
+ .getRemoteUuids(any(BluetoothDevice.class));
+ int flags = 0x01;
+ mNativeInterface.onFeaturesUpdate(getByteAddress(mDevice), flags);
+
+ // Verify Native Interface call
+ Assert.assertFalse(mService.groupSelectActivePreset(0x03, 0x00));
+ Assert.assertTrue(mService.groupSelectActivePreset(0x03, 0x01));
+ verify(mNativeInterface, times(1))
+ .groupSelectActivePreset(eq(0x03), eq(0x01));
+ }
+
+ /**
+ * Test that nextActivePreset properly calls the native method.
+ */
+ @Test
+ public void testNextActivePresetNative() {
+ doReturn(new ParcelUuid[]{BluetoothUuid.HAS}).when(mAdapterService)
+ .getRemoteUuids(any(BluetoothDevice.class));
+ testConnectingDevice(mDevice);
+
+ // Verify Native Interface call
+ Assert.assertTrue(mService.nextActivePreset(mDevice));
+ verify(mNativeInterface, times(1))
+ .nextActivePreset(eq(mDevice));
+ }
+
+ /**
+ * Test that groupNextActivePreset properly calls the native method.
+ */
+ @Test
+ public void testGroupNextActivePresetNative() {
+ doReturn(new ParcelUuid[]{BluetoothUuid.HAS}).when(mAdapterService)
+ .getRemoteUuids(any(BluetoothDevice.class));
+ int flags = 0x01;
+ mNativeInterface.onFeaturesUpdate(getByteAddress(mDevice), flags);
+
+ // Verify Native Interface call
+ Assert.assertTrue(mService.groupNextActivePreset(0x03));
+ verify(mNativeInterface, times(1)).groupNextActivePreset(eq(0x03));
+ }
+
+ /**
+ * Test that previousActivePreset properly calls the native method.
+ */
+ @Test
+ public void testPreviousActivePresetNative() {
+ doReturn(new ParcelUuid[]{BluetoothUuid.HAS}).when(mAdapterService)
+ .getRemoteUuids(any(BluetoothDevice.class));
+ testConnectingDevice(mDevice);
+
+ // Verify Native Interface call
+ Assert.assertTrue(mService.previousActivePreset(mDevice));
+ verify(mNativeInterface, times(1))
+ .previousActivePreset(eq(mDevice));
+ }
+
+ /**
+ * Test that groupPreviousActivePreset properly calls the native method.
+ */
+ @Test
+ public void testGroupPreviousActivePresetNative() {
+ doReturn(new ParcelUuid[]{BluetoothUuid.HAS}).when(mAdapterService)
+ .getRemoteUuids(any(BluetoothDevice.class));
+ testConnectingDevice(mDevice);
+ testConnectingDevice(mDevice2);
+
+ int flags = 0x01;
+ mNativeInterface.onFeaturesUpdate(getByteAddress(mDevice), flags);
+
+ // Verify Native Interface call
+ Assert.assertTrue(mService.groupPreviousActivePreset(0x03));
+ verify(mNativeInterface, times(1)).groupPreviousActivePreset(eq(0x03));
+ }
+
+ /**
+ * Test that getActivePresetIndex returns cached value.
+ */
+ @Test
+ public void testGetActivePresetIndex() {
+ doReturn(new ParcelUuid[]{BluetoothUuid.HAS}).when(mAdapterService)
+ .getRemoteUuids(any(BluetoothDevice.class));
+ testConnectingDevice(mDevice);
+ testOnActivePresetSelected(mDevice, 0x01);
+
+ // Verify cached value
+ Assert.assertEquals(true, mService.getActivePresetIndex(mDevice));
+
+ Intent intent = TestUtils.waitForIntent(TIMEOUT_MS, mIntentQueue.get(mDevice));
+ Assert.assertNotNull(intent);
+ Assert.assertEquals(BluetoothHapClient.ACTION_HAP_ON_ACTIVE_PRESET, intent.getAction());
+ Assert.assertEquals(mDevice, intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE));
+ Assert.assertEquals(0x01,
+ intent.getIntExtra(BluetoothHapClient.EXTRA_HAP_PRESET_INDEX, -1));
+ }
+
+ /**
+ * Test that getPresetInfo properly calls the native method.
+ */
+ @Test
+ public void testGetPresetInfoNative() {
+ doReturn(new ParcelUuid[]{BluetoothUuid.HAS}).when(mAdapterService)
+ .getRemoteUuids(any(BluetoothDevice.class));
+ testConnectingDevice(mDevice);
+
+ // Verify Native Interface call
+ Assert.assertFalse(mService.getPresetInfo(mDevice, 0x00));
+ verify(mNativeInterface, times(0))
+ .getPresetInfo(eq(mDevice), eq(0x00));
+ Assert.assertTrue(mService.getPresetInfo(mDevice, 0x01));
+ verify(mNativeInterface, times(1))
+ .getPresetInfo(eq(mDevice), eq(0x01));
+ }
+
+ /**
+ * Test that setPresetName properly calls the native method.
+ */
+ @Test
+ public void testSetPresetNameNative() {
+ doReturn(new ParcelUuid[]{BluetoothUuid.HAS}).when(mAdapterService)
+ .getRemoteUuids(any(BluetoothDevice.class));
+ testConnectingDevice(mDevice);
+
+ // Verify Native Interface call
+ Assert.assertFalse(mService.setPresetName(mDevice, 0x00, "ExamplePresetName"));
+ verify(mNativeInterface, times(0))
+ .setPresetName(eq(mDevice), eq(0x00), eq("ExamplePresetName"));
+ Assert.assertTrue(mService.setPresetName(mDevice, 0x01, "ExamplePresetName"));
+ verify(mNativeInterface, times(1))
+ .setPresetName(eq(mDevice), eq(0x01), eq("ExamplePresetName"));
+ }
+
+ /**
+ * Test that groupSetPresetName properly calls the native method.
+ */
+ @Test
+ public void testGroupSetPresetName() {
+ doReturn(new ParcelUuid[]{BluetoothUuid.HAS}).when(mAdapterService)
+ .getRemoteUuids(any(BluetoothDevice.class));
+ int flags = 0x21;
+ mNativeInterface.onFeaturesUpdate(getByteAddress(mDevice), flags);
+
+ // Verify Native Interface call
+ Assert.assertFalse(mService.groupSetPresetName(0x03, 0x00, "ExamplePresetName"));
+ Assert.assertFalse(mService.groupSetPresetName(-1, 0x01, "ExamplePresetName"));
+ Assert.assertTrue(mService.groupSetPresetName(0x03, 0x01, "ExamplePresetName"));
+ verify(mNativeInterface, times(1))
+ .groupSetPresetName(eq(0x03), eq(0x01), eq("ExamplePresetName"));
+ }
+
+ /**
+ * Test that native callback generates proper intent.
+ */
+ @Test
+ public void testStackEventDeviceAvailable() {
+ doReturn(new ParcelUuid[]{BluetoothUuid.HAS}).when(mAdapterService)
+ .getRemoteUuids(any(BluetoothDevice.class));
+
+ // Verify getting current preset return an invalid value when there is no such device
+ // available
+ Assert.assertEquals(true, mService.getActivePresetIndex(mDevice));
+
+ Intent intent = TestUtils.waitForIntent(TIMEOUT_MS, mIntentQueue.get(mDevice));
+ Assert.assertNotNull(intent);
+ Assert.assertEquals(BluetoothHapClient.ACTION_HAP_ON_ACTIVE_PRESET, intent.getAction());
+ Assert.assertEquals(mDevice, intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE));
+ Assert.assertEquals(BluetoothHapClient.PRESET_INDEX_UNAVAILABLE,
+ intent.getIntExtra(BluetoothHapClient.EXTRA_HAP_PRESET_INDEX, -1));
+
+ mNativeInterface.onDeviceAvailable(getByteAddress(mDevice), 0x03);
+
+ intent = TestUtils.waitForIntent(TIMEOUT_MS, mIntentQueue.get(mDevice));
+ Assert.assertNotNull(intent);
+ Assert.assertEquals(BluetoothHapClient.ACTION_HAP_DEVICE_AVAILABLE, intent.getAction());
+ Assert.assertEquals(mDevice, intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE));
+ Assert.assertEquals(0x03,
+ intent.getIntExtra(BluetoothHapClient.EXTRA_HAP_FEATURES, 0x03));
+ }
+
+ /**
+ * Test that native callback generates proper intent.
+ */
+ @Test
+ public void testStackEventOnActivePresetSelected() {
+ doReturn(new ParcelUuid[]{BluetoothUuid.HAS}).when(mAdapterService)
+ .getRemoteUuids(any(BluetoothDevice.class));
+
+ mNativeInterface.onActivePresetSelected(getByteAddress(mDevice), 0x01);
+
+ Intent intent = TestUtils.waitForIntent(TIMEOUT_MS, mIntentQueue.get(mDevice));
+ Assert.assertNotNull(intent);
+ Assert.assertEquals(BluetoothHapClient.ACTION_HAP_ON_ACTIVE_PRESET, intent.getAction());
+ Assert.assertEquals(mDevice, intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE));
+ Assert.assertEquals(0x01,
+ intent.getIntExtra(BluetoothHapClient.EXTRA_HAP_PRESET_INDEX, -1));
+
+ // Verify that getting current preset returns a proper value now
+ Assert.assertEquals(true, mService.getActivePresetIndex(mDevice));
+
+ intent = TestUtils.waitForIntent(TIMEOUT_MS, mIntentQueue.get(mDevice));
+ Assert.assertNotNull(intent);
+ Assert.assertEquals(BluetoothHapClient.ACTION_HAP_ON_ACTIVE_PRESET, intent.getAction());
+ Assert.assertEquals(mDevice, intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE));
+ Assert.assertEquals(0x01,
+ intent.getIntExtra(BluetoothHapClient.EXTRA_HAP_PRESET_INDEX, -1));
+ }
+
+ /**
+ * Test that native callback generates proper intent.
+ */
+ @Test
+ public void testStackEventOnCurrentPresetSelectError() {
+ doReturn(new ParcelUuid[]{BluetoothUuid.HAS}).when(mAdapterService)
+ .getRemoteUuids(any(BluetoothDevice.class));
+
+ mNativeInterface.onActivePresetSelectError(getByteAddress(mDevice),
+ BluetoothHapClient.STATUS_INVALID_PRESET_INDEX);
+
+ Intent intent = TestUtils.waitForIntent(TIMEOUT_MS, mIntentQueue.get(mDevice));
+ Assert.assertNotNull(intent);
+ Assert.assertEquals(BluetoothHapClient.ACTION_HAP_ON_ACTIVE_PRESET_SELECT_ERROR,
+ intent.getAction());
+ Assert.assertEquals(mDevice, intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE));
+ Assert.assertEquals(BluetoothHapClient.STATUS_INVALID_PRESET_INDEX,
+ intent.getIntExtra(BluetoothHapClient.EXTRA_HAP_STATUS_CODE, -1));
+ }
+
+ /**
+ * Test that native callback generates proper intent.
+ */
+ @Test
+ public void testStackEventOnPresetInfo() {
+ doReturn(new ParcelUuid[]{BluetoothUuid.HAS}).when(mAdapterService)
+ .getRemoteUuids(any(BluetoothDevice.class));
+
+ int info_reason = BluetoothHapClient.PRESET_INFO_REASON_PRESET_INFO_UPDATE;
+ BluetoothHapPresetInfo[] info =
+ {new BluetoothHapPresetInfo.Builder()
+ .setIndex(0x01)
+ .setName("PresetName")
+ .setWritable(true)
+ .setAvailable(true)
+ .build()};
+ mNativeInterface.onPresetInfo(getByteAddress(mDevice), info_reason, info);
+
+ Intent intent = TestUtils.waitForIntent(TIMEOUT_MS, mIntentQueue.get(mDevice));
+ Assert.assertNotNull(intent);
+ Assert.assertEquals(BluetoothHapClient.ACTION_HAP_ON_PRESET_INFO, intent.getAction());
+ Assert.assertEquals(mDevice, intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE));
+ Assert.assertEquals(info_reason,
+ intent.getIntExtra(BluetoothHapClient.EXTRA_HAP_PRESET_INFO_REASON, -1));
+
+ ArrayList presets =
+ intent.getParcelableArrayListExtra(BluetoothHapClient.EXTRA_HAP_PRESET_INFO);
+ Assert.assertNotNull(presets);
+
+ BluetoothHapPresetInfo preset = (BluetoothHapPresetInfo) presets.get(0);
+ Assert.assertEquals(preset.getIndex(), info[0].getIndex());
+ Assert.assertEquals(preset.getName(), info[0].getName());
+ Assert.assertEquals(preset.isWritable(), info[0].isWritable());
+ Assert.assertEquals(preset.isAvailable(), info[0].isAvailable());
+ }
+
+ /**
+ * Test that native callback generates proper intent.
+ */
+ @Test
+ public void testStackEventOnPresetNameSetError() {
+ doReturn(new ParcelUuid[]{BluetoothUuid.HAS}).when(mAdapterService)
+ .getRemoteUuids(any(BluetoothDevice.class));
+
+ mNativeInterface.onPresetNameSetError(getByteAddress(mDevice), 0x01,
+ BluetoothHapClient.STATUS_SET_NAME_NOT_ALLOWED);
+
+ Intent intent = TestUtils.waitForIntent(TIMEOUT_MS, mIntentQueue.get(mDevice));
+ Assert.assertNotNull(intent);
+ Assert.assertEquals(BluetoothHapClient.ACTION_HAP_ON_PRESET_NAME_SET_ERROR,
+ intent.getAction());
+ Assert.assertEquals(0x01,
+ intent.getIntExtra(BluetoothHapClient.EXTRA_HAP_PRESET_INDEX, -1));
+ Assert.assertEquals(mDevice, intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE));
+ Assert.assertEquals(BluetoothHapClient.STATUS_SET_NAME_NOT_ALLOWED,
+ intent.getIntExtra(BluetoothHapClient.EXTRA_HAP_STATUS_CODE, -1));
+ }
+
+ /**
+ * Test that native callback generates proper intent.
+ */
+ @Test
+ public void testStackEventOnPresetInfoError() {
+ doReturn(new ParcelUuid[]{BluetoothUuid.HAS}).when(mAdapterService)
+ .getRemoteUuids(any(BluetoothDevice.class));
+
+ mNativeInterface.onPresetInfoError(getByteAddress(mDevice), 0x01,
+ BluetoothHapClient.STATUS_INVALID_PRESET_INDEX);
+
+ Intent intent = TestUtils.waitForIntent(TIMEOUT_MS, mIntentQueue.get(mDevice));
+ Assert.assertNotNull(intent);
+ Assert.assertEquals(BluetoothHapClient.ACTION_HAP_ON_PRESET_INFO_GET_ERROR,
+ intent.getAction());
+ Assert.assertEquals(0x01,
+ intent.getIntExtra(BluetoothHapClient.EXTRA_HAP_PRESET_INDEX, -1));
+ Assert.assertEquals(mDevice, intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE));
+ Assert.assertEquals(BluetoothHapClient.STATUS_INVALID_PRESET_INDEX,
+ intent.getIntExtra(BluetoothHapClient.EXTRA_HAP_STATUS_CODE, -1));
+ }
+
+ /**
+ * Helper function to test device connecting
+ */
+ private void prepareConnectingDevice(BluetoothDevice device) {
+ // Prepare intent queue and all the mocks
+ mIntentQueue.put(device, new LinkedBlockingQueue<>());
+ when(mNativeInterface.getDevice(getByteAddress(device))).thenReturn(device);
+ when(mDatabaseManager
+ .getProfileConnectionPolicy(device, BluetoothProfile.HAP_CLIENT))
+ .thenReturn(BluetoothProfile.CONNECTION_POLICY_ALLOWED);
+ doReturn(true).when(mNativeInterface).connectHapClient(any(BluetoothDevice.class));
+ doReturn(true).when(mNativeInterface).disconnectHapClient(any(BluetoothDevice.class));
+ }
+
+ /**
+ * Helper function to test device connecting
+ */
+ private void testConnectingDevice(BluetoothDevice device) {
+ prepareConnectingDevice(device);
+ // Send a connect request
+ Assert.assertTrue("Connect expected to succeed", mService.connect(device));
+ verifyConnectingDevice(device);
+ }
+
+ /**
+ * Helper function to test device connecting
+ */
+ private void verifyConnectingDevice(BluetoothDevice device) {
+ // Verify the connection state broadcast, and that we are in Connecting state
+ verifyConnectionStateIntent(TIMEOUT_MS, device, BluetoothProfile.STATE_CONNECTING,
+ BluetoothProfile.STATE_DISCONNECTED);
+ Assert.assertEquals(BluetoothProfile.STATE_CONNECTING, mService.getConnectionState(device));
+
+ // Send a message to trigger connection completed
+ HapClientStackEvent evt =
+ new HapClientStackEvent(HapClientStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED);
+ evt.device = device;
+ evt.valueInt1 = HapClientStackEvent.CONNECTION_STATE_CONNECTED;
+ mService.messageFromNative(evt);
+
+ // Verify the connection state broadcast, and that we are in Connected state
+ verifyConnectionStateIntent(TIMEOUT_MS, device, BluetoothProfile.STATE_CONNECTED,
+ BluetoothProfile.STATE_CONNECTING);
+ Assert.assertEquals(BluetoothProfile.STATE_CONNECTED, mService.getConnectionState(device));
+
+ evt = new HapClientStackEvent(HapClientStackEvent.EVENT_TYPE_DEVICE_AVAILABLE);
+ evt.device = device;
+ evt.valueInt1 = 0x01; // features
+ mService.messageFromNative(evt);
+
+ Intent intent = TestUtils.waitForIntent(TIMEOUT_MS, mIntentQueue.get(device));
+ Assert.assertNotNull(intent);
+ Assert.assertEquals(BluetoothHapClient.ACTION_HAP_DEVICE_AVAILABLE, intent.getAction());
+ Assert.assertEquals(device, intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE));
+ Assert.assertEquals(evt.valueInt1,
+ intent.getIntExtra(BluetoothHapClient.EXTRA_HAP_FEATURES, -1));
+ }
+
+ private void testOnActivePresetSelected(BluetoothDevice device, int index) {
+ HapClientStackEvent evt =
+ new HapClientStackEvent(HapClientStackEvent.EVENT_TYPE_ON_ACTIVE_PRESET_SELECTED);
+ evt.device = device;
+ evt.valueInt1 = index;
+ mService.messageFromNative(evt);
+
+ Intent intent = TestUtils.waitForIntent(TIMEOUT_MS, mIntentQueue.get(device));
+ Assert.assertNotNull(intent);
+ Assert.assertEquals(BluetoothHapClient.ACTION_HAP_ON_ACTIVE_PRESET, intent.getAction());
+ Assert.assertEquals(device, intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE));
+ Assert.assertEquals(index,
+ intent.getIntExtra(BluetoothHapClient.EXTRA_HAP_PRESET_INDEX, -1));
+ }
+
+ /**
+ * Helper function to test ConnectionStateIntent() method
+ */
+ private void verifyConnectionStateIntent(int timeoutMs, BluetoothDevice device,
+ int newState, int prevState) {
+ Intent intent = TestUtils.waitForIntent(timeoutMs, mIntentQueue.get(device));
+ Assert.assertNotNull(intent);
+ Assert.assertEquals(BluetoothHapClient.ACTION_HAP_CONNECTION_STATE_CHANGED,
+ intent.getAction());
+ Assert.assertEquals(device, intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE));
+ Assert.assertEquals(newState, intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1));
+ Assert.assertEquals(prevState, intent.getIntExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE,
+ -1));
+ }
+
+ /**
+ * Helper function to test okToConnect() method
+ */
+ private void testOkToConnectCase(BluetoothDevice device, int bondState, int policy,
+ boolean expected) {
+ doReturn(bondState).when(mAdapterService).getBondState(device);
+ when(mDatabaseManager.getProfileConnectionPolicy(device, BluetoothProfile.HAP_CLIENT))
+ .thenReturn(policy);
+ Assert.assertEquals(expected, mService.okToConnect(device));
+ }
+
+ /**
+ * Helper function to get byte array for a device address
+ */
+ private byte[] getByteAddress(BluetoothDevice device) {
+ if (device == null) {
+ return Utils.getBytesFromAddress("00:00:00:00:00:00");
+ }
+ return Utils.getBytesFromAddress(device.getAddress());
+ }
+
+ private class HasIntentReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ try {
+ BluetoothDevice device = intent.getParcelableExtra(
+ BluetoothDevice.EXTRA_DEVICE);
+ Assert.assertNotNull(device);
+ LinkedBlockingQueue<Intent> queue = mIntentQueue.get(device);
+ Assert.assertNotNull(queue);
+ queue.put(intent);
+ } catch (InterruptedException e) {
+ Assert.fail("Cannot add Intent to the queue: " + e.getMessage());
+ }
+ }
+ }
+}
diff --git a/framework/api/current.txt b/framework/api/current.txt
index 12dde8c8f5..c9b5f93113 100644
--- a/framework/api/current.txt
+++ b/framework/api/current.txt
@@ -751,6 +751,15 @@ package android.bluetooth {
field protected java.util.List<android.bluetooth.BluetoothGattService> mIncludedServices;
}
+ public final class BluetoothHapClient implements java.lang.AutoCloseable android.bluetooth.BluetoothProfile {
+ method public void close();
+ method protected void finalize();
+ method @NonNull @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) public java.util.List<android.bluetooth.BluetoothDevice> getConnectedDevices();
+ method @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) public int getConnectionState(@NonNull android.bluetooth.BluetoothDevice);
+ method @NonNull @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) public java.util.List<android.bluetooth.BluetoothDevice> getDevicesMatchingConnectionStates(@NonNull int[]);
+ field @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) public static final String ACTION_HAP_CONNECTION_STATE_CHANGED = "android.bluetooth.action.HAP_CONNECTION_STATE_CHANGED";
+ }
+
public final class BluetoothHeadset implements android.bluetooth.BluetoothProfile {
method @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) public java.util.List<android.bluetooth.BluetoothDevice> getConnectedDevices();
method @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) public int getConnectionState(android.bluetooth.BluetoothDevice);
diff --git a/framework/api/system-current.txt b/framework/api/system-current.txt
index 2422446add..ad8052428c 100644
--- a/framework/api/system-current.txt
+++ b/framework/api/system-current.txt
@@ -148,6 +148,24 @@ package android.bluetooth {
field public static final int METADATA_UNTETHERED_RIGHT_LOW_BATTERY_THRESHOLD = 22; // 0x16
}
+ public final class BluetoothHapPresetInfo implements android.os.Parcelable {
+ method public int getIndex();
+ method @NonNull public String getName();
+ method public boolean isAvailable();
+ method public boolean isWritable();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @NonNull public static final android.os.Parcelable.Creator<android.bluetooth.BluetoothHapPresetInfo> CREATOR;
+ }
+
+ public static final class BluetoothHapPresetInfo.Builder {
+ ctor public BluetoothHapPresetInfo.Builder();
+ method @NonNull public android.bluetooth.BluetoothHapPresetInfo build();
+ method @NonNull public android.bluetooth.BluetoothHapPresetInfo.Builder setAvailable(@NonNull boolean);
+ method @NonNull public android.bluetooth.BluetoothHapPresetInfo.Builder setIndex(int);
+ method @NonNull public android.bluetooth.BluetoothHapPresetInfo.Builder setName(@NonNull String);
+ method @NonNull public android.bluetooth.BluetoothHapPresetInfo.Builder setWritable(@NonNull boolean);
+ }
+
public final class BluetoothHeadset implements android.bluetooth.BluetoothProfile {
method @RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_CONNECT, android.Manifest.permission.MODIFY_PHONE_STATE}) public boolean connect(android.bluetooth.BluetoothDevice);
method @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) public boolean disconnect(android.bluetooth.BluetoothDevice);
diff --git a/framework/java/android/bluetooth/BluetoothAdapter.java b/framework/java/android/bluetooth/BluetoothAdapter.java
index 334b4eb3df..10ac8e3502 100644
--- a/framework/java/android/bluetooth/BluetoothAdapter.java
+++ b/framework/java/android/bluetooth/BluetoothAdapter.java
@@ -3475,6 +3475,9 @@ public final class BluetoothAdapter {
} else if (profile == BluetoothProfile.HID_DEVICE) {
BluetoothHidDevice hidDevice = new BluetoothHidDevice(context, listener, this);
return true;
+ } else if (profile == BluetoothProfile.HAP_CLIENT) {
+ BluetoothHapClient HapClient = new BluetoothHapClient(context, listener);
+ return true;
} else if (profile == BluetoothProfile.HEARING_AID) {
if (isHearingAidProfileSupported()) {
BluetoothHearingAid hearingAid = new BluetoothHearingAid(context, listener, this);
@@ -3579,6 +3582,10 @@ public final class BluetoothAdapter {
BluetoothHidDevice hidDevice = (BluetoothHidDevice) proxy;
hidDevice.close();
break;
+ case BluetoothProfile.HAP_CLIENT:
+ BluetoothHapClient HapClient = (BluetoothHapClient) proxy;
+ HapClient.close();
+ break;
case BluetoothProfile.HEARING_AID:
BluetoothHearingAid hearingAid = (BluetoothHearingAid) proxy;
hearingAid.close();
diff --git a/framework/java/android/bluetooth/BluetoothHapClient.java b/framework/java/android/bluetooth/BluetoothHapClient.java
new file mode 100644
index 0000000000..992e906963
--- /dev/null
+++ b/framework/java/android/bluetooth/BluetoothHapClient.java
@@ -0,0 +1,1030 @@
+/*
+ * 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 static android.bluetooth.BluetoothUtils.getSyncTimeout;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
+import android.annotation.SdkConstant;
+import android.bluetooth.annotations.RequiresBluetoothConnectPermission;
+import android.content.AttributionSource;
+import android.content.Context;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.CloseGuard;
+import android.util.Log;
+
+import com.android.modules.utils.SynchronousResultReceiver;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeoutException;
+
+
+/**
+ * This class provides a public APIs to control the Bluetooth Hearing Access Profile client service.
+ *
+ * <p>BluetoothHapClient is a proxy object for controlling the Bluetooth HAP
+ * Service client via IPC. Use {@link BluetoothAdapter#getProfileProxy} to get the
+ * BluetoothHapClient proxy object.
+ */
+public final class BluetoothHapClient implements BluetoothProfile, AutoCloseable {
+ private static final String TAG = "BluetoothHapClient";
+ private static final boolean DBG = false;
+ private static final boolean VDBG = false;
+
+ private CloseGuard mCloseGuard;
+
+ /**
+ * Intent used to broadcast the change in connection state of the Hearing Access Profile Client
+ * service. Please note that in the binaural case, there will be two different LE devices for
+ * the left and right side and each device will have their own connection state changes.
+ *
+ * <p>This intent will have 3 extras:
+ * <ul>
+ * <li> {@link #EXTRA_STATE} - The current state of the profile. </li>
+ * <li> {@link #EXTRA_PREVIOUS_STATE}- The previous state of the profile.</li>
+ * <li> {@link BluetoothDevice#EXTRA_DEVICE} - The remote device. </li>
+ * </ul>
+ *
+ * <p>{@link #EXTRA_STATE} or {@link #EXTRA_PREVIOUS_STATE} can be any of
+ * {@link #STATE_DISCONNECTED}, {@link #STATE_CONNECTING},
+ * {@link #STATE_CONNECTED}, {@link #STATE_DISCONNECTING}.
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ @SdkConstant(SdkConstant.SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_HAP_CONNECTION_STATE_CHANGED =
+ "android.bluetooth.action.HAP_CONNECTION_STATE_CHANGED";
+
+ /**
+ * Intent used to broadcast the device availability change and the availability of its
+ * presets. Please note that in the binaural case, there will be two different LE devices for
+ * the left and right side and each device will have their own availability event.
+ *
+ * <p>This intent will have 2 extras:
+ * <ul>
+ * <li> {@link BluetoothDevice#EXTRA_DEVICE} - The remote device. </li>
+ * <li> {@link #EXTRA_HAP_FEATURES} - Supported features map. </li>
+ * </ul>
+ *
+ * @hide
+ */
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ @SdkConstant(SdkConstant.SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_HAP_DEVICE_AVAILABLE =
+ "android.bluetooth.action.HAP_DEVICE_AVAILABLE";
+
+ /**
+ * Intent used to broadcast HA device's feature set.
+ *
+ * <p>This intent will have 2 extras:
+ * <ul>
+ * <li> {@link BluetoothDevice#EXTRA_DEVICE} - The remote device. </li>
+ * <li> {@link #EXTRA_HAP_FEATURES}- The feature set integer with these possible bit numbers
+ * set: {@link #FEATURE_BIT_NUM_TYPE_MONAURAL}, {@link #FEATURE_BIT_NUM_TYPE_BANDED},
+ * {@link #FEATURE_BIT_NUM_SYNCHRONIZATED_PRESETS},
+ * {@link #FEATURE_BIT_NUM_INDEPENDENT_PRESETS}, {@link #FEATURE_BIT_NUM_DYNAMIC_PRESETS},
+ * {@link #FEATURE_BIT_NUM_WRITABLE_PRESETS}.</li>
+ * </ul>
+ *
+ * @hide
+ */
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ @SdkConstant(SdkConstant.SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_HAP_ON_DEVICE_FEATURES =
+ "android.bluetooth.action.HAP_ON_DEVICE_FEATURES";
+
+ /**
+ * Intent used to broadcast the change of a HA device's active preset.
+ *
+ * <p>This intent will have 2 extras:
+ * <ul>
+ * <li> {@link BluetoothDevice#EXTRA_DEVICE} - The remote device. </li>
+ * <li> {@link #EXTRA_HAP_PRESET_INDEX}- The currently active preset.</li>
+ * </ul>
+ *
+ * @hide
+ */
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ @SdkConstant(SdkConstant.SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_HAP_ON_ACTIVE_PRESET =
+ "android.bluetooth.action.HAP_ON_ACTIVE_PRESET";
+
+ /**
+ * Intent used to broadcast the result of a failed preset change attempt.
+ *
+ * <p>This intent will have 2 extras:
+ * <ul>
+ * <li> {@link BluetoothDevice#EXTRA_DEVICE} - The remote device. </li>
+ * <li> {@link #EXTRA_HAP_STATUS_CODE}- Failure reason.</li>
+ * </ul>
+ *
+ * <p>{@link #EXTRA_HAP_STATUS_CODE} can be any of {@link #STATUS_INVALID_PRESET_INDEX},
+ * {@link #STATUS_OPERATION_NOT_POSSIBLE},{@link #STATUS_OPERATION_NOT_SUPPORTED}.
+ *
+ * @hide
+ */
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ @SdkConstant(SdkConstant.SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_HAP_ON_ACTIVE_PRESET_SELECT_ERROR =
+ "android.bluetooth.action.HAP_ON_ACTIVE_PRESET_SELECT_ERROR";
+
+ /**
+ * Intent used to broadcast preset name change.
+ *
+ * <p>This intent will have 4 extras:
+ * <ul>
+ * <li> {@link BluetoothDevice#EXTRA_DEVICE} - The remote device. </li>
+ * <li> {@link #EXTRA_HAP_PRESET_INFO}- List of preset informations </li>
+ * <li> {@link #EXTRA_HAP_PRESET_INFO_REASON}- Why this preset info notification was sent </li>
+ * notifications or the user should expect more to come. </li>
+ * </ul>
+ *
+ * @hide
+ */
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ @SdkConstant(SdkConstant.SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_HAP_ON_PRESET_INFO =
+ "android.bluetooth.action.HAP_ON_PRESET_INFO";
+
+ /**
+ * Intent used to broadcast result of a failed rename attempt.
+ *
+ * <p>This intent will have 3 extras:
+ * <ul>
+ * <li> {@link BluetoothDevice#EXTRA_DEVICE} - The remote device. </li>
+ * <li> {@link #EXTRA_HAP_PRESET_INDEX}- The currently active preset.</li>
+ * <li> {@link #EXTRA_HAP_STATUS_CODE}- Failure reason code.</li>
+ * </ul>
+ *
+ * <p>{@link #EXTRA_HAP_STATUS_CODE} can be any of {@link #STATUS_SET_NAME_NOT_ALLOWED},
+ * {@link #STATUS_INVALID_PRESET_INDEX}, {@link #STATUS_INVALID_PRESET_NAME_LENGTH}.
+ *
+ * @hide
+ */
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ @SdkConstant(SdkConstant.SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_HAP_ON_PRESET_NAME_SET_ERROR =
+ "android.bluetooth.action.HAP_ON_PRESET_NAME_SET_ERROR";
+
+ /**
+ * Intent used to broadcast the result of a failed name get attempt.
+ *
+ * <p>This intent will have 3 extras:
+ * <ul>
+ * <li> {@link BluetoothDevice#EXTRA_DEVICE} - The remote device. </li>
+ * <li> {@link #EXTRA_HAP_PRESET_INDEX}- The currently active preset.</li>
+ * <li> {@link #EXTRA_HAP_STATUS_CODE}- Failure reason code.</li>
+ * </ul>
+ *
+ * <p>{@link #EXTRA_HAP_STATUS_CODE} can be any of {@link #STATUS_INVALID_PRESET_INDEX},
+ * {@link #STATUS_OPERATION_NOT_POSSIBLE}.
+ *
+ * @hide
+ */
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ @SdkConstant(SdkConstant.SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_HAP_ON_PRESET_INFO_GET_ERROR =
+ "android.bluetooth.action.HAP_ON_PRESET_INFO_GET_ERROR";
+
+ /**
+ * Contains a list of all available presets
+ * @hide
+ */
+ public static final String EXTRA_HAP_FEATURES = "android.bluetooth.extra.HAP_FEATURES";
+
+ /**
+ * Contains a preset identifier
+ * @hide
+ */
+ public static final String EXTRA_HAP_PRESET_INDEX = "android.bluetooth.extra.HAP_PRESET_INDEX";
+
+ /**
+ * Used to report failure reasons.
+ * @hide
+ */
+ public static final String EXTRA_HAP_STATUS_CODE = "android.bluetooth.extra.HAP_STATUS_CODE";
+
+ /**
+ * Used by group events.
+ * @hide
+ */
+ public static final String EXTRA_HAP_GROUP_ID = "android.bluetooth.extra.HAP_GROUP_ID";
+
+ /**
+ * Preset Info reason.
+ * Possible values:
+ * {@link #PRESET_INFO_REASON_ALL_PRESET_INFO} or
+ * {@link #PRESET_INFO_REASON_PRESET_INFO_UPDATE} or
+ * {@link #PRESET_INFO_REASON_PRESET_DELETED} or
+ * {@link #PRESET_INFO_REASON_PRESET_AVAILABILITY_CHANGED} or
+ * {@link #PRESET_INFO_REASON_PRESET_INFO_REQUEST_RESPONSE}
+ * @hide
+ */
+ public static final String EXTRA_HAP_PRESET_INFO_REASON =
+ "android.bluetooth.extra.HAP_PRESET_INFO_REASON";
+
+ /**
+ * Preset Info.
+ * @hide
+ */
+ public static final String EXTRA_HAP_PRESET_INFO = "android.bluetooth.extra.HAP_PRESET_INFO";
+
+ /**
+ * Preset name change failure due to preset being read-only.
+ * @hide
+ */
+ public static final int STATUS_SET_NAME_NOT_ALLOWED =
+ IBluetoothHapClient.STATUS_SET_NAME_NOT_ALLOWED;
+
+ /**
+ * Means that the requested operation is not supported by the HA device.
+ *
+ * <p> It could mean that the requested name change is not supported on
+ * a given preset or the device does not support presets at all.
+ * @hide
+ */
+ public static final int STATUS_OPERATION_NOT_SUPPORTED =
+ IBluetoothHapClient.STATUS_OPERATION_NOT_SUPPORTED;
+
+ /**
+ * Usually means a temporary denial of certain operation. Peer device may report this
+ * status due to various implementation specific reasons. It's different than
+ * the {@link #STATUS_OPERATION_NOT_SUPPORTED} which represents more of a
+ * permanent inability to perform some of the operations.
+ * @hide
+ */
+ public static final int STATUS_OPERATION_NOT_POSSIBLE =
+ IBluetoothHapClient.STATUS_OPERATION_NOT_POSSIBLE;
+
+ /**
+ * Used when preset name change failed due to the passed name parameter being to long.
+ * @hide
+ */
+ public static final int STATUS_INVALID_PRESET_NAME_LENGTH =
+ IBluetoothHapClient.STATUS_INVALID_PRESET_NAME_LENGTH;
+
+ /**
+ * Group operations are not supported.
+ * @hide
+ */
+ public static final int STATUS_GROUP_OPERATION_NOT_SUPPORTED =
+ IBluetoothHapClient.STATUS_GROUP_OPERATION_NOT_SUPPORTED;
+
+ /**
+ * Procedure is already in progress.
+ * @hide
+ */
+ public static final int STATUS_PROCEDURE_ALREADY_IN_PROGRESS =
+ IBluetoothHapClient.STATUS_PROCEDURE_ALREADY_IN_PROGRESS;
+
+ /**
+ * Invalid preset index input parameter used in one of the API calls.
+ * @hide
+ */
+ public static final int STATUS_INVALID_PRESET_INDEX =
+ IBluetoothHapClient.STATUS_INVALID_PRESET_INDEX;
+
+ /**
+ * Represets an invalid index value. This is usually value returned in a currently
+ * active preset request for a device which is not connected. This value shouldn't be used
+ * in the API calls.
+ * @hide
+ */
+ public static final int PRESET_INDEX_UNAVAILABLE = IBluetoothHapClient.PRESET_INDEX_UNAVAILABLE;
+
+ /**
+ * Feature bit.
+ * @hide
+ */
+ public static final int FEATURE_BIT_NUM_TYPE_MONAURAL =
+ IBluetoothHapClient.FEATURE_BIT_NUM_TYPE_MONAURAL;
+
+ /**
+ * Feature bit.
+ * @hide
+ */
+ public static final int FEATURE_BIT_NUM_TYPE_BANDED =
+ IBluetoothHapClient.FEATURE_BIT_NUM_TYPE_BANDED;
+
+ /**
+ * Feature bit.
+ * @hide
+ */
+ public static final int FEATURE_BIT_NUM_SYNCHRONIZATED_PRESETS =
+ IBluetoothHapClient.FEATURE_BIT_NUM_SYNCHRONIZATED_PRESETS;
+
+ /**
+ * Feature bit.
+ * @hide
+ */
+ public static final int FEATURE_BIT_NUM_INDEPENDENT_PRESETS =
+ IBluetoothHapClient.FEATURE_BIT_NUM_INDEPENDENT_PRESETS;
+
+ /**
+ * Feature bit.
+ * @hide
+ */
+ public static final int FEATURE_BIT_NUM_DYNAMIC_PRESETS =
+ IBluetoothHapClient.FEATURE_BIT_NUM_DYNAMIC_PRESETS;
+
+ /**
+ * Feature bit.
+ * @hide
+ */
+ public static final int FEATURE_BIT_NUM_WRITABLE_PRESETS =
+ IBluetoothHapClient.FEATURE_BIT_NUM_WRITABLE_PRESETS;
+
+ /**
+ * Preset Info notification reason.
+ * @hide
+ */
+ public static final int PRESET_INFO_REASON_ALL_PRESET_INFO =
+ IBluetoothHapClient.PRESET_INFO_REASON_ALL_PRESET_INFO;
+
+ /**
+ * Preset Info notification reason.
+ * @hide
+ */
+ public static final int PRESET_INFO_REASON_PRESET_INFO_UPDATE =
+ IBluetoothHapClient.PRESET_INFO_REASON_PRESET_INFO_UPDATE;
+
+ /**
+ * Preset Info notification reason.
+ * @hide
+ */
+ public static final int PRESET_INFO_REASON_PRESET_DELETED =
+ IBluetoothHapClient.PRESET_INFO_REASON_PRESET_DELETED;
+
+ /**
+ * Preset Info notification reason.
+ * @hide
+ */
+ public static final int PRESET_INFO_REASON_PRESET_AVAILABILITY_CHANGED =
+ IBluetoothHapClient.PRESET_INFO_REASON_PRESET_AVAILABILITY_CHANGED;
+
+ /**
+ * Preset Info notification reason.
+ * @hide
+ */
+ public static final int PRESET_INFO_REASON_PRESET_INFO_REQUEST_RESPONSE =
+ IBluetoothHapClient.PRESET_INFO_REASON_PRESET_INFO_REQUEST_RESPONSE;
+
+ /**
+ * Represents invalid group identifier. It's returned when user requests a group identifier
+ * for a device which is not part of any group. This value shouldn't be used in the API calls.
+ * @hide
+ */
+ public static final int HAP_GROUP_UNAVAILABLE = IBluetoothHapClient.GROUP_ID_UNAVAILABLE;
+
+ private final BluetoothAdapter mAdapter;
+ private final AttributionSource mAttributionSource;
+ private final BluetoothProfileConnector<IBluetoothHapClient> mProfileConnector =
+ new BluetoothProfileConnector(this, BluetoothProfile.HAP_CLIENT, "BluetoothHapClient",
+ IBluetoothHapClient.class.getName()) {
+ @Override
+ public IBluetoothHapClient getServiceInterface(IBinder service) {
+ return IBluetoothHapClient.Stub.asInterface(service);
+ }
+ };
+
+ /**
+ * Create a BluetoothHapClient proxy object for interacting with the local
+ * Bluetooth Hearing Access Profile (HAP) client.
+ */
+ /*package*/ BluetoothHapClient(Context context, ServiceListener listener) {
+ mAdapter = BluetoothAdapter.getDefaultAdapter();
+ mAttributionSource = mAdapter.getAttributionSource();
+ mProfileConnector.connect(context, listener);
+ mCloseGuard = new CloseGuard();
+ mCloseGuard.open("close");
+ }
+
+ /**
+ * @hide
+ */
+ protected void finalize() {
+ if (mCloseGuard != null) {
+ mCloseGuard.warnIfOpen();
+ }
+ close();
+ }
+
+ /**
+ * @hide
+ */
+ public void close() {
+ mProfileConnector.disconnect();
+ }
+
+ private IBluetoothHapClient getService() {
+ return mProfileConnector.getService();
+ }
+
+ /**
+ * Set connection policy of the profile
+ *
+ * <p> The device should already be paired.
+ * Connection policy can be one of {@link #CONNECTION_POLICY_ALLOWED},
+ * {@link #CONNECTION_POLICY_FORBIDDEN}, {@link #CONNECTION_POLICY_UNKNOWN}
+ *
+ * @param device Paired bluetooth device
+ * @param connectionPolicy is the connection policy to set to for this profile
+ * @return true if connectionPolicy is set, false on error
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ })
+ public boolean setConnectionPolicy(@NonNull BluetoothDevice device,
+ @ConnectionPolicy int connectionPolicy) {
+ if (DBG) log("setConnectionPolicy(" + device + ", " + connectionPolicy + ")");
+ final IBluetoothHapClient service = getService();
+ final boolean defaultValue = false;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (mAdapter.isEnabled() && isValidDevice(device)
+ && (connectionPolicy == BluetoothProfile.CONNECTION_POLICY_FORBIDDEN
+ || connectionPolicy == BluetoothProfile.CONNECTION_POLICY_ALLOWED)) {
+ try {
+ final SynchronousResultReceiver<Boolean> recv = new SynchronousResultReceiver();
+ service.setConnectionPolicy(device, connectionPolicy, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Get the connection policy of the profile.
+ *
+ * <p> The connection policy can be any of:
+ * {@link #CONNECTION_POLICY_ALLOWED}, {@link #CONNECTION_POLICY_FORBIDDEN},
+ * {@link #CONNECTION_POLICY_UNKNOWN}
+ *
+ * @param device Bluetooth device
+ * @return connection policy of the device
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public @ConnectionPolicy int getConnectionPolicy(@Nullable BluetoothDevice device) {
+ if (VDBG) log("getConnectionPolicy(" + device + ")");
+ final IBluetoothHapClient service = getService();
+ final int defaultValue = BluetoothProfile.CONNECTION_POLICY_FORBIDDEN;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (mAdapter.isEnabled() && isValidDevice(device)) {
+ try {
+ final SynchronousResultReceiver<Integer> recv = new SynchronousResultReceiver();
+ service.getConnectionPolicy(device, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public @NonNull List<BluetoothDevice> getConnectedDevices() {
+ if (VDBG) Log.d(TAG, "getConnectedDevices()");
+ final IBluetoothHapClient service = getService();
+ final List defaultValue = new ArrayList<BluetoothDevice>();
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled()) {
+ try {
+ final SynchronousResultReceiver<List> recv = new SynchronousResultReceiver();
+ service.getConnectedDevices(mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public @NonNull List<BluetoothDevice> getDevicesMatchingConnectionStates(
+ @NonNull int[] states) {
+ if (VDBG) Log.d(TAG, "getDevicesMatchingConnectionStates()");
+ final IBluetoothHapClient service = getService();
+ final List defaultValue = new ArrayList<BluetoothDevice>();
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled()) {
+ try {
+ final SynchronousResultReceiver<List> recv = new SynchronousResultReceiver();
+ service.getDevicesMatchingConnectionStates(states, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public @BluetoothProfile.BtProfileState int getConnectionState(
+ @NonNull BluetoothDevice device) {
+ if (VDBG) Log.d(TAG, "getConnectionState(" + device + ")");
+ final IBluetoothHapClient service = getService();
+ final int defaultValue = BluetoothProfile.STATE_DISCONNECTED;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled() && isValidDevice(device)) {
+ try {
+ final SynchronousResultReceiver<Integer> recv = new SynchronousResultReceiver();
+ service.getConnectionState(device, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Gets the group identifier, which can be used in the group related part of
+ * the API.
+ *
+ * <p>Users are expected to get group identifier for each of the connected
+ * device to discover the device grouping. This allows them to make an informed
+ * decision which devices can be controlled by single group API call and which
+ * require individual device calls.
+ *
+ * <p>Note that some binaural HA devices may not support group operations,
+ * therefore are not considered a valid HAP group. In such case the
+ * {@link #HAP_GROUP_UNAVAILABLE} is returned even when such
+ * device is a valid Le Audio Coordinated Set member.
+ *
+ * @param device
+ * @return valid group identifier or {@link #HAP_GROUP_UNAVAILABLE}
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public int getHapGroup(@NonNull BluetoothDevice device) {
+ final IBluetoothHapClient service = getService();
+ final int defaultValue = HAP_GROUP_UNAVAILABLE;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled() && isValidDevice(device)) {
+ try {
+ final SynchronousResultReceiver<Integer> recv = new SynchronousResultReceiver();
+ service.getHapGroup(device, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Gets the currently active preset for a HA device
+ *
+ * @param device is the device for which we want to set the active preset
+ * @return active preset index
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public boolean getActivePresetIndex(@NonNull BluetoothDevice device) {
+ final IBluetoothHapClient service = getService();
+ final boolean defaultValue = false;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled() && isValidDevice(device)) {
+ try {
+ final SynchronousResultReceiver<Boolean> recv = new SynchronousResultReceiver();
+ service.getActivePresetIndex(device, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Selects the currently active preset for a HA device
+ *
+ * @param device is the device for which we want to set the active preset
+ * @param presetIndex is an index of one of the available presets
+ * @return true if valid request was sent, false otherwise
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = { android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED })
+ public boolean selectActivePreset(@NonNull BluetoothDevice device, int presetIndex) {
+ final IBluetoothHapClient service = getService();
+ final boolean defaultValue = false;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled() && isValidDevice(device)) {
+ try {
+ final SynchronousResultReceiver<Boolean> recv = new SynchronousResultReceiver();
+ service.selectActivePreset(device, presetIndex, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Selects the currently active preset for a HA device group.
+ *
+ * <p> This group call may replace multiple device calls if those are part of the
+ * valid HAS group. Note that binaural HA devices may or may not support group.
+ *
+ * @param groupId is the device group identifier for which want to set the active preset
+ * @param presetIndex is an index of one of the available presets
+ * @return true if valid group request was sent, false otherwise
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = { android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED })
+ public boolean groupSelectActivePreset(int groupId, int presetIndex) {
+ final IBluetoothHapClient service = getService();
+ final boolean defaultValue = false;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled()) {
+ try {
+ final SynchronousResultReceiver<Boolean> recv = new SynchronousResultReceiver();
+ service.groupSelectActivePreset(groupId, presetIndex, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Sets the next preset as a currently active preset for a HA device
+ *
+ * <p> Note that the meaning of 'next' is HA device implementation specific and
+ * does not necessarily mean a higher preset index.
+ *
+ * @param device is the device for which we want to set the active preset
+ * @return true if valid request was sent, false otherwise
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = { android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED })
+ public boolean nextActivePreset(@NonNull BluetoothDevice device) {
+ final IBluetoothHapClient service = getService();
+ final boolean defaultValue = false;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled() && isValidDevice(device)) {
+ try {
+ final SynchronousResultReceiver<Boolean> recv = new SynchronousResultReceiver();
+ service.nextActivePreset(device, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Sets the next preset as a currently active preset for a HA device group
+ *
+ * <p> Note that the meaning of 'next' is HA device implementation specific and
+ * does not necessarily mean a higher preset index.
+ * <p> This group call may replace multiple device calls if those are part of the
+ * valid HAS group. Note that binaural HA devices may or may not support group.
+ *
+ * @param groupId is the device group identifier for which want to set the active preset
+ * @return true if valid group request was sent, false otherwise
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = { android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED })
+ public boolean groupNextActivePreset(int groupId) {
+ final IBluetoothHapClient service = getService();
+ final boolean defaultValue = false;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled()) {
+ try {
+ final SynchronousResultReceiver<Boolean> recv = new SynchronousResultReceiver();
+ service.groupNextActivePreset(groupId, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Sets the previous preset as a currently active preset for a HA device.
+ *
+ * <p> Note that the meaning of 'previous' is HA device implementation specific and
+ * does not necessarily mean a lower preset index.
+ *
+ * @param device is the device for which we want to set the active preset
+ * @return true if valid request was sent, false otherwise
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = { android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED })
+ public boolean previousActivePreset(@NonNull BluetoothDevice device) {
+ final IBluetoothHapClient service = getService();
+ final boolean defaultValue = false;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled() && isValidDevice(device)) {
+ try {
+ final SynchronousResultReceiver<Boolean> recv = new SynchronousResultReceiver();
+ service.previousActivePreset(device, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Sets the previous preset as a currently active preset for a HA device group
+ *
+ * <p> Note the meaning of 'previous' is HA device implementation specific and
+ * does not necessarily mean a lower preset index.
+ * <p> This group call may replace multiple device calls if those are part of the
+ * valid HAS group. Note that binaural HA devices may or may not support group.
+ *
+ * @param groupId is the device group identifier for which want to set the active preset
+ * @return true if valid group request was sent, false otherwise
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = { android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED })
+ public boolean groupPreviousActivePreset(int groupId) {
+ final IBluetoothHapClient service = getService();
+ final boolean defaultValue = false;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled()) {
+ try {
+ final SynchronousResultReceiver<Boolean> recv = new SynchronousResultReceiver();
+ service.groupPreviousActivePreset(groupId, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Requests the preset info
+ *
+ * @param device is the device for which we want to get the preset name
+ * @param presetIndex is an index of one of the available presets
+ * @return true if valid request was sent, false otherwise
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = { android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED })
+ public boolean getPresetInfo(@NonNull BluetoothDevice device, int presetIndex) {
+ final IBluetoothHapClient service = getService();
+ final boolean defaultValue = false;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled() && isValidDevice(device)) {
+ try {
+ final SynchronousResultReceiver<Boolean> recv = new SynchronousResultReceiver();
+ service.getPresetInfo(device, presetIndex, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Requests all presets info
+ *
+ * @param device is the device for which we want to get all presets info
+ * @return true if request was processed, false otherwise
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = { android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED })
+ public boolean getAllPresetsInfo(@NonNull BluetoothDevice device) {
+ final IBluetoothHapClient service = getService();
+ final boolean defaultValue = false;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled() && isValidDevice(device)) {
+ try {
+ final SynchronousResultReceiver<Boolean> recv = new SynchronousResultReceiver();
+ service.getAllPresetsInfo(device, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Requests HAP features
+ *
+ * @param device is the device for which we want to get features for
+ * @return true if request was processed, false otherwise
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = { android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED })
+ public boolean getFeatures(@NonNull BluetoothDevice device) {
+ final IBluetoothHapClient service = getService();
+ final boolean defaultValue = false;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled() && isValidDevice(device)) {
+ try {
+ final SynchronousResultReceiver<Boolean> recv = new SynchronousResultReceiver();
+ service.getFeatures(device, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Sets the preset name
+ *
+ * <p> Note that the name length is restricted to 30 characters.
+ *
+ * @param device is the device for which we want to get the preset name
+ * @param presetIndex is an index of one of the available presets
+ * @param name is a new name for a preset
+ * @return true if valid request was sent, false otherwise
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = { android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED })
+ public boolean setPresetName(@NonNull BluetoothDevice device, int presetIndex,
+ @NonNull String name) {
+ final IBluetoothHapClient service = getService();
+ final boolean defaultValue = false;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled() && isValidDevice(device)) {
+ try {
+ final SynchronousResultReceiver<Boolean> recv = new SynchronousResultReceiver();
+ service.setPresetName(device, presetIndex, name, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Sets the preset name
+ *
+ * <p> Note that the name length is restricted to 30 characters.
+ *
+ * @param groupId is the device group identifier
+ * @param presetIndex is an index of one of the available presets
+ * @param name is a new name for a preset
+ * @return true if valid request was sent, false otherwise
+ * @hide
+ */
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(allOf = { android.Manifest.permission.BLUETOOTH_CONNECT,
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED })
+ public boolean groupSetPresetName(int groupId, int presetIndex, @NonNull String name) {
+ final IBluetoothHapClient service = getService();
+ final boolean defaultValue = false;
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ if (DBG) log(Log.getStackTraceString(new Throwable()));
+ } else if (isEnabled()) {
+ try {
+ final SynchronousResultReceiver<Boolean> recv = new SynchronousResultReceiver();
+ service.groupSetPresetName(groupId, presetIndex, name, mAttributionSource, recv);
+ return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
+ } catch (RemoteException | TimeoutException e) {
+ Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
+ }
+ }
+ return defaultValue;
+ }
+
+ private boolean isEnabled() {
+ if (mAdapter.getState() == BluetoothAdapter.STATE_ON) return true;
+ return false;
+ }
+
+ private boolean isValidDevice(BluetoothDevice device) {
+ if (device == null) return false;
+
+ if (BluetoothAdapter.checkBluetoothAddress(device.getAddress())) return true;
+ return false;
+ }
+
+ private static void log(String msg) {
+ Log.d(TAG, msg);
+ }
+}
diff --git a/framework/java/android/bluetooth/BluetoothHapPresetInfo.java b/framework/java/android/bluetooth/BluetoothHapPresetInfo.java
new file mode 100644
index 0000000000..b8c09ccbb7
--- /dev/null
+++ b/framework/java/android/bluetooth/BluetoothHapPresetInfo.java
@@ -0,0 +1,192 @@
+/*
+ * 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.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Represents the Hearing Access Profile preset.
+ * @hide
+ */
+@SystemApi
+public final class BluetoothHapPresetInfo implements Parcelable {
+ private int mPresetIndex;
+ private String mPresetName;
+ private boolean mIsWritable;
+ private boolean mIsAvailable;
+
+ /**
+ * HapPresetInfo constructor
+ *
+ * @param presetIndex Preset index
+ * @param presetName Preset Name
+ * @param isWritable Is writable flag
+ * @param isAvailable Is available flag
+ */
+ /*package*/ BluetoothHapPresetInfo(int presetIndex, @NonNull String presetName,
+ boolean isWritable, boolean isAvailable) {
+ this.mPresetIndex = presetIndex;
+ this.mPresetName = presetName;
+ this.mIsWritable = isWritable;
+ this.mIsAvailable = isAvailable;
+ }
+
+ /**
+ * HapPresetInfo constructor
+ *
+ * @param in HapPresetInfo parcel
+ */
+ private BluetoothHapPresetInfo(@NonNull Parcel in) {
+ mPresetIndex = in.readInt();
+ mPresetName = in.readString();
+ mIsWritable = in.readBoolean();
+ mIsAvailable = in.readBoolean();
+ }
+
+ /**
+ * HapPresetInfo preset index
+ *
+ * @return Preset index
+ */
+ public int getIndex() {
+ return mPresetIndex;
+ }
+
+ /**
+ * HapPresetInfo preset name
+ *
+ * @return Preset name
+ */
+ public @NonNull String getName() {
+ return mPresetName;
+ }
+
+ /**
+ * HapPresetInfo preset writability
+ *
+ * @return If preset is writable
+ */
+ public boolean isWritable() {
+ return mIsWritable;
+ }
+
+ /**
+ * HapPresetInfo availability
+ *
+ * @return If preset is available
+ */
+ public boolean isAvailable() {
+ return mIsAvailable;
+ }
+
+ /**
+ * HapPresetInfo array creator
+ */
+ public static final @NonNull Creator<BluetoothHapPresetInfo> CREATOR =
+ new Creator<BluetoothHapPresetInfo>() {
+ public BluetoothHapPresetInfo createFromParcel(@NonNull Parcel in) {
+ return new BluetoothHapPresetInfo(in);
+ }
+
+ public BluetoothHapPresetInfo[] newArray(int size) {
+ return new BluetoothHapPresetInfo[size];
+ }
+ };
+
+ /** @hide */
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeInt(mPresetIndex);
+ dest.writeString(mPresetName);
+ dest.writeBoolean(mIsWritable);
+ dest.writeBoolean(mIsAvailable);
+ }
+
+ /**
+ * Builder for {@link BluetoothHapPresetInfo}.
+ * <p> By default, the codec type will be set to
+ * {@link BluetoothHapClient#PRESET_INDEX_UNAVAILABLE}, the name to an empty string,
+ * writability and availability both to false.
+ */
+ public static final class Builder {
+ private int mPresetIndex = BluetoothHapClient.PRESET_INDEX_UNAVAILABLE;
+ private String mPresetName = "";
+ private boolean mIsWritable = false;
+ private boolean mIsAvailable = false;
+
+ /**
+ * Set preset index for HAP preset info.
+ *
+ * @param index of this preset
+ * @return the same Builder instance
+ */
+ public @NonNull Builder setIndex(int index) {
+ mPresetIndex = index;
+ return this;
+ }
+
+ /**
+ * Set preset name for HAP preset info.
+ *
+ * @param name of this preset
+ * @return the same Builder instance
+ */
+ public @NonNull Builder setName(@NonNull String name) {
+ mPresetName = name;
+ return this;
+ }
+
+ /**
+ * Set preset writability for HAP preset info.
+ *
+ * @param isWritable whether preset is writable
+ * @return the same Builder instance
+ */
+ public @NonNull Builder setWritable(@NonNull boolean isWritable) {
+ mIsWritable = isWritable;
+ return this;
+ }
+
+ /**
+ * Set preset availability for HAP preset info.
+ *
+ * @param isAvailable whether preset is currently available to select
+ * @return the same Builder instance
+ */
+ public @NonNull Builder setAvailable(@NonNull boolean isAvailable) {
+ mIsAvailable = isAvailable;
+ return this;
+ }
+
+ /**
+ * Build {@link BluetoothHapPresetInfo}.
+ * @return new BluetoothHapPresetInfo built
+ */
+ public @NonNull BluetoothHapPresetInfo build() {
+ return new BluetoothHapPresetInfo(mPresetIndex, mPresetName, mIsWritable, mIsAvailable);
+ }
+ }
+}
diff --git a/system/bta/Android.bp b/system/bta/Android.bp
index e066ef5fe6..363541be55 100644
--- a/system/bta/Android.bp
+++ b/system/bta/Android.bp
@@ -100,6 +100,11 @@ cc_library_static {
"le_audio/client_parser.cc",
"le_audio/client_audio.cc",
"le_audio/le_audio_types.cc",
+ "has/has_client.cc",
+ "has/has_ctp.cc",
+ "has/has_journal.cc",
+ "has/has_preset.cc",
+ "has/has_types.cc",
"hearing_aid/hearing_aid.cc",
"hearing_aid/hearing_aid_audio_source.cc",
"hf_client/bta_hf_client_act.cc",
@@ -649,3 +654,55 @@ cc_test {
},
},
}
+
+cc_test {
+ name: "bluetooth_has_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",
+ "has/has_client.cc",
+ "has/has_client_test.cc",
+ "has/has_ctp.cc",
+ "has/has_journal.cc",
+ "has/has_preset.cc",
+ "has/has_types.cc",
+ "test/common/bta_gatt_api_mock.cc",
+ "test/common/bta_gatt_queue_mock.cc",
+ "test/common/btif_storage_mock.cc",
+ "test/common/btm_api_mock.cc",
+ "test/common/mock_controller.cc",
+ "test/common/mock_csis_client.cc",
+ ],
+ shared_libs: [
+ "libprotobuf-cpp-lite",
+ "libcrypto",
+ ],
+ static_libs : [
+ "crypto_toolbox_for_tests",
+ "libgmock",
+ "libbt-common",
+ "libbt-protos-lite",
+ ],
+ sanitize: {
+ cfi: true,
+ scs: true,
+ address: true,
+ all_undefined: true,
+ integer_overflow: true,
+ diag: {
+ undefined : true
+ },
+ },
+} \ No newline at end of file
diff --git a/system/bta/has/has_client.cc b/system/bta/has/has_client.cc
new file mode 100644
index 0000000000..7c6bbdc3c9
--- /dev/null
+++ b/system/bta/has/has_client.cc
@@ -0,0 +1,2046 @@
+/*
+ * 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/callback.h>
+#include <base/logging.h>
+#include <base/strings/string_number_conversions.h>
+#include <hardware/bt_gatt_types.h>
+#include <hardware/bt_has.h>
+
+#include <list>
+#include <map>
+#include <string>
+#include <vector>
+
+#include "bta_csis_api.h"
+#include "bta_gatt_api.h"
+#include "bta_gatt_queue.h"
+#include "bta_groups.h"
+#include "bta_has_api.h"
+#include "bta_le_audio_uuids.h"
+#include "btm_int.h"
+#include "btm_sec.h"
+#include "device/include/controller.h"
+#include "gap_api.h"
+#include "gatt_api.h"
+#include "has_types.h"
+#include "osi/include/osi.h"
+#include "osi/include/properties.h"
+
+using base::Closure;
+using bluetooth::Uuid;
+using bluetooth::csis::CsisClient;
+using bluetooth::has::ConnectionState;
+using bluetooth::has::ErrorCode;
+using bluetooth::has::kFeatureBitPresetSynchronizationSupported;
+using bluetooth::has::kHasPresetIndexInvalid;
+using bluetooth::has::PresetInfo;
+using bluetooth::has::PresetInfoReason;
+using le_audio::has::HasClient;
+using le_audio::has::HasCtpGroupOpCoordinator;
+using le_audio::has::HasCtpNtf;
+using le_audio::has::HasCtpOp;
+using le_audio::has::HasDevice;
+using le_audio::has::HasGattOpContext;
+using le_audio::has::HasJournalRecord;
+using le_audio::has::HasPreset;
+using le_audio::has::kControlPointMandatoryOpcodesBitmask;
+using le_audio::has::kControlPointSynchronizedOpcodesBitmask;
+using le_audio::has::kUuidActivePresetIndex;
+using le_audio::has::kUuidHearingAccessService;
+using le_audio::has::kUuidHearingAidFeatures;
+using le_audio::has::kUuidHearingAidPresetControlPoint;
+using le_audio::has::PresetCtpChangeId;
+using le_audio::has::PresetCtpOpcode;
+
+void btif_storage_add_leaudio_has_device(const RawAddress& address,
+ std::vector<uint8_t> presets_bin,
+ uint8_t features,
+ uint8_t active_preset);
+bool btif_storage_get_leaudio_has_presets(const RawAddress& address,
+ std::vector<uint8_t>& presets_bin,
+ uint8_t& active_preset);
+void btif_storage_set_leaudio_has_presets(const RawAddress& address,
+ std::vector<uint8_t> presets_bin);
+bool btif_storage_get_leaudio_has_features(const RawAddress& address,
+ uint8_t& features);
+void btif_storage_set_leaudio_has_features(const RawAddress& address,
+ uint8_t features);
+void btif_storage_set_leaudio_has_active_preset(const RawAddress& address,
+ uint8_t active_preset);
+
+extern bool gatt_profile_get_eatt_support(const RawAddress& remote_bda);
+
+namespace {
+class HasClientImpl;
+HasClientImpl* instance;
+
+/**
+ * -----------------------------------------------------------------------------
+ * Hearing Access Service - Client role
+ * -----------------------------------------------------------------------------
+ * Overview:
+ *
+ * This is Hearing Access Service client class.
+ *
+ * Each connected peer device supporting Hearing Access Service (HAS) is being
+ * connected and has its characteristics discovered. All the characteristics
+ * and descriptors (incl. the optional ones) are being read or written during
+ * this initial connection stage. Encryption is also verified. If all of this
+ * succeeds the appropriate callbacks are being called to notify upper layer
+ * about the successful HAS device connection and its features and the list
+ * of available audio configuration presets.
+ *
+ * Each HA device is expected to have the HAS service instantiated. It must
+ * contain Hearing Aid Features characteristic and optionally Presets Control
+ * Point and Active Preset Index characteristics, allowing the user to read
+ * preset details, switch currently active preset and possibly rename some of
+ * them.
+ *
+ * Hearing Aid Features characteristic informs the client about the type of
+ * Hearign Aids device (Monaural, Binaural or Banded), which operations are
+ * supported via the Preset Control Point characteristic, about dynamically
+ * changing list of available presets, writable presets and the support for
+ * synchronised preset change operations on the Binaural Hearing Aid devices.
+ */
+class HasClientImpl : public HasClient {
+ public:
+ HasClientImpl(bluetooth::has::HasClientCallbacks* callbacks,
+ base::Closure initCb)
+ : gatt_if_(0), callbacks_(callbacks) {
+ BTA_GATTC_AppRegister(
+ [](tBTA_GATTC_EVT event, tBTA_GATTC* p_data) {
+ if (instance && p_data) instance->GattcCallback(event, p_data);
+ },
+ base::Bind(
+ [](base::Closure initCb, uint8_t client_id, uint8_t status) {
+ if (status != GATT_SUCCESS) {
+ LOG(ERROR) << "Can't start Hearing Aid Service client "
+ "profile - no gatt clients left!";
+ return;
+ }
+ instance->gatt_if_ = client_id;
+ initCb.Run();
+ },
+ initCb),
+ true);
+ }
+
+ ~HasClientImpl() override = default;
+
+ void Connect(const RawAddress& address) override {
+ DLOG(INFO) << __func__ << ": " << address;
+
+ std::vector<RawAddress> addresses = {address};
+ auto csis_api = CsisClient::Get();
+ if (csis_api != nullptr) {
+ // Connect entire CAS set of devices
+ auto group_id = csis_api->GetGroupId(
+ address, bluetooth::Uuid::From16Bit(UUID_COMMON_AUDIO_SERVICE));
+ addresses = csis_api->GetDeviceList(group_id);
+ }
+
+ if (addresses.empty()) {
+ LOG(WARNING) << __func__ << ": " << address << " is not part of any set";
+ addresses = {address};
+ }
+
+ for (auto const& addr : addresses) {
+ auto device = std::find_if(devices_.begin(), devices_.end(),
+ HasDevice::MatchAddress(addr));
+ if (device == devices_.end()) {
+ devices_.emplace_back(addr, true);
+ BTA_GATTC_Open(gatt_if_, addr, true, false);
+
+ } else {
+ device->is_connecting_actively = true;
+ if (!device->IsConnected()) BTA_GATTC_Open(gatt_if_, addr, true, false);
+ }
+ }
+ }
+
+ void AddFromStorage(const RawAddress& address, uint8_t features,
+ uint16_t is_acceptlisted) {
+ DLOG(INFO) << __func__ << ": " << address
+ << ", features=" << loghex(features)
+ << ", isAcceptlisted=" << is_acceptlisted;
+
+ /* Notify upper layer about the device */
+ callbacks_->OnDeviceAvailable(address, features);
+ if (is_acceptlisted) {
+ auto device = std::find_if(devices_.begin(), devices_.end(),
+ HasDevice::MatchAddress(address));
+ if (device == devices_.end())
+ devices_.push_back(HasDevice(address, features));
+
+ /* Connect in background */
+ BTA_GATTC_Open(gatt_if_, address, false, false);
+ }
+ }
+
+ void Disconnect(const RawAddress& address) override {
+ DLOG(INFO) << __func__ << ": " << address;
+
+ std::vector<RawAddress> addresses = {address};
+ auto csis_api = CsisClient::Get();
+ if (csis_api != nullptr) {
+ // Disconnect entire CAS set of devices
+ auto group_id = csis_api->GetGroupId(
+ address, bluetooth::Uuid::From16Bit(UUID_COMMON_AUDIO_SERVICE));
+ addresses = csis_api->GetDeviceList(group_id);
+ }
+
+ if (addresses.empty()) {
+ LOG(WARNING) << __func__ << ": " << address << " is not part of any set";
+ addresses = {address};
+ }
+
+ for (auto const& addr : addresses) {
+ auto device = std::find_if(devices_.begin(), devices_.end(),
+ HasDevice::MatchAddress(addr));
+ if (device == devices_.end()) {
+ LOG(WARNING) << "Device not connected to profile" << addr;
+ return;
+ }
+
+ auto conn_id = device->conn_id;
+ auto is_connecting_actively = device->is_connecting_actively;
+ devices_.erase(device);
+
+ if (conn_id != GATT_INVALID_CONN_ID) {
+ BTA_GATTC_Close(conn_id);
+ callbacks_->OnConnectionState(ConnectionState::DISCONNECTED, addr);
+ } else {
+ /* Removes active connection. */
+ if (is_connecting_actively) BTA_GATTC_CancelOpen(gatt_if_, addr, true);
+ }
+
+ /* Removes all registrations for connection. */
+ BTA_GATTC_CancelOpen(0, addr, false);
+ }
+ }
+
+ void UpdateJournalOpEntryStatus(HasDevice& device, HasGattOpContext context,
+ tGATT_STATUS status) {
+ /* Find journal entry by the context and update */
+ auto journal_entry = std::find_if(
+ device.has_journal_.begin(), device.has_journal_.end(),
+ [&context](auto const& record) {
+ if (record.is_operation) {
+ return HasGattOpContext(record.op_context_handle) == context;
+ }
+ return false;
+ });
+
+ if (journal_entry == device.has_journal_.end()) {
+ LOG(WARNING) << "Journaling error or journal length limit was set to "
+ "low. Unable to log the operation outcome.";
+ return;
+ }
+
+ if (journal_entry == device.has_journal_.end()) {
+ LOG(ERROR) << __func__
+ << " Unable to find operation context in the journal!";
+ return;
+ }
+
+ journal_entry->op_status = status;
+ }
+
+ std::optional<HasCtpOp> ExtractPendingCtpOp(uint16_t op_id) {
+ auto op_it =
+ std::find_if(pending_operations_.begin(), pending_operations_.end(),
+ [op_id](auto const& el) { return op_id == el.op_id; });
+
+ if (op_it != pending_operations_.end()) {
+ auto op = *op_it;
+ pending_operations_.erase(op_it);
+
+ return op;
+ }
+ return std::nullopt;
+ }
+
+ void EnqueueCtpOp(HasCtpOp op) { pending_operations_.push_back(op); }
+
+ void OnHasActivePresetCycleStatus(uint16_t conn_id, tGATT_STATUS status,
+ void* user_data) {
+ DLOG(INFO) << __func__ << " status: " << +status;
+
+ auto device = GetDevice(conn_id);
+ if (!device) {
+ LOG(WARNING) << "Device not connected to profile, conn_id=" << +conn_id;
+ return;
+ }
+
+ /* Journal update */
+ LOG_ASSERT(user_data != nullptr) << "Has operation context is missing!";
+ auto context = HasGattOpContext(user_data);
+ UpdateJournalOpEntryStatus(*device, context, status);
+
+ auto op_opt = ExtractPendingCtpOp(context.ctp_op_id);
+ if (status == GATT_SUCCESS) return;
+
+ /* This could be one of the coordinated group preset change request */
+ pending_group_operation_timeouts_.erase(context.ctp_op_id);
+
+ /* Error handling */
+ if (!op_opt.has_value()) {
+ LOG(ERROR) << __func__ << " Unknown operation error";
+ return;
+ }
+ auto op = op_opt.value();
+ callbacks_->OnActivePresetSelectError(op.addr_or_group,
+ GattStatus2SvcErrorCode(status));
+ }
+
+ void OnHasPresetNameSetStatus(uint16_t conn_id, tGATT_STATUS status,
+ void* user_data) {
+ auto device = GetDevice(conn_id);
+ if (!device) {
+ LOG(WARNING) << "Device not connected to profile, conn_id=" << +conn_id;
+ return;
+ }
+
+ LOG_ASSERT(user_data != nullptr) << "Has operation context is missing!";
+ HasGattOpContext context(user_data);
+
+ /* Journal update */
+ UpdateJournalOpEntryStatus(*device, context, status);
+
+ auto op_opt = ExtractPendingCtpOp(context.ctp_op_id);
+ if (status == GATT_SUCCESS) return;
+
+ /* This could be one of the coordinated group preset change request */
+ pending_group_operation_timeouts_.erase(context.ctp_op_id);
+
+ /* Error handling */
+ if (!op_opt.has_value()) {
+ LOG(ERROR) << __func__ << " Unknown operation error";
+ return;
+ }
+ auto op = op_opt.value();
+ callbacks_->OnSetPresetNameError(device->addr, op.index,
+ GattStatus2SvcErrorCode(status));
+ }
+
+ void OnHasPresetNameGetStatus(uint16_t conn_id, tGATT_STATUS status,
+ void* user_data) {
+ auto device = GetDevice(conn_id);
+ if (!device) {
+ LOG(WARNING) << "Device not connected to profile, conn_id=" << +conn_id;
+ return;
+ }
+
+ LOG_ASSERT(user_data != nullptr) << "Has operation context is missing!";
+ HasGattOpContext context(user_data);
+
+ /* Journal update */
+ UpdateJournalOpEntryStatus(*device, context, status);
+
+ auto op_opt = ExtractPendingCtpOp(context.ctp_op_id);
+ if (status == GATT_SUCCESS) return;
+
+ /* Error handling */
+ if (!op_opt.has_value()) {
+ LOG(ERROR) << __func__ << " Unknown operation error";
+ return;
+ }
+ auto op = op_opt.value();
+ callbacks_->OnPresetInfoError(device->addr, op.index,
+ GattStatus2SvcErrorCode(status));
+ }
+
+ void OnHasPresetIndexOperation(uint16_t conn_id, tGATT_STATUS status,
+ void* user_data) {
+ DLOG(INFO) << __func__;
+
+ auto device = GetDevice(conn_id);
+ if (!device) {
+ LOG(WARNING) << "Device not connected to profile, conn_id=" << +conn_id;
+ return;
+ }
+
+ LOG_ASSERT(user_data != nullptr) << "Has operation context is missing!";
+ HasGattOpContext context(user_data);
+
+ /* Journal update */
+ UpdateJournalOpEntryStatus(*device, context, status);
+
+ auto op_opt = ExtractPendingCtpOp(context.ctp_op_id);
+ if (status == GATT_SUCCESS) return;
+
+ /* This could be one of the coordinated group preset change request */
+ pending_group_operation_timeouts_.erase(context.ctp_op_id);
+
+ /* Error handling */
+ if (!op_opt.has_value()) {
+ LOG(ERROR) << __func__ << " Unknown operation error";
+ return;
+ }
+
+ auto op = op_opt.value();
+ if (op.opcode == PresetCtpOpcode::READ_PRESET_BY_INDEX) {
+ callbacks_->OnPresetInfoError(device->addr, op.index,
+ GattStatus2SvcErrorCode(status));
+
+ } else {
+ callbacks_->OnActivePresetSelectError(op.addr_or_group,
+ GattStatus2SvcErrorCode(status));
+ }
+ }
+
+ void CpReadAllPresetsOperation(HasCtpOp operation) {
+ DLOG(INFO) << __func__ << " Operation: " << operation;
+
+ if (std::holds_alternative<int>(operation.addr_or_group)) {
+ LOG(ERROR) << __func__
+ << " Read all presets on the entire group not supported.";
+ callbacks_->OnPresetInfoError(operation.addr_or_group, operation.index,
+ ErrorCode::OPERATION_NOT_POSSIBLE);
+ return;
+ }
+
+ auto device = std::find_if(
+ devices_.begin(), devices_.end(),
+ HasDevice::MatchAddress(std::get<RawAddress>(operation.addr_or_group)));
+ if (device == devices_.end()) {
+ LOG(WARNING) << __func__ << " Device not connected to profile addr: "
+ << std::get<RawAddress>(operation.addr_or_group);
+ callbacks_->OnPresetInfoError(device->addr, operation.index,
+ ErrorCode::OPERATION_NOT_POSSIBLE);
+ return;
+ }
+
+ if (!device->SupportsPresets()) {
+ callbacks_->OnPresetInfoError(device->addr, operation.index,
+ ErrorCode::OPERATION_NOT_SUPPORTED);
+ }
+
+ auto context = HasGattOpContext(operation);
+
+ /* Journal update */
+ device->has_journal_.Append(HasJournalRecord(operation, context));
+
+ /* Write to control point */
+ EnqueueCtpOp(operation);
+ BtaGattQueue::WriteCharacteristic(
+ device->conn_id, device->cp_handle, operation.ToCharacteristicValue(),
+ GATT_WRITE,
+ [](uint16_t conn_id, tGATT_STATUS status, uint16_t handle, uint16_t len,
+ const uint8_t* value, void* user_data) {
+ if (instance)
+ instance->OnHasPresetNameGetStatus(conn_id, status, user_data);
+ },
+ context);
+ }
+
+ ErrorCode CpPresetIndexOperationWriteReq(HasDevice& device,
+ HasCtpOp& operation) {
+ DLOG(INFO) << __func__ << " Operation: " << operation;
+
+ if (!device.IsConnected()) return ErrorCode::OPERATION_NOT_POSSIBLE;
+
+ if (!device.SupportsPresets()) return ErrorCode::OPERATION_NOT_SUPPORTED;
+
+ if (!device.SupportsOperation(operation.opcode))
+ return operation.IsGroupRequest()
+ ? ErrorCode::GROUP_OPERATION_NOT_SUPPORTED
+ : ErrorCode::OPERATION_NOT_SUPPORTED;
+
+ if (!device.IsValidPreset(operation.index))
+ return ErrorCode::INVALID_PRESET_INDEX;
+
+ auto context = HasGattOpContext(operation);
+
+ /* Journal update */
+ device.has_journal_.Append(HasJournalRecord(operation, context));
+
+ /* Write to control point */
+ EnqueueCtpOp(operation);
+ BtaGattQueue::WriteCharacteristic(
+ device.conn_id, device.cp_handle, operation.ToCharacteristicValue(),
+ GATT_WRITE,
+ [](uint16_t conn_id, tGATT_STATUS status, uint16_t handle, uint16_t len,
+ const uint8_t* value, void* user_data) {
+ if (instance)
+ instance->OnHasPresetIndexOperation(conn_id, status, user_data);
+ },
+ context);
+
+ return ErrorCode::NO_ERROR;
+ }
+
+ bool AreAllDevicesAvailable(const std::vector<RawAddress>& addresses) {
+ for (auto& addr : addresses) {
+ auto device = std::find_if(devices_.begin(), devices_.end(),
+ HasDevice::MatchAddress(addr));
+ if (device == devices_.end() || !device->IsConnected()) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ ErrorCode CpPresetOperationCaller(
+ HasCtpOp operation,
+ std::function<ErrorCode(HasDevice& device, HasCtpOp& operation)>
+ write_cb) {
+ DLOG(INFO) << __func__ << " Operation: " << operation;
+ auto status = ErrorCode::NO_ERROR;
+
+ if (operation.IsGroupRequest()) {
+ auto csis_api = CsisClient::Get();
+ if (csis_api == nullptr) {
+ /* No CSIS means no group operations */
+ status = ErrorCode::GROUP_OPERATION_NOT_SUPPORTED;
+
+ } else {
+ auto group_id = operation.GetGroupId();
+ auto addresses = csis_api->GetDeviceList(group_id);
+
+ /* Perform the operation only when all the devices are available */
+ if (!AreAllDevicesAvailable(addresses)) {
+ addresses.clear();
+ }
+
+ if (addresses.empty()) {
+ status = ErrorCode::OPERATION_NOT_POSSIBLE;
+
+ } else {
+ /* Make this a coordinated operation */
+ pending_group_operation_timeouts_.emplace(
+ operation.op_id, HasCtpGroupOpCoordinator(addresses, operation));
+
+ if (operation.IsSyncedOperation()) {
+ status = ErrorCode::GROUP_OPERATION_NOT_SUPPORTED;
+
+ /* Clear the error if we find device to forward the operation */
+ bool was_sent = false;
+ for (auto& addr : addresses) {
+ auto device = std::find_if(devices_.begin(), devices_.end(),
+ HasDevice::MatchAddress(addr));
+ if (device != devices_.end()) {
+ status = write_cb(*device, operation);
+ if (status == ErrorCode::NO_ERROR) {
+ was_sent = true;
+ break;
+ }
+ }
+ }
+ if (!was_sent) status = ErrorCode::OPERATION_NOT_POSSIBLE;
+
+ } else {
+ status = ErrorCode::GROUP_OPERATION_NOT_SUPPORTED;
+
+ for (auto& addr : addresses) {
+ auto device = std::find_if(devices_.begin(), devices_.end(),
+ HasDevice::MatchAddress(addr));
+ if (device != devices_.end()) {
+ status = write_cb(*device, operation);
+ if (status != ErrorCode::NO_ERROR) break;
+ }
+ }
+ }
+
+ /* Erase group op coordinator on error */
+ if (status != ErrorCode::NO_ERROR) {
+ pending_group_operation_timeouts_.erase(operation.op_id);
+ }
+ }
+ }
+
+ } else {
+ auto device = std::find_if(devices_.begin(), devices_.end(),
+ HasDevice::MatchAddress(std::get<RawAddress>(
+ operation.addr_or_group)));
+ status = ErrorCode::OPERATION_NOT_POSSIBLE;
+ if (device != devices_.end()) status = write_cb(*device, operation);
+ }
+
+ return status;
+ }
+
+ void CpPresetIndexOperation(HasCtpOp operation) {
+ LOG(INFO) << __func__ << " Operation: " << operation;
+
+ auto status = CpPresetOperationCaller(
+ operation, [](HasDevice& device, HasCtpOp operation) -> ErrorCode {
+ if (instance)
+ return instance->CpPresetIndexOperationWriteReq(device, operation);
+ return ErrorCode::OPERATION_NOT_POSSIBLE;
+ });
+
+ if (status != ErrorCode::NO_ERROR) {
+ switch (operation.opcode) {
+ case PresetCtpOpcode::READ_PRESET_BY_INDEX:
+ LOG_ASSERT(
+ std::holds_alternative<RawAddress>(operation.addr_or_group))
+ << " Unsupported group operation!";
+
+ callbacks_->OnPresetInfoError(
+ std::get<RawAddress>(operation.addr_or_group), operation.index,
+ status);
+ break;
+ case PresetCtpOpcode::SET_ACTIVE_PRESET:
+ case PresetCtpOpcode::SET_ACTIVE_PRESET_SYNC:
+ callbacks_->OnActivePresetSelectError(operation.addr_or_group,
+ status);
+ break;
+ default:
+ break;
+ }
+ }
+ }
+
+ ErrorCode CpPresetsCycleOperationWriteReq(HasDevice& device,
+ HasCtpOp& operation) {
+ DLOG(INFO) << __func__ << " addr: " << device.addr
+ << " operation: " << operation;
+
+ if (!device.IsConnected()) return ErrorCode::OPERATION_NOT_POSSIBLE;
+
+ if (!device.SupportsPresets()) return ErrorCode::OPERATION_NOT_SUPPORTED;
+
+ if (!device.SupportsOperation(operation.opcode))
+ return operation.IsGroupRequest()
+ ? ErrorCode::GROUP_OPERATION_NOT_SUPPORTED
+ : ErrorCode::OPERATION_NOT_SUPPORTED;
+
+ auto context = HasGattOpContext(operation);
+
+ /* Journal update */
+ device.has_journal_.Append(HasJournalRecord(operation, context));
+
+ /* Write to control point */
+ EnqueueCtpOp(operation);
+ BtaGattQueue::WriteCharacteristic(
+ device.conn_id, device.cp_handle, operation.ToCharacteristicValue(),
+ GATT_WRITE,
+ [](uint16_t conn_id, tGATT_STATUS status, uint16_t handle, uint16_t len,
+ const uint8_t* value, void* user_data) {
+ if (instance)
+ instance->OnHasActivePresetCycleStatus(conn_id, status, user_data);
+ },
+ context);
+ return ErrorCode::NO_ERROR;
+ }
+
+ void CpPresetsCycleOperation(HasCtpOp operation) {
+ DLOG(INFO) << __func__ << " Operation: " << operation;
+
+ auto status = CpPresetOperationCaller(
+ operation, [](HasDevice& device, HasCtpOp operation) -> ErrorCode {
+ if (instance)
+ return instance->CpPresetsCycleOperationWriteReq(device, operation);
+ return ErrorCode::OPERATION_NOT_POSSIBLE;
+ });
+
+ if (status != ErrorCode::NO_ERROR)
+ callbacks_->OnActivePresetSelectError(operation.addr_or_group, status);
+ }
+
+ ErrorCode CpWritePresetNameOperationWriteReq(HasDevice& device,
+ HasCtpOp operation) {
+ DLOG(INFO) << __func__ << " addr: " << device.addr
+ << " operation: " << operation;
+
+ if (!device.IsConnected()) return ErrorCode::OPERATION_NOT_POSSIBLE;
+
+ if (!device.SupportsPresets()) return ErrorCode::OPERATION_NOT_SUPPORTED;
+
+ if (!device.IsValidPreset(operation.index, true))
+ return device.IsValidPreset(operation.index)
+ ? ErrorCode::SET_NAME_NOT_ALLOWED
+ : ErrorCode::INVALID_PRESET_INDEX;
+
+ if (!device.SupportsOperation(operation.opcode))
+ return ErrorCode::OPERATION_NOT_SUPPORTED;
+
+ if (operation.name.value_or("").length() >
+ le_audio::has::HasPreset::kPresetNameLengthLimit)
+ return ErrorCode::INVALID_PRESET_NAME_LENGTH;
+
+ auto context = HasGattOpContext(operation, operation.index);
+
+ /* Journal update */
+ device.has_journal_.Append(HasJournalRecord(operation, context));
+
+ /* Write to control point */
+ EnqueueCtpOp(operation);
+ BtaGattQueue::WriteCharacteristic(
+ device.conn_id, device.cp_handle, operation.ToCharacteristicValue(),
+ GATT_WRITE,
+ [](uint16_t conn_id, tGATT_STATUS status, uint16_t handle, uint16_t len,
+ const uint8_t* value, void* user_data) {
+ if (instance)
+ instance->OnHasPresetNameSetStatus(conn_id, status, user_data);
+ },
+ context);
+
+ return ErrorCode::NO_ERROR;
+ }
+
+ void CpWritePresetNameOperation(HasCtpOp operation) {
+ DLOG(INFO) << __func__ << " operation: " << operation;
+
+ auto status = ErrorCode::NO_ERROR;
+
+ std::vector<RawAddress> addresses;
+ if (operation.IsGroupRequest()) {
+ auto csis_api = CsisClient::Get();
+ if (csis_api != nullptr) {
+ addresses = csis_api->GetDeviceList(operation.GetGroupId());
+
+ /* Make this a coordinated operation */
+ pending_group_operation_timeouts_.emplace(
+ operation.op_id, HasCtpGroupOpCoordinator(addresses, operation));
+ }
+
+ } else {
+ addresses = {operation.GetDeviceAddr()};
+ }
+
+ status = ErrorCode::OPERATION_NOT_POSSIBLE;
+
+ /* Perform the operation only when all the devices are available */
+ if (!AreAllDevicesAvailable(addresses)) {
+ addresses.clear();
+ }
+
+ for (auto& addr : addresses) {
+ auto device = std::find_if(devices_.begin(), devices_.end(),
+ HasDevice::MatchAddress(addr));
+ if (device != devices_.end()) {
+ status = CpWritePresetNameOperationWriteReq(*device, operation);
+ if (status != ErrorCode::NO_ERROR) {
+ LOG(ERROR) << __func__
+ << " Control point write error: " << (int)status;
+ break;
+ }
+ }
+ }
+
+ if (status != ErrorCode::NO_ERROR) {
+ if (operation.IsGroupRequest())
+ pending_group_operation_timeouts_.erase(operation.op_id);
+
+ callbacks_->OnSetPresetNameError(operation.addr_or_group, operation.index,
+ status);
+ }
+ }
+
+ bool shouldRequestSyncedOp(std::variant<RawAddress, int> addr_or_group_id,
+ PresetCtpOpcode opcode) {
+ /* Do not select locally synced ops when not performing group operations,
+ * You never know if the user will make another call for the other devices
+ * in this set even though the may support locally synced operations.
+ */
+ if (std::holds_alternative<RawAddress>(addr_or_group_id)) return false;
+
+ auto csis_api = CsisClient::Get();
+ if (csis_api == nullptr) return false;
+
+ auto addresses = csis_api->GetDeviceList(std::get<int>(addr_or_group_id));
+ if (addresses.empty()) return false;
+
+ for (auto& addr : addresses) {
+ auto device = std::find_if(devices_.begin(), devices_.end(),
+ HasDevice::MatchAddress(addr));
+ if (device != devices_.end()) {
+ if (device->SupportsOperation(opcode)) return true;
+ }
+ }
+
+ return false;
+ }
+
+ void SelectActivePreset(std::variant<RawAddress, int> addr_or_group_id,
+ uint8_t preset_index) override {
+ DLOG(INFO) << __func__;
+
+ auto opcode = shouldRequestSyncedOp(addr_or_group_id,
+ PresetCtpOpcode::SET_ACTIVE_PRESET_SYNC)
+ ? PresetCtpOpcode::SET_ACTIVE_PRESET_SYNC
+ : PresetCtpOpcode::SET_ACTIVE_PRESET;
+
+ CpPresetIndexOperation(HasCtpOp(addr_or_group_id, opcode, preset_index));
+ }
+
+ void NextActivePreset(
+ std::variant<RawAddress, int> addr_or_group_id) override {
+ DLOG(INFO) << __func__;
+
+ auto opcode = shouldRequestSyncedOp(addr_or_group_id,
+ PresetCtpOpcode::SET_NEXT_PRESET_SYNC)
+ ? PresetCtpOpcode::SET_NEXT_PRESET_SYNC
+ : PresetCtpOpcode::SET_NEXT_PRESET;
+
+ CpPresetsCycleOperation(HasCtpOp(addr_or_group_id, opcode));
+ }
+
+ void PreviousActivePreset(
+ std::variant<RawAddress, int> addr_or_group_id) override {
+ DLOG(INFO) << __func__;
+
+ auto opcode = shouldRequestSyncedOp(addr_or_group_id,
+ PresetCtpOpcode::SET_PREV_PRESET_SYNC)
+ ? PresetCtpOpcode::SET_PREV_PRESET_SYNC
+ : PresetCtpOpcode::SET_PREV_PRESET;
+
+ CpPresetsCycleOperation(HasCtpOp(addr_or_group_id, opcode));
+ }
+
+ void GetPresetInfo(const RawAddress& address, uint8_t preset_index) override {
+ auto device = std::find_if(devices_.begin(), devices_.end(),
+ HasDevice::MatchAddress(address));
+ if (device == devices_.end()) {
+ LOG(WARNING) << "Device not connected to profile" << address;
+ return;
+ }
+
+ DLOG(INFO) << __func__ << " preset idx: " << +preset_index;
+
+ /* Due to mandatory control point notifications or indications, preset
+ * details are always up to date. However we have to be able to do the
+ * READ_PRESET_BY_INDEX, to pass the test specification requirements.
+ */
+ if (osi_property_get_bool("persist.bluetooth.has.always_use_preset_cache",
+ true)) {
+ auto* preset = device->GetPreset(preset_index);
+ if (preset == nullptr) {
+ LOG(ERROR) << __func__ << "Invalid preset request" << address;
+ callbacks_->OnPresetInfoError(address, preset_index,
+ ErrorCode::INVALID_PRESET_INDEX);
+ return;
+ }
+
+ callbacks_->OnPresetInfo(address,
+ PresetInfoReason::PRESET_INFO_REQUEST_RESPONSE,
+ {{.preset_index = preset_index,
+ .writable = preset->IsWritable(),
+ .available = preset->IsAvailable(),
+ .preset_name = preset->GetName()}});
+ } else {
+ CpPresetIndexOperation(HasCtpOp(
+ address, PresetCtpOpcode::READ_PRESET_BY_INDEX, preset_index));
+ }
+ }
+
+ void SetPresetName(std::variant<RawAddress, int> addr_or_group_id,
+ uint8_t preset_index, std::string name) override {
+ DLOG(INFO) << __func__ << "preset_idx: " << +preset_index
+ << ", name: " << name;
+
+ CpWritePresetNameOperation(HasCtpOp(addr_or_group_id,
+ PresetCtpOpcode::WRITE_PRESET_NAME,
+ preset_index, name));
+ }
+
+ void CleanUp() {
+ BTA_GATTC_AppDeregister(gatt_if_);
+ for (auto& device : devices_) {
+ if (device.conn_id != GATT_INVALID_CONN_ID)
+ BTA_GATTC_Close(device.conn_id);
+ DoDisconnectCleanUp(device);
+ }
+
+ devices_.clear();
+ pending_operations_.clear();
+ }
+
+ void Dump(int fd) const {
+ std::stringstream stream;
+ if (devices_.size()) {
+ stream << " {\"Known HAS devices\": [";
+ for (const auto& device : devices_) {
+ stream << "\n {";
+ device.Dump(stream);
+ stream << "\n },\n";
+ }
+ stream << " ]}\n\n";
+ } else {
+ stream << " \"No known HAS devices\"\n\n";
+ }
+ dprintf(fd, "%s", stream.str().c_str());
+ }
+
+ void OnGroupOpCoordinatorTimeout(void* p) {
+ LOG(ERROR) << __func__ << ": Coordinated operation timeout: "
+ << " not all the devices notified their state change on time.";
+
+ /* Clear pending group operations */
+ pending_group_operation_timeouts_.clear();
+ HasCtpGroupOpCoordinator::Cleanup();
+ }
+
+ private:
+ void OnEncrypted(HasDevice& device) {
+ DLOG(INFO) << __func__ << ": " << device.addr;
+
+ if (device.isGattServiceValid()) {
+ device.is_connecting_actively = false;
+ NotifyHasDeviceValid(device);
+ callbacks_->OnPresetInfo(device.addr, PresetInfoReason::ALL_PRESET_INFO,
+ device.GetAllPresetInfo());
+ callbacks_->OnActivePresetSelected(device.addr,
+ device.currently_active_preset);
+
+ } else {
+ BTA_GATTC_ServiceSearchRequest(device.conn_id,
+ &kUuidHearingAccessService);
+ }
+ }
+
+ void NotifyHasDeviceValid(const HasDevice& device) {
+ DLOG(INFO) << __func__ << " addr:" << device.addr;
+
+ std::vector<uint8_t> preset_indices;
+ preset_indices.reserve(device.has_presets.size());
+ for (auto const& preset : device.has_presets) {
+ preset_indices.push_back(preset.GetIndex());
+ }
+
+ /* Notify that we are ready to go */
+ callbacks_->OnConnectionState(ConnectionState::CONNECTED, device.addr);
+ }
+
+ void MarkDeviceValidIfInInitialDiscovery(HasDevice& device) {
+ if (device.isGattServiceValid()) return;
+
+ --device.gatt_svc_validation_steps;
+
+ if (device.isGattServiceValid()) {
+ device.is_connecting_actively = false;
+
+ std::vector<uint8_t> presets_bin;
+ if (device.SerializePresets(presets_bin)) {
+ btif_storage_add_leaudio_has_device(device.addr, presets_bin,
+ device.GetFeatures(),
+ device.currently_active_preset);
+ }
+ NotifyHasDeviceValid(device);
+ }
+ }
+
+ void OnGattWriteCcc(uint16_t conn_id, tGATT_STATUS status, uint16_t handle,
+ void* user_data) {
+ DLOG(INFO) << __func__ << ": handle=" << loghex(handle);
+
+ auto device = GetDevice(conn_id);
+ if (!device) {
+ LOG(ERROR) << __func__ << ": unknown conn_id=" << loghex(conn_id);
+ BtaGattQueue::Clean(conn_id);
+ return;
+ }
+
+ HasGattOpContext context(user_data);
+ bool enabling_ntf = context.context_flags &
+ HasGattOpContext::kContextFlagsEnableNotification;
+
+ if (handle == device->features_ccc_handle) {
+ if (status == GATT_SUCCESS)
+ device->features_notifications_enabled = enabling_ntf;
+
+ } else if ((handle == device->active_preset_ccc_handle) ||
+ (handle == device->cp_ccc_handle)) {
+ /* Both of these CCC are mandatory */
+ if (enabling_ntf && (status != GATT_SUCCESS)) {
+ LOG(ERROR) << __func__
+ << ": Failed to register for notifications on handle="
+ << loghex(handle);
+ BTA_GATTC_Close(conn_id);
+ return;
+ }
+ }
+ }
+
+ void OnHasNotification(uint16_t conn_id, uint16_t handle, uint16_t len,
+ const uint8_t* value) {
+ auto device = GetDevice(conn_id);
+ if (!device) {
+ LOG(WARNING) << "Skipping unknown device, conn_id=" << loghex(conn_id);
+ return;
+ }
+
+ if (handle == device->features_handle) {
+ OnHasFeaturesValue(&(*device), GATT_SUCCESS, handle, len, value);
+
+ } else if (handle == device->cp_handle) {
+ OnHasCtpValueNotification(&(*device), len, value);
+
+ } else if (handle == device->active_preset_handle) {
+ OnHasActivePresetValue(&(*device), GATT_SUCCESS, handle, len, value);
+ }
+ }
+
+ /* Gets the device from variant, possibly searching by conn_id */
+ HasDevice* GetDevice(
+ std::variant<uint16_t, HasDevice*> conn_id_device_variant) {
+ HasDevice* device = nullptr;
+
+ if (std::holds_alternative<HasDevice*>(conn_id_device_variant)) {
+ device = std::get<HasDevice*>(conn_id_device_variant);
+ } else {
+ auto it = std::find_if(
+ devices_.begin(), devices_.end(),
+ HasDevice::MatchConnId(std::get<uint16_t>(conn_id_device_variant)));
+ if (it != devices_.end()) device = &(*it);
+ }
+
+ return device;
+ }
+
+ void OnHasFeaturesValue(
+ std::variant<uint16_t, HasDevice*> conn_id_device_variant,
+ tGATT_STATUS status, uint16_t handle, uint16_t len, const uint8_t* value,
+ void* user_data = nullptr) {
+ DLOG(INFO) << __func__;
+
+ auto device = GetDevice(conn_id_device_variant);
+ if (!device) {
+ LOG(ERROR) << __func__ << ": Unknown device!";
+ return;
+ }
+
+ if (status != GATT_SUCCESS) {
+ LOG(ERROR) << __func__ << ": Could not read characteristic at handle="
+ << loghex(handle);
+ BTA_GATTC_Close(device->conn_id);
+ return;
+ }
+
+ if (len != 1) {
+ LOG(ERROR) << "Invalid features value length=" << +len
+ << " at handle=" << loghex(handle);
+ BTA_GATTC_Close(device->conn_id);
+ return;
+ }
+
+ /* Store features value */
+ uint8_t features;
+ STREAM_TO_UINT8(features, value);
+ device->UpdateFeatures(features);
+
+ if (device->isGattServiceValid()) {
+ btif_storage_set_leaudio_has_features(device->addr, features);
+ }
+
+ /* Journal update */
+ device->has_journal_.Append(HasJournalRecord(features, true));
+
+ /* When service is not yet validated, report the available device and
+ * notify features otherwise.
+ */
+ if (!device->isGattServiceValid()) {
+ callbacks_->OnDeviceAvailable(device->addr, device->GetFeatures());
+ } else {
+ callbacks_->OnFeaturesUpdate(device->addr, device->GetFeatures());
+ }
+
+ MarkDeviceValidIfInInitialDiscovery(*device);
+ }
+
+ /* Translates GATT statuses to application specific error codes */
+ static ErrorCode GattStatus2SvcErrorCode(tGATT_STATUS status) {
+ switch (status) {
+ case 0x80:
+ /* Invalid Opcode */
+ /* Unlikely to happen as we would not allow unsupported operations */
+ return ErrorCode::OPERATION_NOT_SUPPORTED;
+ case 0x81:
+ /* Write Name Not Allowed */
+ return ErrorCode::SET_NAME_NOT_ALLOWED;
+ case 0x82:
+ /* Synchronization Not Supported */
+ return ErrorCode::OPERATION_NOT_SUPPORTED;
+ case 0x83:
+ /* Preset Operation Not Possible */
+ return ErrorCode::OPERATION_NOT_POSSIBLE;
+ case 0x84:
+ /* Preset Name Too Long */
+ return ErrorCode::INVALID_PRESET_NAME_LENGTH;
+ case 0xFE:
+ /* Procedure Already in Progress */
+ return ErrorCode::PROCEDURE_ALREADY_IN_PROGRESS;
+ default:
+ return ErrorCode::OPERATION_NOT_POSSIBLE;
+ }
+ }
+
+ void OnHasPresetReadResponseNotification(HasDevice& device) {
+ DLOG(INFO) << __func__;
+
+ while (device.ctp_notifications_.size() != 0) {
+ auto ntf = device.ctp_notifications_.front();
+ /* Process only read response events */
+ if (ntf.opcode != PresetCtpOpcode::READ_PRESET_RESPONSE) break;
+
+ /* Update preset values */
+ if (ntf.preset.has_value()) {
+ device.has_presets.erase(ntf.preset->GetIndex());
+ device.has_presets.insert(ntf.preset.value());
+ }
+
+ /* We currently do READ_ALL_PRESETS only during the service validation.
+ * If service is already valid, this must be the READ_PRESET_BY_INDEX.
+ */
+ if (device.isGattServiceValid()) {
+ auto info = device.GetPresetInfo(ntf.preset.value().GetIndex());
+ if (info.has_value())
+ callbacks_->OnPresetInfo(
+ device.addr, PresetInfoReason::PRESET_INFO_REQUEST_RESPONSE,
+ {{info.value()}});
+ }
+
+ /* Journal update */
+ device.has_journal_.Append(HasJournalRecord(ntf));
+ device.ctp_notifications_.pop_front();
+ }
+
+ auto in_svc_validation = !device.isGattServiceValid();
+ MarkDeviceValidIfInInitialDiscovery(device);
+
+ /* We currently do READ_ALL_PRESETS only during the service validation.
+ * ALL_PRESET_INFO will be sent only during this initial phase.
+ */
+ if (in_svc_validation) {
+ callbacks_->OnPresetInfo(device.addr, PresetInfoReason::ALL_PRESET_INFO,
+ device.GetAllPresetInfo());
+
+ /* If this was the last validation step then send the currently active
+ * preset as well.
+ */
+ if (device.isGattServiceValid())
+ callbacks_->OnActivePresetSelected(device.addr,
+ device.currently_active_preset);
+ }
+ }
+
+ void OnHasPresetGenericUpdate(HasDevice& device) {
+ DLOG(ERROR) << __func__;
+
+ std::vector<PresetInfo> updated_infos;
+ std::vector<PresetInfo> deleted_infos;
+
+ /* Process the entire train of preset changes with generic updates */
+ while (device.ctp_notifications_.size() != 0) {
+ auto nt = device.ctp_notifications_.front();
+
+ /* Break if not a generic update anymore */
+ if (nt.opcode != PresetCtpOpcode::PRESET_CHANGED) break;
+ if (nt.change_id != PresetCtpChangeId::PRESET_GENERIC_UPDATE) break;
+
+ if (nt.preset.has_value()) {
+ /* Erase old value if exist */
+ device.has_presets.erase(nt.preset->GetIndex());
+
+ /* Erase in-between indices */
+ if (nt.prev_index != 0) {
+ auto it = device.has_presets.begin();
+ while (it != device.has_presets.end()) {
+ if ((it->GetIndex() > nt.prev_index) &&
+ (it->GetIndex() < nt.preset->GetIndex())) {
+ auto info = device.GetPresetInfo(it->GetIndex());
+ if (info.has_value()) deleted_infos.push_back(info.value());
+
+ it = device.has_presets.erase(it);
+
+ } else {
+ ++it;
+ }
+ }
+ }
+ /* Update presets */
+ device.has_presets.insert(*nt.preset);
+
+ auto info = device.GetPresetInfo(nt.preset->GetIndex());
+ if (info.has_value()) updated_infos.push_back(info.value());
+ }
+
+ /* Journal update */
+ device.has_journal_.Append(HasJournalRecord(nt));
+ device.ctp_notifications_.pop_front();
+ }
+
+ if (device.isGattServiceValid()) {
+ /* Update preset values in the storage */
+ std::vector<uint8_t> presets_bin;
+ if (device.SerializePresets(presets_bin)) {
+ btif_storage_set_leaudio_has_presets(device.addr, presets_bin);
+ }
+
+ /* Check for the matching coordinated group op. to use group callbacks */
+ for (auto it = pending_group_operation_timeouts_.rbegin();
+ it != pending_group_operation_timeouts_.rend(); ++it) {
+ auto& group_op_coordinator = it->second;
+
+ /* Here we interested only in valid preset name changes */
+ if (!((group_op_coordinator.operation.opcode ==
+ PresetCtpOpcode::WRITE_PRESET_NAME) &&
+ group_op_coordinator.operation.name.has_value()))
+ continue;
+
+ /* Match preset update results with the triggering operation */
+ auto renamed_preset_info = std::find_if(
+ updated_infos.begin(), updated_infos.end(),
+ [&group_op_coordinator](const auto& info) {
+ return (group_op_coordinator.operation.name.value() ==
+ info.preset_name);
+ });
+ if (renamed_preset_info == updated_infos.end()) continue;
+
+ if (group_op_coordinator.SetCompleted(device.addr)) {
+ group_op_coordinator.preset_info_verification_list.push_back(
+ *renamed_preset_info);
+
+ /* Call the proper group operation completion callback */
+ if (group_op_coordinator.IsFullyCompleted()) {
+ callbacks_->OnPresetInfo(
+ group_op_coordinator.operation.GetGroupId(),
+ PresetInfoReason::PRESET_INFO_UPDATE, {*renamed_preset_info});
+ pending_group_operation_timeouts_.erase(it->first);
+ }
+
+ /* Erase it from the 'updated_infos' since later we'll be sending
+ * this as a group callback when the other device completes the
+ * coordinated group name change.
+ *
+ * WARNING: There might an issue with callbacks call reordering due to
+ * some of them being kept for group callbacks called later, when all
+ * the grouped devices complete the coordinated group rename
+ * operation. In most cases this should not be a major problem.
+ */
+ updated_infos.erase(renamed_preset_info);
+ break;
+ }
+ }
+
+ if (!updated_infos.empty())
+ callbacks_->OnPresetInfo(
+ device.addr, PresetInfoReason::PRESET_INFO_UPDATE, updated_infos);
+
+ if (!deleted_infos.empty())
+ callbacks_->OnPresetInfo(device.addr, PresetInfoReason::PRESET_DELETED,
+ deleted_infos);
+ }
+ }
+
+ void OnHasPresetAvailabilityChanged(HasDevice& device) {
+ DLOG(INFO) << __func__;
+
+ std::vector<PresetInfo> infos;
+
+ while (device.ctp_notifications_.size() != 0) {
+ auto nt = device.ctp_notifications_.front();
+
+ /* Process only preset change notifications */
+ if (nt.opcode != PresetCtpOpcode::PRESET_CHANGED) break;
+
+ auto preset = device.has_presets.extract(nt.index).value();
+ auto new_props = preset.GetProperties();
+
+ /* Process only the preset availability changes and then notify */
+ if ((nt.change_id != PresetCtpChangeId::PRESET_AVAILABLE) &&
+ (nt.change_id != PresetCtpChangeId::PRESET_UNAVAILABLE))
+ break;
+
+ /* Availability change */
+ if (nt.change_id == PresetCtpChangeId::PRESET_AVAILABLE) {
+ new_props |= HasPreset::kPropertyAvailable;
+ } else {
+ new_props &= !HasPreset::kPropertyAvailable;
+ }
+ device.has_presets.insert(
+ HasPreset(preset.GetIndex(), new_props, preset.GetName()));
+
+ auto info = device.GetPresetInfo(nt.index);
+ if (info.has_value()) infos.push_back(info.value());
+
+ /* Journal update */
+ device.has_journal_.Append(HasJournalRecord(nt));
+ device.ctp_notifications_.pop_front();
+ }
+
+ /* Update preset storage */
+ if (device.isGattServiceValid()) {
+ std::vector<uint8_t> presets_bin;
+ if (device.SerializePresets(presets_bin)) {
+ btif_storage_set_leaudio_has_presets(device.addr, presets_bin);
+ }
+ }
+
+ callbacks_->OnPresetInfo(
+ device.addr, PresetInfoReason::PRESET_AVAILABILITY_CHANGED, infos);
+ }
+
+ void OnHasPresetDeleted(HasDevice& device) {
+ DLOG(INFO) << __func__;
+
+ std::vector<PresetInfo> infos;
+ bool is_deleted = false;
+
+ while (device.ctp_notifications_.size() != 0) {
+ auto nt = device.ctp_notifications_.front();
+
+ /* Process only preset change notifications */
+ if (nt.opcode != PresetCtpOpcode::PRESET_CHANGED) break;
+
+ /* Process only the deletions and then notify */
+ if (nt.change_id != PresetCtpChangeId::PRESET_DELETED) break;
+
+ auto info = device.GetPresetInfo(nt.index);
+ if (info.has_value()) infos.push_back(info.value());
+
+ if (device.has_presets.count(nt.index)) {
+ is_deleted = true;
+ device.has_presets.erase(nt.index);
+ }
+
+ /* Journal update */
+ device.has_journal_.Append(HasJournalRecord(nt));
+ device.ctp_notifications_.pop_front();
+ }
+
+ /* Update preset storage */
+ if (device.isGattServiceValid()) {
+ std::vector<uint8_t> presets_bin;
+ if (device.SerializePresets(presets_bin)) {
+ btif_storage_set_leaudio_has_presets(device.addr, presets_bin);
+ }
+ }
+
+ if (is_deleted)
+ callbacks_->OnPresetInfo(device.addr, PresetInfoReason::PRESET_DELETED,
+ infos);
+ }
+
+ void ProcessCtpNotificationQueue(HasDevice& device) {
+ std::vector<PresetInfo> infos;
+
+ while (device.ctp_notifications_.size() != 0) {
+ auto ntf = device.ctp_notifications_.front();
+ DLOG(INFO) << __func__ << " ntf: " << ntf;
+
+ if (ntf.opcode == PresetCtpOpcode::PRESET_CHANGED) {
+ switch (ntf.change_id) {
+ case PresetCtpChangeId::PRESET_GENERIC_UPDATE:
+ OnHasPresetGenericUpdate(device);
+ break;
+ case PresetCtpChangeId::PRESET_AVAILABLE:
+ OnHasPresetAvailabilityChanged(device);
+ break;
+ case PresetCtpChangeId::PRESET_UNAVAILABLE:
+ OnHasPresetAvailabilityChanged(device);
+ break;
+ case PresetCtpChangeId::PRESET_DELETED:
+ OnHasPresetDeleted(device);
+ break;
+ default:
+ LOG(ERROR) << __func__ << " Invalid notification: " << ntf;
+ break;
+ }
+
+ } else if (ntf.opcode == PresetCtpOpcode::READ_PRESET_RESPONSE) {
+ OnHasPresetReadResponseNotification(device);
+
+ } else {
+ LOG(ERROR) << __func__ << " Unsupported preset notification: " << ntf;
+ }
+ }
+ }
+
+ void OnHasCtpValueNotification(HasDevice* device, uint16_t len,
+ const uint8_t* value) {
+ auto ntf_opt = HasCtpNtf::FromCharacteristicValue(len, value);
+ if (!ntf_opt.has_value()) {
+ LOG(ERROR) << __func__
+ << " Unhandled notification for device: " << *device;
+ BTA_GATTC_Close(device->conn_id);
+ return;
+ }
+
+ auto ntf = ntf_opt.value();
+ DLOG(INFO) << __func__ << ntf;
+
+ device->ctp_notifications_.push_back(ntf);
+ if (ntf.is_last) ProcessCtpNotificationQueue(*device);
+ }
+
+ void OnHasActivePresetValue(
+ std::variant<uint16_t, HasDevice*> conn_id_device_variant,
+ tGATT_STATUS status, uint16_t handle, uint16_t len, const uint8_t* value,
+ void* user_data = nullptr) {
+ DLOG(INFO) << __func__;
+
+ auto device = GetDevice(conn_id_device_variant);
+ if (!device) {
+ LOG(ERROR) << "Skipping unknown device!";
+ return;
+ }
+
+ if (status != GATT_SUCCESS) {
+ LOG(ERROR) << __func__ << ": Could not read characteristic at handle="
+ << loghex(handle);
+ BTA_GATTC_Close(device->conn_id);
+ return;
+ }
+
+ if (len != 1) {
+ LOG(ERROR) << "Invalid preset value length=" << +len
+ << " at handle=" << loghex(handle);
+ BTA_GATTC_Close(device->conn_id);
+ return;
+ }
+
+ /* Get the active preset value */
+ auto* pp = value;
+ STREAM_TO_UINT8(device->currently_active_preset, pp);
+
+ if (device->isGattServiceValid()) {
+ btif_storage_set_leaudio_has_active_preset(
+ device->addr, device->currently_active_preset);
+ }
+
+ /* Journal update */
+ device->has_journal_.Append(
+ HasJournalRecord(device->currently_active_preset, false));
+
+ /* If svc not marked valid, this might be the last validation step. */
+ MarkDeviceValidIfInInitialDiscovery(*device);
+
+ if (device->isGattServiceValid()) {
+ if (!pending_group_operation_timeouts_.empty()) {
+ for (auto it = pending_group_operation_timeouts_.rbegin();
+ it != pending_group_operation_timeouts_.rend(); ++it) {
+ auto& group_op_coordinator = it->second;
+
+ bool matches = false;
+ switch (group_op_coordinator.operation.opcode) {
+ case PresetCtpOpcode::SET_ACTIVE_PRESET:
+ [[fallthrough]];
+ case PresetCtpOpcode::SET_NEXT_PRESET:
+ [[fallthrough]];
+ case PresetCtpOpcode::SET_PREV_PRESET:
+ [[fallthrough]];
+ case PresetCtpOpcode::SET_ACTIVE_PRESET_SYNC:
+ [[fallthrough]];
+ case PresetCtpOpcode::SET_NEXT_PRESET_SYNC:
+ [[fallthrough]];
+ case PresetCtpOpcode::SET_PREV_PRESET_SYNC: {
+ if (group_op_coordinator.SetCompleted(device->addr)) {
+ matches = true;
+ break;
+ }
+ } break;
+ default:
+ /* Ignore */
+ break;
+ }
+ if (group_op_coordinator.IsFullyCompleted()) {
+ callbacks_->OnActivePresetSelected(
+ group_op_coordinator.operation.GetGroupId(),
+ device->currently_active_preset);
+ pending_group_operation_timeouts_.erase(it->first);
+ }
+ if (matches) break;
+ }
+
+ } else {
+ callbacks_->OnActivePresetSelected(device->addr,
+ device->currently_active_preset);
+ }
+ }
+ }
+
+ /* Cleans up after the device disconnection */
+ void DoDisconnectCleanUp(HasDevice& device,
+ bool invalidate_gatt_service = true) {
+ DLOG(INFO) << __func__ << ": device=" << device.addr;
+
+ /* Deregister from optional features notifications */
+ if (device.features_ccc_handle != GAP_INVALID_HANDLE) {
+ BTA_GATTC_DeregisterForNotifications(gatt_if_, device.addr,
+ device.features_handle);
+ }
+
+ /* Deregister from active presets notifications if presets exist */
+ if (device.active_preset_ccc_handle != GAP_INVALID_HANDLE) {
+ BTA_GATTC_DeregisterForNotifications(gatt_if_, device.addr,
+ device.active_preset_handle);
+ }
+
+ /* Deregister from control point notifications */
+ if (device.cp_ccc_handle != GAP_INVALID_HANDLE) {
+ BTA_GATTC_DeregisterForNotifications(gatt_if_, device.addr,
+ device.cp_handle);
+ }
+
+ if (device.conn_id != GATT_INVALID_CONN_ID) {
+ BtaGattQueue::Clean(device.conn_id);
+ if (invalidate_gatt_service) device.gatt_svc_validation_steps = 0xFE;
+ }
+
+ /* Clear pending operations */
+ auto addr = device.addr;
+ pending_operations_.erase(
+ std::remove_if(
+ pending_operations_.begin(), pending_operations_.end(),
+ [&addr](auto& el) {
+ if (std::holds_alternative<RawAddress>(el.addr_or_group)) {
+ return std::get<RawAddress>(el.addr_or_group) == addr;
+ }
+ return false;
+ }),
+ pending_operations_.end());
+
+ device.ConnectionCleanUp();
+ }
+
+ /* These below are all GATT service discovery, validation, cache & storage */
+ bool CacheAttributeHandles(const gatt::Service& service, HasDevice* device) {
+ DLOG(INFO) << __func__ << ": device=" << device->addr;
+
+ for (const gatt::Characteristic& charac : service.characteristics) {
+ if (charac.uuid == kUuidActivePresetIndex) {
+ /* Find the mandatory CCC descriptor */
+ uint16_t ccc_handle =
+ FindCccHandle(device->conn_id, charac.value_handle);
+ if (ccc_handle == GAP_INVALID_HANDLE) {
+ LOG(ERROR) << __func__
+ << ": no HAS Active Preset CCC descriptor found!";
+ return false;
+ }
+ device->active_preset_ccc_handle = ccc_handle;
+ device->active_preset_handle = charac.value_handle;
+
+ } else if (charac.uuid == kUuidHearingAidPresetControlPoint) {
+ /* Find the mandatory CCC descriptor */
+ uint16_t ccc_handle =
+ FindCccHandle(device->conn_id, charac.value_handle);
+ if (ccc_handle == GAP_INVALID_HANDLE) {
+ LOG(ERROR) << __func__
+ << ": no HAS Control Point CCC descriptor found!";
+ return false;
+ }
+
+ device->cp_ccc_handle = ccc_handle;
+ device->cp_handle = charac.value_handle;
+ } else if (charac.uuid == kUuidHearingAidFeatures) {
+ /* Find the optional CCC descriptor */
+ uint16_t ccc_handle =
+ FindCccHandle(device->conn_id, charac.value_handle);
+ device->features_ccc_handle = ccc_handle;
+ device->features_handle = charac.value_handle;
+ }
+ }
+ return true;
+ }
+
+ bool LoadHasDetailsFromStorage(HasDevice* device) {
+ DLOG(INFO) << __func__ << ": device=" << device->addr;
+
+ std::vector<uint8_t> presets_bin;
+ uint8_t active_preset;
+
+ if (!btif_storage_get_leaudio_has_presets(device->addr, presets_bin,
+ active_preset))
+ return false;
+
+ if (!HasDevice::DeserializePresets(presets_bin.data(), presets_bin.size(),
+ *device))
+ return false;
+
+ VLOG(1) << "Loading HAS service details from storage.";
+
+ device->currently_active_preset = active_preset;
+
+ /* Register for optional features notifications */
+ if (device->features_ccc_handle != GAP_INVALID_HANDLE) {
+ tGATT_STATUS register_status = BTA_GATTC_RegisterForNotifications(
+ gatt_if_, device->addr, device->features_handle);
+ DLOG(INFO) << __func__ << " Registering for notifications, status="
+ << loghex(+register_status);
+ }
+
+ /* Register for presets control point notifications */
+ if (device->cp_ccc_handle != GAP_INVALID_HANDLE) {
+ tGATT_STATUS register_status = BTA_GATTC_RegisterForNotifications(
+ gatt_if_, device->addr, device->cp_handle);
+ DLOG(INFO) << __func__ << " Registering for notifications, status="
+ << loghex(+register_status);
+ }
+
+ /* Register for active presets notifications if presets exist */
+ if (device->active_preset_ccc_handle != GAP_INVALID_HANDLE) {
+ tGATT_STATUS register_status = BTA_GATTC_RegisterForNotifications(
+ gatt_if_, device->addr, device->active_preset_handle);
+ DLOG(INFO) << __func__ << " Registering for notifications, status="
+ << loghex(+register_status);
+ }
+
+ /* Update features and refresh opcode support map */
+ uint8_t val;
+ if (btif_storage_get_leaudio_has_features(device->addr, val))
+ device->UpdateFeatures(val);
+
+ /* With all the details loaded we can already mark it as valid */
+ device->gatt_svc_validation_steps = 0;
+ device->is_connecting_actively = false;
+
+ NotifyHasDeviceValid(*device);
+ callbacks_->OnPresetInfo(device->addr, PresetInfoReason::ALL_PRESET_INFO,
+ device->GetAllPresetInfo());
+ callbacks_->OnActivePresetSelected(device->addr,
+ device->currently_active_preset);
+ return true;
+ }
+
+ bool StartInitialHasDetailsReadAndValidation(const gatt::Service& service,
+ HasDevice* device) {
+ // Validate service structure
+ if (device->features_handle == GAP_INVALID_HANDLE) {
+ /* Missing key characteristic */
+ LOG(ERROR) << __func__ << ": Service has broken structure";
+ return false;
+ }
+
+ if (device->cp_handle != GAP_INVALID_HANDLE) {
+ if (device->active_preset_handle == GAP_INVALID_HANDLE) return false;
+ if (device->active_preset_ccc_handle == GAP_INVALID_HANDLE) return false;
+ }
+
+ /* Number of reads or notifications required to validate the service */
+ device->gatt_svc_validation_steps = 1 + (device->SupportsPresets() ? 2 : 0);
+
+ /* Read the initial features */
+ BtaGattQueue::ReadCharacteristic(
+ device->conn_id, device->features_handle,
+ [](uint16_t conn_id, tGATT_STATUS status, uint16_t handle, uint16_t len,
+ uint8_t* value, void* user_data) {
+ if (instance)
+ instance->OnHasFeaturesValue(conn_id, status, handle, len, value,
+ user_data);
+ },
+ nullptr);
+
+ /* Register for features notifications */
+ if (device->SupportsFeaturesNotification()) {
+ SubscribeForNotifications(device->conn_id, device->addr,
+ device->features_handle,
+ device->features_ccc_handle);
+ } else {
+ LOG(WARNING) << __func__
+ << ": server does not support features notification";
+ }
+
+ /* If Presets are supported we should read them all and subscribe for the
+ * mandatory active preset index notifications.
+ */
+ if (device->SupportsPresets()) {
+ uint16_t ccc_val = gatt_profile_get_eatt_support(device->addr)
+ ? GATT_CHAR_CLIENT_CONFIG_INDICTION |
+ GATT_CHAR_CLIENT_CONFIG_NOTIFICATION
+ : GATT_CHAR_CLIENT_CONFIG_INDICTION;
+ SubscribeForNotifications(device->conn_id, device->addr,
+ device->cp_handle, device->cp_ccc_handle,
+ ccc_val);
+
+ /* Get all the presets */
+ CpReadAllPresetsOperation(
+ HasCtpOp(device->addr, PresetCtpOpcode::READ_ALL_PRESETS));
+
+ /* Read the current active preset index */
+ BtaGattQueue::ReadCharacteristic(
+ device->conn_id, device->active_preset_handle,
+ [](uint16_t conn_id, tGATT_STATUS status, uint16_t handle,
+ uint16_t len, uint8_t* value, void* user_data) {
+ if (instance)
+ instance->OnHasActivePresetValue(conn_id, status, handle, len,
+ value, user_data);
+ },
+ nullptr);
+
+ /* Subscribe for active preset notifications */
+ SubscribeForNotifications(device->conn_id, device->addr,
+ device->active_preset_handle,
+ device->active_preset_ccc_handle);
+ } else {
+ LOG(WARNING) << __func__
+ << ": server can only report HAS features, other "
+ "functionality is disabled";
+ }
+
+ return true;
+ }
+
+ bool OnHasServiceFound(const gatt::Service& service, void* context) {
+ DLOG(INFO) << __func__;
+
+ auto* device = static_cast<HasDevice*>(context);
+
+ /* Initially validate and store GATT service discovery data */
+ if (!CacheAttributeHandles(service, device)) return false;
+
+ /* If deatails are loaded from storage we are done here */
+ if (LoadHasDetailsFromStorage(device)) return true;
+
+ /* No storred details - read all the details and validate */
+ return StartInitialHasDetailsReadAndValidation(service, device);
+ }
+
+ /* These below are all generic event handlers calling in HAS specific code. */
+ void GattcCallback(tBTA_GATTC_EVT event, tBTA_GATTC* p_data) {
+ DLOG(INFO) << __func__ << ": event = " << static_cast<int>(event);
+
+ switch (event) {
+ case BTA_GATTC_DEREG_EVT:
+ break;
+
+ case BTA_GATTC_OPEN_EVT:
+ OnGattConnected(p_data->open);
+ break;
+
+ case BTA_GATTC_CLOSE_EVT:
+ OnGattDisconnected(p_data->close);
+ break;
+
+ case BTA_GATTC_SEARCH_CMPL_EVT:
+ OnGattServiceSearchComplete(p_data->search_cmpl);
+ break;
+
+ case BTA_GATTC_NOTIF_EVT:
+ OnGattNotification(p_data->notify);
+ break;
+
+ case BTA_GATTC_ENC_CMPL_CB_EVT:
+ OnLeEncryptionComplete(p_data->enc_cmpl.remote_bda, BTM_SUCCESS);
+ break;
+
+ case BTA_GATTC_SRVC_CHG_EVT:
+ OnGattServiceChangeEvent(p_data->remote_bda);
+ break;
+
+ case BTA_GATTC_SRVC_DISC_DONE_EVT:
+ OnGattServiceDiscoveryDoneEvent(p_data->remote_bda);
+ break;
+
+ default:
+ break;
+ }
+ }
+
+ void OnGattConnected(const tBTA_GATTC_OPEN& evt) {
+ DLOG(INFO) << __func__ << ": address=" << evt.remote_bda
+ << ", conn_id=" << evt.conn_id;
+
+ auto device = std::find_if(devices_.begin(), devices_.end(),
+ HasDevice::MatchAddress(evt.remote_bda));
+ if (device == devices_.end()) {
+ LOG(WARNING) << "Skipping unknown device, address=" << evt.remote_bda;
+ BTA_GATTC_Close(evt.conn_id);
+ return;
+ }
+
+ if (evt.status != GATT_SUCCESS) {
+ if (!device->is_connecting_actively) {
+ // acceptlist connection failed, that's ok.
+ return;
+ }
+
+ LOG(WARNING) << "Failed to connect to server device";
+ devices_.erase(device);
+ callbacks_->OnConnectionState(ConnectionState::DISCONNECTED,
+ evt.remote_bda);
+ return;
+ }
+
+ device->conn_id = evt.conn_id;
+
+ if (BTM_SecIsSecurityPending(device->addr)) {
+ /* if security collision happened, wait for encryption done
+ * (BTA_GATTC_ENC_CMPL_CB_EVT)
+ */
+ return;
+ }
+
+ /* verify bond */
+ if (BTM_IsEncrypted(device->addr, BT_TRANSPORT_LE)) {
+ /* if link has been encrypted */
+ if (device->isGattServiceValid()) {
+ instance->OnEncrypted(*device);
+ } else {
+ BTA_GATTC_ServiceSearchRequest(device->conn_id,
+ &kUuidHearingAccessService);
+ }
+ return;
+ }
+
+ int result = BTM_SetEncryption(
+ evt.remote_bda, BT_TRANSPORT_LE,
+ [](const RawAddress* bd_addr, tBT_TRANSPORT transport, void* p_ref_data,
+ tBTM_STATUS status) {
+ if (instance) instance->OnLeEncryptionComplete(*bd_addr, status);
+ },
+ nullptr, BTM_BLE_SEC_ENCRYPT);
+
+ DLOG(INFO) << __func__ << ": Encryption request result: " << result;
+ }
+
+ void OnGattDisconnected(const tBTA_GATTC_CLOSE& evt) {
+ auto device = std::find_if(devices_.begin(), devices_.end(),
+ HasDevice::MatchAddress(evt.remote_bda));
+ if (device == devices_.end()) {
+ LOG(WARNING) << "Skipping unknown device disconnect, conn_id="
+ << loghex(evt.conn_id);
+ return;
+ }
+ DLOG(INFO) << __func__ << ": device=" << device->addr
+ << ": reason=" << loghex(static_cast<int>(evt.reason));
+
+ /* Don't notify disconnect state for background connection that failed */
+ if (device->is_connecting_actively || device->isGattServiceValid())
+ callbacks_->OnConnectionState(ConnectionState::DISCONNECTED,
+ evt.remote_bda);
+
+ auto peer_disconnected = (evt.reason == GATT_CONN_TIMEOUT) ||
+ (evt.reason == GATT_CONN_TERMINATE_PEER_USER);
+ DoDisconnectCleanUp(*device, peer_disconnected ? false : true);
+
+ /* Connect in background - is this ok? */
+ if (peer_disconnected) BTA_GATTC_Open(gatt_if_, device->addr, false, false);
+ }
+
+ void OnGattServiceSearchComplete(const tBTA_GATTC_SEARCH_CMPL& evt) {
+ auto device = GetDevice(evt.conn_id);
+ if (!device) {
+ LOG(WARNING) << "Skipping unknown device, conn_id="
+ << loghex(evt.conn_id);
+ return;
+ }
+
+ DLOG(INFO) << __func__;
+
+ /* Ignore if our service data is valid (service discovery initiated by
+ * someone else?)
+ */
+ if (!device->isGattServiceValid()) {
+ if (evt.status != GATT_SUCCESS) {
+ LOG(ERROR) << __func__ << ": Service discovery failed";
+ BTA_GATTC_Close(device->conn_id);
+ return;
+ }
+
+ const std::list<gatt::Service>* all_services =
+ BTA_GATTC_GetServices(device->conn_id);
+
+ auto service =
+ std::find_if(all_services->begin(), all_services->end(),
+ [](const gatt::Service& svc) {
+ return svc.uuid == kUuidHearingAccessService;
+ });
+ if (service == all_services->end()) {
+ LOG(ERROR) << "No service found";
+ BTA_GATTC_Close(device->conn_id);
+ return;
+ }
+
+ /* Call the service specific verifier callback */
+ if (!instance->OnHasServiceFound(*service, &(*device))) {
+ LOG(ERROR) << "Not a valid service!";
+ BTA_GATTC_Close(device->conn_id);
+ return;
+ }
+ }
+ }
+
+ void OnGattNotification(const tBTA_GATTC_NOTIFY& evt) {
+ /* Reject invalid lengths */
+ if (evt.len > GATT_MAX_ATTR_LEN) {
+ LOG(ERROR) << __func__ << ": rejected BTA_GATTC_NOTIF_EVT. is_notify = "
+ << evt.is_notify << ", len=" << static_cast<int>(evt.len);
+ }
+ if (!evt.is_notify) BTA_GATTC_SendIndConfirm(evt.conn_id, evt.handle);
+
+ OnHasNotification(evt.conn_id, evt.handle, evt.len, evt.value);
+ }
+
+ void OnLeEncryptionComplete(const RawAddress& address, uint8_t status) {
+ DLOG(INFO) << __func__ << ": " << address;
+
+ auto device = std::find_if(devices_.begin(), devices_.end(),
+ HasDevice::MatchAddress(address));
+ if (device == devices_.end()) {
+ LOG(WARNING) << "Skipping unknown device" << address;
+ return;
+ }
+
+ if (status != BTM_SUCCESS) {
+ LOG(ERROR) << "encryption failed"
+ << " status: " << +status;
+
+ BTA_GATTC_Close(device->conn_id);
+ return;
+ }
+
+ if (device->isGattServiceValid()) {
+ instance->OnEncrypted(*device);
+ } else {
+ BTA_GATTC_ServiceSearchRequest(device->conn_id,
+ &kUuidHearingAccessService);
+ }
+ }
+
+ void OnGattServiceChangeEvent(const RawAddress& address) {
+ auto device = std::find_if(devices_.begin(), devices_.end(),
+ HasDevice::MatchAddress(address));
+ if (device == devices_.end()) {
+ LOG(WARNING) << "Skipping unknown device" << address;
+ return;
+ }
+
+ DLOG(INFO) << __func__ << ": address=" << address;
+
+ /* Invalidate service discovery results */
+ BtaGattQueue::Clean(device->conn_id);
+ device->ClearSvcData();
+ }
+
+ void OnGattServiceDiscoveryDoneEvent(const RawAddress& address) {
+ auto device = std::find_if(devices_.begin(), devices_.end(),
+ HasDevice::MatchAddress(address));
+ if (device == devices_.end()) {
+ LOG(WARNING) << "Skipping unknown device" << address;
+ return;
+ }
+
+ DLOG(INFO) << __func__ << ": address=" << address;
+
+ if (!device->isGattServiceValid())
+ BTA_GATTC_ServiceSearchRequest(device->conn_id,
+ &kUuidHearingAccessService);
+ }
+
+ static uint16_t FindCccHandle(uint16_t conn_id, uint16_t char_handle) {
+ const gatt::Characteristic* p_char =
+ BTA_GATTC_GetCharacteristic(conn_id, char_handle);
+ if (!p_char) {
+ LOG(WARNING) << __func__ << ": No such characteristic: " << char_handle;
+ return GAP_INVALID_HANDLE;
+ }
+
+ for (const gatt::Descriptor& desc : p_char->descriptors) {
+ if (desc.uuid == Uuid::From16Bit(GATT_UUID_CHAR_CLIENT_CONFIG))
+ return desc.handle;
+ }
+
+ return GAP_INVALID_HANDLE;
+ }
+
+ void SubscribeForNotifications(
+ uint16_t conn_id, const RawAddress& address, uint16_t value_handle,
+ uint16_t ccc_handle,
+ uint16_t ccc_val = GATT_CHAR_CLIENT_CONFIG_NOTIFICATION) {
+ if (value_handle != GAP_INVALID_HANDLE) {
+ tGATT_STATUS register_status =
+ BTA_GATTC_RegisterForNotifications(gatt_if_, address, value_handle);
+ DLOG(INFO) << __func__ << ": BTA_GATTC_RegisterForNotifications, status="
+ << loghex(+register_status)
+ << " value=" << loghex(value_handle)
+ << " ccc=" << loghex(ccc_handle);
+
+ if (register_status != GATT_SUCCESS) return;
+ }
+
+ std::vector<uint8_t> value(2);
+ uint8_t* value_ptr = value.data();
+ UINT16_TO_STREAM(value_ptr, ccc_val);
+ BtaGattQueue::WriteDescriptor(
+ conn_id, ccc_handle, std::move(value), GATT_WRITE,
+ [](uint16_t conn_id, tGATT_STATUS status, uint16_t value_handle,
+ uint16_t len, const uint8_t* value, void* data) {
+ if (instance)
+ instance->OnGattWriteCcc(conn_id, status, value_handle, data);
+ },
+ HasGattOpContext(HasGattOpContext::kContextFlagsEnableNotification));
+ }
+
+ uint8_t gatt_if_;
+ bluetooth::has::HasClientCallbacks* callbacks_;
+ std::list<HasDevice> devices_;
+ std::list<HasCtpOp> pending_operations_;
+
+ typedef std::map<decltype(HasCtpOp::op_id), HasCtpGroupOpCoordinator>
+ has_operation_timeouts_t;
+ has_operation_timeouts_t pending_group_operation_timeouts_;
+};
+
+} // namespace
+
+alarm_t* HasCtpGroupOpCoordinator::operation_timeout_timer = nullptr;
+size_t HasCtpGroupOpCoordinator::ref_cnt = 0u;
+alarm_callback_t HasCtpGroupOpCoordinator::cb = [](void*) {};
+
+void HasClient::Initialize(bluetooth::has::HasClientCallbacks* callbacks,
+ base::Closure initCb) {
+ if (instance) {
+ LOG(ERROR) << "Already initialized!";
+ return;
+ }
+
+ HasCtpGroupOpCoordinator::Initialize([](void* p) {
+ if (instance) instance->OnGroupOpCoordinatorTimeout(p);
+ });
+ instance = new HasClientImpl(callbacks, initCb);
+}
+
+bool HasClient::IsHasClientRunning() { return instance; }
+
+HasClient* HasClient::Get(void) {
+ CHECK(instance);
+ return instance;
+};
+
+void HasClient::AddFromStorage(const RawAddress& addr, uint8_t features,
+ uint16_t is_acceptlisted) {
+ if (!instance) {
+ LOG(ERROR) << "Not initialized yet";
+ }
+
+ instance->AddFromStorage(addr, features, is_acceptlisted);
+};
+
+void HasClient::CleanUp() {
+ HasClientImpl* ptr = instance;
+ instance = nullptr;
+
+ if (ptr) {
+ ptr->CleanUp();
+ delete ptr;
+ }
+
+ HasCtpGroupOpCoordinator::Cleanup();
+};
+
+void HasClient::DebugDump(int fd) {
+ dprintf(fd, "Hearing Access Service Client:\n");
+ if (instance)
+ instance->Dump(fd);
+ else
+ dprintf(fd, " no instance\n\n");
+}
diff --git a/system/bta/has/has_client_test.cc b/system/bta/has/has_client_test.cc
new file mode 100644
index 0000000000..c46e034de7
--- /dev/null
+++ b/system/bta/has/has_client_test.cc
@@ -0,0 +1,3118 @@
+/*
+ * 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/strings/string_number_conversions.h>
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+#include <osi/include/alarm.h>
+#include <osi/test/alarm_mock.h>
+#include <sys/socket.h>
+
+#include <variant>
+
+#include "bta/le_audio/le_audio_types.h"
+#include "bta_csis_api.h"
+#include "bta_gatt_api_mock.h"
+#include "bta_gatt_queue_mock.h"
+#include "bta_has_api.h"
+#include "btif_storage_mock.h"
+#include "btm_api_mock.h"
+#include "gatt/database_builder.h"
+#include "hardware/bt_gatt_types.h"
+#include "has_types.h"
+#include "mock_controller.h"
+#include "mock_csis_client.h"
+
+static std::map<const char*, bool> fake_osi_bool_props;
+
+bool osi_property_get_bool(const char* key, bool default_value) {
+ if (fake_osi_bool_props.count(key)) return fake_osi_bool_props.at(key);
+
+ return default_value;
+}
+
+void osi_property_set_bool(const char* key, bool value) {
+ fake_osi_bool_props.insert_or_assign(key, value);
+}
+
+bool gatt_profile_get_eatt_support(const RawAddress& addr) { return true; }
+
+namespace bluetooth {
+namespace has {
+namespace internal {
+namespace {
+
+using base::HexEncode;
+
+using ::bluetooth::csis::CsisClient;
+using ::bluetooth::has::ConnectionState;
+using ::bluetooth::has::ErrorCode;
+using ::bluetooth::has::HasClientCallbacks;
+using ::bluetooth::has::PresetInfo;
+
+using ::le_audio::has::HasClient;
+using ::le_audio::has::HasCtpGroupOpCoordinator;
+using ::le_audio::has::HasCtpOp;
+using ::le_audio::has::HasDevice;
+using ::le_audio::has::HasPreset;
+
+using ::testing::_;
+using ::testing::AnyNumber;
+using ::testing::DoAll;
+using ::testing::DoDefault;
+using ::testing::Invoke;
+using ::testing::Mock;
+using ::testing::NotNull;
+using ::testing::Return;
+using ::testing::SaveArg;
+using ::testing::Sequence;
+using ::testing::SetArgPointee;
+using ::testing::WithArg;
+
+// Disables most likely false-positives from base::SplitString()
+// extern "C" const char* __asan_default_options() {
+// return "detect_container_overflow=0";
+// }
+
+RawAddress GetTestAddress(int index) {
+ CHECK_LT(index, UINT8_MAX);
+ RawAddress result = {
+ {0xC0, 0xDE, 0xC0, 0xDE, 0x00, static_cast<uint8_t>(index)}};
+ return result;
+}
+
+static uint16_t GetTestConnId(const RawAddress& address) {
+ return address.address[RawAddress::kLength - 1];
+}
+
+class MockHasCallbacks : public HasClientCallbacks {
+ public:
+ MockHasCallbacks() = default;
+ ~MockHasCallbacks() override = default;
+
+ MOCK_METHOD((void), OnConnectionState,
+ (ConnectionState state, const RawAddress& address), (override));
+ MOCK_METHOD((void), OnDeviceAvailable,
+ (const RawAddress& address, uint8_t features), (override));
+ MOCK_METHOD((void), OnFeaturesUpdate,
+ (const RawAddress& address, uint8_t features), (override));
+ MOCK_METHOD((void), OnActivePresetSelected,
+ ((std::variant<RawAddress, int> addr_or_group_id),
+ uint8_t preset_index),
+ (override));
+ MOCK_METHOD((void), OnActivePresetSelectError,
+ ((std::variant<RawAddress, int> addr_or_group_id),
+ ErrorCode result),
+ (override));
+ MOCK_METHOD((void), OnPresetInfo,
+ ((std::variant<RawAddress, int> addr_or_group_id),
+ PresetInfoReason change_id,
+ std::vector<PresetInfo> preset_change_records),
+ (override));
+ MOCK_METHOD((void), OnPresetInfoError,
+ ((std::variant<RawAddress, int> addr_or_group_id),
+ uint8_t preset_index, ErrorCode error_code),
+ (override));
+ MOCK_METHOD((void), OnSetPresetNameError,
+ ((std::variant<RawAddress, int> addr_or_group_id),
+ uint8_t preset_index, ErrorCode error_code),
+ (override));
+
+ private:
+ DISALLOW_COPY_AND_ASSIGN(MockHasCallbacks);
+};
+
+class HasClientTestBase : public ::testing::Test {
+ protected:
+ std::map<uint16_t, uint8_t> current_peer_active_preset_idx_;
+ std::map<uint16_t, uint8_t> current_peer_features_val_;
+ std::map<uint16_t, std::set<HasPreset, HasPreset::ComparatorDesc>>
+ current_peer_presets_;
+
+ struct HasDbBuilder {
+ bool has;
+
+ static constexpr uint16_t kGapSvcStartHdl = 0x0001;
+ static constexpr uint16_t kGapDeviceNameValHdl = 0x0003;
+ static constexpr uint16_t kGapSvcEndHdl = kGapDeviceNameValHdl;
+
+ static constexpr uint16_t kSvcStartHdl = 0x0010;
+ static constexpr uint16_t kFeaturesValHdl = 0x0012;
+ static constexpr uint16_t kPresetsCtpValHdl = 0x0015;
+ static constexpr uint16_t kActivePresetIndexValHdl = 0x0018;
+ static constexpr uint16_t kSvcEndHdl = 0x001E;
+
+ static constexpr uint16_t kGattSvcStartHdl = 0x0090;
+ static constexpr uint16_t kGattSvcChangedValHdl = 0x0092;
+ static constexpr uint16_t kGattSvcEndHdl = kGattSvcChangedValHdl + 1;
+
+ bool features;
+ bool features_ntf;
+
+ bool preset_cp;
+ bool preset_cp_ntf;
+ bool preset_cp_ind;
+
+ bool active_preset_idx;
+ bool active_preset_idx_ntf;
+
+ const gatt::Database Build() {
+ gatt::DatabaseBuilder bob;
+
+ /* Generic Access Service */
+ bob.AddService(kGapSvcStartHdl, kGapSvcEndHdl, Uuid::From16Bit(0x1800),
+ true);
+ /* Device Name Char. */
+ bob.AddCharacteristic(kGapDeviceNameValHdl - 1, kGapDeviceNameValHdl,
+ Uuid::From16Bit(0x2a00), GATT_CHAR_PROP_BIT_READ);
+
+ /* 0x0004-0x000f left empty on purpose */
+ if (has) {
+ bob.AddService(kSvcStartHdl, kSvcEndHdl,
+ ::le_audio::has::kUuidHearingAccessService, true);
+
+ if (features) {
+ bob.AddCharacteristic(
+ kFeaturesValHdl - 1, kFeaturesValHdl,
+ ::le_audio::has::kUuidHearingAidFeatures,
+ GATT_CHAR_PROP_BIT_READ |
+ (features_ntf ? GATT_CHAR_PROP_BIT_NOTIFY : 0));
+
+ if (features_ntf) {
+ bob.AddDescriptor(kFeaturesValHdl + 1,
+ Uuid::From16Bit(GATT_UUID_CHAR_CLIENT_CONFIG));
+ }
+ }
+
+ if (preset_cp) {
+ bob.AddCharacteristic(
+ kPresetsCtpValHdl - 1, kPresetsCtpValHdl,
+ ::le_audio::has::kUuidHearingAidPresetControlPoint,
+ GATT_CHAR_PROP_BIT_WRITE |
+ (preset_cp_ntf ? GATT_CHAR_PROP_BIT_NOTIFY : 0) |
+ (preset_cp_ind ? GATT_CHAR_PROP_BIT_INDICATE : 0));
+
+ if (preset_cp_ntf || preset_cp_ind) {
+ bob.AddDescriptor(kPresetsCtpValHdl + 1,
+ Uuid::From16Bit(GATT_UUID_CHAR_CLIENT_CONFIG));
+ }
+ }
+
+ if (active_preset_idx) {
+ bob.AddCharacteristic(
+ kActivePresetIndexValHdl - 1, kActivePresetIndexValHdl,
+ ::le_audio::has::kUuidActivePresetIndex,
+ GATT_CHAR_PROP_BIT_READ |
+ (active_preset_idx_ntf ? GATT_CHAR_PROP_BIT_NOTIFY : 0));
+
+ if (active_preset_idx_ntf)
+ bob.AddDescriptor(kActivePresetIndexValHdl + 1,
+ Uuid::From16Bit(GATT_UUID_CHAR_CLIENT_CONFIG));
+ }
+ }
+
+ /* GATTS */
+ /* 0x001F-0x0090 left empty on purpose */
+ bob.AddService(kGattSvcStartHdl, kGattSvcEndHdl,
+ Uuid::From16Bit(UUID_SERVCLASS_GATT_SERVER), true);
+ bob.AddCharacteristic(kGattSvcChangedValHdl - 1, kGattSvcChangedValHdl,
+ Uuid::From16Bit(GATT_UUID_GATT_SRV_CHGD),
+ GATT_CHAR_PROP_BIT_NOTIFY);
+ bob.AddDescriptor(kGattSvcChangedValHdl + 1,
+ Uuid::From16Bit(GATT_UUID_CHAR_CLIENT_CONFIG));
+ return bob.Build();
+ };
+ };
+
+ const gatt::Characteristic* FindCharacteristicByValueHandle(
+ const gatt::Service* svc, uint16_t handle) {
+ if (svc == nullptr) return nullptr;
+
+ auto it =
+ std::find_if(svc->characteristics.cbegin(), svc->characteristics.cend(),
+ [handle](const auto& characteristic) {
+ return characteristic.value_handle == handle;
+ });
+ return (it != svc->characteristics.cend()) ? &(*it) : nullptr;
+ }
+
+ void set_sample_database(
+ const RawAddress& address, HasDbBuilder& builder,
+ uint8_t features_val = 0x0,
+ std::optional<std::set<HasPreset, HasPreset::ComparatorDesc>> presets_op =
+ std::nullopt) {
+ uint16_t conn_id = GetTestConnId(address);
+
+ /* For some test cases these defaults are enough */
+ if (!presets_op)
+ presets_op = {{
+ HasPreset(6, HasPreset::kPropertyAvailable, "Universal"),
+ HasPreset(
+ 55, HasPreset::kPropertyAvailable | HasPreset::kPropertyWritable,
+ "YourPreset55"),
+ }};
+ auto& presets = presets_op.value();
+ auto const active_preset = presets.begin();
+
+ services_map[conn_id] = builder.Build().Services();
+ current_peer_features_val_.insert_or_assign(conn_id, features_val);
+ current_peer_active_preset_idx_.insert_or_assign(conn_id,
+ active_preset->GetIndex());
+ current_peer_presets_.insert_or_assign(conn_id, std::move(presets));
+
+ ON_CALL(gatt_queue, ReadCharacteristic(conn_id, _, _, _))
+ .WillByDefault(Invoke([this](uint16_t conn_id, uint16_t handle,
+ GATT_READ_OP_CB cb, void* cb_data) {
+ auto* svc = gatt::FindService(services_map[conn_id], handle);
+ if (svc == nullptr) return;
+
+ std::vector<uint8_t> value;
+ tGATT_STATUS status = GATT_SUCCESS;
+
+ switch (handle) {
+ case HasDbBuilder::kGapDeviceNameValHdl:
+ value.resize(20);
+ break;
+ case HasDbBuilder::kFeaturesValHdl:
+ value.resize(1);
+ value[0] = current_peer_features_val_.at(conn_id);
+ break;
+ case HasDbBuilder::kActivePresetIndexValHdl:
+ value.resize(1);
+ value[0] = current_peer_active_preset_idx_.at(conn_id);
+ break;
+ case HasDbBuilder::kPresetsCtpValHdl:
+ /* passthrough */
+ default:
+ status = GATT_READ_NOT_PERMIT;
+ break;
+ }
+
+ if (cb)
+ cb(conn_id, status, handle, value.size(), value.data(), cb_data);
+ }));
+
+ /* Default action for the Control Point operation writes */
+ ON_CALL(gatt_queue,
+ WriteCharacteristic(conn_id, HasDbBuilder::kPresetsCtpValHdl, _,
+ GATT_WRITE, _, _))
+ .WillByDefault(Invoke([this, address](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) {
+ auto pp = value.data();
+ auto len = value.size();
+ uint8_t op, index;
+
+ const bool indicate = false;
+
+ if (len < 1) {
+ if (cb)
+ cb(conn_id, GATT_INVALID_ATTR_LEN, handle, value.size(),
+ value.data(), cb_data);
+ return;
+ }
+
+ STREAM_TO_UINT8(op, pp)
+ --len;
+ if (op >
+ static_cast<
+ std::underlying_type_t<::le_audio::has::PresetCtpOpcode>>(
+ ::le_audio::has::PresetCtpOpcode::OP_MAX_)) {
+ /* Invalid Opcode */
+ if (cb)
+ cb(conn_id, (tGATT_STATUS)0x80, handle, value.size(),
+ value.data(), cb_data);
+ return;
+ }
+
+ switch (static_cast<::le_audio::has::PresetCtpOpcode>(op)) {
+ case ::le_audio::has::PresetCtpOpcode::READ_ALL_PRESETS:
+ ASSERT_EQ(0u, len);
+ InjectNotifyReadPresetsResponse(conn_id, address, handle, value,
+ indicate, -1, cb, cb_data);
+ break;
+
+ case ::le_audio::has::PresetCtpOpcode::READ_PRESET_BY_INDEX:
+ if (len < 1) {
+ if (cb)
+ cb(conn_id, GATT_INVALID_ATTR_LEN, handle, value.size(),
+ value.data(), cb_data);
+
+ } else {
+ STREAM_TO_UINT8(index, pp);
+ --len;
+ ASSERT_EQ(0u, len);
+
+ InjectNotifyReadPresetsResponse(conn_id, address, handle, value,
+ indicate, index, cb, cb_data);
+ }
+ break;
+
+ case ::le_audio::has::PresetCtpOpcode::SET_ACTIVE_PRESET: {
+ if (len < 1) {
+ if (cb)
+ cb(conn_id, GATT_INVALID_ATTR_LEN, handle, value.size(),
+ value.data(), cb_data);
+ break;
+ }
+ STREAM_TO_UINT8(index, pp);
+ --len;
+ ASSERT_EQ(0u, len);
+
+ auto presets = current_peer_presets_.at(conn_id);
+ if (presets.count(index)) {
+ current_peer_active_preset_idx_.insert_or_assign(conn_id,
+ index);
+ if (cb)
+ cb(conn_id, GATT_SUCCESS, handle, value.size(), value.data(),
+ cb_data);
+ InjectActivePresetNotification(conn_id, address, handle, value,
+ index, cb, cb_data);
+ } else {
+ /* Preset Operation Not Possible */
+ if (cb)
+ cb(conn_id, (tGATT_STATUS)0x83, handle, value.size(),
+ value.data(), cb_data);
+ }
+ } break;
+
+ case ::le_audio::has::PresetCtpOpcode::SET_ACTIVE_PRESET_SYNC: {
+ auto features = current_peer_features_val_.at(conn_id);
+ if ((features & ::bluetooth::has::
+ kFeatureBitPresetSynchronizationSupported) ==
+ 0) {
+ /* Synchronization Not Supported */
+ if (cb)
+ cb(conn_id, (tGATT_STATUS)0x82, handle, value.size(),
+ value.data(), cb_data);
+ break;
+ }
+
+ if (len < 1) {
+ if (cb)
+ cb(conn_id, GATT_INVALID_ATTR_LEN, handle, value.size(),
+ value.data(), cb_data);
+ break;
+ }
+ STREAM_TO_UINT8(index, pp);
+ --len;
+ ASSERT_EQ(0u, len);
+
+ auto csis_api = CsisClient::Get();
+ int group_id = bluetooth::groups::kGroupUnknown;
+ if (csis_api != nullptr) {
+ group_id = csis_api->GetGroupId(
+ address, ::le_audio::uuid::kCapServiceUuid);
+ }
+
+ if (group_id != bluetooth::groups::kGroupUnknown) {
+ if (cb)
+ cb(conn_id, GATT_SUCCESS, handle, value.size(), value.data(),
+ cb_data);
+ /* Send notification from all grouped devices */
+ auto addresses = csis_api->GetDeviceList(group_id);
+ for (auto& addr : addresses) {
+ auto conn = GetTestConnId(addr);
+ InjectActivePresetNotification(conn, addr, handle, value,
+ index, cb, cb_data);
+ }
+ } else {
+ /* Preset Operation Not Possible */
+ if (cb)
+ cb(conn_id, (tGATT_STATUS)0x83, handle, value.size(),
+ value.data(), cb_data);
+ }
+ } break;
+
+ case ::le_audio::has::PresetCtpOpcode::SET_NEXT_PRESET: {
+ ASSERT_EQ(0u, len);
+ ASSERT_NE(0u, current_peer_active_preset_idx_.count(conn_id));
+ ASSERT_NE(0u, current_peer_presets_.count(conn_id));
+
+ auto current_preset = current_peer_active_preset_idx_.at(conn_id);
+ auto presets = current_peer_presets_.at(conn_id);
+ auto current = presets.find(current_preset);
+ if (current != presets.end()) {
+ ++current;
+ if (current == presets.end()) current = presets.begin();
+
+ current_peer_active_preset_idx_.insert_or_assign(
+ conn_id, current->GetIndex());
+ InjectActivePresetNotification(conn_id, address, handle, value,
+ current->GetIndex(), cb,
+ cb_data);
+
+ } else {
+ /* Preset Operation Not Possible */
+ if (cb)
+ cb(conn_id, (tGATT_STATUS)0x83, handle, value.size(),
+ value.data(), cb_data);
+ }
+ } break;
+
+ case ::le_audio::has::PresetCtpOpcode::SET_PREV_PRESET: {
+ ASSERT_EQ(0u, len);
+ ASSERT_NE(0u, current_peer_active_preset_idx_.count(conn_id));
+ ASSERT_NE(0u, current_peer_presets_.count(conn_id));
+
+ auto current_preset = current_peer_active_preset_idx_.at(conn_id);
+ auto presets = current_peer_presets_.at(conn_id);
+ auto rit = presets.rbegin();
+ while (rit != presets.rend()) {
+ if (rit->GetIndex() == current_preset) {
+ rit++;
+ /* Wrap around */
+ if (rit == presets.rend()) {
+ rit = presets.rbegin();
+ }
+ break;
+ }
+ rit++;
+ }
+
+ if (rit != presets.rend()) {
+ if (cb)
+ cb(conn_id, GATT_SUCCESS, handle, value.size(), value.data(),
+ cb_data);
+ current_peer_active_preset_idx_.insert_or_assign(
+ conn_id, rit->GetIndex());
+ InjectActivePresetNotification(conn_id, address, handle, value,
+ rit->GetIndex(), cb, cb_data);
+ } else {
+ /* Preset Operation Not Possible */
+ if (cb)
+ cb(conn_id, (tGATT_STATUS)0x83, handle, value.size(),
+ value.data(), cb_data);
+ }
+ } break;
+
+ case ::le_audio::has::PresetCtpOpcode::SET_NEXT_PRESET_SYNC: {
+ ASSERT_EQ(0u, len);
+ auto features = current_peer_features_val_.at(conn_id);
+ if ((features & ::bluetooth::has::
+ kFeatureBitPresetSynchronizationSupported) ==
+ 0) {
+ /* Synchronization Not Supported */
+ if (cb)
+ cb(conn_id, (tGATT_STATUS)0x82, handle, value.size(),
+ value.data(), cb_data);
+ break;
+ }
+
+ auto current_preset = current_peer_active_preset_idx_.at(conn_id);
+ auto presets = current_peer_presets_.at(conn_id);
+ auto rit = presets.begin();
+ while (rit != presets.end()) {
+ if (rit->GetIndex() == current_preset) {
+ rit++;
+ /* Wrap around */
+ if (rit == presets.end()) {
+ rit = presets.begin();
+ }
+ break;
+ }
+ rit++;
+ }
+
+ if (rit != presets.end()) {
+ auto synced_group = mock_csis_client_module_.GetGroupId(
+ GetTestAddress(conn_id), ::le_audio::uuid::kCapServiceUuid);
+ auto addresses =
+ mock_csis_client_module_.GetDeviceList(synced_group);
+
+ // Emulate locally synced op. - notify from all of the devices
+ for (auto addr : addresses) {
+ auto cid = GetTestConnId(addr);
+ if ((cid == conn_id) && (cb != nullptr))
+ cb(cid, GATT_SUCCESS, handle, value.size(), value.data(),
+ cb_data);
+
+ current_peer_active_preset_idx_.insert_or_assign(
+ conn_id, rit->GetIndex());
+ InjectActivePresetNotification(cid, addr, handle, value,
+ rit->GetIndex(), cb, cb_data);
+ }
+ } else {
+ /* Preset Operation Not Possible */
+ if (cb)
+ cb(conn_id, (tGATT_STATUS)0x83, handle, value.size(),
+ value.data(), cb_data);
+ }
+ } break;
+
+ case ::le_audio::has::PresetCtpOpcode::SET_PREV_PRESET_SYNC: {
+ ASSERT_EQ(0u, len);
+ auto features = current_peer_features_val_.at(conn_id);
+ if ((features & ::bluetooth::has::
+ kFeatureBitPresetSynchronizationSupported) ==
+ 0) {
+ /* Synchronization Not Supported */
+ if (cb)
+ cb(conn_id, (tGATT_STATUS)0x82, handle, value.size(),
+ value.data(), cb_data);
+ break;
+ }
+
+ auto current_preset = current_peer_active_preset_idx_.at(conn_id);
+ auto presets = current_peer_presets_.at(conn_id);
+ auto rit = presets.rbegin();
+ while (rit != presets.rend()) {
+ if (rit->GetIndex() == current_preset) {
+ rit++;
+ /* Wrap around */
+ if (rit == presets.rend()) {
+ rit = presets.rbegin();
+ }
+ break;
+ }
+ rit++;
+ }
+
+ if (rit != presets.rend()) {
+ auto synced_group = mock_csis_client_module_.GetGroupId(
+ GetTestAddress(conn_id), ::le_audio::uuid::kCapServiceUuid);
+ auto addresses =
+ mock_csis_client_module_.GetDeviceList(synced_group);
+
+ // Emulate locally synced op. - notify from all of the devices
+ for (auto addr : addresses) {
+ auto cid = GetTestConnId(addr);
+ if ((cid == conn_id) && (cb != nullptr))
+ cb(cid, GATT_SUCCESS, handle, value.size(), value.data(),
+ cb_data);
+
+ current_peer_active_preset_idx_.insert_or_assign(
+ conn_id, rit->GetIndex());
+ InjectActivePresetNotification(cid, addr, handle, value,
+ rit->GetIndex(), cb, cb_data);
+ }
+ } else {
+ /* Preset Operation Not Possible */
+ if (cb)
+ cb(conn_id, (tGATT_STATUS)0x83, handle, value.size(),
+ value.data(), cb_data);
+ }
+ } break;
+
+ case ::le_audio::has::PresetCtpOpcode::WRITE_PRESET_NAME: {
+ STREAM_TO_UINT8(index, pp);
+ --len;
+ auto name = std::string(pp, pp + len);
+ len = 0;
+
+ ASSERT_NE(0u, current_peer_presets_.count(conn_id));
+ auto presets = current_peer_presets_.at(conn_id);
+ auto rit = presets.rbegin();
+ auto current = rit;
+ while (rit != presets.rend()) {
+ if (rit->GetIndex() == index) {
+ current = rit;
+ rit++;
+ break;
+ }
+ rit++;
+ }
+
+ auto prev_index = (rit == presets.rend()) ? 0 : rit->GetIndex();
+
+ ASSERT_NE(current, presets.rend());
+ if (cb)
+ cb(conn_id, GATT_SUCCESS, handle, value.size(), value.data(),
+ cb_data);
+
+ auto new_preset = HasPreset(current->GetIndex(),
+ current->GetProperties(), name);
+ presets.erase(current->GetIndex());
+ presets.insert(new_preset);
+
+ InjectPresetChanged(
+ conn_id, address, indicate, new_preset, prev_index,
+ ::le_audio::has::PresetCtpChangeId::PRESET_GENERIC_UPDATE,
+ true);
+ } break;
+
+ default:
+ if (cb)
+ cb(conn_id, GATT_INVALID_HANDLE, handle, value.size(),
+ value.data(), cb_data);
+ break;
+ }
+ }));
+ }
+
+ void SetUp(void) override {
+ fake_osi_bool_props.clear();
+
+ controller::SetMockControllerInterface(&controller_interface_);
+ bluetooth::manager::SetMockBtmInterface(&btm_interface);
+ bluetooth::storage::SetMockBtifStorageInterface(&btif_storage_interface_);
+ gatt::SetMockBtaGattInterface(&gatt_interface);
+ gatt::SetMockBtaGattQueue(&gatt_queue);
+ callbacks.reset(new MockHasCallbacks());
+
+ encryption_result = true;
+
+ MockCsisClient::SetMockInstanceForTesting(&mock_csis_client_module_);
+ ON_CALL(mock_csis_client_module_, Get())
+ .WillByDefault(Return(&mock_csis_client_module_));
+ ON_CALL(mock_csis_client_module_, IsCsisClientRunning())
+ .WillByDefault(Return(true));
+
+ /* 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;
+ }));
+
+ ON_CALL(gatt_interface, ServiceSearchRequest(_, _))
+ .WillByDefault(WithArg<0>(Invoke(
+ [&](uint16_t conn_id) { InjectSearchCompleteEvent(conn_id); })));
+
+ /* 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, value.size(), value.data(),
+ cb_data);
+ }));
+
+ /* by default connect only direct connection requests */
+ ON_CALL(gatt_interface, Open(_, _, _, _))
+ .WillByDefault(
+ Invoke([&](tGATT_IF client_if, const RawAddress& remote_bda,
+ bool is_direct, bool opportunistic) {
+ if (is_direct)
+ InjectConnectedEvent(remote_bda, GetTestConnId(remote_bda));
+ }));
+
+ ON_CALL(gatt_interface, Close(_))
+ .WillByDefault(Invoke(
+ [&](uint16_t conn_id) { InjectDisconnectedEvent(conn_id); }));
+ }
+
+ void TearDown(void) override {
+ services_map.clear();
+ gatt::SetMockBtaGattQueue(nullptr);
+ gatt::SetMockBtaGattInterface(nullptr);
+ bluetooth::storage::SetMockBtifStorageInterface(nullptr);
+ bluetooth::manager::SetMockBtmInterface(nullptr);
+ controller::SetMockControllerInterface(nullptr);
+ callbacks.reset();
+
+ current_peer_active_preset_idx_.clear();
+ current_peer_features_val_.clear();
+ }
+
+ void TestAppRegister(void) {
+ BtaAppRegisterCallback app_register_callback;
+ EXPECT_CALL(gatt_interface, AppRegister(_, _, _))
+ .WillOnce(DoAll(SaveArg<0>(&gatt_callback),
+ SaveArg<1>(&app_register_callback)));
+ HasClient::Initialize(callbacks.get(), base::DoNothing());
+ ASSERT_TRUE(gatt_callback);
+ ASSERT_TRUE(app_register_callback);
+ app_register_callback.Run(gatt_if, GATT_SUCCESS);
+ ASSERT_TRUE(HasClient::IsHasClientRunning());
+ Mock::VerifyAndClearExpectations(&gatt_interface);
+ }
+
+ void TestAppUnregister(void) {
+ EXPECT_CALL(gatt_interface, AppDeregister(gatt_if));
+ HasClient::CleanUp();
+ ASSERT_FALSE(HasClient::IsHasClientRunning());
+ gatt_callback = nullptr;
+ }
+
+ void TestConnect(const RawAddress& address) {
+ ON_CALL(btm_interface, BTM_IsEncrypted(address, _))
+ .WillByDefault(DoAll(Return(encryption_result)));
+
+ EXPECT_CALL(gatt_interface, Open(gatt_if, address, true, _));
+ HasClient::Get()->Connect(address);
+
+ Mock::VerifyAndClearExpectations(&*callbacks);
+ Mock::VerifyAndClearExpectations(&gatt_queue);
+ Mock::VerifyAndClearExpectations(&gatt_interface);
+ Mock::VerifyAndClearExpectations(&btm_interface);
+ }
+
+ void TestDisconnect(const RawAddress& address, uint16_t conn_id) {
+ EXPECT_CALL(gatt_interface, CancelOpen(_, address, _)).Times(AnyNumber());
+ if (conn_id != GATT_INVALID_CONN_ID) {
+ assert(0);
+ EXPECT_CALL(gatt_interface, Close(conn_id));
+ } else {
+ EXPECT_CALL(gatt_interface, CancelOpen(gatt_if, address, _));
+ }
+ HasClient::Get()->Disconnect(address);
+ }
+
+ void TestAddFromStorage(const RawAddress& address, uint8_t features,
+ bool auto_connect) {
+ if (auto_connect) {
+ EXPECT_CALL(gatt_interface, Open(gatt_if, address, false, _));
+ HasClient::Get()->AddFromStorage(address, features, auto_connect);
+
+ /* Inject connected event for autoconnect/background connection */
+ InjectConnectedEvent(address, GetTestConnId(address));
+ } else {
+ EXPECT_CALL(gatt_interface, Open(gatt_if, address, _, _)).Times(0);
+ HasClient::Get()->AddFromStorage(address, features, auto_connect);
+ }
+
+ Mock::VerifyAndClearExpectations(&gatt_interface);
+ }
+
+ void InjectConnectedEvent(const RawAddress& address, uint16_t conn_id,
+ tGATT_STATUS status = GATT_SUCCESS) {
+ tBTA_GATTC_OPEN event_data = {
+ .status = status,
+ .conn_id = conn_id,
+ .client_if = gatt_if,
+ .remote_bda = address,
+ .transport = GATT_TRANSPORT_LE,
+ .mtu = 240,
+ };
+
+ connected_devices[conn_id] = address;
+ gatt_callback(BTA_GATTC_OPEN_EVT, (tBTA_GATTC*)&event_data);
+ }
+
+ void InjectDisconnectedEvent(
+ uint16_t conn_id,
+ tGATT_DISCONN_REASON reason = GATT_CONN_TERMINATE_LOCAL_HOST,
+ bool allow_fake_conn = false) {
+ if (!allow_fake_conn) ASSERT_NE(connected_devices.count(conn_id), 0u);
+
+ tBTA_GATTC_CLOSE event_data = {
+ .status = GATT_SUCCESS,
+ .conn_id = conn_id,
+ .client_if = gatt_if,
+ .remote_bda = connected_devices[conn_id],
+ .reason = reason,
+ };
+
+ connected_devices.erase(conn_id);
+ gatt_callback(BTA_GATTC_CLOSE_EVT, (tBTA_GATTC*)&event_data);
+ }
+
+ void InjectSearchCompleteEvent(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 InjectNotificationEvent(const RawAddress& test_address, uint16_t conn_id,
+ uint16_t handle, std::vector<uint8_t> value,
+ bool indicate = false) {
+ tBTA_GATTC_NOTIFY event_data = {
+ .conn_id = conn_id,
+ .bda = test_address,
+ .handle = handle,
+ .len = (uint8_t)value.size(),
+ .is_notify = !indicate,
+ };
+
+ ASSERT_TRUE(value.size() < GATT_MAX_ATTR_LEN);
+ std::copy(value.begin(), value.end(), event_data.value);
+ gatt_callback(BTA_GATTC_NOTIF_EVT, (tBTA_GATTC*)&event_data);
+ }
+
+ void SetEncryptionResult(const RawAddress& address, bool success) {
+ encryption_result = success;
+ ON_CALL(btm_interface, BTM_IsEncrypted(address, _))
+ .WillByDefault(Return(success));
+ ON_CALL(btm_interface, GetSecurityFlagsByTransport(address, NotNull(), _))
+ .WillByDefault(
+ DoAll(SetArgPointee<1>(success ? BTM_SEC_FLAG_ENCRYPTED : 0),
+ Return(true)));
+ if (!success) {
+ 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 InjectNotifyReadPresetResponse(uint16_t conn_id,
+ RawAddress const& address,
+ uint16_t handle, const HasPreset& preset,
+ bool indicate, bool is_last) {
+ std::vector<uint8_t> value;
+
+ value.push_back(
+ static_cast<std::underlying_type_t<::le_audio::has::PresetCtpOpcode>>(
+ ::le_audio::has::PresetCtpOpcode::READ_PRESET_RESPONSE));
+ value.push_back(is_last ? 0x01 : 0x00);
+
+ preset.ToCharacteristicValue(value);
+ InjectNotificationEvent(address, conn_id, handle, value, indicate);
+ }
+
+ void InjectPresetChanged(uint16_t conn_id, RawAddress const& address,
+ bool indicate, const HasPreset& preset,
+ uint8_t prev_index,
+ ::le_audio::has::PresetCtpChangeId change_id,
+ bool is_last) {
+ std::vector<uint8_t> value;
+
+ value.push_back(
+ static_cast<std::underlying_type_t<::le_audio::has::PresetCtpOpcode>>(
+ ::le_audio::has::PresetCtpOpcode::PRESET_CHANGED));
+ value.push_back(static_cast<uint8_t>(change_id));
+ value.push_back(is_last ? 0x01 : 0x00);
+
+ switch (change_id) {
+ case ::le_audio::has::PresetCtpChangeId::PRESET_GENERIC_UPDATE:
+ value.push_back(prev_index);
+ preset.ToCharacteristicValue(value);
+ break;
+ case ::le_audio::has::PresetCtpChangeId::PRESET_DELETED:
+ case ::le_audio::has::PresetCtpChangeId::PRESET_AVAILABLE:
+ case ::le_audio::has::PresetCtpChangeId::PRESET_UNAVAILABLE:
+ default:
+ value.push_back(preset.GetIndex());
+ break;
+ }
+
+ InjectNotificationEvent(address, conn_id, HasDbBuilder::kPresetsCtpValHdl,
+ value, indicate);
+ }
+
+ void InjectNotifyReadPresetsResponse(uint16_t conn_id,
+ RawAddress const& address,
+ uint16_t handle,
+ std::vector<uint8_t> value,
+ bool indicate, int index,
+ GATT_WRITE_OP_CB cb, void* cb_data) {
+ auto presets = current_peer_presets_.at(conn_id);
+ LOG_ASSERT(!presets.empty()) << __func__ << " Mocking error!";
+
+ if (index == -1) {
+ if (cb)
+ cb(conn_id, GATT_SUCCESS, handle, value.size(), value.data(), cb_data);
+ /* Notify all presets */
+ for (auto preset = presets.begin(); preset != presets.end(); preset++) {
+ InjectNotifyReadPresetResponse(conn_id, address, handle, *preset,
+ indicate,
+ (preset == std::prev(presets.end())));
+ }
+ } else {
+ auto preset = presets.find(index);
+ if (preset != presets.end()) {
+ if (cb)
+ cb(conn_id, GATT_SUCCESS, handle, value.size(), value.data(),
+ cb_data);
+ InjectNotifyReadPresetResponse(conn_id, address, handle, *preset,
+ indicate, true);
+ } else {
+ /* operation not possible */
+ if (cb)
+ cb(conn_id, (tGATT_STATUS)0x83, handle, value.size(), value.data(),
+ cb_data);
+ }
+ }
+ }
+
+ void InjectActivePresetNotification(uint16_t conn_id,
+ RawAddress const& address,
+ uint16_t handle,
+ std::vector<uint8_t> wr_value,
+ uint8_t index, GATT_WRITE_OP_CB cb,
+ void* cb_data) {
+ auto presets = current_peer_presets_.at(conn_id);
+ LOG_ASSERT(!presets.empty()) << __func__ << " Mocking error!";
+
+ auto preset = presets.find(index);
+ if (preset == presets.end()) {
+ /* preset operation not possible */
+ if (cb)
+ cb(conn_id, (tGATT_STATUS)0x83, handle, wr_value.size(),
+ wr_value.data(), cb_data);
+ return;
+ }
+
+ std::vector<uint8_t> value;
+ value.push_back(index);
+ InjectNotificationEvent(
+ address, conn_id, HasDbBuilder::kActivePresetIndexValHdl, value, false);
+ }
+
+ void SetSampleDatabaseHasNoFeatures(const RawAddress& address) {
+ HasDbBuilder builder = {
+ .has = true,
+ .features = false,
+ .features_ntf = false,
+ .preset_cp = true,
+ .preset_cp_ntf = false,
+ .preset_cp_ind = true,
+ .active_preset_idx = true,
+ .active_preset_idx_ntf = true,
+ };
+ set_sample_database(address, builder);
+ }
+
+ void SetSampleDatabaseHasNoPresetChange(const RawAddress& address,
+ uint8_t features_value = 0x00) {
+ HasDbBuilder builder = {
+ .has = true,
+ .features = true,
+ .features_ntf = false,
+ .preset_cp = false,
+ .preset_cp_ntf = false,
+ .preset_cp_ind = false,
+ .active_preset_idx = false,
+ .active_preset_idx_ntf = false,
+ };
+ set_sample_database(address, builder, features_value);
+ }
+
+ void SetSampleDatabaseHasNoOptionalNtf(const RawAddress& address,
+ uint8_t features = 0x00) {
+ HasDbBuilder builder = {
+ .has = true,
+ .features = true,
+ .features_ntf = false,
+ .preset_cp = true,
+ .preset_cp_ntf = false,
+ .preset_cp_ind = true,
+ .active_preset_idx = true,
+ .active_preset_idx_ntf = true,
+ };
+ set_sample_database(address, builder, features);
+ }
+
+ void SetSampleDatabaseNoHas(const RawAddress& address,
+ uint8_t features = 0x00) {
+ HasDbBuilder builder = {
+ .has = false,
+ .features = false,
+ .features_ntf = false,
+ .preset_cp = false,
+ .preset_cp_ntf = false,
+ .preset_cp_ind = false,
+ .active_preset_idx = true,
+ .active_preset_idx_ntf = true,
+ };
+ set_sample_database(address, builder, features);
+ }
+
+ void SetSampleDatabaseHasBrokenNoActivePreset(const RawAddress& address,
+ uint8_t features = 0x00) {
+ HasDbBuilder builder = {
+ .has = true,
+ .features = true,
+ .features_ntf = false,
+ .preset_cp = true,
+ .preset_cp_ntf = true,
+ .preset_cp_ind = true,
+ .active_preset_idx = false,
+ .active_preset_idx_ntf = false,
+ };
+ set_sample_database(address, builder, features);
+ }
+
+ void SetSampleDatabaseHasBrokenNoActivePresetNtf(const RawAddress& address,
+ uint8_t features = 0x00) {
+ HasDbBuilder builder = {
+ .has = true,
+ .features = true,
+ .features_ntf = false,
+ .preset_cp = true,
+ .preset_cp_ntf = true,
+ .preset_cp_ind = true,
+ .active_preset_idx = true,
+ .active_preset_idx_ntf = false,
+ };
+ set_sample_database(address, builder, features);
+ }
+
+ void SetSampleDatabaseHasOnlyFeaturesNtf(const RawAddress& address,
+ uint8_t features = 0x00) {
+ HasDbBuilder builder = {
+ .has = true,
+ .features = true,
+ .features_ntf = true,
+ .preset_cp = false,
+ .preset_cp_ntf = false,
+ .preset_cp_ind = false,
+ .active_preset_idx = false,
+ .active_preset_idx_ntf = false,
+ };
+ set_sample_database(address, builder, features);
+ }
+
+ void SetSampleDatabaseHasOnlyFeaturesNoNtf(const RawAddress& address,
+ uint8_t features = 0x00) {
+ HasDbBuilder builder = {
+ .has = true,
+ .features = true,
+ .features_ntf = false,
+ .preset_cp = false,
+ .preset_cp_ntf = false,
+ .preset_cp_ind = false,
+ .active_preset_idx = false,
+ .active_preset_idx_ntf = false,
+ };
+ set_sample_database(address, builder, features);
+ }
+
+ void SetSampleDatabaseHasPresetsNtf(
+ const RawAddress& address,
+ uint8_t features = bluetooth::has::kFeatureBitHearingAidTypeMonaural,
+ std::optional<std::set<HasPreset, HasPreset::ComparatorDesc>> presets =
+ std::nullopt) {
+ HasDbBuilder builder = {
+ .has = true,
+ .features = true,
+ .features_ntf = true,
+ .preset_cp = true,
+ .preset_cp_ntf = true,
+ .preset_cp_ind = true,
+ .active_preset_idx = true,
+ .active_preset_idx_ntf = true,
+ };
+
+ set_sample_database(address, builder, features, presets);
+ }
+
+ void SetSampleDatabaseHasNoPresetsFlagsOnly(const RawAddress& address) {
+ uint8_t features = bluetooth::has::kFeatureBitHearingAidTypeMonaural;
+ HasDbBuilder builder = {
+ .has = true,
+ .features = true,
+ .features_ntf = true,
+ .preset_cp = false,
+ .preset_cp_ntf = false,
+ .preset_cp_ind = false,
+ .active_preset_idx = false,
+ .active_preset_idx_ntf = false,
+ };
+
+ set_sample_database(address, builder, features, std::nullopt);
+ }
+
+ std::unique_ptr<MockHasCallbacks> callbacks;
+ bluetooth::manager::MockBtmInterface btm_interface;
+ bluetooth::storage::MockBtifStorageInterface btif_storage_interface_;
+ controller::MockControllerInterface controller_interface_;
+ gatt::MockBtaGattInterface gatt_interface;
+ gatt::MockBtaGattQueue gatt_queue;
+ MockCsisClient mock_csis_client_module_;
+ tBTA_GATTC_CBACK* gatt_callback;
+ const uint8_t gatt_if = 0xfe;
+ std::map<uint8_t, RawAddress> connected_devices;
+ std::map<uint16_t, std::list<gatt::Service>> services_map;
+ bool encryption_result;
+};
+
+class HasClientTest : public HasClientTestBase {
+ void SetUp(void) override {
+ HasClientTestBase::SetUp();
+ TestAppRegister();
+ }
+ void TearDown(void) override {
+ TestAppUnregister();
+ HasClientTestBase::TearDown();
+ }
+};
+
+TEST_F(HasClientTestBase, test_get_uninitialized) {
+ ASSERT_DEATH(HasClient::Get(), "");
+}
+
+TEST_F(HasClientTestBase, test_initialize) {
+ HasClient::Initialize(callbacks.get(), base::DoNothing());
+ ASSERT_TRUE(HasClient::IsHasClientRunning());
+ HasClient::CleanUp();
+}
+
+TEST_F(HasClientTestBase, test_initialize_twice) {
+ HasClient::Initialize(callbacks.get(), base::DoNothing());
+ HasClient* has_p = HasClient::Get();
+ HasClient::Initialize(callbacks.get(), base::DoNothing());
+ ASSERT_EQ(has_p, HasClient::Get());
+ HasClient::CleanUp();
+}
+
+TEST_F(HasClientTestBase, test_cleanup_initialized) {
+ HasClient::Initialize(callbacks.get(), base::DoNothing());
+ HasClient::CleanUp();
+ ASSERT_FALSE(HasClient::IsHasClientRunning());
+}
+
+TEST_F(HasClientTestBase, test_cleanup_uninitialized) {
+ HasClient::CleanUp();
+ ASSERT_FALSE(HasClient::IsHasClientRunning());
+}
+
+TEST_F(HasClientTestBase, test_app_registration) {
+ TestAppRegister();
+ TestAppUnregister();
+}
+
+TEST_F(HasClientTest, test_connect) { TestConnect(GetTestAddress(1)); }
+
+TEST_F(HasClientTest, test_add_from_storage) {
+ TestAddFromStorage(GetTestAddress(1), 0, true);
+ TestAddFromStorage(GetTestAddress(2), 0, false);
+}
+
+TEST_F(HasClientTest, test_disconnect_non_connected) {
+ const RawAddress test_address = GetTestAddress(1);
+
+ /* Override the default action to prevent us sendind the connected event */
+ EXPECT_CALL(gatt_interface, Open(gatt_if, test_address, true, _))
+ .WillOnce(Return());
+ HasClient::Get()->Connect(test_address);
+ TestDisconnect(test_address, GATT_INVALID_CONN_ID);
+}
+
+TEST_F(HasClientTest, test_has_connected) {
+ const RawAddress test_address = GetTestAddress(1);
+ /* Minimal possible HA device (only feature flags) */
+ SetSampleDatabaseHasNoPresetChange(
+ test_address, bluetooth::has::kFeatureBitHearingAidTypeBinaural);
+
+ EXPECT_CALL(
+ *callbacks,
+ OnDeviceAvailable(test_address,
+ bluetooth::has::kFeatureBitHearingAidTypeBinaural));
+ EXPECT_CALL(*callbacks,
+ OnConnectionState(ConnectionState::CONNECTED, test_address));
+ TestConnect(test_address);
+}
+
+TEST_F(HasClientTest, test_disconnect_connected) {
+ const RawAddress test_address = GetTestAddress(1);
+ /* Minimal possible HA device (only feature flags) */
+ SetSampleDatabaseHasNoPresetChange(
+ test_address, bluetooth::has::kFeatureBitHearingAidTypeBinaural);
+
+ EXPECT_CALL(*callbacks,
+ OnConnectionState(ConnectionState::CONNECTED, test_address))
+ .Times(1);
+ TestConnect(test_address);
+
+ EXPECT_CALL(*callbacks,
+ OnConnectionState(ConnectionState::DISCONNECTED, test_address))
+ .Times(1);
+ TestDisconnect(test_address, 1);
+}
+
+TEST_F(HasClientTest, test_disconnected_while_autoconnect) {
+ const RawAddress test_address = GetTestAddress(1);
+ TestAddFromStorage(test_address,
+ bluetooth::has::kFeatureBitHearingAidTypeBinaural, true);
+ /* autoconnect - don't indicate disconnection */
+ EXPECT_CALL(*callbacks,
+ OnConnectionState(ConnectionState::DISCONNECTED, test_address))
+ .Times(0);
+ /* Verify that the device still can connect in te background */
+ InjectDisconnectedEvent(1, GATT_CONN_TERMINATE_PEER_USER, true);
+}
+
+TEST_F(HasClientTest, test_encryption_failed) {
+ const RawAddress test_address = GetTestAddress(1);
+ SetSampleDatabaseHasNoPresetChange(
+ test_address, bluetooth::has::kFeatureBitHearingAidTypeBinaural);
+ EXPECT_CALL(*callbacks,
+ OnConnectionState(ConnectionState::DISCONNECTED, test_address))
+ .Times(1);
+ EXPECT_CALL(*callbacks,
+ OnConnectionState(ConnectionState::CONNECTED, test_address))
+ .Times(0);
+ SetEncryptionResult(test_address, false);
+ TestConnect(test_address);
+}
+
+TEST_F(HasClientTest, test_reconnect_after_encryption_failed) {
+ const RawAddress test_address = GetTestAddress(1);
+ SetSampleDatabaseHasNoPresetChange(
+ test_address, bluetooth::has::kFeatureBitHearingAidTypeBinaural);
+ EXPECT_CALL(*callbacks,
+ OnConnectionState(ConnectionState::DISCONNECTED, test_address))
+ .Times(1);
+ EXPECT_CALL(*callbacks,
+ OnConnectionState(ConnectionState::CONNECTED, test_address))
+ .Times(0);
+ SetEncryptionResult(test_address, false);
+ TestConnect(test_address);
+
+ EXPECT_CALL(*callbacks,
+ OnConnectionState(ConnectionState::CONNECTED, test_address))
+ .Times(1);
+ SetEncryptionResult(test_address, true);
+ InjectConnectedEvent(test_address, GetTestConnId(test_address));
+}
+
+TEST_F(HasClientTest, test_reconnect_after_encryption_failed_from_storage) {
+ const RawAddress test_address = GetTestAddress(1);
+
+ SetSampleDatabaseHasNoPresetChange(
+ test_address, bluetooth::has::kFeatureBitHearingAidTypeBinaural);
+ SetEncryptionResult(test_address, false);
+ TestAddFromStorage(test_address, 0, true);
+ /* autoconnect - don't indicate disconnection */
+ EXPECT_CALL(*callbacks,
+ OnConnectionState(ConnectionState::DISCONNECTED, test_address))
+ .Times(0);
+ Mock::VerifyAndClearExpectations(&btm_interface);
+
+ /* Fake no persistent storage data */
+ ON_CALL(btif_storage_interface_, GetLeaudioHasPresets(_, _, _))
+ .WillByDefault([]() { return false; });
+
+ EXPECT_CALL(*callbacks,
+ OnConnectionState(ConnectionState::CONNECTED, test_address))
+ .Times(1);
+ SetEncryptionResult(test_address, true);
+ InjectConnectedEvent(test_address, GetTestConnId(test_address));
+}
+
+TEST_F(HasClientTest, test_load_from_storage) {
+ const RawAddress test_address = GetTestAddress(1);
+ SetSampleDatabaseHasPresetsNtf(test_address, kFeatureBitDynamicPresets, {{}});
+ SetEncryptionResult(test_address, true);
+
+ std::set<HasPreset, HasPreset::ComparatorDesc> has_presets = {{
+ HasPreset(5, HasPreset::kPropertyAvailable | HasPreset::kPropertyWritable,
+ "YourWritablePreset5"),
+ HasPreset(55, HasPreset::kPropertyAvailable, "YourPreset55"),
+ }};
+
+ /* Load persistent storage data */
+ ON_CALL(btif_storage_interface_, GetLeaudioHasPresets(test_address, _, _))
+ .WillByDefault([&has_presets](const RawAddress& address,
+ std::vector<uint8_t>& presets_bin,
+ uint8_t& active_preset) {
+ /* Generate presets binary to be used instead the attribute values */
+ HasDevice device(address, 0);
+ device.has_presets = has_presets;
+ active_preset = 55;
+
+ if (device.SerializePresets(presets_bin)) return true;
+
+ return false;
+ });
+
+ EXPECT_CALL(gatt_interface, RegisterForNotifications(gatt_if, _, _))
+ .Times(1 // preset control point
+ + 1 // active preset
+ + 1); // features
+
+ EXPECT_CALL(*callbacks,
+ OnDeviceAvailable(test_address,
+ (kFeatureBitWritablePresets |
+ kFeatureBitPresetSynchronizationSupported |
+ kFeatureBitHearingAidTypeBanded)));
+
+ std::vector<PresetInfo> loaded_preset_details;
+ EXPECT_CALL(*callbacks,
+ OnPresetInfo(std::variant<RawAddress, int>(test_address),
+ PresetInfoReason::ALL_PRESET_INFO, _))
+ .WillOnce(SaveArg<2>(&loaded_preset_details));
+
+ EXPECT_CALL(*callbacks, OnActivePresetSelected(
+ std::variant<RawAddress, int>(test_address), 55));
+
+ EXPECT_CALL(*callbacks,
+ OnConnectionState(ConnectionState::CONNECTED, test_address));
+
+ /* Expect no read or write operations when loading from storage */
+ EXPECT_CALL(gatt_queue, ReadCharacteristic(1, _, _, _)).Times(0);
+ EXPECT_CALL(gatt_queue, WriteDescriptor(1, _, _, _, _, _)).Times(0);
+
+ TestAddFromStorage(test_address,
+ kFeatureBitWritablePresets |
+ kFeatureBitPresetSynchronizationSupported |
+ kFeatureBitHearingAidTypeBanded,
+ true);
+
+ for (auto const& info : loaded_preset_details) {
+ auto preset = has_presets.find(info.preset_index);
+ ASSERT_NE(preset, has_presets.end());
+ if (preset->GetProperties() & HasPreset::kPropertyAvailable)
+ ASSERT_TRUE(info.available);
+ if (preset->GetProperties() & HasPreset::kPropertyWritable)
+ ASSERT_TRUE(info.writable);
+ ASSERT_EQ(preset->GetName(), info.preset_name);
+ }
+}
+
+TEST_F(HasClientTest, test_write_to_storage) {
+ const RawAddress test_address = GetTestAddress(1);
+
+ std::set<HasPreset, HasPreset::ComparatorDesc> has_presets = {{
+ HasPreset(1, HasPreset::kPropertyAvailable, "Universal"),
+ HasPreset(2, HasPreset::kPropertyAvailable | HasPreset::kPropertyWritable,
+ "Preset2"),
+ }};
+ SetSampleDatabaseHasPresetsNtf(
+ test_address,
+ bluetooth::has::kFeatureBitHearingAidTypeBanded |
+ bluetooth::has::kFeatureBitWritablePresets |
+ bluetooth::has::kFeatureBitDynamicPresets,
+ has_presets);
+
+ std::vector<uint8_t> serialized;
+ EXPECT_CALL(
+ btif_storage_interface_,
+ AddLeaudioHasDevice(test_address, _,
+ bluetooth::has::kFeatureBitHearingAidTypeBanded |
+ bluetooth::has::kFeatureBitWritablePresets |
+ bluetooth::has::kFeatureBitDynamicPresets,
+ 1))
+ .WillOnce(SaveArg<1>(&serialized));
+ TestConnect(test_address);
+
+ /* Deserialize the written binary to verify the content */
+ HasDevice clone(test_address,
+ bluetooth::has::kFeatureBitHearingAidTypeBanded |
+ bluetooth::has::kFeatureBitWritablePresets |
+ bluetooth::has::kFeatureBitDynamicPresets);
+ ASSERT_TRUE(HasDevice::DeserializePresets(serialized.data(),
+ serialized.size(), clone));
+ auto storage_info = clone.GetAllPresetInfo();
+ ASSERT_EQ(storage_info.size(), has_presets.size());
+ for (auto const& info : storage_info) {
+ auto preset = has_presets.find(info.preset_index);
+ ASSERT_NE(preset, has_presets.end());
+ if (preset->GetProperties() & HasPreset::kPropertyAvailable)
+ ASSERT_TRUE(info.available);
+ if (preset->GetProperties() & HasPreset::kPropertyWritable)
+ ASSERT_TRUE(info.writable);
+ ASSERT_EQ(preset->GetName(), info.preset_name);
+ }
+}
+
+TEST_F(HasClientTest, test_discovery_basic_has_no_opt_ntf) {
+ const RawAddress test_address = GetTestAddress(1);
+ auto test_conn_id = GetTestConnId(test_address);
+
+ SetSampleDatabaseHasNoOptionalNtf(test_address);
+
+ std::variant<RawAddress, int> addr_or_group = test_address;
+ std::vector<PresetInfo> preset_details;
+ uint8_t active_preset_index;
+ uint8_t has_features;
+
+ EXPECT_CALL(*callbacks, OnDeviceAvailable(test_address, _))
+ .WillOnce(SaveArg<1>(&has_features));
+ EXPECT_CALL(*callbacks,
+ OnConnectionState(ConnectionState::CONNECTED, test_address));
+ EXPECT_CALL(*callbacks, OnPresetInfo(_, PresetInfoReason::ALL_PRESET_INFO, _))
+ .WillOnce(DoAll(SaveArg<0>(&addr_or_group), SaveArg<2>(&preset_details)));
+ EXPECT_CALL(*callbacks, OnActivePresetSelected(_, _))
+ .WillOnce(
+ DoAll(SaveArg<0>(&addr_or_group), SaveArg<1>(&active_preset_index)));
+ TestConnect(test_address);
+
+ /* Verify sample database content */
+ ASSERT_TRUE(std::holds_alternative<RawAddress>(addr_or_group));
+ ASSERT_EQ(std::get<RawAddress>(addr_or_group), test_address);
+ ASSERT_EQ(has_features, 0x00);
+ ASSERT_EQ(active_preset_index,
+ current_peer_presets_.at(test_conn_id).begin()->GetIndex());
+
+ /* Verify presets */
+ uint16_t conn_id = GetTestConnId(test_address);
+ ASSERT_NE(preset_details.size(), 0u);
+ ASSERT_EQ(current_peer_presets_.at(conn_id).size(), preset_details.size());
+
+ for (auto const& preset : current_peer_presets_.at(conn_id)) {
+ auto it =
+ std::find_if(preset_details.cbegin(), preset_details.cend(),
+ [&preset](auto const& preset_info) {
+ return preset_info.preset_index == preset.GetIndex();
+ });
+ ASSERT_NE(it, preset_details.cend());
+ ASSERT_EQ(preset.GetName(), it->preset_name);
+ ASSERT_EQ(preset.IsAvailable(), it->available);
+ ASSERT_EQ(preset.IsWritable(), it->writable);
+ }
+
+ /* Verify active preset is there */
+ ASSERT_EQ(preset_details.size(),
+ current_peer_presets_.at(test_conn_id).size());
+ ASSERT_TRUE(std::find_if(preset_details.begin(), preset_details.end(),
+ [active_preset_index](auto const& preset_info) {
+ return preset_info.preset_index ==
+ active_preset_index;
+ }) != preset_details.end());
+}
+
+TEST_F(HasClientTest, test_discovery_has_not_found) {
+ const RawAddress test_address = GetTestAddress(1);
+ SetSampleDatabaseNoHas(test_address);
+
+ EXPECT_CALL(*callbacks,
+ OnConnectionState(ConnectionState::CONNECTED, test_address))
+ .Times(0);
+ EXPECT_CALL(*callbacks, OnDeviceAvailable(test_address, _)).Times(0);
+ EXPECT_CALL(*callbacks, OnFeaturesUpdate(test_address, _)).Times(0);
+ EXPECT_CALL(*callbacks,
+ OnConnectionState(ConnectionState::DISCONNECTED, test_address));
+
+ TestConnect(test_address);
+}
+
+TEST_F(HasClientTest, test_discovery_has_broken_no_active_preset) {
+ const RawAddress test_address = GetTestAddress(1);
+ SetSampleDatabaseHasBrokenNoActivePreset(test_address);
+
+ EXPECT_CALL(*callbacks,
+ OnConnectionState(ConnectionState::CONNECTED, test_address))
+ .Times(0);
+ EXPECT_CALL(*callbacks, OnDeviceAvailable(test_address, _)).Times(0);
+ EXPECT_CALL(*callbacks, OnFeaturesUpdate(test_address, _)).Times(0);
+ EXPECT_CALL(*callbacks,
+ OnConnectionState(ConnectionState::DISCONNECTED, test_address));
+
+ TestConnect(test_address);
+}
+
+TEST_F(HasClientTest, test_discovery_has_broken_no_active_preset_ntf) {
+ const RawAddress test_address = GetTestAddress(1);
+ SetSampleDatabaseHasBrokenNoActivePresetNtf(test_address);
+
+ EXPECT_CALL(*callbacks,
+ OnConnectionState(ConnectionState::CONNECTED, test_address))
+ .Times(0);
+ EXPECT_CALL(*callbacks, OnDeviceAvailable(test_address, _)).Times(0);
+ EXPECT_CALL(*callbacks, OnFeaturesUpdate(test_address, _)).Times(0);
+ EXPECT_CALL(*callbacks,
+ OnConnectionState(ConnectionState::DISCONNECTED, test_address));
+
+ TestConnect(test_address);
+}
+
+TEST_F(HasClientTest, test_discovery_has_features_ntf) {
+ const RawAddress test_address = GetTestAddress(1);
+ auto test_conn_id = GetTestConnId(test_address);
+ uint8_t has_features;
+
+ SetSampleDatabaseHasOnlyFeaturesNtf(
+ test_address, bluetooth::has::kFeatureBitHearingAidTypeBanded);
+
+ EXPECT_CALL(*callbacks, OnDeviceAvailable(test_address, _))
+ .WillOnce(SaveArg<1>(&has_features));
+ EXPECT_CALL(*callbacks,
+ OnConnectionState(ConnectionState::CONNECTED, test_address))
+ .Times(1);
+
+ /* Verify subscription to features */
+ EXPECT_CALL(gatt_interface, RegisterForNotifications(gatt_if, _, _))
+ .Times(AnyNumber());
+ EXPECT_CALL(gatt_interface,
+ RegisterForNotifications(gatt_if, test_address,
+ HasDbBuilder::kFeaturesValHdl));
+
+ /* Verify features CCC was written */
+ EXPECT_CALL(gatt_queue, WriteDescriptor(test_conn_id, _, _, _, _, _))
+ .Times(AnyNumber());
+ EXPECT_CALL(gatt_queue,
+ WriteDescriptor(test_conn_id, HasDbBuilder::kFeaturesValHdl + 1,
+ std::vector<uint8_t>{0x01, 0x00}, _, _, _));
+ TestConnect(test_address);
+
+ /* Verify features */
+ ASSERT_EQ(has_features, bluetooth::has::kFeatureBitHearingAidTypeBanded);
+
+ uint8_t new_features;
+
+ /* Verify peer features change notification */
+ EXPECT_CALL(*callbacks, OnFeaturesUpdate(test_address, _))
+ .WillOnce(SaveArg<1>(&new_features));
+ InjectNotificationEvent(test_address, test_conn_id,
+ HasDbBuilder::kFeaturesValHdl,
+ std::vector<uint8_t>({0x00}));
+ ASSERT_NE(has_features, new_features);
+}
+
+TEST_F(HasClientTest, test_discovery_has_features_no_ntf) {
+ const RawAddress test_address = GetTestAddress(1);
+ auto test_conn_id = GetTestConnId(test_address);
+ uint8_t has_features;
+
+ SetSampleDatabaseHasOnlyFeaturesNoNtf(
+ test_address, bluetooth::has::kFeatureBitHearingAidTypeBanded);
+
+ EXPECT_CALL(*callbacks, OnDeviceAvailable(test_address, _))
+ .WillOnce(SaveArg<1>(&has_features));
+ EXPECT_CALL(*callbacks,
+ OnConnectionState(ConnectionState::CONNECTED, test_address))
+ .Times(1);
+
+ /* Verify no subscription to features */
+ EXPECT_CALL(gatt_interface, RegisterForNotifications(gatt_if, _, _))
+ .Times(AnyNumber());
+ EXPECT_CALL(gatt_interface,
+ RegisterForNotifications(gatt_if, test_address,
+ HasDbBuilder::kFeaturesValHdl))
+ .Times(0);
+
+ /* Verify no features CCC was written */
+ EXPECT_CALL(gatt_queue, WriteDescriptor(test_conn_id, _, _, _, _, _))
+ .Times(AnyNumber());
+ EXPECT_CALL(gatt_queue,
+ WriteDescriptor(test_conn_id, HasDbBuilder::kFeaturesValHdl + 1,
+ _, _, _, _))
+ .Times(0);
+ TestConnect(test_address);
+
+ /* Verify features */
+ ASSERT_EQ(has_features, bluetooth::has::kFeatureBitHearingAidTypeBanded);
+}
+
+TEST_F(HasClientTest, test_discovery_has_multiple_presets_ntf) {
+ const RawAddress test_address = GetTestAddress(1);
+ SetSampleDatabaseHasPresetsNtf(
+ test_address, bluetooth::has::kFeatureBitHearingAidTypeBanded);
+
+ std::variant<RawAddress, int> addr_or_group = test_address;
+ std::vector<PresetInfo> preset_details;
+ uint8_t active_preset_index;
+ uint8_t has_features;
+
+ EXPECT_CALL(*callbacks, OnDeviceAvailable(test_address, _))
+ .WillOnce(SaveArg<1>(&has_features));
+ EXPECT_CALL(*callbacks,
+ OnConnectionState(ConnectionState::CONNECTED, test_address));
+ EXPECT_CALL(*callbacks, OnPresetInfo(_, PresetInfoReason::ALL_PRESET_INFO, _))
+ .WillOnce(DoAll(SaveArg<0>(&addr_or_group), SaveArg<2>(&preset_details)));
+ EXPECT_CALL(*callbacks, OnActivePresetSelected(_, _))
+ .WillOnce(
+ DoAll(SaveArg<0>(&addr_or_group), SaveArg<1>(&active_preset_index)));
+
+ /* Verify subscription to control point */
+ EXPECT_CALL(gatt_interface, RegisterForNotifications(gatt_if, _, _))
+ .Times(AnyNumber());
+ EXPECT_CALL(gatt_interface,
+ RegisterForNotifications(gatt_if, test_address,
+ HasDbBuilder::kPresetsCtpValHdl));
+
+ /* Verify features CCC was written */
+ EXPECT_CALL(gatt_queue, WriteDescriptor(1, _, _, _, _, _)).Times(AnyNumber());
+ EXPECT_CALL(gatt_queue,
+ WriteDescriptor(1, HasDbBuilder::kPresetsCtpValHdl + 1,
+ std::vector<uint8_t>{0x03, 0x00}, _, _, _));
+ TestConnect(test_address);
+
+ /* Verify features */
+ ASSERT_EQ(has_features, bluetooth::has::kFeatureBitHearingAidTypeBanded);
+}
+
+TEST_F(HasClientTest, test_active_preset_change) {
+ const RawAddress test_address = GetTestAddress(1);
+ auto test_conn_id = GetTestConnId(test_address);
+
+ SetSampleDatabaseHasNoOptionalNtf(test_address);
+
+ uint8_t active_preset_index;
+ EXPECT_CALL(*callbacks, OnDeviceAvailable(test_address, _));
+ EXPECT_CALL(*callbacks,
+ OnPresetInfo(_, PresetInfoReason::ALL_PRESET_INFO, _));
+ EXPECT_CALL(*callbacks, OnActivePresetSelected(_, _))
+ .WillOnce(SaveArg<1>(&active_preset_index));
+ EXPECT_CALL(*callbacks,
+ OnConnectionState(ConnectionState::CONNECTED, test_address));
+ TestConnect(test_address);
+
+ uint8_t new_active_preset;
+ EXPECT_CALL(*callbacks, OnActivePresetSelected(
+ std::variant<RawAddress, int>(test_address), _))
+ .WillOnce(SaveArg<1>(&new_active_preset));
+ InjectNotificationEvent(test_address, test_conn_id,
+ HasDbBuilder::kActivePresetIndexValHdl,
+ std::vector<uint8_t>({0x00}));
+
+ ASSERT_NE(active_preset_index, new_active_preset);
+ ASSERT_EQ(new_active_preset, 0x00);
+}
+
+TEST_F(HasClientTest, test_duplicate_presets) {
+ const RawAddress test_address = GetTestAddress(1);
+ std::vector<PresetInfo> preset_details;
+
+ /* Handle duplicates gracefully */
+ SetSampleDatabaseHasPresetsNtf(
+ test_address, kFeatureBitWritablePresets,
+ {{HasPreset(5,
+ HasPreset::kPropertyAvailable | HasPreset::kPropertyWritable,
+ "YourWritablePreset5"),
+ HasPreset(5,
+ HasPreset::kPropertyAvailable | HasPreset::kPropertyWritable,
+ "YourWritablePreset5")}});
+
+ EXPECT_CALL(*callbacks,
+ OnConnectionState(ConnectionState::CONNECTED, test_address));
+ EXPECT_CALL(*callbacks, OnPresetInfo(_, PresetInfoReason::ALL_PRESET_INFO, _))
+ .WillOnce(SaveArg<2>(&preset_details));
+ TestConnect(test_address);
+
+ /* Verify presets - expect 1, no duplicates */
+ ASSERT_EQ(preset_details.size(), 1u);
+ auto preset = std::find_if(
+ preset_details.begin(), preset_details.end(),
+ [](auto const& preset_info) { return preset_info.preset_index == 5; });
+ ASSERT_TRUE(preset != preset_details.end());
+ ASSERT_EQ("YourWritablePreset5", preset->preset_name);
+ ASSERT_TRUE(preset->available);
+ ASSERT_TRUE(preset->writable);
+}
+
+TEST_F(HasClientTest, test_preset_set_name_invalid_index) {
+ const RawAddress test_address = GetTestAddress(1);
+ SetSampleDatabaseHasPresetsNtf(test_address);
+ TestConnect(test_address);
+
+ EXPECT_CALL(*callbacks,
+ OnSetPresetNameError(std::variant<RawAddress, int>(test_address),
+ 0x40, ErrorCode::INVALID_PRESET_INDEX))
+ .Times(1);
+ EXPECT_CALL(gatt_queue,
+ WriteCharacteristic(1, HasDbBuilder::kPresetsCtpValHdl, _,
+ GATT_WRITE, _, _))
+ .Times(0);
+
+ HasClient::Get()->SetPresetName(test_address, 0x40, "new preset name");
+}
+
+TEST_F(HasClientTest, test_preset_set_name_non_writable) {
+ const RawAddress test_address = GetTestAddress(1);
+ uint16_t test_conn_id = GetTestConnId(test_address);
+
+ SetSampleDatabaseHasPresetsNtf(
+ test_address, kFeatureBitWritablePresets,
+ {{
+ HasPreset(5, HasPreset::kPropertyAvailable, "YourPreset5"),
+ HasPreset(
+ 55, HasPreset::kPropertyAvailable | HasPreset::kPropertyWritable,
+ "YourWritablePreset55"),
+ }});
+ TestConnect(test_address);
+
+ EXPECT_CALL(*callbacks,
+ OnSetPresetNameError(_, _, ErrorCode::SET_NAME_NOT_ALLOWED))
+ .Times(1);
+ EXPECT_CALL(gatt_queue,
+ WriteCharacteristic(1, HasDbBuilder::kPresetsCtpValHdl, _,
+ GATT_WRITE, _, _))
+ .Times(0);
+
+ HasClient::Get()->SetPresetName(
+ test_address, current_peer_presets_.at(test_conn_id).begin()->GetIndex(),
+ "new preset name");
+}
+
+TEST_F(HasClientTest, test_preset_set_name_to_long) {
+ const RawAddress test_address = GetTestAddress(1);
+ uint16_t test_conn_id = GetTestConnId(test_address);
+
+ SetSampleDatabaseHasPresetsNtf(
+ test_address, kFeatureBitWritablePresets,
+ {{HasPreset(5,
+ HasPreset::kPropertyAvailable | HasPreset::kPropertyWritable,
+ "YourWritablePreset")}});
+ TestConnect(test_address);
+
+ EXPECT_CALL(*callbacks,
+ OnSetPresetNameError(_, _, ErrorCode::INVALID_PRESET_NAME_LENGTH))
+ .Times(1);
+ EXPECT_CALL(gatt_queue,
+ WriteCharacteristic(test_conn_id, HasDbBuilder::kPresetsCtpValHdl,
+ _, GATT_WRITE, _, _))
+ .Times(0);
+
+ HasClient::Get()->SetPresetName(test_address, 5,
+ "this name is more than 40 characters long");
+}
+
+TEST_F(HasClientTest, test_preset_set_name) {
+ const RawAddress test_address = GetTestAddress(1);
+ uint16_t test_conn_id = GetTestConnId(test_address);
+
+ SetSampleDatabaseHasPresetsNtf(
+ test_address, kFeatureBitWritablePresets,
+ {{HasPreset(5,
+ HasPreset::kPropertyAvailable | HasPreset::kPropertyWritable,
+ "YourWritablePreset5")}});
+
+ TestConnect(test_address);
+
+ std::vector<uint8_t> value;
+ EXPECT_CALL(*callbacks, OnSetPresetNameError(_, _, _)).Times(0);
+ EXPECT_CALL(gatt_queue,
+ WriteCharacteristic(test_conn_id, HasDbBuilder::kPresetsCtpValHdl,
+ _, GATT_WRITE, _, _));
+
+ std::vector<PresetInfo> updated_preset_details;
+ EXPECT_CALL(*callbacks,
+ OnPresetInfo(std::variant<RawAddress, int>(test_address),
+ PresetInfoReason::PRESET_INFO_UPDATE, _))
+ .WillOnce(SaveArg<2>(&updated_preset_details));
+ HasClient::Get()->SetPresetName(test_address, 5, "new preset name");
+
+ ASSERT_EQ(1u, updated_preset_details.size());
+ ASSERT_EQ(updated_preset_details[0].preset_name, "new preset name");
+}
+
+TEST_F(HasClientTest, test_preset_group_set_name) {
+ /* None of these devices support preset syncing */
+ const RawAddress test_address1 = GetTestAddress(1);
+ SetSampleDatabaseHasPresetsNtf(
+ test_address1, bluetooth::has::kFeatureBitHearingAidTypeBinaural |
+ bluetooth::has::kFeatureBitWritablePresets);
+
+ const RawAddress test_address2 = GetTestAddress(2);
+ SetSampleDatabaseHasPresetsNtf(
+ test_address2, bluetooth::has::kFeatureBitHearingAidTypeBinaural |
+ bluetooth::has::kFeatureBitWritablePresets);
+
+ TestConnect(test_address1);
+ TestConnect(test_address2);
+
+ /* Mock the csis group with two devices */
+ uint8_t not_synced_group = 13;
+ ON_CALL(mock_csis_client_module_, GetDeviceList(not_synced_group))
+ .WillByDefault(
+ Return(std::vector<RawAddress>({{test_address1, test_address2}})));
+ ON_CALL(mock_csis_client_module_,
+ GetGroupId(test_address1, ::le_audio::uuid::kCapServiceUuid))
+ .WillByDefault(Return(not_synced_group));
+ ON_CALL(mock_csis_client_module_,
+ GetGroupId(test_address2, ::le_audio::uuid::kCapServiceUuid))
+ .WillByDefault(Return(not_synced_group));
+
+ std::vector<PresetInfo> preset_details;
+ EXPECT_CALL(*callbacks, OnActivePresetSelected(
+ std::variant<RawAddress, int>(test_address1), 55))
+ .Times(0);
+ EXPECT_CALL(*callbacks, OnActivePresetSelected(
+ std::variant<RawAddress, int>(test_address2), 55))
+ .Times(0);
+
+ /* This should be a group callback */
+ EXPECT_CALL(*callbacks,
+ OnPresetInfo(std::variant<RawAddress, int>(not_synced_group),
+ PresetInfoReason::PRESET_INFO_UPDATE, _))
+ .Times(1)
+ .WillOnce(SaveArg<2>(&preset_details));
+
+ /* No locally synced opcodes support so expect both devices getting writes */
+ EXPECT_CALL(gatt_queue, WriteCharacteristic(GetTestConnId(test_address1),
+ HasDbBuilder::kPresetsCtpValHdl,
+ _, GATT_WRITE, _, _))
+ .Times(1);
+ EXPECT_CALL(gatt_queue, WriteCharacteristic(GetTestConnId(test_address2),
+ HasDbBuilder::kPresetsCtpValHdl,
+ _, GATT_WRITE, _, _))
+ .Times(1);
+
+ HasClient::Get()->SetPresetName(not_synced_group, 55, "new preset name");
+ ASSERT_EQ(preset_details.size(), 1u);
+ ASSERT_EQ(preset_details[0].preset_name, "new preset name");
+ ASSERT_EQ(preset_details[0].preset_index, 55);
+}
+
+TEST_F(HasClientTest, test_multiple_presets_get_name) {
+ const RawAddress test_address = GetTestAddress(1);
+ SetSampleDatabaseHasPresetsNtf(
+ test_address, kFeatureBitWritablePresets,
+ {{
+ HasPreset(
+ 5, HasPreset::kPropertyAvailable | HasPreset::kPropertyWritable,
+ "YourWritablePreset5"),
+ HasPreset(55, HasPreset::kPropertyAvailable, "YourPreset55"),
+ HasPreset(99, 0, "YourPreset99"),
+ }});
+
+ std::vector<PresetInfo> preset_details;
+
+ EXPECT_CALL(*callbacks, OnDeviceAvailable(test_address, _));
+ EXPECT_CALL(*callbacks,
+ OnConnectionState(ConnectionState::CONNECTED, test_address));
+ EXPECT_CALL(*callbacks, OnPresetInfo(_, PresetInfoReason::ALL_PRESET_INFO, _))
+ .WillOnce(SaveArg<2>(&preset_details));
+ TestConnect(test_address);
+
+ /* Get each preset info individually */
+ for (auto const& preset : preset_details) {
+ std::vector<PresetInfo> new_preset_details;
+
+ EXPECT_CALL(*callbacks,
+ OnPresetInfo(std::variant<RawAddress, int>(test_address),
+ PresetInfoReason::PRESET_INFO_REQUEST_RESPONSE, _))
+ .Times(1)
+ .WillOnce(SaveArg<2>(&new_preset_details));
+ HasClient::Get()->GetPresetInfo(test_address, preset.preset_index);
+
+ Mock::VerifyAndClearExpectations(&*callbacks);
+ ASSERT_EQ(1u, new_preset_details.size());
+ ASSERT_EQ(preset.preset_index, new_preset_details[0].preset_index);
+ ASSERT_EQ(preset.preset_name, new_preset_details[0].preset_name);
+ ASSERT_EQ(preset.writable, new_preset_details[0].writable);
+ ASSERT_EQ(preset.available, new_preset_details[0].available);
+ }
+}
+
+TEST_F(HasClientTest, test_presets_get_name_invalid_index) {
+ const RawAddress test_address = GetTestAddress(1);
+ SetSampleDatabaseHasPresetsNtf(test_address);
+ TestConnect(test_address);
+
+ EXPECT_CALL(*callbacks,
+ OnPresetInfoError(std::variant<RawAddress, int>(test_address),
+ 128, ErrorCode::INVALID_PRESET_INDEX));
+ HasClient::Get()->GetPresetInfo(test_address, 128);
+
+ EXPECT_CALL(*callbacks,
+ OnPresetInfoError(std::variant<RawAddress, int>(test_address), 0,
+ ErrorCode::INVALID_PRESET_INDEX));
+ HasClient::Get()->GetPresetInfo(test_address, 0);
+}
+
+TEST_F(HasClientTest, test_presets_changed_generic_update_no_add_or_delete) {
+ const RawAddress test_address = GetTestAddress(1);
+ uint16_t test_conn_id = GetTestConnId(test_address);
+
+ std::set<HasPreset, HasPreset::ComparatorDesc> presets = {{
+ HasPreset(1, HasPreset::kPropertyAvailable, "Universal"),
+ HasPreset(2, HasPreset::kPropertyAvailable | HasPreset::kPropertyWritable,
+ "Preset2"),
+ HasPreset(4, HasPreset::kPropertyAvailable, "Preset4"),
+ HasPreset(7, HasPreset::kPropertyAvailable, "Preset7"),
+ }};
+ SetSampleDatabaseHasPresetsNtf(
+ test_address,
+ bluetooth::has::kFeatureBitHearingAidTypeBanded |
+ bluetooth::has::kFeatureBitDynamicPresets |
+ bluetooth::has::kFeatureBitWritablePresets,
+ presets);
+
+ EXPECT_CALL(*callbacks,
+ OnConnectionState(ConnectionState::CONNECTED, test_address));
+ TestConnect(test_address);
+
+ std::vector<PresetInfo> preset_details;
+ EXPECT_CALL(*callbacks,
+ OnPresetInfo(std::variant<RawAddress, int>(test_address),
+ PresetInfoReason::PRESET_INFO_UPDATE, _))
+ .Times(1)
+ .WillOnce(SaveArg<2>(&preset_details));
+
+ /* Inject generic update on the first preset */
+ auto preset_index = 2;
+ auto new_test_preset = HasPreset(preset_index, 0, "props new name");
+ ASSERT_NE(*current_peer_presets_.at(test_conn_id).find(preset_index),
+ new_test_preset);
+
+ InjectPresetChanged(test_conn_id, test_address, false, new_test_preset,
+ 1 /* prev_index */,
+ ::le_audio::has::PresetCtpChangeId::PRESET_GENERIC_UPDATE,
+ true /* is_last */);
+
+ /* Verify received preset info update on the 2nd preset */
+ ASSERT_EQ(1u, preset_details.size());
+ ASSERT_EQ(new_test_preset.GetIndex(), preset_details[0].preset_index);
+ ASSERT_EQ(new_test_preset.IsAvailable(), preset_details[0].available);
+ ASSERT_EQ(new_test_preset.IsWritable(), preset_details[0].writable);
+ ASSERT_EQ(new_test_preset.GetName(), preset_details[0].preset_name);
+}
+
+TEST_F(HasClientTest, test_presets_changed_generic_update_add_and_delete) {
+ const RawAddress test_address = GetTestAddress(1);
+ uint16_t test_conn_id = GetTestConnId(test_address);
+
+ std::set<HasPreset, HasPreset::ComparatorDesc> presets = {{
+ HasPreset(1, HasPreset::kPropertyAvailable, "Universal"),
+ HasPreset(2, HasPreset::kPropertyAvailable | HasPreset::kPropertyWritable,
+ "Preset2"),
+ HasPreset(4, HasPreset::kPropertyAvailable, "Preset4"),
+ HasPreset(5, HasPreset::kPropertyAvailable, "Preset5"),
+ HasPreset(32, HasPreset::kPropertyAvailable, "Preset32"),
+ }};
+ SetSampleDatabaseHasPresetsNtf(
+ test_address,
+ bluetooth::has::kFeatureBitHearingAidTypeBanded |
+ bluetooth::has::kFeatureBitWritablePresets,
+ presets);
+
+ std::vector<PresetInfo> preset_details;
+ EXPECT_CALL(*callbacks,
+ OnConnectionState(ConnectionState::CONNECTED, test_address));
+ EXPECT_CALL(*callbacks,
+ OnPresetInfo(std::variant<RawAddress, int>(test_address),
+ PresetInfoReason::ALL_PRESET_INFO, _))
+ .Times(1)
+ .WillOnce(SaveArg<2>(&preset_details));
+ TestConnect(test_address);
+
+ /* Expect more OnPresetInfo call */
+ std::vector<PresetInfo> updated_preset_details;
+ EXPECT_CALL(*callbacks,
+ OnPresetInfo(std::variant<RawAddress, int>(test_address),
+ PresetInfoReason::PRESET_INFO_UPDATE, _))
+ .Times(1)
+ .WillOnce(SaveArg<2>(&updated_preset_details));
+
+ /* Expect more OnPresetInfo call */
+ std::vector<PresetInfo> deleted_preset_details;
+ EXPECT_CALL(*callbacks,
+ OnPresetInfo(std::variant<RawAddress, int>(test_address),
+ PresetInfoReason::PRESET_DELETED, _))
+ .Times(1)
+ .WillOnce(SaveArg<2>(&deleted_preset_details));
+
+ /* Inject generic updates */
+ /* First event replaces all the existing presets from 1 to 8 with preset 8
+ */
+ auto new_test_preset1 =
+ HasPreset(8, HasPreset::kPropertyAvailable, "props new name9");
+ InjectPresetChanged(test_conn_id, test_address, false, new_test_preset1,
+ 1 /* prev_index */,
+ ::le_audio::has::PresetCtpChangeId::PRESET_GENERIC_UPDATE,
+ false /* is_last */);
+
+ /* Second event adds preset 9 to the already existing presets 1 and 8 */
+ auto new_test_preset2 =
+ HasPreset(9, HasPreset::kPropertyAvailable | HasPreset::kPropertyWritable,
+ "props new name11");
+ InjectPresetChanged(test_conn_id, test_address, false, new_test_preset2,
+ 8 /* prev_index */,
+ ::le_audio::has::PresetCtpChangeId::PRESET_GENERIC_UPDATE,
+ true /* is_last */);
+
+ /* Verify received preset info - expect presets 1, 32 unchanged, 8, 9
+ * updated, and 2, 4, 5 deleted.
+ */
+ ASSERT_EQ(2u, updated_preset_details.size());
+ ASSERT_EQ(new_test_preset1.GetIndex(),
+ updated_preset_details[0].preset_index);
+ ASSERT_EQ(new_test_preset1.IsAvailable(),
+ updated_preset_details[0].available);
+ ASSERT_EQ(new_test_preset1.IsWritable(), updated_preset_details[0].writable);
+ ASSERT_EQ(new_test_preset1.GetName(), updated_preset_details[0].preset_name);
+ ASSERT_EQ(new_test_preset2.GetIndex(),
+ updated_preset_details[1].preset_index);
+ ASSERT_EQ(new_test_preset2.IsAvailable(),
+ updated_preset_details[1].available);
+ ASSERT_EQ(new_test_preset2.IsWritable(), updated_preset_details[1].writable);
+ ASSERT_EQ(new_test_preset2.GetName(), updated_preset_details[1].preset_name);
+
+ ASSERT_EQ(3u, deleted_preset_details.size());
+ ASSERT_EQ(2, deleted_preset_details[0].preset_index);
+ ASSERT_EQ(4, deleted_preset_details[1].preset_index);
+ ASSERT_EQ(5, deleted_preset_details[2].preset_index);
+}
+
+TEST_F(HasClientTest, test_presets_changed_deleted) {
+ const RawAddress test_address = GetTestAddress(1);
+ uint16_t test_conn_id = GetTestConnId(test_address);
+
+ std::set<HasPreset, HasPreset::ComparatorDesc> presets = {{
+ HasPreset(1, HasPreset::kPropertyAvailable, "Universal"),
+ HasPreset(2, HasPreset::kPropertyAvailable | HasPreset::kPropertyWritable,
+ "Preset2"),
+ }};
+ SetSampleDatabaseHasPresetsNtf(
+ test_address,
+ bluetooth::has::kFeatureBitHearingAidTypeBanded |
+ bluetooth::has::kFeatureBitWritablePresets |
+ bluetooth::has::kFeatureBitDynamicPresets,
+ presets);
+
+ std::vector<PresetInfo> preset_details;
+ EXPECT_CALL(*callbacks,
+ OnConnectionState(ConnectionState::CONNECTED, test_address));
+ EXPECT_CALL(*callbacks,
+ OnPresetInfo(std::variant<RawAddress, int>(test_address),
+ PresetInfoReason::ALL_PRESET_INFO, _))
+ .Times(1)
+ .WillOnce(SaveArg<2>(&preset_details));
+ TestConnect(test_address);
+
+ /* Expect second OnPresetInfo call */
+ std::vector<PresetInfo> deleted_preset_details;
+ EXPECT_CALL(*callbacks,
+ OnPresetInfo(std::variant<RawAddress, int>(test_address),
+ PresetInfoReason::PRESET_DELETED, _))
+ .Times(1)
+ .WillOnce(SaveArg<2>(&deleted_preset_details));
+
+ /* Inject preset deletion of index 2 */
+ auto deleted_index = preset_details[1].preset_index;
+ InjectPresetChanged(test_conn_id, test_address, false,
+ *presets.find(deleted_index), 0 /* prev_index */,
+ ::le_audio::has::PresetCtpChangeId::PRESET_DELETED,
+ true /* is_last */);
+
+ ASSERT_EQ(2u, preset_details.size());
+ ASSERT_EQ(1u, deleted_preset_details.size());
+ ASSERT_EQ(preset_details[1].preset_index,
+ deleted_preset_details[0].preset_index);
+ ASSERT_EQ(preset_details[1].writable, deleted_preset_details[0].writable);
+ ASSERT_EQ(preset_details[1].available, deleted_preset_details[0].available);
+ ASSERT_EQ(preset_details[1].preset_name,
+ deleted_preset_details[0].preset_name);
+}
+
+TEST_F(HasClientTest, test_presets_changed_available) {
+ const RawAddress test_address = GetTestAddress(1);
+ uint16_t test_conn_id = GetTestConnId(test_address);
+
+ std::set<HasPreset, HasPreset::ComparatorDesc> presets = {{
+ HasPreset(1, 0, "Universal"),
+ HasPreset(2, HasPreset::kPropertyAvailable | HasPreset::kPropertyWritable,
+ "Preset2"),
+ }};
+ SetSampleDatabaseHasPresetsNtf(
+ test_address,
+ bluetooth::has::kFeatureBitHearingAidTypeBanded |
+ bluetooth::has::kFeatureBitWritablePresets |
+ bluetooth::has::kFeatureBitDynamicPresets,
+ presets);
+
+ std::vector<PresetInfo> preset_details;
+ EXPECT_CALL(*callbacks,
+ OnConnectionState(ConnectionState::CONNECTED, test_address));
+ EXPECT_CALL(*callbacks,
+ OnPresetInfo(std::variant<RawAddress, int>(test_address),
+ PresetInfoReason::ALL_PRESET_INFO, _))
+ .Times(1)
+ .WillOnce(SaveArg<2>(&preset_details));
+ TestConnect(test_address);
+
+ /* Expect second OnPresetInfo call */
+ std::vector<PresetInfo> changed_preset_details;
+ EXPECT_CALL(*callbacks,
+ OnPresetInfo(std::variant<RawAddress, int>(test_address),
+ PresetInfoReason::PRESET_AVAILABILITY_CHANGED, _))
+ .Times(1)
+ .WillOnce(SaveArg<2>(&changed_preset_details));
+
+ /* Inject preset deletion of index 2 */
+ auto changed_index = preset_details[0].preset_index;
+ InjectPresetChanged(test_conn_id, test_address, false,
+ *presets.find(changed_index), 0 /* prev_index */,
+ ::le_audio::has::PresetCtpChangeId::PRESET_AVAILABLE,
+ true /* is_last */);
+
+ ASSERT_EQ(2u, preset_details.size());
+ ASSERT_EQ(1u, changed_preset_details.size());
+ ASSERT_EQ(preset_details[0].preset_index,
+ changed_preset_details[0].preset_index);
+ ASSERT_EQ(preset_details[0].writable, changed_preset_details[0].writable);
+ ASSERT_EQ(preset_details[0].preset_name,
+ changed_preset_details[0].preset_name);
+ /* This field should have changed */
+ ASSERT_NE(preset_details[0].available, changed_preset_details[0].available);
+ ASSERT_TRUE(changed_preset_details[0].available);
+}
+
+TEST_F(HasClientTest, test_presets_changed_unavailable) {
+ const RawAddress test_address = GetTestAddress(1);
+ uint16_t test_conn_id = GetTestConnId(test_address);
+
+ std::set<HasPreset, HasPreset::ComparatorDesc> presets = {{
+ HasPreset(1, HasPreset::kPropertyAvailable, "Universal"),
+ HasPreset(2, HasPreset::kPropertyAvailable | HasPreset::kPropertyWritable,
+ "Preset2"),
+ }};
+ SetSampleDatabaseHasPresetsNtf(
+ test_address,
+ bluetooth::has::kFeatureBitHearingAidTypeBanded |
+ bluetooth::has::kFeatureBitWritablePresets |
+ bluetooth::has::kFeatureBitDynamicPresets,
+ presets);
+
+ std::vector<PresetInfo> preset_details;
+ EXPECT_CALL(*callbacks,
+ OnConnectionState(ConnectionState::CONNECTED, test_address));
+ EXPECT_CALL(*callbacks,
+ OnPresetInfo(std::variant<RawAddress, int>(test_address),
+ PresetInfoReason::ALL_PRESET_INFO, _))
+ .Times(1)
+ .WillOnce(SaveArg<2>(&preset_details));
+ TestConnect(test_address);
+
+ /* Expect second OnPresetInfo call */
+ std::vector<PresetInfo> changed_preset_details;
+ EXPECT_CALL(*callbacks,
+ OnPresetInfo(std::variant<RawAddress, int>(test_address),
+ PresetInfoReason::PRESET_AVAILABILITY_CHANGED, _))
+ .Times(1)
+ .WillOnce(SaveArg<2>(&changed_preset_details));
+
+ /* Inject preset deletion of index 2 */
+ auto changed_index = preset_details[0].preset_index;
+ InjectPresetChanged(test_conn_id, test_address, false,
+ *presets.find(changed_index), 0 /* prev_index */,
+ ::le_audio::has::PresetCtpChangeId::PRESET_UNAVAILABLE,
+ true /* is_last */);
+
+ ASSERT_EQ(2u, preset_details.size());
+ ASSERT_EQ(1u, changed_preset_details.size());
+ ASSERT_EQ(preset_details[0].preset_index,
+ changed_preset_details[0].preset_index);
+ ASSERT_EQ(preset_details[0].writable, changed_preset_details[0].writable);
+ ASSERT_EQ(preset_details[0].preset_name,
+ changed_preset_details[0].preset_name);
+ /* This field should have changed */
+ ASSERT_NE(preset_details[0].available, changed_preset_details[0].available);
+ ASSERT_FALSE(changed_preset_details[0].available);
+}
+
+TEST_F(HasClientTest, test_select_preset_valid) {
+ const RawAddress test_address = GetTestAddress(1);
+ SetSampleDatabaseHasPresetsNtf(test_address);
+
+ uint8_t active_preset_index = 0;
+ std::vector<PresetInfo> preset_details;
+
+ EXPECT_CALL(*callbacks,
+ OnConnectionState(ConnectionState::CONNECTED, test_address));
+ EXPECT_CALL(*callbacks,
+ OnPresetInfo(std::variant<RawAddress, int>(test_address),
+ PresetInfoReason::ALL_PRESET_INFO, _))
+ .Times(1)
+ .WillOnce(SaveArg<2>(&preset_details));
+ EXPECT_CALL(*callbacks, OnActivePresetSelected(_, _))
+ .WillOnce(SaveArg<1>(&active_preset_index));
+ TestConnect(test_address);
+
+ ASSERT_TRUE(preset_details.size() > 1);
+ ASSERT_EQ(preset_details.front().preset_index, active_preset_index);
+
+ uint8_t new_active_preset_index = 0;
+ EXPECT_CALL(*callbacks, OnActivePresetSelected(_, _))
+ .WillOnce(SaveArg<1>(&new_active_preset_index));
+
+ HasClient::Get()->SelectActivePreset(test_address,
+ preset_details.back().preset_index);
+ Mock::VerifyAndClearExpectations(&*callbacks);
+
+ ASSERT_NE(active_preset_index, new_active_preset_index);
+ ASSERT_EQ(preset_details.back().preset_index, new_active_preset_index);
+}
+
+TEST_F(HasClientTest, test_select_group_preset_invalid_group) {
+ const RawAddress test_address1 = GetTestAddress(1);
+ SetSampleDatabaseHasPresetsNtf(test_address1);
+
+ const RawAddress test_address2 = GetTestAddress(2);
+ SetSampleDatabaseHasPresetsNtf(test_address2);
+
+ TestConnect(test_address1);
+ TestConnect(test_address2);
+
+ /* Mock the csis group with no devices */
+ uint8_t unlucky_group = 13;
+ ON_CALL(mock_csis_client_module_, GetDeviceList(unlucky_group))
+ .WillByDefault(Return(std::vector<RawAddress>()));
+
+ EXPECT_CALL(*callbacks, OnActivePresetSelectError(
+ std::variant<RawAddress, int>(unlucky_group),
+ ErrorCode::OPERATION_NOT_POSSIBLE))
+ .Times(1);
+
+ HasClient::Get()->SelectActivePreset(unlucky_group, 6);
+}
+
+TEST_F(HasClientTest, test_select_group_preset_valid_no_preset_sync_supported) {
+ /* None of these devices support preset syncing */
+ const RawAddress test_address1 = GetTestAddress(1);
+ SetSampleDatabaseHasPresetsNtf(
+ test_address1, bluetooth::has::kFeatureBitHearingAidTypeBinaural);
+
+ const RawAddress test_address2 = GetTestAddress(2);
+ SetSampleDatabaseHasPresetsNtf(
+ test_address2, bluetooth::has::kFeatureBitHearingAidTypeBinaural);
+
+ TestConnect(test_address1);
+ TestConnect(test_address2);
+
+ /* Mock the csis group with two devices */
+ uint8_t not_synced_group = 13;
+ ON_CALL(mock_csis_client_module_, GetDeviceList(not_synced_group))
+ .WillByDefault(
+ Return(std::vector<RawAddress>({{test_address1, test_address2}})));
+ ON_CALL(mock_csis_client_module_,
+ GetGroupId(test_address1, ::le_audio::uuid::kCapServiceUuid))
+ .WillByDefault(Return(not_synced_group));
+ ON_CALL(mock_csis_client_module_,
+ GetGroupId(test_address2, ::le_audio::uuid::kCapServiceUuid))
+ .WillByDefault(Return(not_synced_group));
+
+ uint8_t group_active_preset_index = 0;
+ EXPECT_CALL(*callbacks, OnActivePresetSelected(
+ std::variant<RawAddress, int>(test_address1), 55))
+ .Times(0);
+ EXPECT_CALL(*callbacks, OnActivePresetSelected(
+ std::variant<RawAddress, int>(test_address2), 55))
+ .Times(0);
+ EXPECT_CALL(*callbacks,
+ OnActivePresetSelected(
+ std::variant<RawAddress, int>(not_synced_group), _))
+ .WillOnce(SaveArg<1>(&group_active_preset_index));
+
+ /* No locally synced opcodes support so expect both devices getting writes */
+ EXPECT_CALL(gatt_queue, WriteCharacteristic(GetTestConnId(test_address1),
+ HasDbBuilder::kPresetsCtpValHdl,
+ _, GATT_WRITE, _, _))
+ .Times(1);
+ EXPECT_CALL(gatt_queue, WriteCharacteristic(GetTestConnId(test_address2),
+ HasDbBuilder::kPresetsCtpValHdl,
+ _, GATT_WRITE, _, _))
+ .Times(1);
+
+ HasClient::Get()->SelectActivePreset(not_synced_group, 55);
+ ASSERT_EQ(group_active_preset_index, 55);
+}
+
+TEST_F(HasClientTest, test_select_group_preset_valid_preset_sync_supported) {
+ /* Only one of these devices support preset syncing */
+ const RawAddress test_address1 = GetTestAddress(1);
+ uint16_t test_conn_id1 = GetTestConnId(test_address1);
+ SetSampleDatabaseHasPresetsNtf(
+ test_address1, bluetooth::has::kFeatureBitHearingAidTypeBinaural);
+
+ const RawAddress test_address2 = GetTestAddress(2);
+ uint16_t test_conn_id2 = GetTestConnId(test_address2);
+ SetSampleDatabaseHasPresetsNtf(
+ test_address2,
+ bluetooth::has::kFeatureBitHearingAidTypeBinaural |
+ bluetooth::has::kFeatureBitPresetSynchronizationSupported);
+
+ uint8_t active_preset_index1 = 0;
+ uint8_t active_preset_index2 = 0;
+
+ EXPECT_CALL(*callbacks, OnActivePresetSelected(
+ std::variant<RawAddress, int>(test_address1), _))
+ .WillOnce(SaveArg<1>(&active_preset_index1));
+ TestConnect(test_address1);
+
+ EXPECT_CALL(*callbacks, OnActivePresetSelected(
+ std::variant<RawAddress, int>(test_address2), _))
+ .WillOnce(SaveArg<1>(&active_preset_index2));
+ TestConnect(test_address2);
+
+ /* Mock the csis group with two devices */
+ uint8_t synced_group = 13;
+ ON_CALL(mock_csis_client_module_, GetDeviceList(synced_group))
+ .WillByDefault(
+ Return(std::vector<RawAddress>({{test_address1, test_address2}})));
+ ON_CALL(mock_csis_client_module_,
+ GetGroupId(test_address1, ::le_audio::uuid::kCapServiceUuid))
+ .WillByDefault(Return(synced_group));
+ ON_CALL(mock_csis_client_module_,
+ GetGroupId(test_address2, ::le_audio::uuid::kCapServiceUuid))
+ .WillByDefault(Return(synced_group));
+
+ EXPECT_CALL(*callbacks, OnActivePresetSelectError(
+ _, ErrorCode::GROUP_OPERATION_NOT_SUPPORTED))
+ .Times(0);
+
+ /* Expect callback from the group but not from the devices */
+ uint8_t group_active_preset_index = 0;
+ EXPECT_CALL(*callbacks, OnActivePresetSelected(
+ std::variant<RawAddress, int>(test_address1), _))
+ .Times(0);
+ EXPECT_CALL(*callbacks, OnActivePresetSelected(
+ std::variant<RawAddress, int>(test_address2), _))
+ .Times(0);
+ EXPECT_CALL(*callbacks, OnActivePresetSelected(
+ std::variant<RawAddress, int>(synced_group), _))
+ .WillOnce(SaveArg<1>(&group_active_preset_index));
+
+ /* Expect Ctp write on on this device which forwards operation to the other */
+ EXPECT_CALL(gatt_queue, WriteCharacteristic(test_conn_id1,
+ HasDbBuilder::kPresetsCtpValHdl,
+ _, GATT_WRITE, _, _))
+ .Times(0);
+ EXPECT_CALL(gatt_queue, WriteCharacteristic(test_conn_id2,
+ HasDbBuilder::kPresetsCtpValHdl,
+ _, GATT_WRITE, _, _))
+ .Times(1);
+
+ HasClient::Get()->SelectActivePreset(synced_group, 55);
+ ASSERT_EQ(group_active_preset_index, 55);
+}
+
+TEST_F(HasClientTest, test_select_preset_invalid) {
+ const RawAddress test_address = GetTestAddress(1);
+ uint16_t test_conn_id = GetTestConnId(test_address);
+
+ std::set<HasPreset, HasPreset::ComparatorDesc> presets = {{
+ HasPreset(1, HasPreset::kPropertyAvailable, "Universal"),
+ HasPreset(2, HasPreset::kPropertyAvailable | HasPreset::kPropertyWritable,
+ "Preset2"),
+ }};
+ SetSampleDatabaseHasPresetsNtf(
+ test_address,
+ bluetooth::has::kFeatureBitHearingAidTypeBanded |
+ bluetooth::has::kFeatureBitWritablePresets |
+ bluetooth::has::kFeatureBitDynamicPresets,
+ presets);
+
+ uint8_t active_preset_index = 0;
+ std::vector<PresetInfo> preset_details;
+
+ EXPECT_CALL(*callbacks,
+ OnConnectionState(ConnectionState::CONNECTED, test_address));
+ EXPECT_CALL(*callbacks,
+ OnPresetInfo(std::variant<RawAddress, int>(test_address),
+ PresetInfoReason::ALL_PRESET_INFO, _))
+ .Times(1)
+ .WillOnce(SaveArg<2>(&preset_details));
+ EXPECT_CALL(*callbacks, OnActivePresetSelected(_, _))
+ .WillOnce(SaveArg<1>(&active_preset_index));
+ TestConnect(test_address);
+
+ ASSERT_TRUE(preset_details.size() > 1);
+ ASSERT_EQ(preset_details.front().preset_index, active_preset_index);
+
+ /* Inject preset deletion of index 2 */
+ auto deleted_index = preset_details[1].preset_index;
+ InjectPresetChanged(test_conn_id, test_address, false,
+ *presets.find(deleted_index), 0 /* prev_index */,
+ ::le_audio::has::PresetCtpChangeId::PRESET_DELETED,
+ true /* is_last */);
+
+ EXPECT_CALL(*callbacks, OnActivePresetSelectError(
+ std::variant<RawAddress, int>(test_address),
+ ErrorCode::INVALID_PRESET_INDEX))
+ .Times(1);
+
+ /* Check if preset was actually deleted - try setting it as an active one */
+ HasClient::Get()->SelectActivePreset(test_address,
+ preset_details[1].preset_index);
+}
+
+TEST_F(HasClientTest, test_select_preset_next) {
+ const RawAddress test_address = GetTestAddress(1);
+
+ std::set<HasPreset, HasPreset::ComparatorDesc> presets = {{
+ HasPreset(1, HasPreset::kPropertyAvailable, "Universal"),
+ HasPreset(2, HasPreset::kPropertyAvailable | HasPreset::kPropertyWritable,
+ "Preset2"),
+ }};
+ SetSampleDatabaseHasPresetsNtf(
+ test_address,
+ bluetooth::has::kFeatureBitHearingAidTypeBanded |
+ bluetooth::has::kFeatureBitWritablePresets |
+ bluetooth::has::kFeatureBitDynamicPresets,
+ presets);
+
+ uint8_t active_preset_index = 0;
+ std::vector<PresetInfo> preset_details;
+
+ EXPECT_CALL(*callbacks,
+ OnPresetInfo(std::variant<RawAddress, int>(test_address),
+ PresetInfoReason::ALL_PRESET_INFO, _))
+ .Times(1)
+ .WillOnce(SaveArg<2>(&preset_details));
+ EXPECT_CALL(*callbacks, OnActivePresetSelected(_, _))
+ .WillOnce(SaveArg<1>(&active_preset_index));
+ TestConnect(test_address);
+
+ ASSERT_TRUE(preset_details.size() > 1);
+ ASSERT_EQ(1, active_preset_index);
+
+ /* Verify active preset change */
+ EXPECT_CALL(*callbacks, OnActivePresetSelected(
+ std::variant<RawAddress, int>(test_address), 2));
+ HasClient::Get()->NextActivePreset(test_address);
+}
+
+TEST_F(HasClientTest, test_select_group_preset_next_no_preset_sync_supported) {
+ /* None of these devices support preset syncing */
+ const RawAddress test_address1 = GetTestAddress(1);
+ SetSampleDatabaseHasPresetsNtf(
+ test_address1, bluetooth::has::kFeatureBitHearingAidTypeBinaural);
+
+ const RawAddress test_address2 = GetTestAddress(2);
+ SetSampleDatabaseHasPresetsNtf(
+ test_address2, bluetooth::has::kFeatureBitHearingAidTypeBinaural);
+
+ TestConnect(test_address1);
+ TestConnect(test_address2);
+
+ /* Mock the csis group with two devices */
+ uint8_t not_synced_group = 13;
+ ON_CALL(mock_csis_client_module_, GetDeviceList(not_synced_group))
+ .WillByDefault(
+ Return(std::vector<RawAddress>({{test_address1, test_address2}})));
+ ON_CALL(mock_csis_client_module_,
+ GetGroupId(test_address1, ::le_audio::uuid::kCapServiceUuid))
+ .WillByDefault(Return(not_synced_group));
+ ON_CALL(mock_csis_client_module_,
+ GetGroupId(test_address2, ::le_audio::uuid::kCapServiceUuid))
+ .WillByDefault(Return(not_synced_group));
+
+ uint8_t group_active_preset_index = 0;
+ EXPECT_CALL(*callbacks, OnActivePresetSelected(
+ std::variant<RawAddress, int>(test_address1), 55))
+ .Times(0);
+ EXPECT_CALL(*callbacks, OnActivePresetSelected(
+ std::variant<RawAddress, int>(test_address2), 55))
+ .Times(0);
+ EXPECT_CALL(*callbacks,
+ OnActivePresetSelected(
+ std::variant<RawAddress, int>(not_synced_group), _))
+ .WillOnce(SaveArg<1>(&group_active_preset_index));
+
+ /* No locally synced opcodes support so expect both devices getting writes */
+ EXPECT_CALL(gatt_queue, WriteCharacteristic(GetTestConnId(test_address1),
+ HasDbBuilder::kPresetsCtpValHdl,
+ _, GATT_WRITE, _, _))
+ .Times(1);
+ EXPECT_CALL(gatt_queue, WriteCharacteristic(GetTestConnId(test_address2),
+ HasDbBuilder::kPresetsCtpValHdl,
+ _, GATT_WRITE, _, _))
+ .Times(1);
+
+ HasClient::Get()->NextActivePreset(not_synced_group);
+ ASSERT_EQ(group_active_preset_index, 55);
+}
+
+TEST_F(HasClientTest, test_select_group_preset_next_preset_sync_supported) {
+ /* Only one of these devices support preset syncing */
+ const RawAddress test_address1 = GetTestAddress(1);
+ uint16_t test_conn_id1 = GetTestConnId(test_address1);
+ SetSampleDatabaseHasPresetsNtf(
+ test_address1, bluetooth::has::kFeatureBitHearingAidTypeBinaural);
+
+ const RawAddress test_address2 = GetTestAddress(2);
+ uint16_t test_conn_id2 = GetTestConnId(test_address2);
+ SetSampleDatabaseHasPresetsNtf(
+ test_address2,
+ bluetooth::has::kFeatureBitHearingAidTypeBinaural |
+ bluetooth::has::kFeatureBitPresetSynchronizationSupported);
+
+ uint8_t active_preset_index1 = 0;
+ uint8_t active_preset_index2 = 0;
+
+ EXPECT_CALL(*callbacks, OnActivePresetSelected(
+ std::variant<RawAddress, int>(test_address1), _))
+ .WillOnce(SaveArg<1>(&active_preset_index1));
+ TestConnect(test_address1);
+
+ EXPECT_CALL(*callbacks, OnActivePresetSelected(
+ std::variant<RawAddress, int>(test_address2), _))
+ .WillOnce(SaveArg<1>(&active_preset_index2));
+ TestConnect(test_address2);
+
+ /* Mock the csis group with two devices */
+ uint8_t synced_group = 13;
+ ON_CALL(mock_csis_client_module_, GetDeviceList(synced_group))
+ .WillByDefault(
+ Return(std::vector<RawAddress>({{test_address1, test_address2}})));
+ ON_CALL(mock_csis_client_module_,
+ GetGroupId(test_address1, ::le_audio::uuid::kCapServiceUuid))
+ .WillByDefault(Return(synced_group));
+ ON_CALL(mock_csis_client_module_,
+ GetGroupId(test_address2, ::le_audio::uuid::kCapServiceUuid))
+ .WillByDefault(Return(synced_group));
+
+ EXPECT_CALL(*callbacks, OnActivePresetSelectError(
+ _, ErrorCode::GROUP_OPERATION_NOT_SUPPORTED))
+ .Times(0);
+
+ /* Expect callback from the group but not from the devices */
+ uint8_t group_active_preset_index = 0;
+ EXPECT_CALL(*callbacks, OnActivePresetSelected(
+ std::variant<RawAddress, int>(test_address1), _))
+ .Times(0);
+ EXPECT_CALL(*callbacks, OnActivePresetSelected(
+ std::variant<RawAddress, int>(test_address2), _))
+ .Times(0);
+ EXPECT_CALL(*callbacks, OnActivePresetSelected(
+ std::variant<RawAddress, int>(synced_group), _))
+ .WillOnce(SaveArg<1>(&group_active_preset_index));
+
+ /* Expect Ctp write on on this device which forwards operation to the other */
+ EXPECT_CALL(gatt_queue, WriteCharacteristic(test_conn_id1,
+ HasDbBuilder::kPresetsCtpValHdl,
+ _, GATT_WRITE, _, _))
+ .Times(0);
+ EXPECT_CALL(gatt_queue, WriteCharacteristic(test_conn_id2,
+ HasDbBuilder::kPresetsCtpValHdl,
+ _, GATT_WRITE, _, _))
+ .Times(1);
+
+ HasClient::Get()->NextActivePreset(synced_group);
+ ASSERT_EQ(group_active_preset_index, 55);
+}
+
+TEST_F(HasClientTest, test_select_preset_prev) {
+ const RawAddress test_address = GetTestAddress(1);
+
+ std::set<HasPreset, HasPreset::ComparatorDesc> presets = {{
+ HasPreset(1, HasPreset::kPropertyAvailable, "Universal"),
+ HasPreset(2, HasPreset::kPropertyAvailable | HasPreset::kPropertyWritable,
+ "Preset2"),
+ }};
+ SetSampleDatabaseHasPresetsNtf(
+ test_address,
+ bluetooth::has::kFeatureBitHearingAidTypeBanded |
+ bluetooth::has::kFeatureBitWritablePresets |
+ bluetooth::has::kFeatureBitDynamicPresets,
+ presets);
+
+ uint8_t active_preset_index = 0;
+ std::vector<PresetInfo> preset_details;
+
+ EXPECT_CALL(*callbacks,
+ OnPresetInfo(std::variant<RawAddress, int>(test_address),
+ PresetInfoReason::ALL_PRESET_INFO, _))
+ .Times(1)
+ .WillOnce(SaveArg<2>(&preset_details));
+ ON_CALL(*callbacks, OnActivePresetSelected(_, _))
+ .WillByDefault(SaveArg<1>(&active_preset_index));
+ TestConnect(test_address);
+
+ HasClient::Get()->SelectActivePreset(test_address, 2);
+ ASSERT_TRUE(preset_details.size() > 1);
+ ASSERT_EQ(2, active_preset_index);
+
+ /* Verify active preset change */
+ EXPECT_CALL(*callbacks, OnActivePresetSelected(
+ std::variant<RawAddress, int>(test_address), 1));
+ HasClient::Get()->PreviousActivePreset(test_address);
+}
+
+TEST_F(HasClientTest, test_select_group_preset_prev_no_preset_sync_supported) {
+ /* None of these devices support preset syncing */
+ const RawAddress test_address1 = GetTestAddress(1);
+ SetSampleDatabaseHasPresetsNtf(
+ test_address1, bluetooth::has::kFeatureBitHearingAidTypeBinaural);
+
+ const RawAddress test_address2 = GetTestAddress(2);
+ SetSampleDatabaseHasPresetsNtf(
+ test_address2, bluetooth::has::kFeatureBitHearingAidTypeBinaural);
+
+ TestConnect(test_address1);
+ TestConnect(test_address2);
+
+ /* Mock the csis group with two devices */
+ uint8_t not_synced_group = 13;
+ ON_CALL(mock_csis_client_module_, GetDeviceList(not_synced_group))
+ .WillByDefault(
+ Return(std::vector<RawAddress>({{test_address1, test_address2}})));
+ ON_CALL(mock_csis_client_module_,
+ GetGroupId(test_address1, ::le_audio::uuid::kCapServiceUuid))
+ .WillByDefault(Return(not_synced_group));
+ ON_CALL(mock_csis_client_module_,
+ GetGroupId(test_address2, ::le_audio::uuid::kCapServiceUuid))
+ .WillByDefault(Return(not_synced_group));
+
+ uint8_t group_active_preset_index = 0;
+ EXPECT_CALL(*callbacks, OnActivePresetSelected(
+ std::variant<RawAddress, int>(test_address1), 55))
+ .Times(0);
+ EXPECT_CALL(*callbacks, OnActivePresetSelected(
+ std::variant<RawAddress, int>(test_address2), 55))
+ .Times(0);
+ EXPECT_CALL(*callbacks,
+ OnActivePresetSelected(
+ std::variant<RawAddress, int>(not_synced_group), _))
+ .WillOnce(SaveArg<1>(&group_active_preset_index));
+
+ /* No locally synced opcodes support so expect both devices getting writes */
+ EXPECT_CALL(gatt_queue, WriteCharacteristic(GetTestConnId(test_address1),
+ HasDbBuilder::kPresetsCtpValHdl,
+ _, GATT_WRITE, _, _))
+ .Times(1);
+ EXPECT_CALL(gatt_queue, WriteCharacteristic(GetTestConnId(test_address2),
+ HasDbBuilder::kPresetsCtpValHdl,
+ _, GATT_WRITE, _, _))
+ .Times(1);
+
+ HasClient::Get()->PreviousActivePreset(not_synced_group);
+ ASSERT_EQ(group_active_preset_index, 55);
+}
+
+TEST_F(HasClientTest, test_select_group_preset_prev_preset_sync_supported) {
+ /* Only one of these devices support preset syncing */
+ const RawAddress test_address1 = GetTestAddress(1);
+ uint16_t test_conn_id1 = GetTestConnId(test_address1);
+ SetSampleDatabaseHasPresetsNtf(
+ test_address1, bluetooth::has::kFeatureBitHearingAidTypeBinaural);
+
+ const RawAddress test_address2 = GetTestAddress(2);
+ uint16_t test_conn_id2 = GetTestConnId(test_address2);
+ SetSampleDatabaseHasPresetsNtf(
+ test_address2,
+ bluetooth::has::kFeatureBitHearingAidTypeBinaural |
+ bluetooth::has::kFeatureBitPresetSynchronizationSupported);
+
+ uint8_t active_preset_index1 = 0;
+ uint8_t active_preset_index2 = 0;
+
+ EXPECT_CALL(*callbacks, OnActivePresetSelected(
+ std::variant<RawAddress, int>(test_address1), _))
+ .WillOnce(SaveArg<1>(&active_preset_index1));
+ TestConnect(test_address1);
+
+ EXPECT_CALL(*callbacks, OnActivePresetSelected(
+ std::variant<RawAddress, int>(test_address2), _))
+ .WillOnce(SaveArg<1>(&active_preset_index2));
+ TestConnect(test_address2);
+
+ /* Mock the csis group with two devices */
+ uint8_t synced_group = 13;
+ ON_CALL(mock_csis_client_module_, GetDeviceList(synced_group))
+ .WillByDefault(
+ Return(std::vector<RawAddress>({{test_address1, test_address2}})));
+ ON_CALL(mock_csis_client_module_,
+ GetGroupId(test_address1, ::le_audio::uuid::kCapServiceUuid))
+ .WillByDefault(Return(synced_group));
+ ON_CALL(mock_csis_client_module_,
+ GetGroupId(test_address2, ::le_audio::uuid::kCapServiceUuid))
+ .WillByDefault(Return(synced_group));
+
+ EXPECT_CALL(*callbacks, OnActivePresetSelectError(
+ _, ErrorCode::GROUP_OPERATION_NOT_SUPPORTED))
+ .Times(0);
+
+ /* Expect callback from the group but not from the devices */
+ uint8_t group_active_preset_index = 0;
+ EXPECT_CALL(*callbacks, OnActivePresetSelected(
+ std::variant<RawAddress, int>(test_address1), _))
+ .Times(0);
+ EXPECT_CALL(*callbacks, OnActivePresetSelected(
+ std::variant<RawAddress, int>(test_address2), _))
+ .Times(0);
+ EXPECT_CALL(*callbacks, OnActivePresetSelected(
+ std::variant<RawAddress, int>(synced_group), _))
+ .WillOnce(SaveArg<1>(&group_active_preset_index));
+
+ /* Expect Ctp write on on this device which forwards operation to the other */
+ EXPECT_CALL(gatt_queue, WriteCharacteristic(test_conn_id1,
+ HasDbBuilder::kPresetsCtpValHdl,
+ _, GATT_WRITE, _, _))
+ .Times(0);
+ EXPECT_CALL(gatt_queue, WriteCharacteristic(test_conn_id2,
+ HasDbBuilder::kPresetsCtpValHdl,
+ _, GATT_WRITE, _, _))
+ .Times(1);
+
+ HasClient::Get()->PreviousActivePreset(synced_group);
+ ASSERT_EQ(group_active_preset_index, 55);
+}
+
+TEST_F(HasClientTest, test_select_has_no_presets) {
+ const RawAddress test_address = GetTestAddress(1);
+ SetSampleDatabaseHasNoPresetsFlagsOnly(test_address);
+
+ EXPECT_CALL(*callbacks, OnDeviceAvailable(test_address, _)).Times(1);
+ EXPECT_CALL(*callbacks,
+ OnConnectionState(ConnectionState::CONNECTED, test_address))
+ .Times(1);
+ TestConnect(test_address);
+
+ /* Test this not so useful service */
+ EXPECT_CALL(*callbacks,
+ OnActivePresetSelectError(_, ErrorCode::OPERATION_NOT_SUPPORTED))
+ .Times(3);
+
+ HasClient::Get()->SelectActivePreset(test_address, 0x01);
+ HasClient::Get()->NextActivePreset(test_address);
+ HasClient::Get()->PreviousActivePreset(test_address);
+}
+
+static int GetSocketBufferSize(int sockfd) {
+ int socket_buffer_size;
+ socklen_t optlen = sizeof(socket_buffer_size);
+ getsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, (void*)&socket_buffer_size,
+ &optlen);
+ return socket_buffer_size;
+}
+
+bool SimpleJsonValidator(int fd, int* dumpsys_byte_cnt) {
+ std::ostringstream ss;
+
+ char buf{0};
+ bool within_double_quotes{false};
+ int left_bracket{0}, right_bracket{0};
+ int left_sq_bracket{0}, right_sq_bracket{0};
+ while (read(fd, &buf, 1) != -1) {
+ switch (buf) {
+ (*dumpsys_byte_cnt)++;
+ case '"':
+ within_double_quotes = !within_double_quotes;
+ break;
+ case '{':
+ if (!within_double_quotes) {
+ left_bracket++;
+ }
+ break;
+ case '}':
+ if (!within_double_quotes) {
+ right_bracket++;
+ }
+ break;
+ case '[':
+ if (!within_double_quotes) {
+ left_sq_bracket++;
+ }
+ break;
+ case ']':
+ if (!within_double_quotes) {
+ right_sq_bracket++;
+ }
+ break;
+ default:
+ break;
+ }
+ ss << buf;
+ }
+ LOG(ERROR) << __func__ << ": " << ss.str();
+ return (left_bracket == right_bracket) &&
+ (left_sq_bracket == right_sq_bracket);
+}
+
+TEST_F(HasClientTest, test_dumpsys) {
+ const RawAddress test_address = GetTestAddress(1);
+
+ std::set<HasPreset, HasPreset::ComparatorDesc> presets = {{
+ HasPreset(1, HasPreset::kPropertyAvailable, "Universal"),
+ HasPreset(2, HasPreset::kPropertyAvailable | HasPreset::kPropertyWritable,
+ "Preset2"),
+ }};
+ SetSampleDatabaseHasPresetsNtf(
+ test_address,
+ bluetooth::has::kFeatureBitHearingAidTypeBanded |
+ bluetooth::has::kFeatureBitWritablePresets |
+ bluetooth::has::kFeatureBitDynamicPresets,
+ presets);
+
+ uint8_t active_preset_index = 0;
+ std::vector<PresetInfo> preset_details;
+
+ EXPECT_CALL(*callbacks,
+ OnPresetInfo(std::variant<RawAddress, int>(test_address),
+ PresetInfoReason::ALL_PRESET_INFO, _))
+ .Times(1)
+ .WillOnce(SaveArg<2>(&preset_details));
+ ON_CALL(*callbacks, OnActivePresetSelected(_, _))
+ .WillByDefault(SaveArg<1>(&active_preset_index));
+ TestConnect(test_address);
+
+ int sv[2];
+ ASSERT_EQ(0, socketpair(AF_LOCAL, SOCK_STREAM | SOCK_NONBLOCK, 0, sv));
+ int socket_buffer_size = GetSocketBufferSize(sv[0]);
+
+ HasClient::Get()->DebugDump(sv[0]);
+ int dumpsys_byte_cnt = 0;
+ ASSERT_TRUE(dumpsys_byte_cnt < socket_buffer_size);
+ ASSERT_TRUE(SimpleJsonValidator(sv[1], &dumpsys_byte_cnt));
+}
+
+class HasTypesTest : public ::testing::Test {
+ protected:
+ void SetUp(void) override { fake_osi_bool_props.clear(); }
+
+ void TearDown(void) override {}
+}; // namespace
+
+TEST_F(HasTypesTest, test_has_preset_serialize) {
+ HasPreset preset(0x01,
+ HasPreset::kPropertyAvailable | HasPreset::kPropertyWritable,
+ "My Writable Preset01");
+
+ auto sp_sz = preset.SerializedSize();
+ std::vector<uint8_t> serialized(sp_sz);
+
+ ASSERT_EQ(1 + // preset index
+ 1 + // properties
+ 1 + // name length
+ preset.GetName().length(),
+ sp_sz);
+
+ /* Serialize should move the received buffer pointer by the size of data
+ */
+ ASSERT_EQ(preset.Serialize(serialized.data(), serialized.size()),
+ serialized.data() + serialized.size());
+
+ /* Deserialize */
+ HasPreset clone;
+ ASSERT_EQ(HasPreset::Deserialize(serialized.data(), serialized.size(), clone),
+ serialized.data() + serialized.size());
+
+ /* Verify */
+ ASSERT_EQ(preset.GetIndex(), clone.GetIndex());
+ ASSERT_EQ(preset.GetProperties(), clone.GetProperties());
+ ASSERT_EQ(preset.GetName(), clone.GetName());
+}
+
+TEST_F(HasTypesTest, test_has_preset_serialize_output_buffer_to_small) {
+ HasPreset preset(0x01,
+ HasPreset::kPropertyAvailable | HasPreset::kPropertyWritable,
+ "My Writable Preset01");
+
+ /* On failure, the offset should still point on .data() */
+ std::vector<uint8_t> serialized(preset.SerializedSize() - 1);
+ ASSERT_EQ(preset.Serialize(serialized.data(), serialized.size()),
+ serialized.data());
+ ASSERT_EQ(preset.Serialize(serialized.data(), 0), serialized.data());
+ ASSERT_EQ(preset.Serialize(serialized.data(), 1), serialized.data());
+ ASSERT_EQ(preset.Serialize(serialized.data(), 10), serialized.data());
+}
+
+TEST_F(HasTypesTest, test_has_preset_serialize_name_to_long) {
+ HasPreset preset(0x01,
+ HasPreset::kPropertyAvailable | HasPreset::kPropertyWritable,
+ "This name is more than 40 characters long");
+
+ /* On failure, the offset should still point on .data() */
+ std::vector<uint8_t> serialized(preset.SerializedSize());
+ EXPECT_EQ(preset.Serialize(serialized.data(), serialized.size()),
+ serialized.data());
+}
+
+TEST_F(HasTypesTest, test_has_preset_deserialize_input_buffer_to_small) {
+ HasPreset preset(0x01,
+ HasPreset::kPropertyAvailable | HasPreset::kPropertyWritable,
+ "My Writable Preset01");
+
+ std::vector<uint8_t> serialized(preset.SerializedSize());
+
+ /* Serialize should move the received buffer pointer by the size of data
+ */
+ ASSERT_EQ(preset.Serialize(serialized.data(), serialized.size()),
+ serialized.data() + serialized.size());
+
+ /* Deserialize */
+ HasPreset clone;
+ ASSERT_EQ(HasPreset::Deserialize(serialized.data(), 0, clone),
+ serialized.data());
+ ASSERT_EQ(HasPreset::Deserialize(serialized.data(), 1, clone),
+ serialized.data());
+ ASSERT_EQ(HasPreset::Deserialize(serialized.data(), 11, clone),
+ serialized.data());
+ ASSERT_EQ(
+ HasPreset::Deserialize(serialized.data(), serialized.size() - 1, clone),
+ serialized.data());
+}
+
+TEST_F(HasTypesTest, test_has_presets_serialize) {
+ HasPreset preset(0x01,
+ HasPreset::kPropertyAvailable | HasPreset::kPropertyWritable,
+ "My Writable Preset01");
+
+ HasPreset preset2(0x02, 0, "Nonwritable Unavailable Preset");
+
+ HasDevice has_device(GetTestAddress(1));
+ has_device.has_presets.insert(preset);
+ has_device.has_presets.insert(preset2);
+
+ auto out_buf_sz = has_device.SerializedPresetsSize();
+ ASSERT_EQ(out_buf_sz, preset.SerializedSize() + preset2.SerializedSize() + 2);
+
+ /* Serialize should append to the vector */
+ std::vector<uint8_t> serialized;
+ ASSERT_TRUE(has_device.SerializePresets(serialized));
+ ASSERT_EQ(out_buf_sz, serialized.size());
+
+ /* Deserialize */
+ HasDevice clone(GetTestAddress(1));
+ ASSERT_TRUE(HasDevice::DeserializePresets(serialized.data(),
+ serialized.size(), clone));
+
+ /* Verify */
+ ASSERT_EQ(clone.has_presets.size(), has_device.has_presets.size());
+ ASSERT_NE(0u, clone.has_presets.count(0x01));
+ ASSERT_NE(0u, clone.has_presets.count(0x02));
+
+ ASSERT_EQ(clone.has_presets.find(0x01)->GetIndex(),
+ has_device.has_presets.find(0x01)->GetIndex());
+ ASSERT_EQ(clone.has_presets.find(0x01)->GetProperties(),
+ has_device.has_presets.find(0x01)->GetProperties());
+ ASSERT_EQ(clone.has_presets.find(0x01)->GetName(),
+ has_device.has_presets.find(0x01)->GetName());
+
+ ASSERT_EQ(clone.has_presets.find(0x02)->GetIndex(),
+ has_device.has_presets.find(0x02)->GetIndex());
+ ASSERT_EQ(clone.has_presets.find(0x02)->GetProperties(),
+ has_device.has_presets.find(0x02)->GetProperties());
+ ASSERT_EQ(clone.has_presets.find(0x02)->GetName(),
+ has_device.has_presets.find(0x02)->GetName());
+}
+
+TEST_F(HasTypesTest, test_group_op_coordinator_init) {
+ HasCtpGroupOpCoordinator::Initialize([](void*) {
+ /* Do nothing */
+ });
+ ASSERT_EQ(0u, HasCtpGroupOpCoordinator::ref_cnt);
+ auto address1 = GetTestAddress(1);
+ auto address2 = GetTestAddress(2);
+
+ EXPECT_CALL(*AlarmMock::Get(), AlarmNew(_)).Times(1);
+ HasCtpGroupOpCoordinator wrapper(
+ {address1, address2},
+ HasCtpOp(0x01, ::le_audio::has::PresetCtpOpcode::READ_PRESET_BY_INDEX,
+ 6));
+ ASSERT_EQ(2u, wrapper.ref_cnt);
+
+ EXPECT_CALL(*AlarmMock::Get(), AlarmFree(_)).Times(1);
+ HasCtpGroupOpCoordinator::Cleanup();
+ ASSERT_EQ(0u, wrapper.ref_cnt);
+}
+
+TEST_F(HasTypesTest, test_group_op_coordinator_copy) {
+ HasCtpGroupOpCoordinator::Initialize([](void*) {
+ /* Do nothing */
+ });
+ ASSERT_EQ(0u, HasCtpGroupOpCoordinator::ref_cnt);
+ auto address1 = GetTestAddress(1);
+ auto address2 = GetTestAddress(2);
+
+ EXPECT_CALL(*AlarmMock::Get(), AlarmNew(_)).Times(1);
+ HasCtpGroupOpCoordinator wrapper(
+ {address1, address2},
+ HasCtpOp(0x01, ::le_audio::has::PresetCtpOpcode::READ_PRESET_BY_INDEX,
+ 6));
+ HasCtpGroupOpCoordinator wrapper2(
+ {address1},
+ HasCtpOp(0x01, ::le_audio::has::PresetCtpOpcode::READ_PRESET_BY_INDEX,
+ 6));
+ ASSERT_EQ(3u, wrapper.ref_cnt);
+ HasCtpGroupOpCoordinator wrapper3 = wrapper2;
+ auto* wrapper4 =
+ new HasCtpGroupOpCoordinator(HasCtpGroupOpCoordinator(wrapper2));
+ ASSERT_EQ(5u, wrapper.ref_cnt);
+
+ delete wrapper4;
+ ASSERT_EQ(4u, wrapper.ref_cnt);
+
+ EXPECT_CALL(*AlarmMock::Get(), AlarmFree(_)).Times(1);
+ HasCtpGroupOpCoordinator::Cleanup();
+ ASSERT_EQ(0u, wrapper.ref_cnt);
+}
+
+TEST_F(HasTypesTest, test_group_op_coordinator_completion) {
+ HasCtpGroupOpCoordinator::Initialize([](void*) {
+ /* Do nothing */
+ LOG(INFO) << __func__ << " callback call";
+ });
+ ASSERT_EQ(0u, HasCtpGroupOpCoordinator::ref_cnt);
+ auto address1 = GetTestAddress(1);
+ auto address2 = GetTestAddress(2);
+ auto address3 = GetTestAddress(3);
+
+ EXPECT_CALL(*AlarmMock::Get(), AlarmNew(_)).Times(1);
+ HasCtpGroupOpCoordinator wrapper(
+ {address1, address3},
+ HasCtpOp(0x01, ::le_audio::has::PresetCtpOpcode::READ_PRESET_BY_INDEX,
+ 6));
+ HasCtpGroupOpCoordinator wrapper2(
+ {address2},
+ HasCtpOp(0x01, ::le_audio::has::PresetCtpOpcode::READ_PRESET_BY_INDEX,
+ 6));
+ ASSERT_EQ(3u, wrapper.ref_cnt);
+
+ EXPECT_CALL(*AlarmMock::Get(), AlarmFree(_)).Times(0);
+ ASSERT_FALSE(wrapper.IsFullyCompleted());
+
+ wrapper.SetCompleted(address1);
+ ASSERT_EQ(2u, wrapper.ref_cnt);
+
+ wrapper.SetCompleted(address3);
+ ASSERT_EQ(1u, wrapper.ref_cnt);
+ ASSERT_FALSE(wrapper.IsFullyCompleted());
+ Mock::VerifyAndClearExpectations(&*AlarmMock::Get());
+
+ /* Non existing address completion */
+ EXPECT_CALL(*AlarmMock::Get(), AlarmFree(_)).Times(0);
+ wrapper.SetCompleted(address2);
+ Mock::VerifyAndClearExpectations(&*AlarmMock::Get());
+ ASSERT_EQ(1u, wrapper.ref_cnt);
+
+ /* Last device address completion */
+ EXPECT_CALL(*AlarmMock::Get(), AlarmFree(_)).Times(1);
+ wrapper2.SetCompleted(address2);
+ Mock::VerifyAndClearExpectations(&*AlarmMock::Get());
+ ASSERT_TRUE(wrapper.IsFullyCompleted());
+ ASSERT_EQ(0u, wrapper.ref_cnt);
+
+ EXPECT_CALL(*AlarmMock::Get(), AlarmFree(_)).Times(0);
+ HasCtpGroupOpCoordinator::Cleanup();
+}
+
+} // namespace
+} // namespace internal
+} // namespace has
+} // namespace bluetooth
diff --git a/system/bta/has/has_ctp.cc b/system/bta/has/has_ctp.cc
new file mode 100644
index 0000000000..1b88dcadfb
--- /dev/null
+++ b/system/bta/has/has_ctp.cc
@@ -0,0 +1,285 @@
+/*
+ * 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 "has_ctp.h"
+
+namespace le_audio {
+namespace has {
+
+static bool ParsePresetGenericUpdate(uint16_t& len, const uint8_t* value,
+ HasCtpNtf& ntf) {
+ if (len < sizeof(ntf.prev_index) + HasPreset::kCharValueMinSize) {
+ LOG(ERROR) << "Invalid preset value length=" << +len
+ << " for generic update.";
+ return false;
+ }
+
+ STREAM_TO_UINT8(ntf.index, value);
+ len -= 1;
+
+ ntf.preset = HasPreset::FromCharacteristicValue(len, value);
+ return true;
+}
+
+static bool ParsePresetIndex(uint16_t& len, const uint8_t* value,
+ HasCtpNtf& ntf) {
+ if (len < sizeof(ntf.index)) {
+ LOG(ERROR) << __func__ << "Invalid preset value length=" << +len
+ << " for generic update.";
+ return false;
+ }
+
+ STREAM_TO_UINT8(ntf.index, value);
+ len -= 1;
+ return true;
+}
+
+static bool ParsePresetReadResponse(uint16_t& len, const uint8_t* value,
+ HasCtpNtf& ntf) {
+ if (len < sizeof(ntf.is_last) + HasPreset::kCharValueMinSize) {
+ LOG(ERROR) << "Invalid preset value length=" << +len;
+ return false;
+ }
+
+ STREAM_TO_UINT8(ntf.is_last, value);
+ len -= 1;
+
+ ntf.preset = HasPreset::FromCharacteristicValue(len, value);
+ return true;
+}
+
+static bool ParsePresetChanged(uint16_t len, const uint8_t* value,
+ HasCtpNtf& ntf) {
+ if (len < sizeof(ntf.is_last) + sizeof(ntf.change_id)) {
+ LOG(ERROR) << __func__ << "Invalid preset value length=" << +len;
+ return false;
+ }
+
+ uint8_t change_id;
+ STREAM_TO_UINT8(change_id, value);
+ len -= 1;
+ if (change_id > static_cast<std::underlying_type_t<PresetCtpChangeId>>(
+ PresetCtpChangeId::CHANGE_ID_MAX_)) {
+ LOG(ERROR) << __func__ << "Invalid preset chenge_id=" << change_id;
+ return false;
+ }
+ ntf.change_id = PresetCtpChangeId(change_id);
+ STREAM_TO_UINT8(ntf.is_last, value);
+ len -= 1;
+
+ switch (ntf.change_id) {
+ case PresetCtpChangeId::PRESET_GENERIC_UPDATE:
+ return ParsePresetGenericUpdate(len, value, ntf);
+ case PresetCtpChangeId::PRESET_AVAILABLE:
+ return ParsePresetIndex(len, value, ntf);
+ case PresetCtpChangeId::PRESET_UNAVAILABLE:
+ return ParsePresetIndex(len, value, ntf);
+ case PresetCtpChangeId::PRESET_DELETED:
+ return ParsePresetIndex(len, value, ntf);
+ default:
+ return false;
+ }
+
+ return true;
+}
+
+std::optional<HasCtpNtf> HasCtpNtf::FromCharacteristicValue(
+ uint16_t len, const uint8_t* value) {
+ if (len < 3) {
+ LOG(ERROR) << __func__ << " Invalid Cp notification.";
+ return std::nullopt;
+ }
+
+ uint8_t op;
+ STREAM_TO_UINT8(op, value);
+ --len;
+
+ if ((op != static_cast<std::underlying_type_t<PresetCtpOpcode>>(
+ PresetCtpOpcode::READ_PRESET_RESPONSE)) &&
+ (op != static_cast<std::underlying_type_t<PresetCtpOpcode>>(
+ PresetCtpOpcode::PRESET_CHANGED))) {
+ LOG(ERROR) << __func__
+ << ": Received invalid opcode in control point notification: "
+ << ++op;
+ return std::nullopt;
+ }
+
+ HasCtpNtf ntf;
+ ntf.opcode = PresetCtpOpcode(op);
+ if (ntf.opcode == le_audio::has::PresetCtpOpcode::PRESET_CHANGED) {
+ if (!ParsePresetChanged(len, value, ntf)) return std::nullopt;
+
+ } else if (ntf.opcode ==
+ le_audio::has::PresetCtpOpcode::READ_PRESET_RESPONSE) {
+ if (!ParsePresetReadResponse(len, value, ntf)) return std::nullopt;
+ }
+
+ return ntf;
+}
+
+uint16_t HasCtpOp::last_op_id_ = 0;
+
+std::vector<uint8_t> HasCtpOp::ToCharacteristicValue() const {
+ std::vector<uint8_t> value;
+ auto* pp = value.data();
+
+ switch (opcode) {
+ case PresetCtpOpcode::READ_ALL_PRESETS:
+ value.resize(1);
+ pp = value.data();
+ UINT8_TO_STREAM(
+ pp, static_cast<std::underlying_type_t<PresetCtpOpcode>>(opcode));
+ break;
+
+ case PresetCtpOpcode::READ_PRESET_BY_INDEX:
+ case PresetCtpOpcode::SET_ACTIVE_PRESET:
+ case PresetCtpOpcode::SET_ACTIVE_PRESET_SYNC:
+ value.resize(2);
+ pp = value.data();
+ UINT8_TO_STREAM(
+ pp, static_cast<std::underlying_type_t<PresetCtpOpcode>>(opcode));
+ UINT8_TO_STREAM(pp, index);
+ break;
+
+ case PresetCtpOpcode::SET_NEXT_PRESET:
+ case PresetCtpOpcode::SET_NEXT_PRESET_SYNC:
+ case PresetCtpOpcode::SET_PREV_PRESET:
+ case PresetCtpOpcode::SET_PREV_PRESET_SYNC:
+ value.resize(1);
+ pp = value.data();
+ UINT8_TO_STREAM(
+ pp, static_cast<std::underlying_type_t<PresetCtpOpcode>>(opcode));
+ break;
+
+ case PresetCtpOpcode::WRITE_PRESET_NAME: {
+ auto name_str = name.value_or("");
+ value.resize(2 + name_str.length());
+ pp = value.data();
+
+ UINT8_TO_STREAM(
+ pp, static_cast<std::underlying_type_t<PresetCtpOpcode>>(opcode));
+ UINT8_TO_STREAM(pp, index);
+ memcpy(pp, name_str.c_str(), name_str.length());
+ } break;
+
+ default:
+ LOG_ASSERT(false) << __func__ << "Bad control point operation!";
+ break;
+ }
+
+ return value;
+}
+
+#define CASE_SET_PTR_TO_TOKEN_STR(en) \
+ case (en): \
+ ch = #en; \
+ break;
+
+std::ostream& operator<<(std::ostream& out, const PresetCtpChangeId value) {
+ const char* ch = 0;
+ switch (value) {
+ CASE_SET_PTR_TO_TOKEN_STR(PresetCtpChangeId::PRESET_GENERIC_UPDATE);
+ CASE_SET_PTR_TO_TOKEN_STR(PresetCtpChangeId::PRESET_DELETED);
+ CASE_SET_PTR_TO_TOKEN_STR(PresetCtpChangeId::PRESET_AVAILABLE);
+ CASE_SET_PTR_TO_TOKEN_STR(PresetCtpChangeId::PRESET_UNAVAILABLE);
+ default:
+ ch = "INVALID_CHANGE_ID";
+ break;
+ }
+ return out << ch;
+}
+
+std::ostream& operator<<(std::ostream& out, const PresetCtpOpcode value) {
+ const char* ch = 0;
+ switch (value) {
+ CASE_SET_PTR_TO_TOKEN_STR(PresetCtpOpcode::READ_ALL_PRESETS);
+ CASE_SET_PTR_TO_TOKEN_STR(PresetCtpOpcode::READ_PRESET_BY_INDEX);
+ CASE_SET_PTR_TO_TOKEN_STR(PresetCtpOpcode::READ_PRESET_RESPONSE);
+ CASE_SET_PTR_TO_TOKEN_STR(PresetCtpOpcode::PRESET_CHANGED);
+ CASE_SET_PTR_TO_TOKEN_STR(PresetCtpOpcode::WRITE_PRESET_NAME);
+ CASE_SET_PTR_TO_TOKEN_STR(PresetCtpOpcode::SET_ACTIVE_PRESET);
+ CASE_SET_PTR_TO_TOKEN_STR(PresetCtpOpcode::SET_NEXT_PRESET);
+ CASE_SET_PTR_TO_TOKEN_STR(PresetCtpOpcode::SET_PREV_PRESET);
+ CASE_SET_PTR_TO_TOKEN_STR(PresetCtpOpcode::SET_ACTIVE_PRESET_SYNC);
+ CASE_SET_PTR_TO_TOKEN_STR(PresetCtpOpcode::SET_NEXT_PRESET_SYNC);
+ CASE_SET_PTR_TO_TOKEN_STR(PresetCtpOpcode::SET_PREV_PRESET_SYNC);
+ default:
+ ch = "NOT_A_VALID_OPCODE";
+ break;
+ }
+ return out << ch;
+}
+#undef SET_CH_TO_TOKENIZED
+
+std::ostream& operator<<(std::ostream& out, const HasCtpOp& op) {
+ out << "\"HasCtpOp\": {";
+ if (std::holds_alternative<int>(op.addr_or_group)) {
+ out << "\"group_id\": " << std::get<int>(op.addr_or_group);
+ } else if (std::holds_alternative<RawAddress>(op.addr_or_group)) {
+ out << "\"address\": \"" << std::get<RawAddress>(op.addr_or_group) << "\"";
+ } else {
+ out << "\"bad value\"";
+ }
+ out << ", \"id\": " << op.op_id << ", \"opcode\": \"" << op.opcode << "\""
+ << ", \"index\": " << +op.index << ", \"name\": \""
+ << op.name.value_or("<none>") << "\""
+ << "}";
+ return out;
+}
+
+std::ostream& operator<<(std::ostream& out, const HasCtpNtf& ntf) {
+ out << "\"HasCtpNtf\": {";
+ out << "\"opcode\": \"" << ntf.opcode << "\"";
+
+ if (ntf.opcode == PresetCtpOpcode::READ_PRESET_RESPONSE) {
+ out << ", \"is_last\": " << (ntf.is_last ? "\"True\"" : "\"False\"");
+ if (ntf.preset.has_value()) {
+ out << ", \"preset\": " << ntf.preset.value();
+ } else {
+ out << ", \"preset\": \"None\"";
+ }
+
+ } else if (ntf.opcode == PresetCtpOpcode::PRESET_CHANGED) {
+ out << ", \"change_id\": " << ntf.change_id;
+ out << ", \"is_last\": " << (ntf.is_last ? "\"True\"" : "\"False\"");
+ switch (ntf.change_id) {
+ case PresetCtpChangeId::PRESET_GENERIC_UPDATE:
+ out << ", \"prev_index\": " << +ntf.prev_index;
+ if (ntf.preset.has_value()) {
+ out << ", \"preset\": {" << ntf.preset.value() << "}";
+ } else {
+ out << ", \"preset\": \"None\"";
+ }
+ break;
+ case PresetCtpChangeId::PRESET_DELETED:
+ FALLTHROUGH;
+ case PresetCtpChangeId::PRESET_AVAILABLE:
+ FALLTHROUGH;
+ case PresetCtpChangeId::PRESET_UNAVAILABLE:
+ out << ", \"index\": " << +ntf.index;
+ break;
+ default:
+ break;
+ }
+ }
+ out << "}";
+
+ return out;
+}
+
+} // namespace has
+} // namespace le_audio
diff --git a/system/bta/has/has_ctp.h b/system/bta/has/has_ctp.h
new file mode 100644
index 0000000000..38cfe45514
--- /dev/null
+++ b/system/bta/has/has_ctp.h
@@ -0,0 +1,257 @@
+/*
+ * 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 <list>
+#include <optional>
+
+#include "hardware/bt_has.h"
+#include "has_preset.h"
+#include "osi/include/alarm.h"
+
+namespace le_audio {
+namespace has {
+/* HAS control point Change Id */
+enum class PresetCtpChangeId : uint8_t {
+ PRESET_GENERIC_UPDATE = 0,
+ PRESET_DELETED,
+ PRESET_AVAILABLE,
+ PRESET_UNAVAILABLE,
+ /* NOTICE: Values below are for internal use only of this particular
+ * implementation, and do not correspond to any bluetooth specification.
+ */
+ CHANGE_ID_MAX_ = PRESET_UNAVAILABLE,
+};
+std::ostream& operator<<(std::ostream& out, const PresetCtpChangeId value);
+
+/* HAS control point Opcodes */
+enum class PresetCtpOpcode : uint8_t {
+ READ_ALL_PRESETS = 0,
+ READ_PRESET_BY_INDEX,
+ READ_PRESET_RESPONSE,
+ PRESET_CHANGED,
+ WRITE_PRESET_NAME,
+ SET_ACTIVE_PRESET,
+ SET_NEXT_PRESET,
+ SET_PREV_PRESET,
+ SET_ACTIVE_PRESET_SYNC,
+ SET_NEXT_PRESET_SYNC,
+ SET_PREV_PRESET_SYNC,
+ /* NOTICE: Values below are for internal use only of this particular
+ * implementation, and do not correspond to any bluetooth specification.
+ */
+ OP_MAX_ = SET_PREV_PRESET_SYNC,
+ OP_NONE_ = OP_MAX_ + 1,
+};
+std::ostream& operator<<(std::ostream& out, const PresetCtpOpcode value);
+
+static constexpr uint16_t PresetCtpOpcode2Bitmask(PresetCtpOpcode op) {
+ return ((uint16_t)0b1 << static_cast<std::underlying_type_t<PresetCtpOpcode>>(
+ op));
+}
+
+/* Mandatory opcodes if control point characteristic exists */
+static constexpr uint16_t kControlPointMandatoryOpcodesBitmask =
+ PresetCtpOpcode2Bitmask(PresetCtpOpcode::READ_ALL_PRESETS) |
+ PresetCtpOpcode2Bitmask(PresetCtpOpcode::READ_PRESET_BY_INDEX) |
+ PresetCtpOpcode2Bitmask(PresetCtpOpcode::SET_ACTIVE_PRESET) |
+ PresetCtpOpcode2Bitmask(PresetCtpOpcode::SET_NEXT_PRESET) |
+ PresetCtpOpcode2Bitmask(PresetCtpOpcode::SET_PREV_PRESET);
+
+/* Optional coordinated operation opcodes */
+static constexpr uint16_t kControlPointSynchronizedOpcodesBitmask =
+ PresetCtpOpcode2Bitmask(PresetCtpOpcode::SET_ACTIVE_PRESET_SYNC) |
+ PresetCtpOpcode2Bitmask(PresetCtpOpcode::SET_NEXT_PRESET_SYNC) |
+ PresetCtpOpcode2Bitmask(PresetCtpOpcode::SET_PREV_PRESET_SYNC);
+
+/* Represents HAS Control Point value notification */
+struct HasCtpNtf {
+ PresetCtpOpcode opcode;
+ PresetCtpChangeId change_id;
+ bool is_last;
+ union {
+ uint8_t index;
+ uint8_t prev_index;
+ };
+ std::optional<HasPreset> preset;
+
+ static std::optional<HasCtpNtf> FromCharacteristicValue(uint16_t len,
+ const uint8_t* value);
+};
+std::ostream& operator<<(std::ostream& out, const HasCtpNtf& value);
+
+/* Represents HAS Control Point operation request */
+struct HasCtpOp {
+ std::variant<RawAddress, int> addr_or_group;
+ PresetCtpOpcode opcode;
+ uint8_t index;
+ std::optional<std::string> name;
+ uint16_t op_id;
+
+ HasCtpOp(std::variant<RawAddress, int> addr_or_group_id, PresetCtpOpcode op,
+ uint8_t index = bluetooth::has::kHasPresetIndexInvalid,
+ std::optional<std::string> name = std::nullopt)
+ : addr_or_group(addr_or_group_id), opcode(op), index(index), name(name) {
+ /* Skip 0 on roll-over */
+ last_op_id_ += 1;
+ if (last_op_id_ == 0) last_op_id_ = 1;
+ op_id = last_op_id_;
+ }
+
+ std::vector<uint8_t> ToCharacteristicValue(void) const;
+
+ bool IsGroupRequest() const {
+ return std::holds_alternative<int>(addr_or_group);
+ }
+
+ int GetGroupId() const {
+ return std::holds_alternative<int>(addr_or_group)
+ ? std::get<int>(addr_or_group)
+ : -1;
+ }
+
+ RawAddress GetDeviceAddr() const {
+ return std::holds_alternative<RawAddress>(addr_or_group)
+ ? std::get<RawAddress>(addr_or_group)
+ : RawAddress::kEmpty;
+ }
+
+ bool IsSyncedOperation() const {
+ return (opcode == PresetCtpOpcode::SET_ACTIVE_PRESET_SYNC) ||
+ (opcode == PresetCtpOpcode::SET_NEXT_PRESET_SYNC) ||
+ (opcode == PresetCtpOpcode::SET_PREV_PRESET_SYNC);
+ }
+
+ private:
+ /* It's fine for this to roll-over eventually */
+ static uint16_t last_op_id_;
+};
+std::ostream& operator<<(std::ostream& out, const HasCtpOp& value);
+
+/* Used to track group operations. SetCompleted() allows to mark
+ * a single device as operation-completed when notification is received.
+ * When all the devices are SetComplete'd, timeout timer is being canceled and
+ * a group operation can be considered completed (IsFullyCompleted() == true).
+ *
+ * NOTICE: A single callback and reference counter is being used for all the
+ * coordinator instances, therefore creating more instances result
+ * in timeout timer being rescheduled. User should remove all the
+ * pending op. coordinators in the timer timeout callback.
+ */
+struct HasCtpGroupOpCoordinator {
+ std::list<RawAddress> devices;
+ HasCtpOp operation;
+ std::list<bluetooth::has::PresetInfo> preset_info_verification_list;
+
+ static size_t ref_cnt;
+ static alarm_t* operation_timeout_timer;
+ static constexpr uint16_t kOperationTimeoutMs = 10000u;
+ static alarm_callback_t cb;
+
+ static void Initialize(alarm_callback_t c = nullptr) {
+ operation_timeout_timer = nullptr;
+ ref_cnt = 0;
+ cb = c;
+ }
+
+ static void Cleanup() {
+ if (operation_timeout_timer != nullptr) {
+ if (alarm_is_scheduled(operation_timeout_timer)) {
+ DLOG(INFO) << __func__ << +ref_cnt;
+ alarm_cancel(operation_timeout_timer);
+ }
+ alarm_free(operation_timeout_timer);
+ operation_timeout_timer = nullptr;
+ }
+
+ ref_cnt = 0;
+ }
+
+ static bool IsFullyCompleted() { return ref_cnt == 0; }
+ static bool IsPending() { return ref_cnt != 0; }
+
+ HasCtpGroupOpCoordinator() = delete;
+ HasCtpGroupOpCoordinator& operator=(const HasCtpGroupOpCoordinator&) = delete;
+ /* NOTICE: It cannot be non-copyable if we want to put it into the std::map.
+ * The default copy constructor and copy assignment operator would break the
+ * reference counting, so we must increment ref_cnt for all the temporary
+ * copies.
+ */
+ HasCtpGroupOpCoordinator(const HasCtpGroupOpCoordinator& other)
+ : devices(other.devices),
+ operation(other.operation),
+ preset_info_verification_list(other.preset_info_verification_list) {
+ ref_cnt += other.devices.size();
+ }
+
+ HasCtpGroupOpCoordinator(const std::vector<RawAddress>& targets,
+ HasCtpOp operation)
+ : operation(operation) {
+ LOG_ASSERT(targets.size() != 0) << " Empty device list error.";
+ if (targets.size() != 1) {
+ LOG_ASSERT(operation.IsGroupRequest()) << " Must be a group operation!";
+ LOG_ASSERT(operation.GetGroupId() != -1) << " Must set valid group_id!";
+ }
+
+ devices = std::list<RawAddress>(targets.cbegin(), targets.cend());
+
+ ref_cnt += devices.size();
+ if (operation_timeout_timer == nullptr) {
+ operation_timeout_timer = alarm_new("GroupOpTimer");
+ }
+
+ if (alarm_is_scheduled(operation_timeout_timer))
+ alarm_cancel(operation_timeout_timer);
+
+ LOG_ASSERT(cb != nullptr) << " Timeout timer callback not set!";
+ alarm_set_on_mloop(operation_timeout_timer, kOperationTimeoutMs, cb,
+ nullptr);
+ }
+
+ ~HasCtpGroupOpCoordinator() {
+ /* Check if cleanup wasn't already called */
+ if (ref_cnt != 0) {
+ ref_cnt -= devices.size();
+ if (ref_cnt == 0) {
+ Cleanup();
+ }
+ }
+ }
+
+ bool SetCompleted(RawAddress addr) {
+ auto result = false;
+
+ auto it = std::find(devices.begin(), devices.end(), addr);
+ if (it != devices.end()) {
+ devices.erase(it);
+ --ref_cnt;
+ result = true;
+ }
+
+ if (ref_cnt == 0) {
+ alarm_cancel(operation_timeout_timer);
+ alarm_free(operation_timeout_timer);
+ operation_timeout_timer = nullptr;
+ }
+
+ return result;
+ }
+};
+
+} // namespace has
+} // namespace le_audio
diff --git a/system/bta/has/has_journal.cc b/system/bta/has/has_journal.cc
new file mode 100644
index 0000000000..6953eebaad
--- /dev/null
+++ b/system/bta/has/has_journal.cc
@@ -0,0 +1,56 @@
+/*
+ * 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 "has_journal.h"
+
+#include "internal_include/bt_trace.h"
+
+namespace le_audio {
+namespace has {
+
+std::ostream& operator<<(std::ostream& os, const HasJournalRecord& r) {
+ os << "{";
+
+ char eventtime[20];
+ char temptime[20];
+ struct tm* tstamp = localtime(&r.timestamp.tv_sec);
+ strftime(temptime, sizeof(temptime), "%H:%M:%S", tstamp);
+ snprintf(eventtime, sizeof(eventtime), "%s.%03ld", temptime,
+ r.timestamp.tv_nsec / 1000000);
+ os << "\"time\": \"" << eventtime << "\", ";
+
+ if (r.is_operation) {
+ os << std::get<HasCtpOp>(r.event);
+ os << ", \"status\": \"" << loghex(r.op_status) << "\"";
+
+ } else if (r.is_notification) {
+ os << std::get<HasCtpNtf>(r.event) << ", ";
+
+ } else if (r.is_active_preset_change) {
+ os << "\"Active preset changed\": {\"active_preset_idx\": "
+ << +std::get<uint8_t>(r.event) << "}";
+
+ } else {
+ os << "\"Features changed\": {\"features\": \""
+ << loghex(std::get<uint8_t>(r.event)) << "\"}";
+ }
+
+ os << "}";
+ return os;
+}
+} // namespace has
+} // namespace le_audio
diff --git a/system/bta/has/has_journal.h b/system/bta/has/has_journal.h
new file mode 100644
index 0000000000..1d38f462ab
--- /dev/null
+++ b/system/bta/has/has_journal.h
@@ -0,0 +1,113 @@
+/*
+ * 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 <sys/time.h>
+#include <time.h>
+
+#include <list>
+#include <variant>
+
+#include "common/time_util.h"
+#include "has_ctp.h"
+
+/* Journal and journal entry classes used by the state dumping functionality. */
+namespace le_audio {
+namespace has {
+static constexpr uint8_t kHasJournalNumRecords = 20;
+
+struct HasJournalRecord {
+ /* Indicates which value the `event` contains (due to ambiguous uint8_t) */
+ bool is_operation : 1, is_notification : 1, is_features_change : 1,
+ is_active_preset_change : 1;
+ std::variant<HasCtpOp, HasCtpNtf, uint8_t> event;
+ struct timespec timestamp;
+
+ /* Operation context handle to match on GATT write response */
+ void* op_context_handle;
+
+ /* Status of the operation to be set once it gets completed */
+ uint8_t op_status;
+
+ HasJournalRecord(const HasCtpOp& op, void* context)
+ : event(op), op_context_handle(context) {
+ clock_gettime(CLOCK_REALTIME, &timestamp);
+ is_operation = true;
+ is_notification = false;
+ is_features_change = false;
+ is_active_preset_change = false;
+ }
+
+ HasJournalRecord(const HasCtpNtf& ntf) : event(ntf) {
+ clock_gettime(CLOCK_REALTIME, &timestamp);
+ is_operation = false;
+ is_notification = true;
+ is_features_change = false;
+ is_active_preset_change = false;
+ }
+
+ HasJournalRecord(uint8_t value, bool is_feat_change) : event(value) {
+ clock_gettime(CLOCK_REALTIME, &timestamp);
+ is_operation = false;
+ is_notification = false;
+ if (is_feat_change) {
+ is_active_preset_change = false;
+ is_features_change = true;
+ } else {
+ is_active_preset_change = true;
+ is_features_change = false;
+ }
+ }
+};
+std::ostream& operator<<(std::ostream& os, const HasJournalRecord& r);
+
+template <class valT, size_t cache_max>
+class CacheList {
+ public:
+ valT& Append(valT data) {
+ items_.push_front(std::move(data));
+
+ if (items_.size() > cache_max) {
+ items_.pop_back();
+ }
+
+ return items_.front();
+ }
+
+ using iterator = typename std::list<valT>::iterator;
+ iterator begin(void) { return items_.begin(); }
+ iterator end(void) { return items_.end(); }
+
+ using const_iterator = typename std::list<valT>::const_iterator;
+ const_iterator begin(void) const { return items_.begin(); }
+ const_iterator end(void) const { return items_.end(); }
+
+ void Erase(iterator it) {
+ if (it != items_.end()) items_.erase(it);
+ }
+
+ void Clear(void) { items_.clear(); }
+ bool isEmpty(void) { return items_.empty(); }
+
+ private:
+ typename std::list<valT> items_;
+};
+
+using HasJournal = CacheList<HasJournalRecord, kHasJournalNumRecords>;
+} // namespace has
+} // namespace le_audio
diff --git a/system/bta/has/has_preset.cc b/system/bta/has/has_preset.cc
new file mode 100644
index 0000000000..b54831150f
--- /dev/null
+++ b/system/bta/has/has_preset.cc
@@ -0,0 +1,116 @@
+/*
+ * 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 "has_preset.h"
+
+namespace le_audio {
+namespace has {
+
+std::optional<HasPreset> HasPreset::FromCharacteristicValue(
+ uint16_t& len, const uint8_t* value) {
+ if ((len < kCharValueMinSize) ||
+ (len > kCharValueMinSize + kPresetNameLengthLimit)) {
+ LOG(ERROR) << __func__ << " Preset record to long: " << len;
+ return std::nullopt;
+ }
+
+ HasPreset preset;
+ STREAM_TO_UINT8(preset.index_, value);
+ --len;
+ STREAM_TO_UINT8(preset.properties_, value);
+ --len;
+ preset.name_ = std::string(value, value + len);
+
+ return preset;
+}
+
+void HasPreset::ToCharacteristicValue(std::vector<uint8_t>& value) const {
+ auto initial_offset = value.size();
+
+ value.resize(value.size() + kCharValueMinSize + name_.size());
+ auto pp = value.data() + initial_offset;
+
+ UINT8_TO_STREAM(pp, index_);
+ UINT8_TO_STREAM(pp, properties_);
+ ARRAY_TO_STREAM(pp, name_.c_str(), (int)name_.size());
+}
+
+uint8_t* HasPreset::Serialize(uint8_t* p_out, size_t buffer_size) const {
+ if (buffer_size < SerializedSize()) {
+ LOG(ERROR) << "Invalid output buffer size!";
+ return p_out;
+ }
+
+ uint8_t name_len = name_.length();
+ if (name_len > kPresetNameLengthLimit) {
+ LOG(ERROR) << __func__
+ << " Invalid preset name length. Cannot be serialized!";
+ return p_out;
+ }
+
+ /* Serialized data length */
+ UINT8_TO_STREAM(p_out, name_len + 2);
+
+ UINT8_TO_STREAM(p_out, index_);
+ UINT8_TO_STREAM(p_out, properties_);
+ ARRAY_TO_STREAM(p_out, name_.c_str(), (int)name_.size());
+ return p_out;
+}
+
+const uint8_t* HasPreset::Deserialize(const uint8_t* p_in, size_t len,
+ HasPreset& preset) {
+ const uint8_t nonamed_size = HasPreset(0, 0).SerializedSize();
+ auto* p_curr = p_in;
+
+ if (len < nonamed_size) {
+ LOG(ERROR) << "Invalid buffer size " << +len << ". Cannot deserialize.";
+ return p_in;
+ }
+
+ uint8_t serialized_data_len;
+ STREAM_TO_UINT8(serialized_data_len, p_curr);
+ if (serialized_data_len < 2) {
+ LOG(ERROR) << __func__ << " Invalid data size. Cannot be deserialized!";
+ return p_in;
+ }
+
+ auto name_len = serialized_data_len - 2;
+ if ((name_len > kPresetNameLengthLimit) ||
+ ((size_t)nonamed_size + name_len > len)) {
+ LOG(ERROR) << __func__
+ << " Invalid preset name length. Cannot be deserialized!";
+ return p_in;
+ }
+
+ STREAM_TO_UINT8(preset.index_, p_curr);
+ STREAM_TO_UINT8(preset.properties_, p_curr);
+ if (name_len) preset.name_ = std::string((const char*)p_curr, name_len);
+
+ return p_curr + name_len;
+}
+
+std::ostream& operator<<(std::ostream& os, const HasPreset& b) {
+ os << "{\"index\": " << +b.GetIndex();
+ os << ", \"name\": \"" << b.GetName() << "\"";
+ os << ", \"is_available\": " << (b.IsAvailable() ? "\"True\"" : "\"False\"");
+ os << ", \"is_writable\": " << (b.IsWritable() ? "\"True\"" : "\"False\"");
+ os << "}";
+ return os;
+}
+
+} // namespace has
+} // namespace le_audio
diff --git a/system/bta/has/has_preset.h b/system/bta/has/has_preset.h
new file mode 100644
index 0000000000..fae011ade2
--- /dev/null
+++ b/system/bta/has/has_preset.h
@@ -0,0 +1,114 @@
+/*
+ * 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 <optional>
+#include <string>
+
+#include "bt_types.h"
+#include "hardware/bt_has.h"
+
+namespace le_audio {
+namespace has {
+/* Represents preset instance. It stores properties such as preset name,
+ * preset index and if it supports renaming. Also stores all the needed
+ * GATT characteristics and descriptor informations.
+ */
+class HasPreset {
+ private:
+ mutable std::string name_;
+ mutable uint8_t properties_;
+ uint8_t index_;
+
+ public:
+ static constexpr size_t kCharValueMinSize = 1 /*index*/ + 1 /*properties*/;
+
+ static constexpr uint8_t kPropertyWritable = 0x01;
+ static constexpr uint8_t kPropertyAvailable = 0x02;
+
+ static constexpr uint8_t kPresetNameLengthLimit = 40;
+
+ HasPreset(uint8_t index, uint8_t props = 0,
+ std::optional<std::string> name = std::nullopt)
+ : properties_(props), index_(index) {
+ name_ = name.value_or("");
+ }
+ HasPreset()
+ : name_(""),
+ properties_(0),
+ index_(bluetooth::has::kHasPresetIndexInvalid) {}
+
+ auto& GetName() const { return name_; }
+ decltype(index_) GetIndex() const { return index_; }
+ decltype(properties_) GetProperties() const { return properties_; }
+ bool IsWritable() const { return properties_ & kPropertyWritable; }
+ bool IsAvailable() const { return properties_ & kPropertyAvailable; }
+
+ HasPreset& operator=(const HasPreset& other) {
+ LOG_ASSERT(index_ == other.GetIndex())
+ << "Assigning immutable preset index!";
+
+ if ((this != &other) && (*this != other)) {
+ index_ = other.GetIndex();
+ name_ = other.GetName();
+ }
+ return *this;
+ }
+
+ bool operator==(const HasPreset& b) const {
+ return (index_ == b.index_) && (properties_ == b.properties_) &&
+ (name_ == b.name_);
+ }
+ bool operator!=(const HasPreset& b) const {
+ return (index_ != b.index_) || (properties_ != b.properties_) ||
+ (name_ != b.name_);
+ }
+ bool operator<(const HasPreset& b) const { return index_ < b.index_; }
+ friend std::ostream& operator<<(std::ostream& os, const HasPreset& b);
+
+ struct ComparatorDesc {
+ using is_transparent = void;
+ bool operator()(HasPreset const& a, int index) const {
+ return a.index_ < index;
+ }
+ bool operator()(int index, HasPreset const& a) const {
+ return index < a.index_;
+ }
+ bool operator()(HasPreset const& a, HasPreset const& b) const {
+ return a.index_ < b.index_;
+ }
+ };
+
+ static std::optional<HasPreset> FromCharacteristicValue(uint16_t& len,
+ const uint8_t* value);
+ void ToCharacteristicValue(std::vector<uint8_t>& value) const;
+
+ /* Calculates buffer space that the preset will use when serialized */
+ uint8_t SerializedSize() const {
+ return (sizeof(index_) + sizeof(properties_) + 1 /* name length */
+ + name_.length());
+ }
+ /* Serializes into binary blob for the persistent storage */
+ uint8_t* Serialize(uint8_t* p_out, size_t buffer_size) const;
+ /* Deserializes binary blob read from the persistent storage */
+ static const uint8_t* Deserialize(const uint8_t* p_in, size_t len,
+ HasPreset& preset);
+};
+
+} // namespace has
+} // namespace le_audio
diff --git a/system/bta/has/has_types.cc b/system/bta/has/has_types.cc
new file mode 100644
index 0000000000..6d886687f9
--- /dev/null
+++ b/system/bta/has/has_types.cc
@@ -0,0 +1,30 @@
+/*
+ * 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 "has_types.h"
+
+namespace le_audio {
+namespace has {
+
+std::ostream& operator<<(std::ostream& os, const HasDevice& b) {
+ os << "HAP device: {"
+ << "addr: " << b.addr << ", conn id: " << b.conn_id << "}";
+ return os;
+}
+
+} // namespace has
+} // namespace le_audio
diff --git a/system/bta/has/has_types.h b/system/bta/has/has_types.h
new file mode 100644
index 0000000000..89ed82a8ce
--- /dev/null
+++ b/system/bta/has/has_types.h
@@ -0,0 +1,417 @@
+/*
+ * 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 <numeric>
+#include <optional>
+#include <set>
+#include <vector>
+
+#include "bta_gatt_api.h"
+#include "gap_api.h"
+#include "hardware/bt_has.h"
+#include "has_ctp.h"
+#include "has_journal.h"
+#include "has_preset.h"
+
+namespace le_audio {
+namespace has {
+
+/* Helper class to pass some minimal context through the GATT operation API. */
+union HasGattOpContext {
+ public:
+ void* ptr = nullptr;
+ struct {
+ /* Ctp. Operation ID or 0 if not a control point operation context */
+ uint16_t ctp_op_id;
+
+ /* Additional user flags */
+ uint8_t context_flags;
+ };
+
+ /* Flags describing operation context */
+ static constexpr uint8_t kContextFlagsEnableNotification = 0x01;
+ static constexpr uint8_t kIsNotNull = 0x02;
+
+ static constexpr uint8_t kStatusCodeNotSet = 0xF0;
+
+ HasGattOpContext(const HasCtpOp& ctp_op, uint8_t flags = 0) {
+ ctp_op_id = ctp_op.op_id;
+ /* Differ from nullptr in at least 1 bit when everything else is 0 */
+ context_flags = flags | kIsNotNull;
+ }
+ HasGattOpContext(uint8_t flags) : ctp_op_id(0) {
+ context_flags = flags | kIsNotNull;
+ }
+ HasGattOpContext(void* pp) {
+ ptr = pp;
+ /* Differ from nullptr in at least 1 bit when everything else is 0 */
+ context_flags |= kIsNotNull;
+ }
+ operator void*() { return ptr; }
+};
+
+/* Context must be constrained to void* size to pass through the GATT API */
+static_assert(sizeof(HasGattOpContext) <= sizeof(void*));
+
+/* Service UUIDs */
+/* FIXME: actually these were not yet assigned - using placeholders for now. */
+static const bluetooth::Uuid kUuidHearingAccessService =
+ bluetooth::Uuid::From16Bit(0xEEEE);
+static const bluetooth::Uuid kUuidHearingAidFeatures =
+ bluetooth::Uuid::From16Bit(0xEEED);
+static const bluetooth::Uuid kUuidHearingAidPresetControlPoint =
+ bluetooth::Uuid::From16Bit(0xEEEC);
+static const bluetooth::Uuid kUuidActivePresetIndex =
+ bluetooth::Uuid::From16Bit(0xEEEB);
+
+/* Base device class for the GATT-based service clients */
+class GattServiceDevice {
+ public:
+ RawAddress addr;
+ uint16_t conn_id = GATT_INVALID_CONN_ID;
+ uint16_t service_handle = GAP_INVALID_HANDLE;
+ bool is_connecting_actively = false;
+
+ uint8_t gatt_svc_validation_steps = 0xFE;
+ bool isGattServiceValid() { return gatt_svc_validation_steps == 0; }
+
+ GattServiceDevice(const RawAddress& addr, bool connecting_actively = false)
+ : addr(addr), is_connecting_actively(connecting_actively) {}
+
+ GattServiceDevice() : GattServiceDevice(RawAddress::kEmpty) {}
+
+ bool IsConnected() const { return conn_id != GATT_INVALID_CONN_ID; }
+
+ class MatchAddress {
+ private:
+ RawAddress addr;
+
+ public:
+ MatchAddress(RawAddress addr) : addr(addr) {}
+ bool operator()(const GattServiceDevice& other) const {
+ return (addr == other.addr);
+ }
+ };
+
+ class MatchConnId {
+ private:
+ uint16_t conn_id;
+
+ public:
+ MatchConnId(uint16_t conn_id) : conn_id(conn_id) {}
+ bool operator()(const GattServiceDevice& other) const {
+ return (conn_id == other.conn_id);
+ }
+ };
+
+ void Dump(std::ostream& os) const {
+ os << "\"addr\": \"" << addr << "\"";
+ os << ", \"conn_id\": " << conn_id;
+ os << ", \"is_gatt_service_valid\": "
+ << (gatt_svc_validation_steps == 0 ? "\"True\"" : "\"False\"") << "("
+ << +gatt_svc_validation_steps << ")";
+ os << ", \"is_connecting_actively\": "
+ << (is_connecting_actively ? "\"True\"" : "\"False\"");
+ }
+};
+
+/* Build on top of the base GattServiceDevice extends the base device context
+ * with service specific informations such as the currently active preset,
+ * all available presets, and supported optional operations. It also stores
+ * HAS service specific GATT informations such as characteristic handles.
+ */
+class HasDevice : public GattServiceDevice {
+ uint8_t features = 0x00;
+ uint16_t supported_opcodes_bitmask = 0x0000;
+
+ void RefreshSupportedOpcodesBitmask(void) {
+ supported_opcodes_bitmask = 0;
+
+ /* Some opcodes are mandatory but the characteristics aren't - these are
+ * conditional then.
+ */
+ if ((cp_handle != GAP_INVALID_HANDLE) &&
+ (active_preset_handle != GAP_INVALID_HANDLE)) {
+ supported_opcodes_bitmask |= kControlPointMandatoryOpcodesBitmask;
+ }
+
+ if (features & bluetooth::has::kFeatureBitPresetSynchronizationSupported) {
+ supported_opcodes_bitmask |= kControlPointMandatoryOpcodesBitmask;
+ supported_opcodes_bitmask |= kControlPointSynchronizedOpcodesBitmask;
+ }
+
+ if (features & bluetooth::has::kFeatureBitWritablePresets) {
+ supported_opcodes_bitmask |=
+ PresetCtpOpcode2Bitmask(PresetCtpOpcode::WRITE_PRESET_NAME);
+ }
+ }
+
+ public:
+ /* Char handle and current ccc value */
+ uint16_t active_preset_handle = GAP_INVALID_HANDLE;
+ uint16_t active_preset_ccc_handle = GAP_INVALID_HANDLE;
+ uint16_t cp_handle = GAP_INVALID_HANDLE;
+ uint16_t cp_ccc_handle = GAP_INVALID_HANDLE;
+ uint16_t features_handle = GAP_INVALID_HANDLE;
+ uint16_t features_ccc_handle = GAP_INVALID_HANDLE;
+
+ bool features_notifications_enabled = false;
+
+ /* Presets in the ascending order of their indices */
+ std::set<HasPreset, HasPreset::ComparatorDesc> has_presets;
+ uint8_t currently_active_preset = bluetooth::has::kHasPresetIndexInvalid;
+
+ std::list<HasCtpNtf> ctp_notifications_;
+ HasJournal has_journal_;
+
+ HasDevice(const RawAddress& addr, uint8_t features)
+ : GattServiceDevice(addr) {
+ UpdateFeatures(features);
+ }
+
+ void ConnectionCleanUp() {
+ conn_id = GATT_INVALID_CONN_ID;
+ is_connecting_actively = false;
+ ctp_notifications_.clear();
+ }
+
+ using GattServiceDevice::GattServiceDevice;
+
+ uint8_t GetFeatures() const { return features; }
+
+ void UpdateFeatures(uint8_t new_features) {
+ features = new_features;
+ /* Update the dependent supported feature set */
+ RefreshSupportedOpcodesBitmask();
+ }
+
+ void ClearSvcData() {
+ GattServiceDevice::service_handle = GAP_INVALID_HANDLE;
+ GattServiceDevice::gatt_svc_validation_steps = 0xFE;
+
+ active_preset_handle = GAP_INVALID_HANDLE;
+ active_preset_ccc_handle = GAP_INVALID_HANDLE;
+ cp_handle = GAP_INVALID_HANDLE;
+ cp_ccc_handle = GAP_INVALID_HANDLE;
+ features_handle = GAP_INVALID_HANDLE;
+ features_ccc_handle = GAP_INVALID_HANDLE;
+
+ features = 0;
+ features_notifications_enabled = false;
+
+ supported_opcodes_bitmask = 0x00;
+ currently_active_preset = bluetooth::has::kHasPresetIndexInvalid;
+
+ has_presets.clear();
+ }
+
+ inline bool SupportsPresets() const {
+ return (active_preset_handle != GAP_INVALID_HANDLE) &&
+ (cp_handle != GAP_INVALID_HANDLE);
+ }
+
+ inline bool SupportsActivePresetNotification() const {
+ return active_preset_ccc_handle != GAP_INVALID_HANDLE;
+ }
+
+ inline bool SupportsFeaturesNotification() const {
+ return features_ccc_handle != GAP_INVALID_HANDLE;
+ }
+
+ inline bool HasFeaturesNotificationEnabled() const {
+ return features_notifications_enabled;
+ }
+
+ inline bool SupportsOperation(PresetCtpOpcode op) {
+ auto mask = PresetCtpOpcode2Bitmask(op);
+ return (supported_opcodes_bitmask & mask) == mask;
+ }
+
+ bool IsValidPreset(uint8_t preset_index, bool writable_only = false) const {
+ if (has_presets.count(preset_index)) {
+ return writable_only ? has_presets.find(preset_index)->IsWritable()
+ : true;
+ }
+ return false;
+ }
+
+ const HasPreset* GetPreset(uint8_t preset_index,
+ bool writable_only = false) const {
+ if (has_presets.count(preset_index)) {
+ decltype(has_presets)::iterator preset = has_presets.find(preset_index);
+ if (writable_only) return preset->IsWritable() ? &*preset : nullptr;
+ return &*preset;
+ }
+ return nullptr;
+ }
+
+ std::optional<bluetooth::has::PresetInfo> GetPresetInfo(uint8_t index) const {
+ if (has_presets.count(index)) {
+ auto preset = *has_presets.find(index);
+ return bluetooth::has::PresetInfo({.preset_index = preset.GetIndex(),
+ .writable = preset.IsWritable(),
+ .available = preset.IsAvailable(),
+ .preset_name = preset.GetName()});
+ }
+ return std::nullopt;
+ }
+
+ std::vector<bluetooth::has::PresetInfo> GetAllPresetInfo() const {
+ std::vector<bluetooth::has::PresetInfo> all_info;
+ all_info.reserve(has_presets.size());
+
+ for (auto const& preset : has_presets) {
+ DLOG(INFO) << __func__ << " preset: " << preset;
+ all_info.push_back({.preset_index = preset.GetIndex(),
+ .writable = preset.IsWritable(),
+ .available = preset.IsAvailable(),
+ .preset_name = preset.GetName()});
+ }
+ return all_info;
+ }
+
+ /* Calculates the buffer space that all the preset will use when serialized */
+ uint8_t SerializedPresetsSize() const {
+ /* Two additional bytes are for the header and the number of presets */
+ return std::accumulate(has_presets.begin(), has_presets.end(), 0,
+ [](uint8_t current, auto const& preset) {
+ return current + preset.SerializedSize();
+ }) +
+ 2;
+ }
+
+ /* Serializes all the presets into a binary blob for persistent storage */
+ bool SerializePresets(std::vector<uint8_t>& out) const {
+ auto buffer_size = SerializedPresetsSize();
+ auto buffer_offset = out.size();
+
+ out.resize(out.size() + buffer_size);
+ auto p_out = out.data() + buffer_offset;
+
+ UINT8_TO_STREAM(p_out, kHasDeviceBinaryBlobHdr);
+ UINT8_TO_STREAM(p_out, has_presets.size());
+
+ auto* const p_end = p_out + buffer_size;
+ for (auto& preset : has_presets) {
+ if (p_out + preset.SerializedSize() >= p_end) {
+ LOG(ERROR) << "Serialization error.";
+ return false;
+ }
+ p_out = preset.Serialize(p_out, p_end - p_out);
+ }
+
+ return true;
+ }
+
+ /* Deserializes all the presets from a binary blob read from the persistent
+ * storage.
+ */
+ static bool DeserializePresets(const uint8_t* p_in, size_t len,
+ HasDevice& device) {
+ HasPreset preset;
+ if (len < 2 + preset.SerializedSize()) {
+ LOG(ERROR) << "Deserialization error. Invalid input buffer size length.";
+ return false;
+ }
+ auto* p_end = p_in + len;
+
+ uint8_t hdr;
+ STREAM_TO_UINT8(hdr, p_in);
+ if (hdr != kHasDeviceBinaryBlobHdr) {
+ LOG(ERROR) << __func__ << " Deserialization error. Bad header.";
+ return false;
+ }
+
+ uint8_t num_presets;
+ STREAM_TO_UINT8(num_presets, p_in);
+
+ device.has_presets.clear();
+ while (p_in < p_end) {
+ auto* p_new = HasPreset::Deserialize(p_in, p_end - p_in, preset);
+ if (p_new <= p_in) {
+ LOG(ERROR) << "Deserialization error. Invalid preset found.";
+ device.has_presets.clear();
+ return false;
+ }
+
+ device.has_presets.insert(preset);
+ p_in = p_new;
+ }
+
+ return device.has_presets.size() == num_presets;
+ }
+
+ friend std::ostream& operator<<(std::ostream& os, const HasDevice& b);
+
+ void Dump(std::ostream& os) const {
+ GattServiceDevice::Dump(os);
+ os << ", \"features\": \"" << loghex(features) << "\"";
+ os << ", \"features_notifications_enabled\": "
+ << (features_notifications_enabled ? "\"Enabled\"" : "\"Disabled\"");
+ os << ", \"ctp_notifications size\": " << ctp_notifications_.size();
+ os << ",\n";
+
+ os << " "
+ << "\"presets\": [";
+ for (auto const& preset : has_presets) {
+ os << "\n " << preset << ",";
+ }
+ os << "\n ],\n";
+
+ os << " "
+ << "\"Ctp. notifications process queue\": {";
+ if (ctp_notifications_.size() != 0) {
+ size_t ntf_pos = 0;
+ for (auto const& ntf : ctp_notifications_) {
+ os << "\n ";
+ if (ntf_pos == 0) {
+ os << "\"latest\": ";
+ } else {
+ os << "\"-" << ntf_pos << "\": ";
+ }
+
+ os << ntf << ",";
+ ++ntf_pos;
+ }
+ }
+ os << "\n },\n";
+
+ os << " "
+ << "\"event history\": {";
+ size_t pos = 0;
+ for (auto const& record : has_journal_) {
+ os << "\n ";
+ if (pos == 0) {
+ os << "\"latest\": ";
+ } else {
+ os << "\"-" << pos << "\": ";
+ }
+
+ os << record << ",";
+ ++pos;
+ }
+ os << "\n }";
+ }
+
+ private:
+ static constexpr int kHasDeviceBinaryBlobHdr = 0x55;
+};
+
+} // namespace has
+} // namespace le_audio
diff --git a/system/bta/include/bta_has_api.h b/system/bta/include/bta_has_api.h
new file mode 100644
index 0000000000..ae9cc5255a
--- /dev/null
+++ b/system/bta/include/bta_has_api.h
@@ -0,0 +1,54 @@
+/*
+ * 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 <string>
+
+#include "hardware/bt_has.h"
+
+namespace le_audio {
+namespace has {
+class HasClient {
+ public:
+ virtual ~HasClient() = default;
+
+ static void Initialize(bluetooth::has::HasClientCallbacks* callbacks,
+ base::Closure initCb);
+ static void CleanUp();
+ static HasClient* Get();
+ static void DebugDump(int fd);
+ static bool IsHasClientRunning();
+ static void AddFromStorage(const RawAddress& addr, uint8_t features,
+ uint16_t is_acceptlisted);
+ virtual void Connect(const RawAddress& addr) = 0;
+ virtual void Disconnect(const RawAddress& addr) = 0;
+ virtual void SelectActivePreset(
+ std::variant<RawAddress, int> addr_or_group_id, uint8_t preset_index) = 0;
+ virtual void NextActivePreset(
+ std::variant<RawAddress, int> addr_or_group_id) = 0;
+ virtual void PreviousActivePreset(
+ std::variant<RawAddress, int> addr_or_group_id) = 0;
+ virtual void GetPresetInfo(const RawAddress& addr, uint8_t preset_index) = 0;
+ virtual void SetPresetName(std::variant<RawAddress, int> addr_or_group_id,
+ uint8_t preset_index, std::string name) = 0;
+};
+
+} // namespace has
+} // namespace le_audio
diff --git a/system/bta/include/bta_le_audio_uuids.h b/system/bta/include/bta_le_audio_uuids.h
new file mode 100644
index 0000000000..9ce42ebaba
--- /dev/null
+++ b/system/bta/include/bta_le_audio_uuids.h
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * 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
+
+/* Common Audio Service */
+#define UUID_COMMON_AUDIO_SERVICE 0x1853
diff --git a/system/bta/le_audio/le_audio_types.h b/system/bta/le_audio/le_audio_types.h
index 48b6901d3d..382683ad3c 100644
--- a/system/bta/le_audio/le_audio_types.h
+++ b/system/bta/le_audio/le_audio_types.h
@@ -33,6 +33,7 @@
#include "bta_groups.h"
#include "bta_le_audio_api.h"
+#include "bta_le_audio_uuids.h"
#include "btm_iso_api_types.h"
namespace le_audio {
@@ -59,7 +60,7 @@ namespace uuid {
* CSIS
*/
static const bluetooth::Uuid kCapServiceUuid =
- bluetooth::Uuid::From16Bit(0x1853);
+ bluetooth::Uuid::From16Bit(UUID_COMMON_AUDIO_SERVICE);
/* Assigned numbers for attributes */
static const bluetooth::Uuid kPublishedAudioCapabilityServiceUuid =
diff --git a/system/bta/test/common/btif_storage_mock.cc b/system/bta/test/common/btif_storage_mock.cc
index 1d389c6805..ab481e453a 100644
--- a/system/bta/test/common/btif_storage_mock.cc
+++ b/system/bta/test/common/btif_storage_mock.cc
@@ -36,4 +36,47 @@ void btif_storage_set_leaudio_autoconnect(RawAddress const& addr,
void btif_storage_remove_leaudio(RawAddress const& addr) {
LOG_ASSERT(btif_storage_interface) << "Mock storage module not set!";
btif_storage_interface->RemoveLeaudio(addr);
+}
+
+void btif_storage_add_leaudio_has_device(const RawAddress& address,
+ std::vector<uint8_t> presets_bin,
+ uint8_t features,
+ uint8_t active_preset) {
+ LOG_ASSERT(btif_storage_interface) << "Mock storage module not set!";
+ btif_storage_interface->AddLeaudioHasDevice(address, presets_bin, features,
+ active_preset);
+};
+
+bool btif_storage_get_leaudio_has_presets(const RawAddress& address,
+ std::vector<uint8_t>& presets_bin,
+ uint8_t& active_preset) {
+ if (btif_storage_interface)
+ return btif_storage_interface->GetLeaudioHasPresets(address, presets_bin,
+ active_preset);
+
+ return false;
+};
+
+void btif_storage_set_leaudio_has_presets(const RawAddress& address,
+ std::vector<uint8_t> presets_bin) {
+ LOG_ASSERT(btif_storage_interface) << "Mock storage module not set!";
+ btif_storage_interface->SetLeaudioHasPresets(address, presets_bin);
+}
+
+bool btif_storage_get_leaudio_has_features(const RawAddress& address,
+ uint8_t& features) {
+ LOG_ASSERT(btif_storage_interface) << "Mock storage module not set!";
+ return btif_storage_interface->GetLeaudioHasFeatures(address, features);
+}
+
+void btif_storage_set_leaudio_has_features(const RawAddress& address,
+ uint8_t features) {
+ LOG_ASSERT(btif_storage_interface) << "Mock storage module not set!";
+ btif_storage_interface->SetLeaudioHasFeatures(address, features);
+}
+
+void btif_storage_set_leaudio_has_active_preset(const RawAddress& address,
+ uint8_t active_preset) {
+ LOG_ASSERT(btif_storage_interface) << "Mock storage module not set!";
+ btif_storage_interface->SetLeaudioHasActivePreset(address, active_preset);
} \ No newline at end of file
diff --git a/system/bta/test/common/btif_storage_mock.h b/system/bta/test/common/btif_storage_mock.h
index 1087bc9c59..e4ff516a1a 100644
--- a/system/bta/test/common/btif_storage_mock.h
+++ b/system/bta/test/common/btif_storage_mock.h
@@ -28,6 +28,21 @@ class BtifStorageInterface {
virtual void AddLeaudioAutoconnect(RawAddress const& addr,
bool autoconnect) = 0;
virtual void RemoveLeaudio(RawAddress const& addr) = 0;
+ virtual void AddLeaudioHasDevice(const RawAddress& address,
+ std::vector<uint8_t> presets_bin,
+ uint8_t features, uint8_t active_preset) = 0;
+ virtual void SetLeaudioHasPresets(const RawAddress& address,
+ std::vector<uint8_t> presets_bin) = 0;
+ virtual bool GetLeaudioHasFeatures(const RawAddress& address,
+ uint8_t& features) = 0;
+ virtual void SetLeaudioHasFeatures(const RawAddress& address,
+ uint8_t features) = 0;
+ virtual void SetLeaudioHasActivePreset(const RawAddress& address,
+ uint8_t active_preset) = 0;
+ virtual bool GetLeaudioHasPresets(const RawAddress& address,
+ std::vector<uint8_t>& presets_bin,
+ uint8_t& active_preset) = 0;
+
virtual ~BtifStorageInterface() = default;
};
@@ -36,6 +51,23 @@ class MockBtifStorageInterface : public BtifStorageInterface {
MOCK_METHOD((void), AddLeaudioAutoconnect,
(RawAddress const& addr, bool autoconnect), (override));
MOCK_METHOD((void), RemoveLeaudio, (RawAddress const& addr), (override));
+ MOCK_METHOD((void), AddLeaudioHasDevice,
+ (const RawAddress& address, std::vector<uint8_t> presets_bin,
+ uint8_t features, uint8_t active_preset),
+ (override));
+ MOCK_METHOD((bool), GetLeaudioHasPresets,
+ (const RawAddress& address, std::vector<uint8_t>& presets_bin,
+ uint8_t& active_preset),
+ (override));
+ MOCK_METHOD((void), SetLeaudioHasPresets,
+ (const RawAddress& address, std::vector<uint8_t> presets_bin),
+ (override));
+ MOCK_METHOD((bool), GetLeaudioHasFeatures,
+ (const RawAddress& address, uint8_t& features), (override));
+ MOCK_METHOD((void), SetLeaudioHasFeatures,
+ (const RawAddress& address, uint8_t features), (override));
+ MOCK_METHOD((void), SetLeaudioHasActivePreset,
+ (const RawAddress& address, uint8_t active_preset), (override));
};
/**
diff --git a/system/btif/Android.bp b/system/btif/Android.bp
index 50871ff912..60f1eebc7f 100644
--- a/system/btif/Android.bp
+++ b/system/btif/Android.bp
@@ -129,6 +129,7 @@ cc_defaults {
"src/btif_gatt_test.cc",
"src/btif_gatt_util.cc",
"src/btif_vc.cc",
+ "src/btif_has_client.cc",
"src/btif_hearing_aid.cc",
"src/btif_hf.cc",
"src/btif_hf_client.cc",
@@ -437,6 +438,7 @@ cc_test {
":TestMockBtaCsis",
":TestMockBtaGatt",
":TestMockBtaGroups",
+ ":TestMockBtaHas",
":TestMockBtaHd",
":TestMockBtaHearingAid",
":TestMockBtaHf",
diff --git a/system/btif/include/btif_storage.h b/system/btif/include/btif_storage.h
index a8f72b39d7..3e154b1d75 100644
--- a/system/btif/include/btif_storage.h
+++ b/system/btif/include/btif_storage.h
@@ -289,6 +289,16 @@ void btif_storage_remove_leaudio(const RawAddress& address);
/** Load bonded Le Audio devices */
void btif_storage_load_bonded_leaudio(void);
+/** Loads information about bonded HAS devices */
+void btif_storage_load_bonded_leaudio_has_devices(void);
+
+/** Deletes the bonded HAS device info from NVRAM */
+void btif_storage_remove_leaudio_has(const RawAddress& address);
+
+/** Set/Unset the HAS device acceptlist flag. */
+void btif_storage_set_leaudio_has_acceptlist(const RawAddress& address,
+ bool add_to_acceptlist);
+
/*******************************************************************************
*
* Function btif_storage_is_retricted_device
diff --git a/system/btif/src/bluetooth.cc b/system/btif/src/bluetooth.cc
index 2e4eee8db1..ee865fad31 100644
--- a/system/btif/src/bluetooth.cc
+++ b/system/btif/src/bluetooth.cc
@@ -32,6 +32,7 @@
#include <hardware/bt_av.h>
#include <hardware/bt_csis.h>
#include <hardware/bt_gatt.h>
+#include <hardware/bt_has.h>
#include <hardware/bt_hd.h>
#include <hardware/bt_hearing_aid.h>
#include <hardware/bt_hf_client.h>
@@ -49,6 +50,7 @@
#include "bt_utils.h"
#include "bta/include/bta_csis_api.h"
+#include "bta/include/bta_has_api.h"
#include "bta/include/bta_hearing_aid_api.h"
#include "bta/include/bta_hf_client_api.h"
#include "bta/include/bta_le_audio_api.h"
@@ -88,6 +90,7 @@
#include "types/raw_address.h"
using bluetooth::csis::CsisClientInterface;
+using bluetooth::has::HasClientInterface;
using bluetooth::hearing_aid::HearingAidInterface;
#ifndef TARGET_FLOSS
using bluetooth::le_audio::LeAudioBroadcasterInterface;
@@ -136,6 +139,8 @@ extern const btsdp_interface_t* btif_sdp_get_interface();
/*Hearing Aid client*/
extern HearingAidInterface* btif_hearing_aid_get_interface();
#ifndef TARGET_FLOSS
+/* Hearing Access client */
+extern HasClientInterface* btif_has_client_get_interface();
/* LeAudio testi client */
extern LeAudioClientInterface* btif_le_audio_get_interface();
/* LeAudio Broadcaster */
@@ -420,6 +425,7 @@ static void dump(int fd, const char** arguments) {
osi_allocator_debug_dump(fd);
alarm_debug_dump(fd);
bluetooth::csis::CsisClient::DebugDump(fd);
+ le_audio::has::HasClient::DebugDump(fd);
HearingAid::DebugDump(fd);
#ifndef TARGET_FLOSS
LeAudioClient::DebugDump(fd);
@@ -480,6 +486,9 @@ static const void* get_profile_interface(const char* profile_id) {
if (is_profile(profile_id, BT_PROFILE_HEARING_AID_ID))
return btif_hearing_aid_get_interface();
+ if (is_profile(profile_id, BT_PROFILE_HAP_CLIENT_ID))
+ return btif_has_client_get_interface();
+
if (is_profile(profile_id, BT_KEYSTORE_ID))
return bluetooth::bluetooth_keystore::getBluetoothKeystoreInterface();
diff --git a/system/btif/src/btif_dm.cc b/system/btif/src/btif_dm.cc
index 3efb3276c8..989f7399a2 100644
--- a/system/btif/src/btif_dm.cc
+++ b/system/btif/src/btif_dm.cc
@@ -95,6 +95,8 @@ const Uuid UUID_VC = Uuid::FromString("1844");
const Uuid UUID_CSIS = Uuid::FromString("1846");
const Uuid UUID_LE_AUDIO = Uuid::FromString("184E");
const Uuid UUID_LE_MIDI = Uuid::FromString("03B80E5A-EDE8-4B33-A751-6CE34EC4C700");
+/* FIXME: Not known yet, using a placeholder instead. */
+const Uuid UUID_HAS = Uuid::FromString("EEEEEEEE-EEEE-EEEE-EEEE-EEEEEEEEEEEE");
const bool enable_address_consolidate = false; // TODO remove
#define COD_MASK 0x07FF
@@ -1333,8 +1335,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 ||
- uuid == UUID_VC || uuid == UUID_CSIS || uuid == UUID_LE_AUDIO ||
- uuid == UUID_LE_MIDI);
+ uuid == UUID_VC || uuid == UUID_CSIS || uuid == UUID_LE_AUDIO ||
+ uuid == UUID_LE_MIDI || uuid == UUID_HAS);
}
/*******************************************************************************
diff --git a/system/btif/src/btif_has_client.cc b/system/btif/src/btif_has_client.cc
new file mode 100644
index 0000000000..e480077d49
--- /dev/null
+++ b/system/btif/src/btif_has_client.cc
@@ -0,0 +1,244 @@
+/*
+ * 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/location.h>
+#include <base/logging.h>
+#include <hardware/bluetooth.h>
+#include <hardware/bt_has.h>
+
+#include <bitset>
+#include <string>
+#include <vector>
+
+#include "bta_has_api.h"
+#include "btif_common.h"
+#include "btif_storage.h"
+#include "stack/include/btu.h"
+
+using base::Bind;
+using base::Owned;
+using base::Passed;
+using base::Unretained;
+using bluetooth::has::ConnectionState;
+using bluetooth::has::ErrorCode;
+using bluetooth::has::HasClientCallbacks;
+using bluetooth::has::HasClientInterface;
+using bluetooth::has::PresetInfo;
+using bluetooth::has::PresetInfoReason;
+
+using le_audio::has::HasClient;
+
+namespace {
+std::unique_ptr<HasClientInterface> has_client_instance;
+
+class HearingAaccessClientServiceInterfaceImpl : public HasClientInterface,
+ public HasClientCallbacks {
+ ~HearingAaccessClientServiceInterfaceImpl() override = default;
+
+ void Init(HasClientCallbacks* callbacks) override {
+ DVLOG(2) << __func__;
+ this->callbacks_ = callbacks;
+
+ do_in_main_thread(
+ FROM_HERE,
+ Bind(&HasClient::Initialize, this,
+ jni_thread_wrapper(
+ FROM_HERE,
+ Bind(&btif_storage_load_bonded_leaudio_has_devices))));
+ }
+
+ void Connect(const RawAddress& addr) override {
+ DVLOG(2) << __func__ << " addr: " << addr;
+ do_in_main_thread(FROM_HERE, Bind(&HasClient::Connect,
+ Unretained(HasClient::Get()), addr));
+
+ do_in_jni_thread(
+ FROM_HERE, Bind(&btif_storage_set_leaudio_has_acceptlist, addr, true));
+ }
+
+ void Disconnect(const RawAddress& addr) override {
+ DVLOG(2) << __func__ << " addr: " << addr;
+ do_in_main_thread(FROM_HERE, Bind(&HasClient::Disconnect,
+ Unretained(HasClient::Get()), addr));
+
+ do_in_jni_thread(
+ FROM_HERE, Bind(&btif_storage_set_leaudio_has_acceptlist, addr, false));
+ }
+
+ void SelectActivePreset(std::variant<RawAddress, int> addr_or_group_id,
+ uint8_t preset_index) override {
+ DVLOG(2) << __func__ << " preset_index: " << preset_index;
+
+ do_in_main_thread(
+ FROM_HERE,
+ Bind(&HasClient::SelectActivePreset, Unretained(HasClient::Get()),
+ std::move(addr_or_group_id), preset_index));
+ }
+
+ void NextActivePreset(
+ std::variant<RawAddress, int> addr_or_group_id) override {
+ DVLOG(2) << __func__;
+
+ do_in_main_thread(FROM_HERE, Bind(&HasClient::NextActivePreset,
+ Unretained(HasClient::Get()),
+ std::move(addr_or_group_id)));
+ }
+
+ void PreviousActivePreset(
+ std::variant<RawAddress, int> addr_or_group_id) override {
+ DVLOG(2) << __func__;
+
+ do_in_main_thread(FROM_HERE, Bind(&HasClient::PreviousActivePreset,
+ Unretained(HasClient::Get()),
+ std::move(addr_or_group_id)));
+ }
+
+ void GetPresetInfo(const RawAddress& addr, uint8_t preset_index) override {
+ DVLOG(2) << __func__ << " addr: " << addr
+ << " preset_index: " << preset_index;
+
+ do_in_main_thread(
+ FROM_HERE, Bind(&HasClient::GetPresetInfo, Unretained(HasClient::Get()),
+ addr, preset_index));
+ }
+
+ void SetPresetName(std::variant<RawAddress, int> addr_or_group_id,
+ uint8_t preset_index, std::string preset_name) override {
+ DVLOG(2) << __func__ << " preset_index: " << preset_index
+ << " preset_name: " << preset_name;
+
+ do_in_main_thread(
+ FROM_HERE, Bind(&HasClient::SetPresetName, Unretained(HasClient::Get()),
+ std::move(addr_or_group_id), preset_index,
+ std::move(preset_name)));
+ }
+
+ void RemoveDevice(const RawAddress& addr) override {
+ DVLOG(2) << __func__ << " addr: " << addr;
+
+ /* RemoveDevice can be called on devices that don't have BAS enabled */
+ if (HasClient::IsHasClientRunning()) {
+ do_in_main_thread(FROM_HERE, Bind(&HasClient::Disconnect,
+ Unretained(HasClient::Get()), addr));
+ }
+
+ do_in_jni_thread(FROM_HERE, Bind(&btif_storage_remove_leaudio_has, addr));
+ }
+
+ void Cleanup(void) override {
+ DVLOG(2) << __func__;
+ do_in_main_thread(FROM_HERE, Bind(&HasClient::CleanUp));
+ }
+
+ void OnConnectionState(ConnectionState state,
+ const RawAddress& addr) override {
+ DVLOG(2) << __func__ << " addr: " << addr;
+ do_in_jni_thread(FROM_HERE, Bind(&HasClientCallbacks::OnConnectionState,
+ Unretained(callbacks_), state, addr));
+ }
+
+ void OnDeviceAvailable(const RawAddress& addr, uint8_t features) override {
+ DVLOG(2) << __func__ << " addr: " << addr << " features: " << features;
+
+ do_in_jni_thread(FROM_HERE, Bind(&HasClientCallbacks::OnDeviceAvailable,
+ Unretained(callbacks_), addr, features));
+ }
+
+ void OnFeaturesUpdate(const RawAddress& addr, uint8_t features) override {
+ DVLOG(2) << __func__ << " addr: " << addr
+ << " ha_features: " << std::bitset<8>(features);
+
+ do_in_jni_thread(FROM_HERE, Bind(&HasClientCallbacks::OnFeaturesUpdate,
+ Unretained(callbacks_), addr, features));
+ }
+
+ void OnActivePresetSelected(std::variant<RawAddress, int> addr_or_group_id,
+ uint8_t preset_index) override {
+ DVLOG(2) << __func__ << " preset_index: " << preset_index;
+
+ do_in_jni_thread(FROM_HERE,
+ Bind(&HasClientCallbacks::OnActivePresetSelected,
+ Unretained(callbacks_), std::move(addr_or_group_id),
+ preset_index));
+ }
+
+ void OnActivePresetSelectError(std::variant<RawAddress, int> addr_or_group_id,
+ ErrorCode result_code) override {
+ DVLOG(2) << __func__ << " result_code: "
+ << static_cast<std::underlying_type<ErrorCode>::type>(result_code);
+
+ do_in_jni_thread(
+ FROM_HERE,
+ Bind(&HasClientCallbacks::OnActivePresetSelectError,
+ Unretained(callbacks_), std::move(addr_or_group_id), result_code));
+ }
+
+ void OnPresetInfo(std::variant<RawAddress, int> addr_or_group_id,
+ PresetInfoReason change_id,
+ std::vector<PresetInfo> detail_records) override {
+ DVLOG(2) << __func__;
+ for (const auto& rec : detail_records) {
+ DVLOG(2) << "\t index: " << +rec.preset_index << ", change_id: "
+ << (std::underlying_type<PresetInfoReason>::type)change_id
+ << ", writable: " << rec.writable
+ << ", available: " << rec.available
+ << ", name: " << rec.preset_name;
+ }
+
+ do_in_jni_thread(FROM_HERE,
+ Bind(&HasClientCallbacks::OnPresetInfo,
+ Unretained(callbacks_), std::move(addr_or_group_id),
+ change_id, std::move(detail_records)));
+ }
+
+ void OnPresetInfoError(std::variant<RawAddress, int> addr_or_group_id,
+ uint8_t preset_index, ErrorCode result_code) override {
+ DVLOG(2) << __func__ << " result_code: "
+ << static_cast<std::underlying_type<ErrorCode>::type>(result_code);
+
+ do_in_jni_thread(
+ FROM_HERE,
+ Bind(&HasClientCallbacks::OnPresetInfoError, Unretained(callbacks_),
+ std::move(addr_or_group_id), preset_index, result_code));
+ }
+
+ void OnSetPresetNameError(std::variant<RawAddress, int> addr_or_group_id,
+ uint8_t preset_index,
+ ErrorCode result_code) override {
+ DVLOG(2) << __func__ << " result_code: "
+ << static_cast<std::underlying_type<ErrorCode>::type>(result_code);
+
+ do_in_jni_thread(
+ FROM_HERE,
+ Bind(&HasClientCallbacks::OnSetPresetNameError, Unretained(callbacks_),
+ std::move(addr_or_group_id), preset_index, result_code));
+ }
+
+ private:
+ HasClientCallbacks* callbacks_;
+};
+
+} /* namespace */
+
+HasClientInterface* btif_has_client_get_interface(void) {
+ if (!has_client_instance)
+ has_client_instance.reset(new HearingAaccessClientServiceInterfaceImpl());
+
+ return has_client_instance.get();
+}
diff --git a/system/btif/src/btif_storage.cc b/system/btif/src/btif_storage.cc
index 0bdd087ff3..feb2c87ac3 100644
--- a/system/btif/src/btif_storage.cc
+++ b/system/btif/src/btif_storage.cc
@@ -43,6 +43,7 @@
#include "bta_csis_api.h"
#include "bta_groups.h"
+#include "bta_has_api.h"
#include "bta_hd_api.h"
#include "bta_hearing_aid_api.h"
#include "bta_hh_api.h"
@@ -1863,6 +1864,142 @@ void btif_storage_remove_leaudio(const RawAddress& address) {
btif_config_set_int(addrstr, BTIF_STORAGE_LEAUDIO_AUTOCONNECT, false);
}
+constexpr char HAS_IS_ACCEPTLISTED[] = "LeAudioHasIsAcceptlisted";
+constexpr char HAS_FEATURES[] = "LeAudioHasFlags";
+constexpr char HAS_ACTIVE_PRESET[] = "LeAudioHasActivePreset";
+constexpr char HAS_SERIALIZED_PRESETS[] = "LeAudioHasSerializedPresets";
+
+void btif_storage_add_leaudio_has_device(const RawAddress& address,
+ std::vector<uint8_t> presets_bin,
+ uint8_t features,
+ uint8_t active_preset) {
+ do_in_jni_thread(
+ FROM_HERE,
+ Bind(
+ [](const RawAddress& address, std::vector<uint8_t> presets_bin,
+ uint8_t features, uint8_t active_preset) {
+ const std::string& name = address.ToString();
+
+ btif_config_set_int(name, HAS_FEATURES, features);
+ btif_config_set_int(name, HAS_ACTIVE_PRESET, active_preset);
+ btif_config_set_bin(name, HAS_SERIALIZED_PRESETS,
+ presets_bin.data(), presets_bin.size());
+
+ btif_config_set_int(name, HAS_IS_ACCEPTLISTED, true);
+ btif_config_save();
+ },
+ address, std::move(presets_bin), features, active_preset));
+}
+
+void btif_storage_set_leaudio_has_active_preset(const RawAddress& address,
+ uint8_t active_preset) {
+ do_in_jni_thread(FROM_HERE,
+ Bind(
+ [](const RawAddress& address, uint8_t active_preset) {
+ const std::string& name = address.ToString();
+
+ btif_config_set_int(name, HAS_ACTIVE_PRESET,
+ active_preset);
+ btif_config_save();
+ },
+ address, active_preset));
+}
+
+bool btif_storage_get_leaudio_has_features(const RawAddress& address,
+ uint8_t& features) {
+ std::string name = address.ToString();
+
+ int value;
+ if (!btif_config_get_int(name, HAS_FEATURES, &value)) return false;
+
+ features = value;
+ return true;
+}
+
+void btif_storage_set_leaudio_has_features(const RawAddress& address,
+ uint8_t features) {
+ do_in_jni_thread(FROM_HERE,
+ Bind(
+ [](const RawAddress& address, uint8_t features) {
+ const std::string& name = address.ToString();
+
+ btif_config_set_int(name, HAS_FEATURES, features);
+ btif_config_save();
+ },
+ address, features));
+}
+
+void btif_storage_load_bonded_leaudio_has_devices() {
+ for (const auto& bd_addr : btif_config_get_paired_devices()) {
+ const std::string& name = bd_addr.ToString();
+
+ if (!btif_config_exist(name, HAS_IS_ACCEPTLISTED) &&
+ !btif_config_exist(name, HAS_FEATURES))
+ continue;
+
+ int value;
+ uint16_t is_acceptlisted = 0;
+ if (btif_config_get_int(name, HAS_IS_ACCEPTLISTED, &value))
+ is_acceptlisted = value;
+
+ uint8_t features = 0;
+ if (btif_config_get_int(name, HAS_FEATURES, &value)) features = value;
+
+ do_in_main_thread(FROM_HERE, Bind(&le_audio::has::HasClient::AddFromStorage,
+ bd_addr, features, is_acceptlisted));
+ }
+}
+
+void btif_storage_remove_leaudio_has(const RawAddress& address) {
+ std::string addrstr = address.ToString();
+ btif_config_remove(addrstr, HAS_IS_ACCEPTLISTED);
+ btif_config_remove(addrstr, HAS_FEATURES);
+ btif_config_remove(addrstr, HAS_ACTIVE_PRESET);
+ btif_config_remove(addrstr, HAS_SERIALIZED_PRESETS);
+ btif_config_save();
+}
+
+void btif_storage_set_leaudio_has_acceptlist(const RawAddress& address,
+ bool add_to_acceptlist) {
+ std::string addrstr = address.ToString();
+
+ btif_config_set_int(addrstr, HAS_IS_ACCEPTLISTED, add_to_acceptlist);
+ btif_config_save();
+}
+
+void btif_storage_set_leaudio_has_presets(const RawAddress& address,
+ std::vector<uint8_t> presets_bin) {
+ do_in_jni_thread(
+ FROM_HERE,
+ Bind(
+ [](const RawAddress& address, std::vector<uint8_t> presets_bin) {
+ const std::string& name = address.ToString();
+
+ btif_config_set_bin(name, HAS_SERIALIZED_PRESETS,
+ presets_bin.data(), presets_bin.size());
+ btif_config_save();
+ },
+ address, std::move(presets_bin)));
+}
+
+bool btif_storage_get_leaudio_has_presets(const RawAddress& address,
+ std::vector<uint8_t>& presets_bin,
+ uint8_t& active_preset) {
+ std::string name = address.ToString();
+
+ int value;
+ if (!btif_config_get_int(name, HAS_ACTIVE_PRESET, &value)) return false;
+ active_preset = value;
+
+ auto bin_sz = btif_config_get_bin_length(name, HAS_SERIALIZED_PRESETS);
+ presets_bin.resize(bin_sz);
+ if (!btif_config_get_bin(name, HAS_SERIALIZED_PRESETS, presets_bin.data(),
+ &bin_sz))
+ return false;
+
+ return true;
+}
+
/** Adds the bonded Le Audio device grouping info into the NVRAM */
void btif_storage_add_groups(const RawAddress& addr) {
std::vector<uint8_t> group_info;
diff --git a/system/include/hardware/bluetooth.h b/system/include/hardware/bluetooth.h
index 5e6c5098a0..65c9db2f33 100644
--- a/system/include/hardware/bluetooth.h
+++ b/system/include/hardware/bluetooth.h
@@ -49,6 +49,7 @@
#define BT_PROFILE_AV_RC_ID "avrcp"
#define BT_PROFILE_AV_RC_CTRL_ID "avrcp_ctrl"
#define BT_PROFILE_HEARING_AID_ID "hearing_aid"
+#define BT_PROFILE_HAP_CLIENT_ID "has_client"
#define BT_PROFILE_LE_AUDIO_ID "le_audio"
#define BT_KEYSTORE_ID "bluetooth_keystore"
#define BT_ACTIVITY_ATTRIBUTION_ID "activity_attribution"
diff --git a/system/include/hardware/bt_has.h b/system/include/hardware/bt_has.h
new file mode 100644
index 0000000000..4fdd316852
--- /dev/null
+++ b/system/include/hardware/bt_has.h
@@ -0,0 +1,155 @@
+/*
+ * 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>
+#include <vector>
+
+namespace bluetooth {
+namespace has {
+
+/** Connection State */
+enum class ConnectionState : uint8_t {
+ DISCONNECTED = 0,
+ CONNECTING,
+ CONNECTED,
+ DISCONNECTING,
+};
+
+/** Results codes for the failed preset operations */
+enum class ErrorCode : uint8_t {
+ NO_ERROR = 0,
+ SET_NAME_NOT_ALLOWED,
+ OPERATION_NOT_SUPPORTED,
+ OPERATION_NOT_POSSIBLE,
+ INVALID_PRESET_NAME_LENGTH,
+ INVALID_PRESET_INDEX,
+ GROUP_OPERATION_NOT_SUPPORTED,
+ PROCEDURE_ALREADY_IN_PROGRESS,
+};
+
+enum class PresetInfoReason : uint8_t {
+ ALL_PRESET_INFO = 0,
+ PRESET_INFO_UPDATE,
+ PRESET_DELETED,
+ PRESET_AVAILABILITY_CHANGED,
+ PRESET_INFO_REQUEST_RESPONSE,
+};
+
+struct PresetInfo {
+ uint8_t preset_index;
+
+ bool writable;
+ bool available;
+ std::string preset_name;
+};
+
+/** Service supported feature bits */
+static constexpr uint8_t kFeatureBitHearingAidTypeBinaural = 0x00;
+static constexpr uint8_t kFeatureBitHearingAidTypeMonaural = 0x01;
+static constexpr uint8_t kFeatureBitHearingAidTypeBanded = 0x02;
+static constexpr uint8_t kFeatureBitPresetSynchronizationSupported = 0x04;
+static constexpr uint8_t kFeatureBitIndependentPresets = 0x08;
+static constexpr uint8_t kFeatureBitDynamicPresets = 0x10;
+static constexpr uint8_t kFeatureBitWritablePresets = 0x20;
+
+/** Invalid values for the group and preset identifiers */
+static constexpr uint8_t kHasPresetIndexInvalid = 0x00;
+static constexpr int kHasGroupIdInvalid = -1;
+
+class HasClientCallbacks {
+ public:
+ virtual ~HasClientCallbacks() = default;
+
+ /** Callback for profile connection state change */
+ virtual void OnConnectionState(ConnectionState state,
+ const RawAddress& addr) = 0;
+
+ /** Callback for the new available device */
+ virtual void OnDeviceAvailable(const RawAddress& addr, uint8_t features) = 0;
+
+ /** Callback for getting device HAS flags */
+ virtual void OnFeaturesUpdate(const RawAddress& addr, uint8_t features) = 0;
+
+ /** Callback for the currently active preset */
+ virtual void OnActivePresetSelected(
+ std::variant<RawAddress, int> addr_or_group_id, uint8_t preset_index) = 0;
+
+ /** Callbacks for the active preset selection error */
+ virtual void OnActivePresetSelectError(
+ std::variant<RawAddress, int> addr_or_group_id, ErrorCode error_code) = 0;
+
+ /** Callbacks for the preset details event */
+ virtual void OnPresetInfo(std::variant<RawAddress, int> addr_or_group_id,
+ PresetInfoReason change_id,
+ std::vector<PresetInfo> info_records) = 0;
+
+ /** Callback for the preset details get error */
+ virtual void OnPresetInfoError(std::variant<RawAddress, int> addr_or_group_id,
+ uint8_t preset_index,
+ ErrorCode error_code) = 0;
+
+ /** Callback for the preset name set error */
+ virtual void OnSetPresetNameError(
+ std::variant<RawAddress, int> addr_or_group_id, uint8_t preset_index,
+ ErrorCode error_code) = 0;
+};
+
+class HasClientInterface {
+ public:
+ virtual ~HasClientInterface() = default;
+
+ /** Register the Hearing Aid Service Client profile callbacks */
+ virtual void Init(HasClientCallbacks* callbacks) = 0;
+
+ /** Connect to HAS service */
+ virtual void Connect(const RawAddress& addr) = 0;
+
+ /** Disconnect from HAS service */
+ virtual void Disconnect(const RawAddress& addr) = 0;
+
+ /** Select preset by the index as currently active */
+ virtual void SelectActivePreset(
+ std::variant<RawAddress, int> addr_or_group_id, uint8_t preset_index) = 0;
+
+ /** Select next preset as currently active */
+ virtual void NextActivePreset(
+ std::variant<RawAddress, int> addr_or_group_id) = 0;
+
+ /** Select previous preset as currently active */
+ virtual void PreviousActivePreset(
+ std::variant<RawAddress, int> addr_or_group_id) = 0;
+
+ /** Get preset name by the index */
+ virtual void GetPresetInfo(const RawAddress& addr, uint8_t preset_index) = 0;
+
+ /** Set preset name by the index */
+ virtual void SetPresetName(std::variant<RawAddress, int> addr_or_group_id,
+ uint8_t preset_index, std::string name) = 0;
+
+ /** Called when HAS capable device is unbonded */
+ virtual void RemoveDevice(const RawAddress& addr) = 0;
+
+ /** Closes the interface */
+ virtual void Cleanup(void) = 0;
+};
+
+} // namespace has
+} // namespace bluetooth
diff --git a/system/osi/test/alarm_mock.h b/system/osi/test/alarm_mock.h
index 44c1c9e8e4..baa1cb02dd 100644
--- a/system/osi/test/alarm_mock.h
+++ b/system/osi/test/alarm_mock.h
@@ -10,6 +10,7 @@ class AlarmMock {
MOCK_METHOD1(AlarmCancel, void(alarm_t*));
MOCK_METHOD4(AlarmSetOnMloop, void(alarm_t* alarm, uint64_t interval_ms,
alarm_callback_t cb, void* data));
+ MOCK_METHOD1(AlarmIsScheduled, bool(const alarm_t*));
alarm_t* AlarmNewImpl(const char* name) {
AlarmNew(name);
@@ -50,3 +51,9 @@ void alarm_set_on_mloop(alarm_t* alarm, uint64_t interval_ms,
alarm_callback_t cb, void* data) {
AlarmMock::Get()->AlarmSetOnMloop(alarm, interval_ms, cb, data);
}
+
+bool alarm_is_scheduled(const alarm_t* alarm) {
+ return AlarmMock::Get()->AlarmIsScheduled(alarm);
+}
+
+void alarm_cancel(alarm_t* alarm) { AlarmMock::Get()->AlarmCancel(alarm); }
diff --git a/system/test/Android.bp b/system/test/Android.bp
index 3dd50c6167..7f4a1f54c4 100644
--- a/system/test/Android.bp
+++ b/system/test/Android.bp
@@ -62,6 +62,13 @@ filegroup {
}
filegroup {
+ name: "TestMockBtaHas",
+ srcs: [
+ "mock/mock_bta_has.cc",
+ ],
+}
+
+filegroup {
name: "TestMockBtaHd",
srcs: [
"mock/mock_bta_hd*.cc",
@@ -148,6 +155,7 @@ filegroup {
":TestMockBtaDm",
":TestMockBtaGatt",
":TestMockBtaGroups",
+ ":TestMockBtaHas",
":TestMockBtaHd",
":TestMockBtaHearingAid",
":TestMockBtaHf",
diff --git a/system/test/mock/mock_bta_has.cc b/system/test/mock/mock_bta_has.cc
new file mode 100644
index 0000000000..2b92b58353
--- /dev/null
+++ b/system/test/mock/mock_bta_has.cc
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * 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 <string>
+
+extern std::map<std::string, int> mock_function_count_map;
+
+#include <base/bind.h>
+#include <base/bind_helpers.h>
+
+#include "bta/include/bta_has_api.h"
+#include "types/raw_address.h"
+
+#ifndef UNUSED_ATTR
+#define UNUSED_ATTR
+#endif
+
+namespace le_audio {
+namespace has {
+
+void HasClient::Initialize(bluetooth::has::HasClientCallbacks*,
+ base::RepeatingCallback<void()>) {
+ mock_function_count_map[__func__]++;
+}
+void HasClient::CleanUp() { mock_function_count_map[__func__]++; }
+void HasClient::DebugDump(int) { mock_function_count_map[__func__]++; }
+bool HasClient::IsHasClientRunning() {
+ mock_function_count_map[__func__]++;
+ return false;
+}
+void HasClient::AddFromStorage(RawAddress const&, unsigned char,
+ unsigned short) {
+ mock_function_count_map[__func__]++;
+}
+HasClient* HasClient::Get() {
+ mock_function_count_map[__func__]++;
+ return nullptr;
+}
+
+} // namespace has
+} // namespace le_audio
diff --git a/system/test/stub/osi.cc b/system/test/stub/osi.cc
index b6842ef1bd..1bbd31bc62 100644
--- a/system/test/stub/osi.cc
+++ b/system/test/stub/osi.cc
@@ -590,7 +590,7 @@ void ringbuffer_free(ringbuffer_t* rb) { mock_function_count_map[__func__]++; }
bool osi_property_get_bool(const char* key, bool default_value) {
mock_function_count_map[__func__]++;
- return false;
+ return default_value;
}
int osi_property_get(const char* key, char* value, const char* default_value) {
mock_function_count_map[__func__]++;