diff options
author | Jakub Tyszkowski <jakub.tyszkowski@codecoup.pl> | 2021-04-08 15:08:35 +0000 |
---|---|---|
committer | Jack He <siyuanh@google.com> | 2022-02-02 15:18:56 -0800 |
commit | b87fc995f2e21c0cbcac9b0f9374203f90c9ece8 (patch) | |
tree | cf5329f7cf17cf6ea7ac8052357bc7df92f50fc5 | |
parent | a014314c9490a18fd4b900302d873206df09a3e0 (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)
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, ×tamp); + 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, ×tamp); + 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, ×tamp); + 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__]++; |