Merge "Change getEnabledComponentOverrides return type"
diff --git a/jni/com_android_bluetooth_a2dp_sink.cpp b/jni/com_android_bluetooth_a2dp_sink.cpp
index d7cbeb7..a74b18d 100644
--- a/jni/com_android_bluetooth_a2dp_sink.cpp
+++ b/jni/com_android_bluetooth_a2dp_sink.cpp
@@ -256,7 +256,7 @@
int register_com_android_bluetooth_a2dp_sink(JNIEnv* env) {
return jniRegisterNativeMethods(
- env, "com/android/bluetooth/a2dpsink/A2dpSinkService", sMethods,
+ env, "com/android/bluetooth/a2dpsink/A2dpSinkNativeInterface", sMethods,
NELEM(sMethods));
}
}
diff --git a/jni/com_android_bluetooth_gatt.cpp b/jni/com_android_bluetooth_gatt.cpp
index bf6cba8..642e589 100644
--- a/jni/com_android_bluetooth_gatt.cpp
+++ b/jni/com_android_bluetooth_gatt.cpp
@@ -302,12 +302,22 @@
conn_id, status, p_data->handle, jb.get());
}
-void btgattc_write_characteristic_cb(int conn_id, int status, uint16_t handle) {
+void btgattc_write_characteristic_cb(int conn_id, int status, uint16_t handle,
+ uint16_t len, const uint8_t* value) {
CallbackEnv sCallbackEnv(__func__);
if (!sCallbackEnv.valid()) return;
+ ScopedLocalRef<jbyteArray> jb(sCallbackEnv.get(), NULL);
+ if (status == 0) { // Success
+ jb.reset(sCallbackEnv->NewByteArray(len));
+ sCallbackEnv->SetByteArrayRegion(jb.get(), 0, len, (jbyte*)value);
+ } else {
+ uint8_t value = 0;
+ jb.reset(sCallbackEnv->NewByteArray(1));
+ sCallbackEnv->SetByteArrayRegion(jb.get(), 0, 1, (jbyte*)&value);
+ }
sCallbackEnv->CallVoidMethod(mCallbacksObj, method_onWriteCharacteristic,
- conn_id, status, handle);
+ conn_id, status, handle, jb.get());
}
void btgattc_execute_write_cb(int conn_id, int status) {
@@ -336,12 +346,22 @@
status, p_data.handle, jb.get());
}
-void btgattc_write_descriptor_cb(int conn_id, int status, uint16_t handle) {
+void btgattc_write_descriptor_cb(int conn_id, int status, uint16_t handle,
+ uint16_t len, const uint8_t* value) {
CallbackEnv sCallbackEnv(__func__);
if (!sCallbackEnv.valid()) return;
+ ScopedLocalRef<jbyteArray> jb(sCallbackEnv.get(), NULL);
+ if (status == 0) { // Success
+ jb.reset(sCallbackEnv->NewByteArray(len));
+ sCallbackEnv->SetByteArrayRegion(jb.get(), 0, len, (jbyte*)value);
+ } else {
+ uint8_t value = 0;
+ jb.reset(sCallbackEnv->NewByteArray(1));
+ sCallbackEnv->SetByteArrayRegion(jb.get(), 0, 1, (jbyte*)&value);
+ }
sCallbackEnv->CallVoidMethod(mCallbacksObj, method_onWriteDescriptor, conn_id,
- status, handle);
+ status, handle, jb.get());
}
void btgattc_remote_rssi_cb(int client_if, const RawAddress& bda, int rssi,
@@ -983,7 +1003,7 @@
method_onReadCharacteristic =
env->GetMethodID(clazz, "onReadCharacteristic", "(III[B)V");
method_onWriteCharacteristic =
- env->GetMethodID(clazz, "onWriteCharacteristic", "(III)V");
+ env->GetMethodID(clazz, "onWriteCharacteristic", "(III[B)V");
method_onExecuteCompleted =
env->GetMethodID(clazz, "onExecuteCompleted", "(II)V");
method_onSearchCompleted =
@@ -991,7 +1011,7 @@
method_onReadDescriptor =
env->GetMethodID(clazz, "onReadDescriptor", "(III[B)V");
method_onWriteDescriptor =
- env->GetMethodID(clazz, "onWriteDescriptor", "(III)V");
+ env->GetMethodID(clazz, "onWriteDescriptor", "(III[B)V");
method_onNotify =
env->GetMethodID(clazz, "onNotify", "(ILjava/lang/String;IZ[B)V");
method_onRegisterForNotifications =
diff --git a/jni/com_android_bluetooth_hfpclient.cpp b/jni/com_android_bluetooth_hfpclient.cpp
index 83c6b20..763885e 100644
--- a/jni/com_android_bluetooth_hfpclient.cpp
+++ b/jni/com_android_bluetooth_hfpclient.cpp
@@ -22,10 +22,15 @@
#include "hardware/bt_hf_client.h"
#include "utils/Log.h"
+#include <shared_mutex>
+
namespace android {
static bthf_client_interface_t* sBluetoothHfpClientInterface = NULL;
+static std::shared_mutex interface_mutex;
+
static jobject mCallbacksObj = NULL;
+static std::shared_mutex callbacks_mutex;
static jmethodID method_onConnectionStateChanged;
static jmethodID method_onAudioStateChanged;
@@ -68,8 +73,9 @@
bthf_client_connection_state_t state,
unsigned int peer_feat,
unsigned int chld_feat) {
+ std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
- if (!sCallbackEnv.valid()) return;
+ if (!sCallbackEnv.valid() || mCallbacksObj == NULL) return;
ScopedLocalRef<jbyteArray> addr(sCallbackEnv.get(), marshall_bda(bd_addr));
if (!addr.get()) return;
@@ -82,8 +88,9 @@
static void audio_state_cb(const RawAddress* bd_addr,
bthf_client_audio_state_t state) {
+ std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
- if (!sCallbackEnv.valid()) return;
+ if (!sCallbackEnv.valid() || mCallbacksObj == NULL) return;
ScopedLocalRef<jbyteArray> addr(sCallbackEnv.get(), marshall_bda(bd_addr));
if (!addr.get()) return;
@@ -93,8 +100,9 @@
}
static void vr_cmd_cb(const RawAddress* bd_addr, bthf_client_vr_state_t state) {
+ std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
- if (!sCallbackEnv.valid()) return;
+ if (!sCallbackEnv.valid() || mCallbacksObj == NULL) return;
ScopedLocalRef<jbyteArray> addr(sCallbackEnv.get(), marshall_bda(bd_addr));
if (!addr.get()) return;
@@ -105,8 +113,9 @@
static void network_state_cb(const RawAddress* bd_addr,
bthf_client_network_state_t state) {
+ std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
- if (!sCallbackEnv.valid()) return;
+ if (!sCallbackEnv.valid() || mCallbacksObj == NULL) return;
ScopedLocalRef<jbyteArray> addr(sCallbackEnv.get(), marshall_bda(bd_addr));
if (!addr.get()) return;
@@ -117,7 +126,9 @@
static void network_roaming_cb(const RawAddress* bd_addr,
bthf_client_service_type_t type) {
+ std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
+ if (!sCallbackEnv.valid() || mCallbacksObj == NULL) return;
ScopedLocalRef<jbyteArray> addr(sCallbackEnv.get(), marshall_bda(bd_addr));
if (!addr.get()) return;
@@ -127,8 +138,9 @@
}
static void network_signal_cb(const RawAddress* bd_addr, int signal) {
+ std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
- if (!sCallbackEnv.valid()) return;
+ if (!sCallbackEnv.valid() || mCallbacksObj == NULL) return;
ScopedLocalRef<jbyteArray> addr(sCallbackEnv.get(), marshall_bda(bd_addr));
if (!addr.get()) return;
@@ -138,8 +150,9 @@
}
static void battery_level_cb(const RawAddress* bd_addr, int level) {
+ std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
- if (!sCallbackEnv.valid()) return;
+ if (!sCallbackEnv.valid() || mCallbacksObj == NULL) return;
ScopedLocalRef<jbyteArray> addr(sCallbackEnv.get(), marshall_bda(bd_addr));
if (!addr.get()) return;
@@ -149,8 +162,9 @@
}
static void current_operator_cb(const RawAddress* bd_addr, const char* name) {
+ std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
- if (!sCallbackEnv.valid()) return;
+ if (!sCallbackEnv.valid() || mCallbacksObj == NULL) return;
ScopedLocalRef<jbyteArray> addr(sCallbackEnv.get(), marshall_bda(bd_addr));
if (!addr.get()) return;
@@ -169,8 +183,9 @@
}
static void call_cb(const RawAddress* bd_addr, bthf_client_call_t call) {
+ std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
- if (!sCallbackEnv.valid()) return;
+ if (!sCallbackEnv.valid() || mCallbacksObj == NULL) return;
ScopedLocalRef<jbyteArray> addr(sCallbackEnv.get(), marshall_bda(bd_addr));
if (!addr.get()) return;
@@ -181,8 +196,9 @@
static void callsetup_cb(const RawAddress* bd_addr,
bthf_client_callsetup_t callsetup) {
+ std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
- if (!sCallbackEnv.valid()) return;
+ if (!sCallbackEnv.valid() || mCallbacksObj == NULL) return;
ScopedLocalRef<jbyteArray> addr(sCallbackEnv.get(), marshall_bda(bd_addr));
if (!addr.get()) return;
@@ -197,8 +213,9 @@
static void callheld_cb(const RawAddress* bd_addr,
bthf_client_callheld_t callheld) {
+ std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
- if (!sCallbackEnv.valid()) return;
+ if (!sCallbackEnv.valid() || mCallbacksObj == NULL) return;
ScopedLocalRef<jbyteArray> addr(sCallbackEnv.get(), marshall_bda(bd_addr));
if (!addr.get()) return;
@@ -209,8 +226,9 @@
static void resp_and_hold_cb(const RawAddress* bd_addr,
bthf_client_resp_and_hold_t resp_and_hold) {
+ std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
- if (!sCallbackEnv.valid()) return;
+ if (!sCallbackEnv.valid() || mCallbacksObj == NULL) return;
ScopedLocalRef<jbyteArray> addr(sCallbackEnv.get(), marshall_bda(bd_addr));
if (!addr.get()) return;
@@ -220,8 +238,9 @@
}
static void clip_cb(const RawAddress* bd_addr, const char* number) {
+ std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
- if (!sCallbackEnv.valid()) return;
+ if (!sCallbackEnv.valid() || mCallbacksObj == NULL) return;
ScopedLocalRef<jbyteArray> addr(sCallbackEnv.get(), marshall_bda(bd_addr));
if (!addr.get()) return;
@@ -240,8 +259,9 @@
}
static void call_waiting_cb(const RawAddress* bd_addr, const char* number) {
+ std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
- if (!sCallbackEnv.valid()) return;
+ if (!sCallbackEnv.valid() || mCallbacksObj == NULL) return;
ScopedLocalRef<jbyteArray> addr(sCallbackEnv.get(), marshall_bda(bd_addr));
if (!addr.get()) return;
@@ -264,8 +284,9 @@
bthf_client_call_state_t state,
bthf_client_call_mpty_type_t mpty,
const char* number) {
+ std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
- if (!sCallbackEnv.valid()) return;
+ if (!sCallbackEnv.valid() || mCallbacksObj == NULL) return;
ScopedLocalRef<jbyteArray> addr(sCallbackEnv.get(), marshall_bda(bd_addr));
if (!addr.get()) return;
@@ -285,8 +306,9 @@
static void volume_change_cb(const RawAddress* bd_addr,
bthf_client_volume_type_t type, int volume) {
+ std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
- if (!sCallbackEnv.valid()) return;
+ if (!sCallbackEnv.valid() || mCallbacksObj == NULL) return;
ScopedLocalRef<jbyteArray> addr(sCallbackEnv.get(), marshall_bda(bd_addr));
if (!addr.get()) return;
@@ -296,8 +318,9 @@
static void cmd_complete_cb(const RawAddress* bd_addr,
bthf_client_cmd_complete_t type, int cme) {
+ std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
- if (!sCallbackEnv.valid()) return;
+ if (!sCallbackEnv.valid() || mCallbacksObj == NULL) return;
ScopedLocalRef<jbyteArray> addr(sCallbackEnv.get(), marshall_bda(bd_addr));
if (!addr.get()) return;
@@ -307,8 +330,9 @@
static void subscriber_info_cb(const RawAddress* bd_addr, const char* name,
bthf_client_subscriber_service_type_t type) {
+ std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
- if (!sCallbackEnv.valid()) return;
+ if (!sCallbackEnv.valid() || mCallbacksObj == NULL) return;
ScopedLocalRef<jbyteArray> addr(sCallbackEnv.get(), marshall_bda(bd_addr));
if (!addr.get()) return;
@@ -328,8 +352,9 @@
static void in_band_ring_cb(const RawAddress* bd_addr,
bthf_client_in_band_ring_state_t in_band) {
+ std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
- if (!sCallbackEnv.valid()) return;
+ if (!sCallbackEnv.valid() || mCallbacksObj == NULL) return;
ScopedLocalRef<jbyteArray> addr(sCallbackEnv.get(), marshall_bda(bd_addr));
if (!addr.get()) return;
@@ -339,8 +364,9 @@
static void last_voice_tag_number_cb(const RawAddress* bd_addr,
const char* number) {
+ std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
- if (!sCallbackEnv.valid()) return;
+ if (!sCallbackEnv.valid() || mCallbacksObj == NULL) return;
ScopedLocalRef<jbyteArray> addr(sCallbackEnv.get(), marshall_bda(bd_addr));
if (!addr.get()) return;
@@ -359,8 +385,9 @@
}
static void ring_indication_cb(const RawAddress* bd_addr) {
+ std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
- if (!sCallbackEnv.valid()) return;
+ if (!sCallbackEnv.valid() || mCallbacksObj == NULL) return;
ScopedLocalRef<jbyteArray> addr(sCallbackEnv.get(), marshall_bda(bd_addr));
if (!addr.get()) return;
@@ -370,8 +397,9 @@
static void unknown_event_cb(const RawAddress* bd_addr,
const char* eventString) {
+ std::shared_lock<std::shared_mutex> lock(callbacks_mutex);
CallbackEnv sCallbackEnv(__func__);
- if (!sCallbackEnv.valid()) return;
+ if (!sCallbackEnv.valid() || mCallbacksObj == NULL) return;
ScopedLocalRef<jbyteArray> addr(sCallbackEnv.get(), marshall_bda(bd_addr));
if (!addr.get()) return;
@@ -446,6 +474,9 @@
static void initializeNative(JNIEnv* env, jobject object) {
ALOGD("%s: HfpClient", __func__);
+ std::unique_lock<std::shared_mutex> interface_lock(interface_mutex);
+ std::unique_lock<std::shared_mutex> callbacks_lock(callbacks_mutex);
+
const bt_interface_t* btInf = getBluetoothInterface();
if (btInf == NULL) {
ALOGE("Bluetooth module is not loaded");
@@ -484,6 +515,9 @@
}
static void cleanupNative(JNIEnv* env, jobject object) {
+ std::unique_lock<std::shared_mutex> interface_lock(interface_mutex);
+ std::unique_lock<std::shared_mutex> callbacks_lock(callbacks_mutex);
+
const bt_interface_t* btInf = getBluetoothInterface();
if (btInf == NULL) {
ALOGE("Bluetooth module is not loaded");
@@ -504,6 +538,7 @@
}
static jboolean connectNative(JNIEnv* env, jobject object, jbyteArray address) {
+ std::shared_lock<std::shared_mutex> lock(interface_mutex);
if (!sBluetoothHfpClientInterface) return JNI_FALSE;
jbyte* addr = env->GetByteArrayElements(address, NULL);
@@ -522,6 +557,7 @@
static jboolean disconnectNative(JNIEnv* env, jobject object,
jbyteArray address) {
+ std::shared_lock<std::shared_mutex> lock(interface_mutex);
if (!sBluetoothHfpClientInterface) return JNI_FALSE;
jbyte* addr = env->GetByteArrayElements(address, NULL);
@@ -541,6 +577,7 @@
static jboolean connectAudioNative(JNIEnv* env, jobject object,
jbyteArray address) {
+ std::shared_lock<std::shared_mutex> lock(interface_mutex);
if (!sBluetoothHfpClientInterface) return JNI_FALSE;
jbyte* addr = env->GetByteArrayElements(address, NULL);
@@ -560,6 +597,7 @@
static jboolean disconnectAudioNative(JNIEnv* env, jobject object,
jbyteArray address) {
+ std::shared_lock<std::shared_mutex> lock(interface_mutex);
if (!sBluetoothHfpClientInterface) return JNI_FALSE;
jbyte* addr = env->GetByteArrayElements(address, NULL);
@@ -579,6 +617,7 @@
static jboolean startVoiceRecognitionNative(JNIEnv* env, jobject object,
jbyteArray address) {
+ std::shared_lock<std::shared_mutex> lock(interface_mutex);
if (!sBluetoothHfpClientInterface) return JNI_FALSE;
jbyte* addr = env->GetByteArrayElements(address, NULL);
@@ -598,6 +637,7 @@
static jboolean stopVoiceRecognitionNative(JNIEnv* env, jobject object,
jbyteArray address) {
+ std::shared_lock<std::shared_mutex> lock(interface_mutex);
if (!sBluetoothHfpClientInterface) return JNI_FALSE;
jbyte* addr = env->GetByteArrayElements(address, NULL);
@@ -617,6 +657,7 @@
static jboolean setVolumeNative(JNIEnv* env, jobject object, jbyteArray address,
jint volume_type, jint volume) {
+ std::shared_lock<std::shared_mutex> lock(interface_mutex);
if (!sBluetoothHfpClientInterface) return JNI_FALSE;
jbyte* addr = env->GetByteArrayElements(address, NULL);
@@ -636,6 +677,7 @@
static jboolean dialNative(JNIEnv* env, jobject object, jbyteArray address,
jstring number_str) {
+ std::shared_lock<std::shared_mutex> lock(interface_mutex);
if (!sBluetoothHfpClientInterface) return JNI_FALSE;
jbyte* addr = env->GetByteArrayElements(address, NULL);
@@ -664,6 +706,7 @@
static jboolean dialMemoryNative(JNIEnv* env, jobject object,
jbyteArray address, jint location) {
+ std::shared_lock<std::shared_mutex> lock(interface_mutex);
if (!sBluetoothHfpClientInterface) return JNI_FALSE;
jbyte* addr = env->GetByteArrayElements(address, NULL);
@@ -685,6 +728,7 @@
static jboolean handleCallActionNative(JNIEnv* env, jobject object,
jbyteArray address, jint action,
jint index) {
+ std::shared_lock<std::shared_mutex> lock(interface_mutex);
if (!sBluetoothHfpClientInterface) return JNI_FALSE;
jbyte* addr = env->GetByteArrayElements(address, NULL);
@@ -705,6 +749,7 @@
static jboolean queryCurrentCallsNative(JNIEnv* env, jobject object,
jbyteArray address) {
+ std::shared_lock<std::shared_mutex> lock(interface_mutex);
if (!sBluetoothHfpClientInterface) return JNI_FALSE;
jbyte* addr = env->GetByteArrayElements(address, NULL);
@@ -725,6 +770,7 @@
static jboolean queryCurrentOperatorNameNative(JNIEnv* env, jobject object,
jbyteArray address) {
+ std::shared_lock<std::shared_mutex> lock(interface_mutex);
if (!sBluetoothHfpClientInterface) return JNI_FALSE;
jbyte* addr = env->GetByteArrayElements(address, NULL);
@@ -746,6 +792,7 @@
static jboolean retrieveSubscriberInfoNative(JNIEnv* env, jobject object,
jbyteArray address) {
+ std::shared_lock<std::shared_mutex> lock(interface_mutex);
if (!sBluetoothHfpClientInterface) return JNI_FALSE;
jbyte* addr = env->GetByteArrayElements(address, NULL);
@@ -766,6 +813,7 @@
static jboolean sendDtmfNative(JNIEnv* env, jobject object, jbyteArray address,
jbyte code) {
+ std::shared_lock<std::shared_mutex> lock(interface_mutex);
if (!sBluetoothHfpClientInterface) return JNI_FALSE;
jbyte* addr = env->GetByteArrayElements(address, NULL);
@@ -786,6 +834,7 @@
static jboolean requestLastVoiceTagNumberNative(JNIEnv* env, jobject object,
jbyteArray address) {
+ std::shared_lock<std::shared_mutex> lock(interface_mutex);
if (!sBluetoothHfpClientInterface) return JNI_FALSE;
jbyte* addr = env->GetByteArrayElements(address, NULL);
@@ -809,6 +858,7 @@
static jboolean sendATCmdNative(JNIEnv* env, jobject object, jbyteArray address,
jint cmd, jint val1, jint val2,
jstring arg_str) {
+ std::shared_lock<std::shared_mutex> lock(interface_mutex);
if (!sBluetoothHfpClientInterface) return JNI_FALSE;
jbyte* addr = env->GetByteArrayElements(address, NULL);
diff --git a/jni/com_android_bluetooth_le_audio.cpp b/jni/com_android_bluetooth_le_audio.cpp
index 7b4036c..659a694 100644
--- a/jni/com_android_bluetooth_le_audio.cpp
+++ b/jni/com_android_bluetooth_le_audio.cpp
@@ -218,6 +218,47 @@
return JNI_TRUE;
}
+static jboolean groupAddNodeNative(JNIEnv* env, jobject object, jint group_id,
+ jbyteArray address) {
+ jbyte* addr = env->GetByteArrayElements(address, nullptr);
+
+ if (!sLeAudioClientInterface) {
+ LOG(ERROR) << __func__ << ": Failed to get the Bluetooth LeAudio Interface";
+ return JNI_FALSE;
+ }
+
+ if (!addr) {
+ jniThrowIOException(env, EINVAL);
+ return JNI_FALSE;
+ }
+
+ RawAddress* tmpraw = (RawAddress*)addr;
+ sLeAudioClientInterface->GroupAddNode(group_id, *tmpraw);
+ env->ReleaseByteArrayElements(address, addr, 0);
+
+ return JNI_TRUE;
+}
+
+static jboolean groupRemoveNodeNative(JNIEnv* env, jobject object,
+ jint group_id, jbyteArray address) {
+
+ if (!sLeAudioClientInterface) {
+ LOG(ERROR) << __func__ << ": Failed to get the Bluetooth LeAudio Interface";
+ return JNI_FALSE;
+ }
+
+ jbyte* addr = env->GetByteArrayElements(address, nullptr);
+ if (!addr) {
+ jniThrowIOException(env, EINVAL);
+ return JNI_FALSE;
+ }
+
+ RawAddress* tmpraw = (RawAddress*)addr;
+ sLeAudioClientInterface->GroupRemoveNode(group_id, *tmpraw);
+ env->ReleaseByteArrayElements(address, addr, 0);
+ return JNI_TRUE;
+}
+
static void groupSetActiveNative(JNIEnv* env, jobject object, jint group_id) {
LOG(INFO) << __func__;
@@ -235,6 +276,8 @@
{"cleanupNative", "()V", (void*)cleanupNative},
{"connectLeAudioNative", "([B)Z", (void*)connectLeAudioNative},
{"disconnectLeAudioNative", "([B)Z", (void*)disconnectLeAudioNative},
+ {"groupAddNodeNative", "(I[B)Z", (void*)groupAddNodeNative},
+ {"groupRemoveNodeNative", "(I[B)Z", (void*)groupRemoveNodeNative},
{"groupSetActiveNative", "(I)V", (void*)groupSetActiveNative},
};
diff --git a/res/values/config.xml b/res/values/config.xml
index b99b8fb..1b0a315 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -125,6 +125,9 @@
<!-- Flag whether or not to keep polling AG with CLCC for call information every 2 seconds -->
<bool name="hfp_clcc_poll_during_call">true</bool>
+ <!-- Time delay in milliseconds between consecutive polling AG with CLCC for call info -->
+ <integer name="hfp_clcc_poll_interval_during_call">2000</integer>
+
<!-- Package that is providing the exposure notification service -->
<string name="exposure_notification_package">com.google.android.gms</string>
diff --git a/src/com/android/bluetooth/a2dpsink/A2dpSinkNativeInterface.java b/src/com/android/bluetooth/a2dpsink/A2dpSinkNativeInterface.java
new file mode 100644
index 0000000..75c34d8
--- /dev/null
+++ b/src/com/android/bluetooth/a2dpsink/A2dpSinkNativeInterface.java
@@ -0,0 +1,193 @@
+/*
+ * Copyright (C) 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.
+ */
+
+package com.android.bluetooth.a2dpsink;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.util.Log;
+
+import com.android.bluetooth.Utils;
+import com.android.internal.annotations.GuardedBy;
+
+/**
+ * A2DP Sink Native Interface to/from JNI.
+ */
+public class A2dpSinkNativeInterface {
+ private static final String TAG = "A2dpSinkNativeInterface";
+ private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
+ private BluetoothAdapter mAdapter;
+
+ @GuardedBy("INSTANCE_LOCK")
+ private static A2dpSinkNativeInterface sInstance;
+ private static final Object INSTANCE_LOCK = new Object();
+
+ static {
+ classInitNative();
+ }
+
+ private A2dpSinkNativeInterface() {
+ mAdapter = BluetoothAdapter.getDefaultAdapter();
+ if (mAdapter == null) {
+ Log.wtf(TAG, "No Bluetooth Adapter Available");
+ }
+ }
+
+ /**
+ * Get singleton instance.
+ */
+ public static A2dpSinkNativeInterface getInstance() {
+ synchronized (INSTANCE_LOCK) {
+ if (sInstance == null) {
+ sInstance = new A2dpSinkNativeInterface();
+ }
+ return sInstance;
+ }
+ }
+
+ /**
+ * Initializes the native interface and sets the max number of connected devices
+ *
+ * @param maxConnectedAudioDevices The maximum number of devices that can be connected at once
+ */
+ public void init(int maxConnectedAudioDevices) {
+ initNative(maxConnectedAudioDevices);
+ }
+
+ /**
+ * Cleanup the native interface.
+ */
+ public void cleanup() {
+ cleanupNative();
+ }
+
+ /**
+ * Initiates an A2DP connection to a remote device.
+ *
+ * @param device the remote device
+ * @return true on success, otherwise false.
+ */
+ public boolean connectA2dpSink(BluetoothDevice device) {
+ return connectA2dpNative(Utils.getByteAddress(device));
+ }
+
+ /**
+ * Disconnects A2DP from a remote device.
+ *
+ * @param device the remote device
+ * @return true on success, otherwise false.
+ */
+ public boolean disconnectA2dpSink(BluetoothDevice device) {
+ return disconnectA2dpNative(Utils.getByteAddress(device));
+ }
+
+ /**
+ * Set a BluetoothDevice as the active device
+ *
+ * The active device is the only one that will receive passthrough commands and the only one
+ * that will have its audio decoded.
+ *
+ * Sending null for the active device will make no device active.
+ *
+ * @param device
+ * @return True if the active device request has been scheduled
+ */
+ public boolean setActiveDevice(BluetoothDevice device) {
+ // Translate to byte address for JNI. Use an all 0 MAC for no active device
+ byte[] address = null;
+ if (device != null) {
+ address = Utils.getByteAddress(device);
+ } else {
+ address = Utils.getBytesFromAddress("00:00:00:00:00:00");
+ }
+ return setActiveDeviceNative(address);
+ }
+
+ /**
+ * Inform A2DP decoder of the current audio focus
+ *
+ * @param focusGranted
+ */
+ public void informAudioFocusState(int focusGranted) {
+ informAudioFocusStateNative(focusGranted);
+ }
+
+ /**
+ * Inform A2DP decoder the desired audio gain
+ *
+ * @param gain
+ */
+ public void informAudioTrackGain(float gain) {
+ informAudioTrackGainNative(gain);
+ }
+
+ /**
+ * Send a stack event up to the A2DP Sink Service
+ */
+ private void sendMessageToService(StackEvent event) {
+ A2dpSinkService service = A2dpSinkService.getA2dpSinkService();
+ if (service != null) {
+ service.messageFromNative(event);
+ } else {
+ Log.e(TAG, "Event ignored, service not available: " + event);
+ }
+ }
+
+ /**
+ * For the JNI to send messages about connection state changes
+ */
+ public void onConnectionStateChanged(byte[] address, int state) {
+ StackEvent event =
+ StackEvent.connectionStateChanged(mAdapter.getRemoteDevice(address), state);
+ if (DBG) {
+ Log.d(TAG, "onConnectionStateChanged: " + event);
+ }
+ sendMessageToService(event);
+ }
+
+ /**
+ * For the JNI to send messages about audio stream state changes
+ */
+ public void onAudioStateChanged(byte[] address, int state) {
+ StackEvent event = StackEvent.audioStateChanged(mAdapter.getRemoteDevice(address), state);
+ if (DBG) {
+ Log.d(TAG, "onAudioStateChanged: " + event);
+ }
+ sendMessageToService(event);
+ }
+
+ /**
+ * For the JNI to send messages about audio configuration changes
+ */
+ public void onAudioConfigChanged(byte[] address, int sampleRate, int channelCount) {
+ StackEvent event = StackEvent.audioConfigChanged(
+ mAdapter.getRemoteDevice(address), sampleRate, channelCount);
+ if (DBG) {
+ Log.d(TAG, "onAudioConfigChanged: " + event);
+ }
+ sendMessageToService(event);
+ }
+
+ // Native methods that call into the JNI interface
+ private static native void classInitNative();
+ private native void initNative(int maxConnectedAudioDevices);
+ private native void cleanupNative();
+ private native boolean connectA2dpNative(byte[] address);
+ private native boolean disconnectA2dpNative(byte[] address);
+ private native boolean setActiveDeviceNative(byte[] address);
+ private native void informAudioFocusStateNative(int focusGranted);
+ private native void informAudioTrackGainNative(float gain);
+}
diff --git a/src/com/android/bluetooth/a2dpsink/A2dpSinkService.java b/src/com/android/bluetooth/a2dpsink/A2dpSinkService.java
index 5e0a164..fc54444 100644
--- a/src/com/android/bluetooth/a2dpsink/A2dpSinkService.java
+++ b/src/com/android/bluetooth/a2dpsink/A2dpSinkService.java
@@ -49,7 +49,7 @@
private AdapterService mAdapterService;
private DatabaseManager mDatabaseManager;
- protected Map<BluetoothDevice, A2dpSinkStateMachine> mDeviceStateMap =
+ private Map<BluetoothDevice, A2dpSinkStateMachine> mDeviceStateMap =
new ConcurrentHashMap<>(1);
private final Object mStreamHandlerLock = new Object();
@@ -60,9 +60,7 @@
private A2dpSinkStreamHandler mA2dpSinkStreamHandler;
private static A2dpSinkService sService;
- static {
- classInitNative();
- }
+ A2dpSinkNativeInterface mNativeInterface;
@Override
protected boolean start() {
@@ -70,12 +68,15 @@
"AdapterService cannot be null when A2dpSinkService starts");
mDatabaseManager = Objects.requireNonNull(AdapterService.getAdapterService().getDatabase(),
"DatabaseManager cannot be null when A2dpSinkService starts");
+ mNativeInterface = A2dpSinkNativeInterface.getInstance();
+
+ mMaxConnectedAudioDevices = mAdapterService.getMaxConnectedAudioDevices();
+ mNativeInterface.init(mMaxConnectedAudioDevices);
synchronized (mStreamHandlerLock) {
- mA2dpSinkStreamHandler = new A2dpSinkStreamHandler(this, this);
+ mA2dpSinkStreamHandler = new A2dpSinkStreamHandler(this, mNativeInterface);
}
- mMaxConnectedAudioDevices = mAdapterService.getMaxConnectedAudioDevices();
- initNative(mMaxConnectedAudioDevices);
+
setA2dpSinkService(this);
return true;
}
@@ -83,7 +84,7 @@
@Override
protected boolean stop() {
setA2dpSinkService(null);
- cleanupNative();
+ mNativeInterface.cleanup();
for (A2dpSinkStateMachine stateMachine : mDeviceStateMap.values()) {
stateMachine.quitNow();
}
@@ -117,16 +118,8 @@
* Set the device that should be allowed to actively stream
*/
public boolean setActiveDevice(BluetoothDevice device) {
- // Translate to byte address for JNI. Use an all 0 MAC for no active device
- byte[] address = null;
- if (device != null) {
- address = Utils.getByteAddress(device);
- } else {
- address = Utils.getBytesFromAddress("00:00:00:00:00:00");
- }
-
synchronized (mActiveDeviceLock) {
- if (setActiveDeviceNative(address)) {
+ if (mNativeInterface.setActiveDevice(device)) {
mActiveDevice = device;
return true;
}
@@ -340,6 +333,10 @@
+ ", InstanceMap start state: " + sb.toString());
}
+ if (device == null) {
+ throw new IllegalArgumentException("Null device");
+ }
+
A2dpSinkStateMachine stateMachine = mDeviceStateMap.get(device);
// a state machine instance doesn't exist. maybe it is already gone?
if (stateMachine == null) {
@@ -356,7 +353,15 @@
return true;
}
- void removeStateMachine(A2dpSinkStateMachine stateMachine) {
+ /**
+ * Remove a device's state machine.
+ *
+ * Called by the state machines when they disconnect.
+ *
+ * Visible for testing so it can be mocked and verified on.
+ */
+ @VisibleForTesting
+ public void removeStateMachine(A2dpSinkStateMachine stateMachine) {
mDeviceStateMap.remove(stateMachine.getDevice());
}
@@ -365,7 +370,8 @@
}
protected A2dpSinkStateMachine getOrCreateStateMachine(BluetoothDevice device) {
- A2dpSinkStateMachine newStateMachine = new A2dpSinkStateMachine(device, this);
+ A2dpSinkStateMachine newStateMachine =
+ new A2dpSinkStateMachine(device, this, mNativeInterface);
A2dpSinkStateMachine existingStateMachine =
mDeviceStateMap.putIfAbsent(device, newStateMachine);
// Given null is not a valid value in our map, ConcurrentHashMap will return null if the
@@ -377,6 +383,11 @@
return existingStateMachine;
}
+ @VisibleForTesting
+ protected A2dpSinkStateMachine getStateMachineForDevice(BluetoothDevice device) {
+ return mDeviceStateMap.get(device);
+ }
+
List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
if (DBG) Log.d(TAG, "getDevicesMatchingConnectionStates" + Arrays.toString(states));
List<BluetoothDevice> deviceList = new ArrayList<>();
@@ -406,6 +417,7 @@
* {@link BluetoothProfile#STATE_DISCONNECTING} if this profile is being disconnected
*/
public int getConnectionState(BluetoothDevice device) {
+ if (device == null) return BluetoothProfile.STATE_DISCONNECTED;
A2dpSinkStateMachine stateMachine = mDeviceStateMap.get(device);
return (stateMachine == null) ? BluetoothProfile.STATE_DISCONNECTED
: stateMachine.getState();
@@ -475,6 +487,7 @@
}
BluetoothAudioConfig getAudioConfig(BluetoothDevice device) {
+ if (device == null) return null;
A2dpSinkStateMachine stateMachine = mDeviceStateMap.get(device);
// a state machine instance doesn't exist. maybe it is already gone?
if (stateMachine == null) {
@@ -483,53 +496,37 @@
return stateMachine.getAudioConfig();
}
- /* JNI interfaces*/
-
- private static native void classInitNative();
-
- private native void initNative(int maxConnectedAudioDevices);
-
- private native void cleanupNative();
-
- native boolean connectA2dpNative(byte[] address);
-
- native boolean disconnectA2dpNative(byte[] address);
-
/**
- * set A2DP state machine as the active device
- * the active device is the only one that will receive passthrough commands and the only one
- * that will have its audio decoded
- *
- * @hide
- * @param address
- * @return active device request has been scheduled
+ * Receive and route a stack event from the JNI
*/
- public native boolean setActiveDeviceNative(byte[] address);
+ protected void messageFromNative(StackEvent event) {
+ switch (event.mType) {
+ case StackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED:
+ onConnectionStateChanged(event);
+ return;
+ case StackEvent.EVENT_TYPE_AUDIO_STATE_CHANGED:
+ onAudioStateChanged(event);
+ return;
+ case StackEvent.EVENT_TYPE_AUDIO_CONFIG_CHANGED:
+ onAudioConfigChanged(event);
+ return;
+ default:
+ Log.e(TAG, "Received unknown stack event of type " + event.mType);
+ return;
+ }
+ }
- /**
- * inform A2DP decoder of the current audio focus
- *
- * @param focusGranted
- */
- @VisibleForTesting
- public native void informAudioFocusStateNative(int focusGranted);
-
- /**
- * inform A2DP decoder the desired audio gain
- *
- * @param gain
- */
- @VisibleForTesting
- public native void informAudioTrackGainNative(float gain);
-
- private void onConnectionStateChanged(byte[] address, int state) {
- StackEvent event = StackEvent.connectionStateChanged(
- BluetoothAdapter.getDefaultAdapter().getRemoteDevice(address), state);
- A2dpSinkStateMachine stateMachine = getOrCreateStateMachine(event.mDevice);
+ private void onConnectionStateChanged(StackEvent event) {
+ BluetoothDevice device = event.mDevice;
+ if (device == null) {
+ return;
+ }
+ A2dpSinkStateMachine stateMachine = getOrCreateStateMachine(device);
stateMachine.sendMessage(A2dpSinkStateMachine.STACK_EVENT, event);
}
- private void onAudioStateChanged(byte[] address, int state) {
+ private void onAudioStateChanged(StackEvent event) {
+ int state = event.mState;
synchronized (mStreamHandlerLock) {
if (mA2dpSinkStreamHandler == null) {
Log.e(TAG, "Received audio state change before we've been started");
@@ -541,15 +538,18 @@
|| state == StackEvent.AUDIO_STATE_REMOTE_SUSPEND) {
mA2dpSinkStreamHandler.obtainMessage(
A2dpSinkStreamHandler.SRC_STR_STOP).sendToTarget();
+ } else {
+ Log.w(TAG, "Unhandled audio state change, state=" + state);
}
}
}
- private void onAudioConfigChanged(byte[] address, int sampleRate, int channelCount) {
- StackEvent event = StackEvent.audioConfigChanged(
- BluetoothAdapter.getDefaultAdapter().getRemoteDevice(address), sampleRate,
- channelCount);
- A2dpSinkStateMachine stateMachine = getOrCreateStateMachine(event.mDevice);
+ private void onAudioConfigChanged(StackEvent event) {
+ BluetoothDevice device = event.mDevice;
+ if (device == null) {
+ return;
+ }
+ A2dpSinkStateMachine stateMachine = getOrCreateStateMachine(device);
stateMachine.sendMessage(A2dpSinkStateMachine.STACK_EVENT, event);
}
}
diff --git a/src/com/android/bluetooth/a2dpsink/A2dpSinkStateMachine.java b/src/com/android/bluetooth/a2dpsink/A2dpSinkStateMachine.java
index 8fba61a..921753c 100644
--- a/src/com/android/bluetooth/a2dpsink/A2dpSinkStateMachine.java
+++ b/src/com/android/bluetooth/a2dpsink/A2dpSinkStateMachine.java
@@ -55,6 +55,7 @@
protected final BluetoothDevice mDevice;
protected final byte[] mDeviceAddress;
protected final A2dpSinkService mService;
+ protected final A2dpSinkNativeInterface mNativeInterface;
protected final Disconnected mDisconnected;
protected final Connecting mConnecting;
protected final Connected mConnected;
@@ -63,11 +64,13 @@
protected int mMostRecentState = BluetoothProfile.STATE_DISCONNECTED;
protected BluetoothAudioConfig mAudioConfig = null;
- A2dpSinkStateMachine(BluetoothDevice device, A2dpSinkService service) {
+ A2dpSinkStateMachine(BluetoothDevice device, A2dpSinkService service,
+ A2dpSinkNativeInterface nativeInterface) {
super(TAG);
mDevice = device;
mDeviceAddress = Utils.getByteAddress(mDevice);
mService = service;
+ mNativeInterface = nativeInterface;
if (DBG) Log.d(TAG, device.toString());
mDisconnected = new Disconnected();
@@ -83,10 +86,6 @@
setInitialState(mDisconnected);
}
- protected String getConnectionStateChangedIntent() {
- return BluetoothA2dpSink.ACTION_CONNECTION_STATE_CHANGED;
- }
-
/**
* Get the current connection state
*
@@ -177,7 +176,7 @@
== BluetoothProfile.CONNECTION_POLICY_FORBIDDEN) {
Log.w(TAG, "Ignore incoming connection, profile is"
+ " turned off for " + mDevice);
- mService.disconnectA2dpNative(mDeviceAddress);
+ mNativeInterface.disconnectA2dpSink(mDevice);
} else {
mConnecting.mIncomingConnection = true;
transitionTo(mConnecting);
@@ -204,7 +203,7 @@
sendMessageDelayed(CONNECT_TIMEOUT, CONNECT_TIMEOUT_MS);
if (!mIncomingConnection) {
- mService.connectA2dpNative(mDeviceAddress);
+ mNativeInterface.connectA2dpSink(mDevice);
}
super.enter();
@@ -256,7 +255,7 @@
switch (message.what) {
case DISCONNECT:
transitionTo(mDisconnecting);
- mService.disconnectA2dpNative(mDeviceAddress);
+ mNativeInterface.disconnectA2dpSink(mDevice);
return true;
case STACK_EVENT:
processStackEvent((StackEvent) message.obj);
diff --git a/src/com/android/bluetooth/a2dpsink/A2dpSinkStreamHandler.java b/src/com/android/bluetooth/a2dpsink/A2dpSinkStreamHandler.java
index 84a6fcd..2ba66a2 100644
--- a/src/com/android/bluetooth/a2dpsink/A2dpSinkStreamHandler.java
+++ b/src/com/android/bluetooth/a2dpsink/A2dpSinkStreamHandler.java
@@ -18,7 +18,6 @@
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHeadsetClientCall;
-import android.content.Context;
import android.content.pm.PackageManager;
import android.media.AudioAttributes;
import android.media.AudioFocusRequest;
@@ -81,7 +80,7 @@
// Private variables.
private A2dpSinkService mA2dpSinkService;
- private Context mContext;
+ private A2dpSinkNativeInterface mNativeInterface;
private AudioManager mAudioManager;
// Keep track if the remote device is providing audio
private boolean mStreamAvailable = false;
@@ -111,10 +110,11 @@
}
};
- public A2dpSinkStreamHandler(A2dpSinkService a2dpSinkService, Context context) {
+ public A2dpSinkStreamHandler(A2dpSinkService a2dpSinkService,
+ A2dpSinkNativeInterface nativeInterface) {
mA2dpSinkService = a2dpSinkService;
- mContext = context;
- mAudioManager = context.getSystemService(AudioManager.class);
+ mNativeInterface = nativeInterface;
+ mAudioManager = mA2dpSinkService.getSystemService(AudioManager.class);
}
/**
@@ -206,7 +206,7 @@
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
// Make the volume duck.
- int duckPercent = mContext.getResources()
+ int duckPercent = mA2dpSinkService.getResources()
.getInteger(R.integer.a2dp_sink_duck_percent);
if (duckPercent < 0 || duckPercent > 100) {
Log.e(TAG, "Invalid duck percent using default.");
@@ -300,7 +300,7 @@
.setUsage(AudioAttributes.USAGE_MEDIA)
.build();
- mMediaPlayer = MediaPlayer.create(mContext, R.raw.silent, attrs,
+ mMediaPlayer = MediaPlayer.create(mA2dpSinkService, R.raw.silent, attrs,
mAudioManager.generateAudioSessionId());
if (mMediaPlayer == null) {
Log.e(TAG, "Failed to initialize media player. You may not get media key events");
@@ -342,18 +342,18 @@
}
private void startFluorideStreaming() {
- mA2dpSinkService.informAudioFocusStateNative(STATE_FOCUS_GRANTED);
- mA2dpSinkService.informAudioTrackGainNative(1.0f);
+ mNativeInterface.informAudioFocusState(STATE_FOCUS_GRANTED);
+ mNativeInterface.informAudioTrackGain(1.0f);
requestMediaKeyFocus();
}
private void stopFluorideStreaming() {
releaseMediaKeyFocus();
- mA2dpSinkService.informAudioFocusStateNative(STATE_FOCUS_LOST);
+ mNativeInterface.informAudioFocusState(STATE_FOCUS_LOST);
}
private void setFluorideAudioTrackGain(float gain) {
- mA2dpSinkService.informAudioTrackGainNative(gain);
+ mNativeInterface.informAudioTrackGain(gain);
}
private void sendAvrcpPause() {
@@ -380,20 +380,18 @@
return false;
}
- synchronized int getAudioFocus() {
- return mAudioFocus;
- }
-
private boolean isIotDevice() {
- return mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_EMBEDDED);
+ return mA2dpSinkService.getPackageManager().hasSystemFeature(
+ PackageManager.FEATURE_EMBEDDED);
}
private boolean isTvDevice() {
- return mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK);
+ return mA2dpSinkService.getPackageManager().hasSystemFeature(
+ PackageManager.FEATURE_LEANBACK);
}
private boolean shouldRequestFocus() {
- return mContext.getResources()
+ return mA2dpSinkService.getResources()
.getBoolean(R.bool.a2dp_sink_automatically_request_audio_focus);
}
diff --git a/src/com/android/bluetooth/a2dpsink/StackEvent.java b/src/com/android/bluetooth/a2dpsink/StackEvent.java
index 68a6ce9..5d0947f 100644
--- a/src/com/android/bluetooth/a2dpsink/StackEvent.java
+++ b/src/com/android/bluetooth/a2dpsink/StackEvent.java
@@ -47,16 +47,24 @@
@Override
public String toString() {
+ String s = "StackEvent<device=" + mDevice + ", type =";
switch (mType) {
case EVENT_TYPE_CONNECTION_STATE_CHANGED:
- return "EVENT_TYPE_CONNECTION_STATE_CHANGED " + mState;
+ s += "EVENT_TYPE_CONNECTION_STATE_CHANGED, state=" + mState;
+ break;
case EVENT_TYPE_AUDIO_STATE_CHANGED:
- return "EVENT_TYPE_AUDIO_STATE_CHANGED " + mState;
+ s += "EVENT_TYPE_AUDIO_STATE_CHANGED, state=" + mState;
+ break;
case EVENT_TYPE_AUDIO_CONFIG_CHANGED:
- return "EVENT_TYPE_AUDIO_CONFIG_CHANGED " + mSampleRate + ":" + mChannelCount;
+ s += "EVENT_TYPE_AUDIO_CONFIG_CHANGED, sampleRate=" + mSampleRate
+ + ", channelCount=" + mChannelCount;
+ break;
default:
- return "Unknown";
+ s += "Unknown";
+ break;
}
+ s += ">";
+ return s;
}
static StackEvent connectionStateChanged(BluetoothDevice device, int state) {
diff --git a/src/com/android/bluetooth/audio_util/helpers/PlayStatus.java b/src/com/android/bluetooth/audio_util/helpers/PlayStatus.java
index 922b151..75a760b 100644
--- a/src/com/android/bluetooth/audio_util/helpers/PlayStatus.java
+++ b/src/com/android/bluetooth/audio_util/helpers/PlayStatus.java
@@ -31,7 +31,7 @@
static final byte REV_SEEK = 4;
static final byte ERROR = -1;
- public long position = 0xFFFFFFFFFFFFFFFFL;
+ public long position = 0;
public long duration = 0x00L;
public byte state = STOPPED;
@@ -41,7 +41,7 @@
if (state == null) return ret;
ret.state = playbackStateToAvrcpState(state.getState());
- ret.position = state.getPosition();
+ ret.position = (state.getPosition() > 0) ? state.getPosition() : 0;
ret.duration = duration;
return ret;
}
diff --git a/src/com/android/bluetooth/avrcp/AvrcpVolumeManager.java b/src/com/android/bluetooth/avrcp/AvrcpVolumeManager.java
index c834a5f..39f1f14 100644
--- a/src/com/android/bluetooth/avrcp/AvrcpVolumeManager.java
+++ b/src/com/android/bluetooth/avrcp/AvrcpVolumeManager.java
@@ -22,6 +22,7 @@
import android.bluetooth.BluetoothDevice;
import android.content.Context;
import android.content.SharedPreferences;
+import android.media.AudioDeviceAttributes;
import android.media.AudioDeviceCallback;
import android.media.AudioDeviceInfo;
import android.media.AudioManager;
@@ -76,7 +77,11 @@
private void switchVolumeDevice(@NonNull BluetoothDevice device) {
// Inform the audio manager that the device has changed
d("switchVolumeDevice: Set Absolute volume support to " + mDeviceMap.get(device));
- mAudioManager.avrcpSupportsAbsoluteVolume(device.getAddress(), mDeviceMap.get(device));
+ mAudioManager.setDeviceVolumeBehavior(new AudioDeviceAttributes(
+ AudioDeviceAttributes.ROLE_OUTPUT, AudioDeviceInfo.TYPE_BLUETOOTH_A2DP,
+ device.getAddress()),
+ mDeviceMap.get(device) ? AudioManager.DEVICE_VOLUME_BEHAVIOR_ABSOLUTE
+ : AudioManager.DEVICE_VOLUME_BEHAVIOR_VARIABLE);
// Get the current system volume and try to get the preference volume
int savedVolume = getVolume(device, sNewDeviceVolume);
diff --git a/src/com/android/bluetooth/btservice/AdapterService.java b/src/com/android/bluetooth/btservice/AdapterService.java
index d615402..fc8906f 100644
--- a/src/com/android/bluetooth/btservice/AdapterService.java
+++ b/src/com/android/bluetooth/btservice/AdapterService.java
@@ -456,8 +456,6 @@
mJniCallbacks = new JniCallbacks(this, mAdapterProperties);
mBluetoothKeystoreService = new BluetoothKeystoreService(isCommonCriteriaMode());
mBluetoothKeystoreService.start();
- mActivityAttributionService = new ActivityAttributionService();
- mActivityAttributionService.start();
int configCompareResult = mBluetoothKeystoreService.getCompareResult();
// Start tracking Binder latency for the bluetooth process.
@@ -512,6 +510,9 @@
mBluetoothSocketManagerBinder = new BluetoothSocketManagerBinder(this);
+ mActivityAttributionService = new ActivityAttributionService();
+ mActivityAttributionService.start();
+
setAdapterService(this);
invalidateBluetoothCaches();
@@ -820,10 +821,6 @@
mSdpManager = null;
}
- if (mBluetoothKeystoreService != null) {
- mBluetoothKeystoreService.cleanup();
- }
-
if (mActivityAttributionService != null) {
mActivityAttributionService.cleanup();
}
@@ -842,6 +839,11 @@
mJniCallbacks.cleanup();
}
+ if (mBluetoothKeystoreService != null) {
+ debugLog("cleanup(): mBluetoothKeystoreService.cleanup()");
+ mBluetoothKeystoreService.cleanup();
+ }
+
if (mPhonePolicy != null) {
mPhonePolicy.cleanup();
}
@@ -2376,6 +2378,34 @@
}
@Override
+ public int isCisCentralSupported() {
+ AdapterService service = getService();
+ if (service == null) {
+ return BluetoothStatusCodes.ERROR_BLUETOOTH_NOT_ENABLED;
+ }
+
+ if (service.mAdapterProperties.isLeConnectedIsochronousStreamCentralSupported()) {
+ return BluetoothStatusCodes.SUCCESS;
+ }
+
+ return BluetoothStatusCodes.ERROR_FEATURE_NOT_SUPPORTED;
+ }
+
+ @Override
+ public int isLePeriodicAdvertisingSyncTransferSenderSupported() {
+ AdapterService service = getService();
+ if (service == null) {
+ return BluetoothStatusCodes.ERROR_BLUETOOTH_NOT_ENABLED;
+ }
+
+ if (service.mAdapterProperties.isLePeriodicAdvertisingSyncTransferSenderSupported()) {
+ return BluetoothStatusCodes.SUCCESS;
+ }
+
+ return BluetoothStatusCodes.ERROR_FEATURE_NOT_SUPPORTED;
+ }
+
+ @Override
public int getLeMaximumAdvertisingDataLength() {
AdapterService service = getService();
if (service == null) {
@@ -3394,12 +3424,12 @@
}
// Copy the traffic objects whose byte counts are > 0
- final UidTraffic[] result = arrayLen > 0 ? new UidTraffic[arrayLen] : null;
+ final List<UidTraffic> result = new ArrayList<>();
int putIdx = 0;
for (int i = 0; i < mUidTraffic.size(); i++) {
final UidTraffic traffic = mUidTraffic.valueAt(i);
if (traffic.getTxBytes() != 0 || traffic.getRxBytes() != 0) {
- result[putIdx++] = traffic.clone();
+ result.add(traffic.clone());
}
}
@@ -3716,6 +3746,7 @@
private static final String GD_L2CAP_FLAG = "INIT_gd_l2cap";
private static final String GD_RUST_FLAG = "INIT_gd_rust";
private static final String GD_LINK_POLICY_FLAG = "INIT_gd_link_policy";
+ private static final String GATT_ROBUST_CACHING_FLAG = "INIT_gatt_robust_caching";
/**
* Logging flags logic (only applies to DEBUG and VERBOSE levels):
@@ -3768,6 +3799,9 @@
if (DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_BLUETOOTH, GD_LINK_POLICY_FLAG, false)) {
initFlags.add(String.format("%s=%s", GD_LINK_POLICY_FLAG, "true"));
}
+ if (DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_BLUETOOTH, GATT_ROBUST_CACHING_FLAG, true)) {
+ initFlags.add(String.format("%s=%s", GATT_ROBUST_CACHING_FLAG, "true"));
+ }
if (DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_BLUETOOTH,
LOGGING_DEBUG_ENABLED_FOR_ALL_FLAG, false)) {
initFlags.add(String.format("%s=%s", LOGGING_DEBUG_ENABLED_FOR_ALL_FLAG, "true"));
@@ -3784,7 +3818,7 @@
initFlags.add(String.format("%s=%s", LOGGING_DEBUG_DISABLED_FOR_TAGS_FLAG,
debugLoggingDisabledTags));
}
- if (DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_BLUETOOTH, BTAA_HCI_LOG_FLAG, false)) {
+ if (DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_BLUETOOTH, BTAA_HCI_LOG_FLAG, true)) {
initFlags.add(String.format("%s=%s", BTAA_HCI_LOG_FLAG, "true"));
}
return initFlags.toArray(new String[0]);
diff --git a/src/com/android/bluetooth/btservice/AdapterState.java b/src/com/android/bluetooth/btservice/AdapterState.java
index fd81208..7216d78 100644
--- a/src/com/android/bluetooth/btservice/AdapterState.java
+++ b/src/com/android/bluetooth/btservice/AdapterState.java
@@ -18,9 +18,9 @@
import android.bluetooth.BluetoothAdapter;
import android.os.Message;
+import android.os.SystemProperties;
import android.util.Log;
-import com.android.bluetooth.R;
import com.android.bluetooth.telephony.BluetoothInCallService;
import com.android.internal.util.State;
import com.android.internal.util.StateMachine;
@@ -72,6 +72,11 @@
static final int BLE_STOP_TIMEOUT = 11;
static final int BLE_START_TIMEOUT = 12;
+ static final String BLE_START_TIMEOUT_DELAY_PROPERTY =
+ "ro.bluetooth.ble_start_timeout_delay";
+ static final String BLE_STOP_TIMEOUT_DELAY_PROPERTY =
+ "ro.bluetooth.ble_stop_timeout_delay";
+
static final int BLE_START_TIMEOUT_DELAY = 4000;
static final int BLE_STOP_TIMEOUT_DELAY = 1000;
static final int BREDR_START_TIMEOUT_DELAY = 4000;
@@ -264,7 +269,10 @@
@Override
public void enter() {
super.enter();
- sendMessageDelayed(BLE_START_TIMEOUT, BLE_START_TIMEOUT_DELAY);
+ final int timeoutDelay = SystemProperties.getInt(
+ BLE_START_TIMEOUT_DELAY_PROPERTY, BLE_START_TIMEOUT_DELAY);
+ Log.d(TAG, "Start Timeout Delay: " + timeoutDelay);
+ sendMessageDelayed(BLE_START_TIMEOUT, timeoutDelay);
mAdapterService.bringUpBle();
}
@@ -385,7 +393,10 @@
public void enter() {
super.enter();
mAdapterService.enableBluetoothInCallService(false);
- sendMessageDelayed(BLE_STOP_TIMEOUT, BLE_STOP_TIMEOUT_DELAY);
+ final int timeoutDelay = SystemProperties.getInt(
+ BLE_STOP_TIMEOUT_DELAY_PROPERTY, BLE_STOP_TIMEOUT_DELAY);
+ Log.d(TAG, "Stop Timeout Delay: " + timeoutDelay);
+ sendMessageDelayed(BLE_STOP_TIMEOUT, timeoutDelay);
mAdapterService.bringDownBle();
}
diff --git a/src/com/android/bluetooth/btservice/RemoteDevices.java b/src/com/android/bluetooth/btservice/RemoteDevices.java
index a28105a..2f0a8d4 100644
--- a/src/com/android/bluetooth/btservice/RemoteDevices.java
+++ b/src/com/android/bluetooth/btservice/RemoteDevices.java
@@ -600,6 +600,9 @@
if (sAdapterService.getState() == BluetoothAdapter.STATE_ON) {
sAdapterService.deviceUuidUpdated(bdDevice);
sendUuidIntent(bdDevice, device);
+ } else if (sAdapterService.getState()
+ == BluetoothAdapter.STATE_BLE_ON) {
+ sAdapterService.deviceUuidUpdated(bdDevice);
}
break;
case AbstractionLayer.BT_PROPERTY_TYPE_OF_DEVICE:
diff --git a/src/com/android/bluetooth/btservice/bluetoothKeystore/BluetoothKeystoreService.java b/src/com/android/bluetooth/btservice/bluetoothKeystore/BluetoothKeystoreService.java
index df40e35..3375797 100644
--- a/src/com/android/bluetooth/btservice/bluetoothKeystore/BluetoothKeystoreService.java
+++ b/src/com/android/bluetooth/btservice/bluetoothKeystore/BluetoothKeystoreService.java
@@ -192,7 +192,6 @@
debugLog("cleanup() called before start()");
return;
}
-
// Mark service as stopped
setBluetoothKeystoreService(null);
@@ -213,7 +212,6 @@
@VisibleForTesting
public void cleanupForCommonCriteriaModeEnable() {
try {
- Thread.sleep(100);
setEncryptKeyOrRemoveKey(CONFIG_FILE_PREFIX, CONFIG_FILE_HASH);
} catch (InterruptedException e) {
reportBluetoothKeystoreException(e, "Interrupted while operating.");
@@ -362,16 +360,10 @@
if (decryptedString.isEmpty()) {
cleanupAll();
} else if (decryptedString.equals(CONFIG_FILE_HASH)) {
- backupConfigEncryptionFile();
readHashFile(CONFIG_FILE_PATH, CONFIG_FILE_PREFIX);
- //save Map
- if (mNameDecryptKey.containsKey(CONFIG_FILE_PREFIX)
- && mNameDecryptKey.get(CONFIG_FILE_PREFIX).equals(
- mNameDecryptKey.get(CONFIG_BACKUP_PREFIX))) {
- infoLog("Since the hash is same with previous, don't need encrypt again.");
- } else {
- mPendingEncryptKey.put(prefixString);
- }
+ mPendingEncryptKey.put(CONFIG_FILE_PREFIX);
+ readHashFile(CONFIG_BACKUP_PATH, CONFIG_BACKUP_PREFIX);
+ mPendingEncryptKey.put(CONFIG_BACKUP_PREFIX);
saveEncryptedKey();
}
return;
@@ -474,6 +466,7 @@
}
if (!keyEncryptedLines.isEmpty()) {
Files.write(Paths.get(CONFIG_FILE_ENCRYPTION_PATH), keyEncryptedLines);
+ Files.write(Paths.get(CONFIG_BACKUP_ENCRYPTION_PATH), keyEncryptedLines);
}
} catch (IOException e) {
throw new RuntimeException("write encryption file fail");
@@ -503,20 +496,6 @@
return mNameDecryptKey;
}
- private void backupConfigEncryptionFile() throws IOException {
- if (Files.exists(Paths.get(CONFIG_FILE_ENCRYPTION_PATH))) {
- Files.move(Paths.get(CONFIG_FILE_ENCRYPTION_PATH),
- Paths.get(CONFIG_BACKUP_ENCRYPTION_PATH),
- StandardCopyOption.REPLACE_EXISTING);
- }
- if (mNameEncryptKey.containsKey(CONFIG_FILE_PREFIX)) {
- mNameEncryptKey.put(CONFIG_BACKUP_PREFIX, mNameEncryptKey.get(CONFIG_FILE_PREFIX));
- }
- if (mNameDecryptKey.containsKey(CONFIG_FILE_PREFIX)) {
- mNameDecryptKey.put(CONFIG_BACKUP_PREFIX, mNameDecryptKey.get(CONFIG_FILE_PREFIX));
- }
- }
-
private boolean doesComparePass(int item) {
return (mCompareResult & item) == item;
}
diff --git a/src/com/android/bluetooth/csip/CsipSetCoordinatorService.java b/src/com/android/bluetooth/csip/CsipSetCoordinatorService.java
index e34dca1..61f4cdf 100644
--- a/src/com/android/bluetooth/csip/CsipSetCoordinatorService.java
+++ b/src/com/android/bluetooth/csip/CsipSetCoordinatorService.java
@@ -592,6 +592,10 @@
}
ParcelUuid uuid = mGroupIdToUuidMap.get(groupId);
+ if (mCallbacks.get(uuid) == null) {
+ Log.e(TAG, " There is no clients for uuid: " + uuid);
+ return;
+ }
for (Map.Entry<Executor, IBluetoothCsipSetCoordinatorCallback> entry :
mCallbacks.get(uuid).entrySet()) {
diff --git a/src/com/android/bluetooth/gatt/CallbackInfo.java b/src/com/android/bluetooth/gatt/CallbackInfo.java
index 330422d..f6035b3 100644
--- a/src/com/android/bluetooth/gatt/CallbackInfo.java
+++ b/src/com/android/bluetooth/gatt/CallbackInfo.java
@@ -26,16 +26,38 @@
public String address;
public int status;
public int handle;
+ public byte[] value;
- CallbackInfo(String address, int status, int handle) {
+ static class Builder {
+ private String mAddress;
+ private int mStatus;
+ private int mHandle;
+ private byte[] mValue;
+
+ Builder(String address, int status) {
+ mAddress = address;
+ mStatus = status;
+ }
+
+ Builder setHandle(int handle) {
+ mHandle = handle;
+ return this;
+ }
+
+ Builder setValue(byte[] value) {
+ mValue = value;
+ return this;
+ }
+
+ CallbackInfo build() {
+ return new CallbackInfo(mAddress, mStatus, mHandle, mValue);
+ }
+ }
+
+ private CallbackInfo(String address, int status, int handle, byte[] value) {
this.address = address;
this.status = status;
this.handle = handle;
}
-
- CallbackInfo(String address, int status) {
- this.address = address;
- this.status = status;
- }
}
diff --git a/src/com/android/bluetooth/gatt/GattService.java b/src/com/android/bluetooth/gatt/GattService.java
index 17f7fca..e7b6aac 100644
--- a/src/com/android/bluetooth/gatt/GattService.java
+++ b/src/com/android/bluetooth/gatt/GattService.java
@@ -30,6 +30,7 @@
import android.bluetooth.BluetoothGattDescriptor;
import android.bluetooth.BluetoothGattService;
import android.bluetooth.BluetoothProfile;
+import android.bluetooth.BluetoothStatusCodes;
import android.bluetooth.IBluetoothGatt;
import android.bluetooth.IBluetoothGattCallback;
import android.bluetooth.IBluetoothGattServerCallback;
@@ -734,7 +735,7 @@
int authReq, byte[] value, AttributionSource attributionSource) {
GattService service = getService();
if (service == null) {
- return BluetoothGatt.GATT_WRITE_REQUEST_FAIL;
+ return BluetoothStatusCodes.ERROR_PROFILE_SERVICE_NOT_BOUND;
}
return service.writeCharacteristic(clientIf, address, handle, writeType, authReq, value,
attributionSource);
@@ -751,13 +752,14 @@
}
@Override
- public void writeDescriptor(int clientIf, String address, int handle, int authReq,
+ public int writeDescriptor(int clientIf, String address, int handle, int authReq,
byte[] value, AttributionSource attributionSource) {
GattService service = getService();
if (service == null) {
- return;
+ return BluetoothStatusCodes.ERROR_PROFILE_SERVICE_NOT_BOUND;
}
- service.writeDescriptor(clientIf, address, handle, authReq, value, attributionSource);
+ return service.writeDescriptor(clientIf, address, handle, authReq, value,
+ attributionSource);
}
@Override
@@ -1694,7 +1696,8 @@
}
}
- void onWriteCharacteristic(int connId, int status, int handle) throws RemoteException {
+ void onWriteCharacteristic(int connId, int status, int handle, byte[] data)
+ throws RemoteException {
String address = mClientMap.addressByConnId(connId);
synchronized (mPermits) {
Log.d(TAG, "onWriteCharacteristic() - increasing permit for address="
@@ -1703,7 +1706,8 @@
}
if (VDBG) {
- Log.d(TAG, "onWriteCharacteristic() - address=" + address + ", status=" + status);
+ Log.d(TAG, "onWriteCharacteristic() - address=" + address + ", status=" + status
+ + ", length=" + data.length);
}
ClientMap.App app = mClientMap.getByConnId(connId);
@@ -1712,12 +1716,15 @@
}
if (!app.isCongested) {
- app.callback.onCharacteristicWrite(address, status, handle);
+ app.callback.onCharacteristicWrite(address, status, handle, data);
} else {
if (status == BluetoothGatt.GATT_CONNECTION_CONGESTED) {
status = BluetoothGatt.GATT_SUCCESS;
}
- CallbackInfo callbackInfo = new CallbackInfo(address, status, handle);
+ CallbackInfo callbackInfo = new CallbackInfo.Builder(address, status)
+ .setHandle(handle)
+ .setValue(data)
+ .build();
app.queueCallback(callbackInfo);
}
}
@@ -1749,16 +1756,18 @@
}
}
- void onWriteDescriptor(int connId, int status, int handle) throws RemoteException {
+ void onWriteDescriptor(int connId, int status, int handle, byte[] data)
+ throws RemoteException {
String address = mClientMap.addressByConnId(connId);
if (VDBG) {
- Log.d(TAG, "onWriteDescriptor() - address=" + address + ", status=" + status);
+ Log.d(TAG, "onWriteDescriptor() - address=" + address + ", status=" + status
+ + ", length=" + data.length);
}
ClientMap.App app = mClientMap.getByConnId(connId);
if (app != null) {
- app.callback.onDescriptorWrite(address, status, handle);
+ app.callback.onDescriptorWrite(address, status, handle, data);
}
}
@@ -2181,7 +2190,7 @@
return;
}
app.callback.onCharacteristicWrite(callbackInfo.address, callbackInfo.status,
- callbackInfo.handle);
+ callbackInfo.handle, callbackInfo.value);
}
}
}
@@ -2915,7 +2924,7 @@
byte[] value, AttributionSource attributionSource) {
if (!Utils.checkConnectPermissionForDataDelivery(
this, attributionSource, "GattService writeCharacteristic")) {
- return BluetoothGatt.GATT_WRITE_REQUEST_FAIL;
+ return BluetoothStatusCodes.ERROR_MISSING_BLUETOOTH_CONNECT_PERMISSION;
}
if (VDBG) {
@@ -2929,12 +2938,12 @@
Integer connId = mClientMap.connIdByAddress(clientIf, address);
if (connId == null) {
Log.e(TAG, "writeCharacteristic() - No connection for " + address + "...");
- return BluetoothGatt.GATT_WRITE_REQUEST_FAIL;
+ return BluetoothStatusCodes.ERROR_DEVICE_NOT_CONNECTED;
}
if (!permissionCheck(connId, handle)) {
Log.w(TAG, "writeCharacteristic() - permission check failed!");
- return BluetoothGatt.GATT_WRITE_REQUEST_FAIL;
+ return BluetoothStatusCodes.ERROR_MISSING_BLUETOOTH_PRIVILEGED_PERMISSION;
}
Log.d(TAG, "writeCharacteristic() - trying to acquire permit.");
@@ -2943,19 +2952,19 @@
AtomicBoolean atomicBoolean = mPermits.get(address);
if (atomicBoolean == null) {
Log.d(TAG, "writeCharacteristic() - atomicBoolean uninitialized!");
- return BluetoothGatt.GATT_WRITE_REQUEST_FAIL;
+ return BluetoothStatusCodes.ERROR_DEVICE_NOT_CONNECTED;
}
boolean success = atomicBoolean.get();
if (!success) {
- Log.d(TAG, "writeCharacteristic() - no permit available.");
- return BluetoothGatt.GATT_WRITE_REQUEST_BUSY;
+ Log.d(TAG, "writeCharacteristic() - no permit available.");
+ return BluetoothStatusCodes.ERROR_GATT_WRITE_REQUEST_BUSY;
}
atomicBoolean.set(false);
}
gattClientWriteCharacteristicNative(connId, handle, writeType, authReq, value);
- return BluetoothGatt.GATT_WRITE_REQUEST_SUCCESS;
+ return BluetoothStatusCodes.SUCCESS;
}
@RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
@@ -2985,11 +2994,11 @@
}
@RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
- void writeDescriptor(int clientIf, String address, int handle, int authReq, byte[] value,
+ int writeDescriptor(int clientIf, String address, int handle, int authReq, byte[] value,
AttributionSource attributionSource) {
if (!Utils.checkConnectPermissionForDataDelivery(
this, attributionSource, "GattService writeDescriptor")) {
- return;
+ return BluetoothStatusCodes.ERROR_MISSING_BLUETOOTH_CONNECT_PERMISSION;
}
if (VDBG) {
Log.d(TAG, "writeDescriptor() - address=" + address);
@@ -2998,15 +3007,16 @@
Integer connId = mClientMap.connIdByAddress(clientIf, address);
if (connId == null) {
Log.e(TAG, "writeDescriptor() - No connection for " + address + "...");
- return;
+ return BluetoothStatusCodes.ERROR_DEVICE_NOT_CONNECTED;
}
if (!permissionCheck(connId, handle)) {
Log.w(TAG, "writeDescriptor() - permission check failed!");
- return;
+ return BluetoothStatusCodes.ERROR_MISSING_BLUETOOTH_PRIVILEGED_PERMISSION;
}
gattClientWriteDescriptorNative(connId, handle, authReq, value);
+ return BluetoothStatusCodes.SUCCESS;
}
@RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
@@ -3410,7 +3420,7 @@
if (status == BluetoothGatt.GATT_CONNECTION_CONGESTED) {
status = BluetoothGatt.GATT_SUCCESS;
}
- app.queueCallback(new CallbackInfo(address, status));
+ app.queueCallback(new CallbackInfo.Builder(address, status).build());
}
}
diff --git a/src/com/android/bluetooth/hfp/HeadsetPhoneState.java b/src/com/android/bluetooth/hfp/HeadsetPhoneState.java
index 50094cf1..c1c43c8 100644
--- a/src/com/android/bluetooth/hfp/HeadsetPhoneState.java
+++ b/src/com/android/bluetooth/hfp/HeadsetPhoneState.java
@@ -22,6 +22,7 @@
import android.telephony.PhoneStateListener;
import android.telephony.ServiceState;
import android.telephony.SignalStrength;
+import android.telephony.SignalStrengthUpdateRequest;
import android.telephony.SubscriptionManager;
import android.telephony.SubscriptionManager.OnSubscriptionsChangedListener;
import android.telephony.TelephonyManager;
@@ -70,6 +71,7 @@
private final HashMap<BluetoothDevice, Integer> mDeviceEventMap = new HashMap<>();
private PhoneStateListener mPhoneStateListener;
private final OnSubscriptionsChangedListener mOnSubscriptionsChangedListener;
+ private SignalStrengthUpdateRequest mSignalStrengthUpdateRequest;
HeadsetPhoneState(HeadsetService headsetService) {
Objects.requireNonNull(headsetService, "headsetService is null");
@@ -85,6 +87,9 @@
mOnSubscriptionsChangedListener = new HeadsetPhoneStateOnSubscriptionChangedListener();
mSubscriptionManager.addOnSubscriptionsChangedListener(command -> mHandler.post(command),
mOnSubscriptionsChangedListener);
+ mSignalStrengthUpdateRequest = new SignalStrengthUpdateRequest.Builder()
+ .setSystemThresholdReportingRequestedWhileIdle(true)
+ .build();
}
/**
@@ -156,6 +161,9 @@
Log.i(TAG, "startListenForPhoneState(), subId=" + subId + ", enabled_events=" + events);
mPhoneStateListener = new HeadsetPhoneStateListener(command -> mHandler.post(command));
mTelephonyManager.listen(mPhoneStateListener, events);
+ if ((events & PhoneStateListener.LISTEN_SIGNAL_STRENGTHS) != 0) {
+ mTelephonyManager.setSignalStrengthUpdateRequest(mSignalStrengthUpdateRequest);
+ }
}
private void stopListenForPhoneState() {
@@ -167,6 +175,7 @@
+ getTelephonyEventsToListen());
mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_NONE);
mPhoneStateListener = null;
+ mTelephonyManager.clearSignalStrengthUpdateRequest(mSignalStrengthUpdateRequest);
}
int getCindService() {
diff --git a/src/com/android/bluetooth/hfp/HeadsetService.java b/src/com/android/bluetooth/hfp/HeadsetService.java
index 29117ef..7c3784e 100644
--- a/src/com/android/bluetooth/hfp/HeadsetService.java
+++ b/src/com/android/bluetooth/hfp/HeadsetService.java
@@ -1645,7 +1645,7 @@
// Suspend A2DP when call about is about to become active
if (mActiveDevice != null && callState != HeadsetHalConstants.CALL_STATE_DISCONNECTED
&& !mSystemInterface.isCallIdle() && isCallIdleBefore) {
- mSystemInterface.getAudioManager().setParameters("A2dpSuspended=true");
+ mSystemInterface.getAudioManager().setA2dpSuspended(true);
}
});
doForEachConnectedStateMachine(
@@ -1655,7 +1655,7 @@
if (callState == HeadsetHalConstants.CALL_STATE_IDLE
&& mSystemInterface.isCallIdle() && !isAudioOn()) {
// Resume A2DP when call ended and SCO is not connected
- mSystemInterface.getAudioManager().setParameters("A2dpSuspended=false");
+ mSystemInterface.getAudioManager().setA2dpSuspended(false);
}
});
@@ -1813,7 +1813,7 @@
}
// Unsuspend A2DP when SCO connection is gone and call state is idle
if (mSystemInterface.isCallIdle()) {
- mSystemInterface.getAudioManager().setParameters("A2dpSuspended=false");
+ mSystemInterface.getAudioManager().setA2dpSuspended(false);
}
}
}
@@ -1928,6 +1928,7 @@
@Override
public void dump(StringBuilder sb) {
+ boolean isScoOn = mSystemInterface.getAudioManager().isBluetoothScoOn();
synchronized (mStateMachines) {
super.dump(sb);
ProfileService.println(sb, "mMaxHeadsetConnections: " + mMaxHeadsetConnections);
@@ -1948,9 +1949,7 @@
ProfileService.println(sb, "mForceScoAudio: " + mForceScoAudio);
ProfileService.println(sb, "mCreated: " + mCreated);
ProfileService.println(sb, "mStarted: " + mStarted);
- ProfileService.println(sb,
- "AudioManager.isBluetoothScoOn(): " + mSystemInterface.getAudioManager()
- .isBluetoothScoOn());
+ ProfileService.println(sb, "AudioManager.isBluetoothScoOn(): " + isScoOn);
ProfileService.println(sb, "Telecom.isInCall(): " + mSystemInterface.isInCall());
ProfileService.println(sb, "Telecom.isRinging(): " + mSystemInterface.isRinging());
for (HeadsetStateMachine stateMachine : mStateMachines.values()) {
diff --git a/src/com/android/bluetooth/hfp/HeadsetStateMachine.java b/src/com/android/bluetooth/hfp/HeadsetStateMachine.java
index fe3f98a..bbc7878 100644
--- a/src/com/android/bluetooth/hfp/HeadsetStateMachine.java
+++ b/src/com/android/bluetooth/hfp/HeadsetStateMachine.java
@@ -80,12 +80,6 @@
private static final String TAG = "HeadsetStateMachine";
private static final boolean DBG = false;
- private static final String HEADSET_NAME = "bt_headset_name";
- private static final String HEADSET_NREC = "bt_headset_nrec";
- private static final String HEADSET_WBS = "bt_wbs";
- private static final String HEADSET_AUDIO_FEATURE_ON = "on";
- private static final String HEADSET_AUDIO_FEATURE_OFF = "off";
-
static final int CONNECT = 1;
static final int DISCONNECT = 2;
static final int CONNECT_AUDIO = 3;
@@ -114,6 +108,10 @@
// NOTE: the value is not "final" - it is modified in the unit tests
@VisibleForTesting static int sConnectTimeoutMs = 30000;
+ // Number of times we should retry disconnecting audio before
+ // disconnecting the device.
+ private static final int MAX_RETRY_DISCONNECT_AUDIO = 3;
+
private static final HeadsetAgIndicatorEnableState DEFAULT_AG_INDICATOR_ENABLE_STATE =
new HeadsetAgIndicatorEnableState(true, true, true, true);
@@ -143,12 +141,15 @@
private HeadsetAgIndicatorEnableState mAgIndicatorEnableState;
// The timestamp when the device entered connecting/connected state
private long mConnectingTimestampMs = Long.MIN_VALUE;
- // Audio Parameters like NREC
- private final HashMap<String, String> mAudioParams = new HashMap<>();
+ // Audio Parameters
+ private boolean mHasNrecEnabled = false;
+ private boolean mHasWbsEnabled = false;
// AT Phone book keeps a group of states used by AT+CPBR commands
private final AtPhonebook mPhonebook;
// HSP specific
private boolean mNeedDialingOutReply;
+ // Audio disconnect timeout retry count
+ private int mAudioDisconnectRetry = 0;
// Keys are AT commands, and values are the company IDs.
private static final Map<String, Integer> VENDOR_SPECIFIC_AT_COMMAND_COMPANY_ID;
@@ -221,7 +222,8 @@
if (mPhonebook != null) {
mPhonebook.cleanup();
}
- mAudioParams.clear();
+ mHasWbsEnabled = false;
+ mHasNrecEnabled = false;
}
public void dump(StringBuilder sb) {
@@ -316,8 +318,7 @@
BluetoothStatsLog.write(BluetoothStatsLog.BLUETOOTH_SCO_CONNECTION_STATE_CHANGED,
mAdapterService.obfuscateAddress(device),
getConnectionStateFromAudioState(toState),
- TextUtils.equals(mAudioParams.get(HEADSET_WBS), HEADSET_AUDIO_FEATURE_ON)
- ? BluetoothHfpProtoEnums.SCO_CODEC_MSBC
+ mHasWbsEnabled ? BluetoothHfpProtoEnums.SCO_CODEC_MSBC
: BluetoothHfpProtoEnums.SCO_CODEC_CVSD,
mAdapterService.getMetricId(device));
mHeadsetService.onAudioStateChangedFromStateMachine(device, fromState, toState);
@@ -450,7 +451,8 @@
mPhonebook.resetAtState();
updateAgIndicatorEnableState(null);
mNeedDialingOutReply = false;
- mAudioParams.clear();
+ mHasWbsEnabled = false;
+ mHasNrecEnabled = false;
broadcastStateTransitions();
// Remove the state machine for unbonded devices
if (mPrevState != null
@@ -1042,6 +1044,10 @@
// state. This is to prevent auto connect attempts from disconnecting
// devices that previously successfully connected.
removeDeferredMessages(CONNECT);
+ } else if (mPrevState == mAudioDisconnecting) {
+ // Reset audio disconnecting retry count. Either the disconnection was successful
+ // or the retry count reached MAX_RETRY_DISCONNECT_AUDIO.
+ mAudioDisconnectRetry = 0;
}
broadcastStateTransitions();
}
@@ -1073,9 +1079,9 @@
break;
case CONNECT_AUDIO:
stateLogD("CONNECT_AUDIO, device=" + mDevice);
- mSystemInterface.getAudioManager().setParameters("A2dpSuspended=true");
+ mSystemInterface.getAudioManager().setA2dpSuspended(true);
if (!mNativeInterface.connectAudio(mDevice)) {
- mSystemInterface.getAudioManager().setParameters("A2dpSuspended=false");
+ mSystemInterface.getAudioManager().setA2dpSuspended(false);
stateLogE("Failed to connect SCO audio for " + mDevice);
// No state change involved, fire broadcast immediately
broadcastAudioState(mDevice, BluetoothHeadset.STATE_AUDIO_DISCONNECTED,
@@ -1283,7 +1289,7 @@
stateLogW("CONNECT_AUDIO device is not connected " + device);
break;
}
- stateLogW("CONNECT_AUDIO device auido is already connected " + device);
+ stateLogW("CONNECT_AUDIO device audio is already connected " + device);
break;
}
case DISCONNECT_AUDIO: {
@@ -1385,8 +1391,18 @@
stateLogW("CONNECT_TIMEOUT for unknown device " + device);
break;
}
- stateLogW("CONNECT_TIMEOUT");
- transitionTo(mConnected);
+ if (mAudioDisconnectRetry == MAX_RETRY_DISCONNECT_AUDIO) {
+ stateLogW("CONNECT_TIMEOUT: Disconnecting device");
+ // Restoring state to Connected with message DISCONNECT
+ deferMessage(obtainMessage(DISCONNECT, mDevice));
+ transitionTo(mConnected);
+ } else {
+ mAudioDisconnectRetry += 1;
+ stateLogW("CONNECT_TIMEOUT: retrying "
+ + (MAX_RETRY_DISCONNECT_AUDIO - mAudioDisconnectRetry)
+ + " more time(s)");
+ transitionTo(mAudioOn);
+ }
break;
}
default:
@@ -1407,6 +1423,8 @@
break;
case HeadsetHalConstants.AUDIO_STATE_CONNECTED:
stateLogW("processAudioEvent: audio disconnection failed");
+ // Audio connected, resetting disconnect retry.
+ mAudioDisconnectRetry = 0;
transitionTo(mAudioOn);
break;
case HeadsetHalConstants.AUDIO_STATE_CONNECTING:
@@ -1508,15 +1526,12 @@
}
private void setAudioParameters() {
- String keyValuePairs = String.join(";", new String[]{
- HEADSET_NAME + "=" + getCurrentDeviceName(),
- HEADSET_NREC + "=" + mAudioParams.getOrDefault(HEADSET_NREC,
- HEADSET_AUDIO_FEATURE_OFF),
- HEADSET_WBS + "=" + mAudioParams.getOrDefault(HEADSET_WBS,
- HEADSET_AUDIO_FEATURE_OFF)
- });
- Log.i(TAG, "setAudioParameters for " + mDevice + ": " + keyValuePairs);
- mSystemInterface.getAudioManager().setParameters(keyValuePairs);
+ AudioManager am = mSystemInterface.getAudioManager();
+ Log.i(TAG, "setAudioParameters for " + mDevice + ":"
+ + " Name=" + getCurrentDeviceName()
+ + " hasNrecEnabled=" + mHasNrecEnabled
+ + " hasWbsEnabled=" + mHasWbsEnabled);
+ am.setBluetoothHeadsetProperties(getCurrentDeviceName(), mHasNrecEnabled, mHasWbsEnabled);
}
private String parseUnknownAt(String atString) {
@@ -1645,32 +1660,28 @@
}
private void processNoiseReductionEvent(boolean enable) {
- String prevNrec = mAudioParams.getOrDefault(HEADSET_NREC, HEADSET_AUDIO_FEATURE_OFF);
- String newNrec = enable ? HEADSET_AUDIO_FEATURE_ON : HEADSET_AUDIO_FEATURE_OFF;
- mAudioParams.put(HEADSET_NREC, newNrec);
- log("processNoiseReductionEvent: " + HEADSET_NREC + " change " + prevNrec + " -> "
- + newNrec);
+ log("processNoiseReductionEvent: " + mHasNrecEnabled + " -> " + enable);
+ mHasNrecEnabled = enable;
if (getAudioState() == BluetoothHeadset.STATE_AUDIO_CONNECTED) {
setAudioParameters();
}
}
private void processWBSEvent(int wbsConfig) {
- String prevWbs = mAudioParams.getOrDefault(HEADSET_WBS, HEADSET_AUDIO_FEATURE_OFF);
+ boolean prevWbs = mHasWbsEnabled;
switch (wbsConfig) {
case HeadsetHalConstants.BTHF_WBS_YES:
- mAudioParams.put(HEADSET_WBS, HEADSET_AUDIO_FEATURE_ON);
+ mHasWbsEnabled = true;
break;
case HeadsetHalConstants.BTHF_WBS_NO:
case HeadsetHalConstants.BTHF_WBS_NONE:
- mAudioParams.put(HEADSET_WBS, HEADSET_AUDIO_FEATURE_OFF);
+ mHasWbsEnabled = false;
break;
default:
Log.e(TAG, "processWBSEvent: unknown wbsConfig " + wbsConfig);
return;
}
- log("processWBSEvent: " + HEADSET_NREC + " change " + prevWbs + " -> " + mAudioParams.get(
- HEADSET_WBS));
+ log("processWBSEvent: " + prevWbs + " -> " + mHasWbsEnabled);
}
@RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE)
@@ -2053,7 +2064,7 @@
events |= PhoneStateListener.LISTEN_SERVICE_STATE;
}
if (mAgIndicatorEnableState != null && mAgIndicatorEnableState.signal) {
- events |= PhoneStateListener.LISTEN_ALWAYS_REPORTED_SIGNAL_STRENGTH;
+ events |= PhoneStateListener.LISTEN_SIGNAL_STRENGTHS;
}
mSystemInterface.getHeadsetPhoneState().listenForPhoneState(mDevice, events);
}
diff --git a/src/com/android/bluetooth/hfpclient/HeadsetClientService.java b/src/com/android/bluetooth/hfpclient/HeadsetClientService.java
index 319372a..8c45847 100644
--- a/src/com/android/bluetooth/hfpclient/HeadsetClientService.java
+++ b/src/com/android/bluetooth/hfpclient/HeadsetClientService.java
@@ -39,6 +39,7 @@
import com.android.bluetooth.btservice.ProfileService;
import com.android.bluetooth.btservice.storage.DatabaseManager;
import com.android.bluetooth.hfpclient.connserv.HfpClientConnectionService;
+import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import java.util.ArrayList;
@@ -59,7 +60,11 @@
private static final boolean DBG = false;
private static final String TAG = "HeadsetClientService";
- private HashMap<BluetoothDevice, HeadsetClientStateMachine> mStateMachineMap = new HashMap<>();
+ // This is also used as a lock for shared data in {@link HeadsetClientService}
+ @GuardedBy("mStateMachineMap")
+ private final HashMap<BluetoothDevice, HeadsetClientStateMachine> mStateMachineMap =
+ new HashMap<>();
+
private static HeadsetClientService sHeadsetClientService;
private NativeInterface mNativeInterface = null;
private HandlerThread mSmThread = null;
@@ -71,6 +76,8 @@
// Maxinum number of devices we can try connecting to in one session
private static final int MAX_STATE_MACHINES_POSSIBLE = 100;
+ private final Object mStartStopLock = new Object();
+
public static final String HFP_CLIENT_STOP_TAG = "hfp_client_stop_tag";
@Override
@@ -79,83 +86,94 @@
}
@Override
- protected synchronized boolean start() {
- if (DBG) {
- Log.d(TAG, "start()");
+ protected boolean start() {
+ synchronized (mStartStopLock) {
+ if (DBG) {
+ Log.d(TAG, "start()");
+ }
+ if (getHeadsetClientService() != null) {
+ Log.w(TAG, "start(): start called without stop");
+ return false;
+ }
+
+ mDatabaseManager = Objects.requireNonNull(
+ AdapterService.getAdapterService().getDatabase(),
+ "DatabaseManager cannot be null when HeadsetClientService starts");
+
+ // Setup the JNI service
+ mNativeInterface = NativeInterface.getInstance();
+ mNativeInterface.initialize();
+
+ mBatteryManager = getSystemService(BatteryManager.class);
+
+ mAudioManager = getSystemService(AudioManager.class);
+ if (mAudioManager == null) {
+ Log.e(TAG, "AudioManager service doesn't exist?");
+ } else {
+ // start AudioManager in a known state
+ mAudioManager.setHfpEnabled(false);
+ }
+
+ mSmFactory = new HeadsetClientStateMachineFactory();
+ synchronized (mStateMachineMap) {
+ mStateMachineMap.clear();
+ }
+
+ IntentFilter filter = new IntentFilter(AudioManager.VOLUME_CHANGED_ACTION);
+ filter.addAction(Intent.ACTION_BATTERY_CHANGED);
+ registerReceiver(mBroadcastReceiver, filter);
+
+ // Start the HfpClientConnectionService to create connection with telecom when HFP
+ // connection is available.
+ Intent startIntent = new Intent(this, HfpClientConnectionService.class);
+ startService(startIntent);
+
+ // Create the thread on which all State Machines will run
+ mSmThread = new HandlerThread("HeadsetClient.SM");
+ mSmThread.start();
+
+ setHeadsetClientService(this);
+ return true;
}
- if (sHeadsetClientService != null) {
- Log.w(TAG, "start(): start called without stop");
- return false;
- }
-
- mDatabaseManager = Objects.requireNonNull(AdapterService.getAdapterService().getDatabase(),
- "DatabaseManager cannot be null when HeadsetClientService starts");
-
- // Setup the JNI service
- mNativeInterface = NativeInterface.getInstance();
- mNativeInterface.initialize();
-
- mBatteryManager = getSystemService(BatteryManager.class);
-
- mAudioManager = getSystemService(AudioManager.class);
- if (mAudioManager == null) {
- Log.e(TAG, "AudioManager service doesn't exist?");
- } else {
- // start AudioManager in a known state
- mAudioManager.setParameters("hfp_enable=false");
- }
-
- mSmFactory = new HeadsetClientStateMachineFactory();
- mStateMachineMap.clear();
-
- IntentFilter filter = new IntentFilter(AudioManager.VOLUME_CHANGED_ACTION);
- filter.addAction(Intent.ACTION_BATTERY_CHANGED);
- registerReceiver(mBroadcastReceiver, filter);
-
- // Start the HfpClientConnectionService to create connection with telecom when HFP
- // connection is available.
- Intent startIntent = new Intent(this, HfpClientConnectionService.class);
- startService(startIntent);
-
- // Create the thread on which all State Machines will run
- mSmThread = new HandlerThread("HeadsetClient.SM");
- mSmThread.start();
-
- setHeadsetClientService(this);
- return true;
}
@Override
- protected synchronized boolean stop() {
- if (sHeadsetClientService == null) {
- Log.w(TAG, "stop() called without start()");
- return false;
+ protected boolean stop() {
+ synchronized (mStartStopLock) {
+ synchronized (HeadsetClientService.class) {
+ if (sHeadsetClientService == null) {
+ Log.w(TAG, "stop() called without start()");
+ return false;
+ }
+
+ // Stop the HfpClientConnectionService.
+ Intent stopIntent = new Intent(this, HfpClientConnectionService.class);
+ sHeadsetClientService.stopService(stopIntent);
+ }
+
+ setHeadsetClientService(null);
+
+ unregisterReceiver(mBroadcastReceiver);
+
+ synchronized (mStateMachineMap) {
+ for (Iterator<Map.Entry<BluetoothDevice, HeadsetClientStateMachine>> it =
+ mStateMachineMap.entrySet().iterator(); it.hasNext(); ) {
+ HeadsetClientStateMachine sm =
+ mStateMachineMap.get((BluetoothDevice) it.next().getKey());
+ sm.doQuit();
+ it.remove();
+ }
+ }
+
+ // Stop the handler thread
+ mSmThread.quit();
+ mSmThread = null;
+
+ mNativeInterface.cleanup();
+ mNativeInterface = null;
+
+ return true;
}
-
- // Stop the HfpClientConnectionService.
- Intent stopIntent = new Intent(this, HfpClientConnectionService.class);
- sHeadsetClientService.stopService(stopIntent);
-
- setHeadsetClientService(null);
-
- unregisterReceiver(mBroadcastReceiver);
-
- for (Iterator<Map.Entry<BluetoothDevice, HeadsetClientStateMachine>> it =
- mStateMachineMap.entrySet().iterator(); it.hasNext(); ) {
- HeadsetClientStateMachine sm =
- mStateMachineMap.get((BluetoothDevice) it.next().getKey());
- sm.doQuit();
- it.remove();
- }
-
- // Stop the handler thread
- mSmThread.quit();
- mSmThread = null;
-
- mNativeInterface.cleanup();
- mNativeInterface = null;
-
- return true;
}
private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
@@ -182,8 +200,8 @@
"Setting volume to audio manager: " + streamValue + " hands free: "
+ hfVol);
}
- mAudioManager.setParameters("hfp_volume=" + hfVol);
- synchronized (this) {
+ mAudioManager.setHfpVolume(hfVol);
+ synchronized (mStateMachineMap) {
for (HeadsetClientStateMachine sm : mStateMachineMap.values()) {
if (sm != null) {
sm.sendMessage(HeadsetClientStateMachine.SET_SPEAKER_VOLUME,
@@ -206,7 +224,7 @@
"Send battery level update BIEV(2," + batteryLevel + ") command");
}
- synchronized (this) {
+ synchronized (mStateMachineMap) {
for (HeadsetClientStateMachine sm : mStateMachineMap.values()) {
if (sm != null) {
sm.sendMessage(HeadsetClientStateMachine.SEND_BIEV,
@@ -527,17 +545,16 @@
if (DBG) {
Log.d(TAG, "connect " + device);
}
- HeadsetClientStateMachine sm = getStateMachine(device);
- if (sm == null) {
- Log.e(TAG, "Cannot allocate SM for device " + device);
- return false;
- }
-
if (getConnectionPolicy(device) == BluetoothProfile.CONNECTION_POLICY_FORBIDDEN) {
Log.w(TAG, "Connection not allowed: <" + device.getAddress()
+ "> is CONNECTION_POLICY_FORBIDDEN");
return false;
}
+ HeadsetClientStateMachine sm = getStateMachine(device, true);
+ if (sm == null) {
+ Log.e(TAG, "Cannot allocate SM for device " + device);
+ return false;
+ }
sm.sendMessage(HeadsetClientStateMachine.CONNECT, device);
return true;
@@ -552,7 +569,7 @@
public boolean disconnect(BluetoothDevice device) {
HeadsetClientStateMachine sm = getStateMachine(device);
if (sm == null) {
- Log.e(TAG, "Cannot allocate SM for device " + device);
+ Log.e(TAG, "SM does not exist for device " + device);
return false;
}
@@ -566,24 +583,31 @@
return true;
}
- public synchronized List<BluetoothDevice> getConnectedDevices() {
+ /**
+ * @return A list of connected {@link BluetoothDevice}.
+ */
+ public List<BluetoothDevice> getConnectedDevices() {
ArrayList<BluetoothDevice> connectedDevices = new ArrayList<>();
- for (BluetoothDevice bd : mStateMachineMap.keySet()) {
- HeadsetClientStateMachine sm = mStateMachineMap.get(bd);
- if (sm != null && sm.getConnectionState(bd) == BluetoothProfile.STATE_CONNECTED) {
- connectedDevices.add(bd);
+ synchronized (mStateMachineMap) {
+ for (BluetoothDevice bd : mStateMachineMap.keySet()) {
+ HeadsetClientStateMachine sm = mStateMachineMap.get(bd);
+ if (sm != null && sm.getConnectionState(bd) == BluetoothProfile.STATE_CONNECTED) {
+ connectedDevices.add(bd);
+ }
}
}
return connectedDevices;
}
- private synchronized List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
+ private List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) {
List<BluetoothDevice> devices = new ArrayList<BluetoothDevice>();
- for (BluetoothDevice bd : mStateMachineMap.keySet()) {
- for (int state : states) {
- HeadsetClientStateMachine sm = mStateMachineMap.get(bd);
- if (sm != null && sm.getConnectionState(bd) == state) {
- devices.add(bd);
+ synchronized (mStateMachineMap) {
+ for (BluetoothDevice bd : mStateMachineMap.keySet()) {
+ for (int state : states) {
+ HeadsetClientStateMachine sm = mStateMachineMap.get(bd);
+ if (sm != null && sm.getConnectionState(bd) == state) {
+ devices.add(bd);
+ }
}
}
}
@@ -599,11 +623,12 @@
* {@link BluetoothProfile#STATE_CONNECTED} if this profile is connected, or
* {@link BluetoothProfile#STATE_DISCONNECTING} if this profile is being disconnected
*/
- public synchronized int getConnectionState(BluetoothDevice device) {
- HeadsetClientStateMachine sm = mStateMachineMap.get(device);
+ public int getConnectionState(BluetoothDevice device) {
+ HeadsetClientStateMachine sm = getStateMachine(device);
if (sm != null) {
return sm.getConnectionState(device);
}
+
return BluetoothProfile.STATE_DISCONNECTED;
}
@@ -659,7 +684,7 @@
boolean startVoiceRecognition(BluetoothDevice device) {
HeadsetClientStateMachine sm = getStateMachine(device);
if (sm == null) {
- Log.e(TAG, "Cannot allocate SM for device " + device);
+ Log.e(TAG, "SM does not exist for device " + device);
return false;
}
int connectionState = sm.getConnectionState(device);
@@ -673,7 +698,7 @@
boolean stopVoiceRecognition(BluetoothDevice device) {
HeadsetClientStateMachine sm = getStateMachine(device);
if (sm == null) {
- Log.e(TAG, "Cannot allocate SM for device " + device);
+ Log.e(TAG, "SM does not exist for device " + device);
return false;
}
int connectionState = sm.getConnectionState(device);
@@ -693,7 +718,7 @@
public int getAudioState(BluetoothDevice device) {
HeadsetClientStateMachine sm = getStateMachine(device);
if (sm == null) {
- Log.e(TAG, "Cannot allocate SM for device " + device);
+ Log.e(TAG, "SM does not exist for device " + device);
return -1;
}
@@ -703,7 +728,7 @@
boolean connectAudio(BluetoothDevice device) {
HeadsetClientStateMachine sm = getStateMachine(device);
if (sm == null) {
- Log.e(TAG, "Cannot allocate SM for device " + device);
+ Log.e(TAG, "SM does not exist for device " + device);
return false;
}
@@ -720,7 +745,7 @@
boolean disconnectAudio(BluetoothDevice device) {
HeadsetClientStateMachine sm = getStateMachine(device);
if (sm == null) {
- Log.e(TAG, "Cannot allocate SM for device " + device);
+ Log.e(TAG, "SM does not exist for device " + device);
return false;
}
@@ -734,7 +759,7 @@
boolean holdCall(BluetoothDevice device) {
HeadsetClientStateMachine sm = getStateMachine(device);
if (sm == null) {
- Log.e(TAG, "Cannot allocate SM for device " + device);
+ Log.e(TAG, "SM does not exist for device " + device);
return false;
}
@@ -750,7 +775,7 @@
boolean acceptCall(BluetoothDevice device, int flag) {
/* Phonecalls from a single device are supported, hang up any calls on the other phone */
- synchronized (this) {
+ synchronized (mStateMachineMap) {
for (Map.Entry<BluetoothDevice, HeadsetClientStateMachine> entry : mStateMachineMap
.entrySet()) {
if (entry.getValue() == null || entry.getKey().equals(device)) {
@@ -771,7 +796,7 @@
}
HeadsetClientStateMachine sm = getStateMachine(device);
if (sm == null) {
- Log.e(TAG, "Cannot allocate SM for device " + device);
+ Log.e(TAG, "SM does not exist for device " + device);
return false;
}
@@ -788,7 +813,7 @@
boolean rejectCall(BluetoothDevice device) {
HeadsetClientStateMachine sm = getStateMachine(device);
if (sm == null) {
- Log.e(TAG, "Cannot allocate SM for device " + device);
+ Log.e(TAG, "SM does not exist for device " + device);
return false;
}
@@ -806,7 +831,7 @@
boolean terminateCall(BluetoothDevice device, UUID uuid) {
HeadsetClientStateMachine sm = getStateMachine(device);
if (sm == null) {
- Log.e(TAG, "Cannot allocate SM for device " + device);
+ Log.e(TAG, "SM does not exist for device " + device);
return false;
}
@@ -825,7 +850,7 @@
boolean enterPrivateMode(BluetoothDevice device, int index) {
HeadsetClientStateMachine sm = getStateMachine(device);
if (sm == null) {
- Log.e(TAG, "Cannot allocate SM for device " + device);
+ Log.e(TAG, "SM does not exist for device " + device);
return false;
}
@@ -844,7 +869,7 @@
BluetoothHeadsetClientCall dial(BluetoothDevice device, String number) {
HeadsetClientStateMachine sm = getStateMachine(device);
if (sm == null) {
- Log.e(TAG, "Cannot allocate SM for device " + device);
+ Log.e(TAG, "SM does not exist for device " + device);
return null;
}
@@ -867,7 +892,7 @@
public boolean sendDTMF(BluetoothDevice device, byte code) {
HeadsetClientStateMachine sm = getStateMachine(device);
if (sm == null) {
- Log.e(TAG, "Cannot allocate SM for device " + device);
+ Log.e(TAG, "SM does not exist for device " + device);
return false;
}
@@ -889,7 +914,7 @@
public List<BluetoothHeadsetClientCall> getCurrentCalls(BluetoothDevice device) {
HeadsetClientStateMachine sm = getStateMachine(device);
if (sm == null) {
- Log.e(TAG, "Cannot allocate SM for device " + device);
+ Log.e(TAG, "SM does not exist for device " + device);
return null;
}
@@ -903,7 +928,7 @@
public boolean explicitCallTransfer(BluetoothDevice device) {
HeadsetClientStateMachine sm = getStateMachine(device);
if (sm == null) {
- Log.e(TAG, "Cannot allocate SM for device " + device);
+ Log.e(TAG, "SM does not exist for device " + device);
return false;
}
@@ -921,7 +946,7 @@
public boolean sendVendorAtCommand(BluetoothDevice device, int vendorId, String atCommand) {
HeadsetClientStateMachine sm = getStateMachine(device);
if (sm == null) {
- Log.e(TAG, "Cannot allocate SM for device " + device);
+ Log.e(TAG, "SM does not exist for device " + device);
return false;
}
@@ -939,7 +964,7 @@
public Bundle getCurrentAgEvents(BluetoothDevice device) {
HeadsetClientStateMachine sm = getStateMachine(device);
if (sm == null) {
- Log.e(TAG, "Cannot allocate SM for device " + device);
+ Log.e(TAG, "SM does not exist for device " + device);
return null;
}
@@ -953,7 +978,7 @@
public Bundle getCurrentAgFeatures(BluetoothDevice device) {
HeadsetClientStateMachine sm = getStateMachine(device);
if (sm == null) {
- Log.e(TAG, "Cannot allocate SM for device " + device);
+ Log.e(TAG, "SM does not exist for device " + device);
return null;
}
int connectionState = sm.getConnectionState(device);
@@ -965,57 +990,115 @@
// Handle messages from native (JNI) to java
public void messageFromNative(StackEvent stackEvent) {
- HeadsetClientStateMachine sm = getStateMachine(stackEvent.device);
- if (sm == null) {
- Log.w(TAG, "No SM found for event " + stackEvent);
- }
+ Objects.requireNonNull(stackEvent.device,
+ "Device should never be null, event: " + stackEvent);
+ HeadsetClientStateMachine sm = getStateMachine(stackEvent.device,
+ isConnectionEvent(stackEvent));
+ if (sm == null) {
+ throw new IllegalStateException(
+ "State machine not found for stack event: " + stackEvent);
+ }
sm.sendMessage(StackEvent.STACK_EVENT, stackEvent);
}
- // State machine management
- private synchronized HeadsetClientStateMachine getStateMachine(BluetoothDevice device) {
+ private boolean isConnectionEvent(StackEvent stackEvent) {
+ if (stackEvent.type == StackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED) {
+ if ((stackEvent.valueInt == HeadsetClientHalConstants.CONNECTION_STATE_CONNECTING)
+ || (stackEvent.valueInt
+ == HeadsetClientHalConstants.CONNECTION_STATE_CONNECTED)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private HeadsetClientStateMachine getStateMachine(BluetoothDevice device) {
+ return getStateMachine(device, false);
+ }
+
+ private HeadsetClientStateMachine getStateMachine(BluetoothDevice device,
+ boolean isConnectionEvent) {
if (device == null) {
Log.e(TAG, "getStateMachine failed: Device cannot be null");
return null;
}
- HeadsetClientStateMachine sm = mStateMachineMap.get(device);
+ HeadsetClientStateMachine sm;
+ synchronized (mStateMachineMap) {
+ sm = mStateMachineMap.get(device);
+ }
+
if (sm != null) {
if (DBG) {
Log.d(TAG, "Found SM for device " + device);
}
- return sm;
+ } else if (isConnectionEvent) {
+ // The only time a new state machine should be created when none was found is for
+ // connection events.
+ sm = allocateStateMachine(device);
+ if (sm == null) {
+ Log.e(TAG, "SM could not be allocated for device " + device);
+ }
}
-
- // There is a possibility of a DOS attack if someone populates here with a lot of fake
- // BluetoothAddresses. If it so happens instead of blowing up we can atleast put a limit on
- // how long the attack would survive
- if (mStateMachineMap.keySet().size() > MAX_STATE_MACHINES_POSSIBLE) {
- Log.e(TAG, "Max state machines reached, possible DOS attack "
- + MAX_STATE_MACHINES_POSSIBLE);
- return null;
- }
-
- // Allocate a new SM
- Log.d(TAG, "Creating a new state machine");
- sm = mSmFactory.make(this, mSmThread, mNativeInterface);
- mStateMachineMap.put(device, sm);
return sm;
}
+ private HeadsetClientStateMachine allocateStateMachine(BluetoothDevice device) {
+ if (device == null) {
+ Log.e(TAG, "allocateStateMachine failed: Device cannot be null");
+ return null;
+ }
+
+ if (getHeadsetClientService() == null) {
+ // Preconditions: {@code setHeadsetClientService(this)} is the last thing {@code start}
+ // does, and {@code setHeadsetClientService(null)} is (one of) the first thing
+ // {@code stop does}.
+ Log.e(TAG, "Cannot allocate SM if service has begun stopping or has not completed"
+ + " startup.");
+ return null;
+ }
+
+ synchronized (mStateMachineMap) {
+ HeadsetClientStateMachine sm = mStateMachineMap.get(device);
+ if (sm != null) {
+ if (DBG) {
+ Log.d(TAG, "allocateStateMachine: SM already exists for device " + device);
+ }
+ return sm;
+ }
+
+ // There is a possibility of a DOS attack if someone populates here with a lot of fake
+ // BluetoothAddresses. If it so happens instead of blowing up we can at least put a
+ // limit on how long the attack would survive
+ if (mStateMachineMap.keySet().size() > MAX_STATE_MACHINES_POSSIBLE) {
+ Log.e(TAG, "Max state machines reached, possible DOS attack "
+ + MAX_STATE_MACHINES_POSSIBLE);
+ return null;
+ }
+
+ // Allocate a new SM
+ Log.d(TAG, "Creating a new state machine");
+ sm = mSmFactory.make(this, mSmThread, mNativeInterface);
+ mStateMachineMap.put(device, sm);
+ return sm;
+ }
+ }
+
// Check if any of the state machines have routed the SCO audio stream.
- synchronized boolean isScoRouted() {
- for (Map.Entry<BluetoothDevice, HeadsetClientStateMachine> entry : mStateMachineMap
- .entrySet()) {
- if (entry.getValue() != null) {
- int audioState = entry.getValue().getAudioState(entry.getKey());
- if (audioState == BluetoothHeadsetClient.STATE_AUDIO_CONNECTED) {
- if (DBG) {
- Log.d(TAG, "Device " + entry.getKey() + " audio state " + audioState
- + " Connected");
+ boolean isScoRouted() {
+ synchronized (mStateMachineMap) {
+ for (Map.Entry<BluetoothDevice, HeadsetClientStateMachine> entry : mStateMachineMap
+ .entrySet()) {
+ if (entry.getValue() != null) {
+ int audioState = entry.getValue().getAudioState(entry.getKey());
+ if (audioState == BluetoothHeadsetClient.STATE_AUDIO_CONNECTED) {
+ if (DBG) {
+ Log.d(TAG, "Device " + entry.getKey() + " audio state " + audioState
+ + " Connected");
+ }
+ return true;
}
- return true;
}
}
}
@@ -1023,18 +1106,22 @@
}
@Override
- public synchronized void dump(StringBuilder sb) {
+ public void dump(StringBuilder sb) {
super.dump(sb);
- for (HeadsetClientStateMachine sm : mStateMachineMap.values()) {
- if (sm != null) {
- sm.dump(sb);
+ synchronized (mStateMachineMap) {
+ for (HeadsetClientStateMachine sm : mStateMachineMap.values()) {
+ if (sm != null) {
+ sm.dump(sb);
+ }
}
}
}
// For testing
- protected synchronized Map<BluetoothDevice, HeadsetClientStateMachine> getStateMachineMap() {
- return mStateMachineMap;
+ protected Map<BluetoothDevice, HeadsetClientStateMachine> getStateMachineMap() {
+ synchronized (mStateMachineMap) {
+ return mStateMachineMap;
+ }
}
protected void setSMFactory(HeadsetClientStateMachineFactory factory) {
@@ -1049,7 +1136,7 @@
int batteryLevel = mBatteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY);
int batteryIndicatorID = 2;
- synchronized (this) {
+ synchronized (mStateMachineMap) {
for (HeadsetClientStateMachine sm : mStateMachineMap.values()) {
if (sm != null) {
sm.sendMessage(HeadsetClientStateMachine.SEND_BIEV,
diff --git a/src/com/android/bluetooth/hfpclient/HeadsetClientStateMachine.java b/src/com/android/bluetooth/hfpclient/HeadsetClientStateMachine.java
index 2703831..13eeee9 100644
--- a/src/com/android/bluetooth/hfpclient/HeadsetClientStateMachine.java
+++ b/src/com/android/bluetooth/hfpclient/HeadsetClientStateMachine.java
@@ -483,7 +483,9 @@
if (mCalls.size() > 0) {
if (mService.getResources().getBoolean(R.bool.hfp_clcc_poll_during_call)) {
- sendMessageDelayed(QUERY_CURRENT_CALLS, QUERY_CURRENT_CALLS_WAIT_MILLIS);
+ sendMessageDelayed(QUERY_CURRENT_CALLS,
+ mService.getResources().getInteger(
+ R.integer.hfp_clcc_poll_interval_during_call));
} else {
if (getCall(BluetoothHeadsetClientCall.CALL_STATE_INCOMING) != null) {
logD("Still have incoming call; polling");
@@ -813,9 +815,9 @@
}
logD("hfp_enable=" + enable);
if (enable && !sAudioIsRouted) {
- mAudioManager.setParameters("hfp_enable=true");
+ mAudioManager.setHfpEnabled(true);
} else if (!enable) {
- mAudioManager.setParameters("hfp_enable=false");
+ mAudioManager.setHfpEnabled(false);
}
sAudioIsRouted = enable;
}
@@ -1330,9 +1332,16 @@
break;
case QUERY_CURRENT_CALLS:
removeMessages(QUERY_CURRENT_CALLS);
- if (mCalls.size() > 0) {
- // If there are ongoing calls periodically check their status.
- sendMessageDelayed(QUERY_CURRENT_CALLS, QUERY_CURRENT_CALLS_WAIT_MILLIS);
+ // If there are ongoing calls periodically check their status.
+ if (mCalls.size() > 1
+ && mService.getResources().getBoolean(
+ R.bool.hfp_clcc_poll_during_call)) {
+ sendMessageDelayed(QUERY_CURRENT_CALLS,
+ mService.getResources().getInteger(
+ R.integer.hfp_clcc_poll_interval_during_call));
+ } else if (mCalls.size() > 0) {
+ sendMessageDelayed(QUERY_CURRENT_CALLS,
+ QUERY_CURRENT_CALLS_WAIT_MILLIS);
}
queryCallsStart();
break;
@@ -1583,7 +1592,7 @@
// routing is handled by the bluetooth stack itself. The only reason to do so is
// because Bluetooth SCO connection from the HF role is not entirely supported
// for routing and volume purposes.
- // NOTE: All calls here are routed via the setParameters which changes the
+ // NOTE: All calls here are routed via AudioManager methods which changes the
// routing at the Audio HAL level.
if (mService.isScoRouted()) {
@@ -1605,15 +1614,15 @@
logD("hfp_enable=true mAudioWbs is " + mAudioWbs);
if (mAudioWbs) {
logD("Setting sampling rate as 16000");
- mAudioManager.setParameters("hfp_set_sampling_rate=16000");
+ mAudioManager.setHfpSamplingRate(16000);
} else {
logD("Setting sampling rate as 8000");
- mAudioManager.setParameters("hfp_set_sampling_rate=8000");
+ mAudioManager.setHfpSamplingRate(8000);
}
logD("hf_volume " + hfVol);
routeHfpAudio(true);
mAudioFocusRequest = requestAudioFocus();
- mAudioManager.setParameters("hfp_volume=" + hfVol);
+ mAudioManager.setHfpVolume(hfVol);
transitionTo(mAudioOn);
break;
diff --git a/src/com/android/bluetooth/le_audio/LeAudioNativeInterface.java b/src/com/android/bluetooth/le_audio/LeAudioNativeInterface.java
index c104c3d..0048d4f 100644
--- a/src/com/android/bluetooth/le_audio/LeAudioNativeInterface.java
+++ b/src/com/android/bluetooth/le_audio/LeAudioNativeInterface.java
@@ -177,6 +177,24 @@
}
/**
+ * Add new Node into a group.
+ * @param groupId group identifier
+ * @param device remote device
+ */
+ public boolean groupAddNode(int groupId, BluetoothDevice device) {
+ return groupAddNodeNative(groupId, getByteAddress(device));
+ }
+
+ /**
+ * Add new Node into a group.
+ * @param groupId group identifier
+ * @param device remote device
+ */
+ public boolean groupRemoveNode(int groupId, BluetoothDevice device) {
+ return groupRemoveNodeNative(groupId, getByteAddress(device));
+ }
+
+ /**
* Set active group.
* @param groupId group ID to set as active
*/
@@ -190,5 +208,7 @@
private native void cleanupNative();
private native boolean connectLeAudioNative(byte[] address);
private native boolean disconnectLeAudioNative(byte[] address);
+ private native boolean groupAddNodeNative(int groupId, byte[] address);
+ private native boolean groupRemoveNodeNative(int groupId, byte[] address);
private native void groupSetActiveNative(int groupId);
}
diff --git a/src/com/android/bluetooth/le_audio/LeAudioService.java b/src/com/android/bluetooth/le_audio/LeAudioService.java
index 05513c9..5983c33 100644
--- a/src/com/android/bluetooth/le_audio/LeAudioService.java
+++ b/src/com/android/bluetooth/le_audio/LeAudioService.java
@@ -23,18 +23,27 @@
import android.annotation.RequiresPermission;
import android.annotation.SuppressLint;
import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothCsipSetCoordinator;
import android.bluetooth.BluetoothLeAudio;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothUuid;
+import android.bluetooth.BluetoothVolumeControl;
+import android.bluetooth.IBluetoothCsipSetCoordinator;
+import android.bluetooth.IBluetoothCsipSetCoordinatorCallback;
import android.bluetooth.IBluetoothLeAudio;
import android.content.AttributionSource;
+import android.bluetooth.IBluetoothVolumeControl;
import android.content.BroadcastReceiver;
+import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
+import android.content.ServiceConnection;
import android.media.AudioManager;
import android.os.HandlerThread;
+import android.os.IBinder;
import android.os.ParcelUuid;
+import android.os.RemoteException;
import android.util.Log;
import com.android.bluetooth.Utils;
@@ -42,7 +51,9 @@
import com.android.bluetooth.btservice.ProfileService;
import com.android.bluetooth.btservice.ServiceFactory;
import com.android.bluetooth.btservice.storage.DatabaseManager;
+import com.android.bluetooth.csip.CsipSetCoordinatorService;
import com.android.bluetooth.mcp.McpService;
+import com.android.bluetooth.vc.VolumeControlService;
import com.android.internal.annotations.VisibleForTesting;
import java.util.ArrayList;
@@ -105,15 +116,101 @@
private final Map<BluetoothDevice, LeAudioStateMachine> mStateMachines = new HashMap<>();
private final Map<BluetoothDevice, Integer> mDeviceGroupIdMap = new ConcurrentHashMap<>();
+ private final Map<BluetoothDevice, Integer> mSetMemberAvailable = new ConcurrentHashMap<>();
private int mActiveDeviceGroupId = LE_AUDIO_GROUP_ID_INVALID;
private final int mContextSupportingInputAudio =
- BluetoothLeAudio.CONTEXT_TYPE_COMMUNICATION;
+ BluetoothLeAudio.CONTEXT_TYPE_COMMUNICATION |
+ BluetoothLeAudio.CONTEXT_TYPE_MAN_MACHINE;
+
private final int mContextSupportingOutputAudio = BluetoothLeAudio.CONTEXT_TYPE_COMMUNICATION |
- BluetoothLeAudio.CONTEXT_TYPE_MEDIA;
+ BluetoothLeAudio.CONTEXT_TYPE_MEDIA |
+ BluetoothLeAudio.CONTEXT_TYPE_INSTRUCTIONAL |
+ BluetoothLeAudio.CONTEXT_TYPE_ATTENTION_SEEKING |
+ BluetoothLeAudio.CONTEXT_TYPE_IMMEDIATE_ALERT |
+ BluetoothLeAudio.CONTEXT_TYPE_MAN_MACHINE |
+ BluetoothLeAudio.CONTEXT_TYPE_EMERGENCY_ALERT |
+ BluetoothLeAudio.CONTEXT_TYPE_RINGTONE |
+ BluetoothLeAudio.CONTEXT_TYPE_TV |
+ BluetoothLeAudio.CONTEXT_TYPE_LIVE |
+ BluetoothLeAudio.CONTEXT_TYPE_GAME;
private BroadcastReceiver mBondStateChangedReceiver;
private BroadcastReceiver mConnectionStateChangedReceiver;
+ class MyCsipSetCoordinatorCallbacks extends IBluetoothCsipSetCoordinatorCallback.Stub {
+ @Override
+ public void onCsisSetMemberAvailable(BluetoothDevice device, int groupId) {
+ synchronized (LeAudioService.this) {
+ LeAudioService.this.setMemberAvailable(device, groupId);
+ }
+ }
+ };
+
+ private volatile MyCsipSetCoordinatorCallbacks mCsipSetCoordinatorCallback =
+ new MyCsipSetCoordinatorCallbacks();
+ private volatile IBluetoothCsipSetCoordinator mCsipSetCoordinatorProxy;
+ private final ServiceConnection mCsipSetCoordinatorProxyConnection = new ServiceConnection() {
+ @Override
+ public void onServiceConnected(ComponentName className, IBinder service) {
+ if (DBG) {
+ Log.d(TAG, "CsisClientProxy connected");
+ }
+ synchronized (LeAudioService.this) {
+ mCsipSetCoordinatorProxy = IBluetoothCsipSetCoordinator.Stub.asInterface(service);
+ CsipSetCoordinatorService mCsipSetCoordinatorService =
+ CsipSetCoordinatorService.getCsipSetCoordinatorService();
+ if (mCsipSetCoordinatorService == null) {
+ Log.e(TAG, "CsisClientService is null on LeAudioService starts");
+ return;
+ }
+ mCsipSetCoordinatorService.registerCsisMemberObserver(
+ LeAudioService.this.getMainExecutor(),
+ BluetoothUuid.CAP,
+ mCsipSetCoordinatorCallback);
+ }
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName className) {
+ if (DBG) {
+ Log.d(TAG, "CsisClientProxy disconnected");
+ }
+ synchronized (LeAudioService.this) {
+ mCsipSetCoordinatorProxy = null;
+ }
+ }
+ };
+
+ private volatile IBluetoothVolumeControl mVolumeControlProxy;
+ VolumeControlService mVolumeControlService = null;
+ private final ServiceConnection mVolumeControlProxyConnection = new ServiceConnection() {
+ @Override
+ public void onServiceConnected(ComponentName className, IBinder service) {
+ if (DBG) {
+ Log.d(TAG, "mVolumeControlProxyConnection connected");
+ }
+ synchronized (LeAudioService.this) {
+
+ mVolumeControlProxy = IBluetoothVolumeControl.Stub.asInterface(service);
+ mVolumeControlService =
+ VolumeControlService.getVolumeControlService();
+ if (mVolumeControlService == null) {
+ Log.e(TAG, "VolumeControlService is null when LeAudioService starts");
+ }
+ }
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName className) {
+ if (DBG) {
+ Log.d(TAG, "mVolumeControlProxy disconnected");
+ }
+ synchronized (LeAudioService.this) {
+ mVolumeControlProxy = null;
+ }
+ }
+ };
+
@Override
protected IProfileServiceBinder initBinder() {
return new BluetoothLeAudioBinder(this);
@@ -148,6 +245,7 @@
mStateMachinesThread.start();
mDeviceGroupIdMap.clear();
+ mSetMemberAvailable.clear();
mGroupDescriptors.clear();
// Setup broadcast receivers
@@ -160,6 +258,12 @@
mConnectionStateChangedReceiver = new ConnectionStateChangedReceiver();
registerReceiver(mConnectionStateChangedReceiver, filter);
+ /* Bind Csis Service */
+ bindCsisClientService();
+
+ /* Bind Volume control service */
+ bindVolumeControlService();
+
// Mark service as started
setLeAudioService(this);
@@ -176,6 +280,20 @@
return true;
}
+ setActiveDevice(null);
+ //Don't wait for async call with INACTIVE group status, clean active
+ //device for active group.
+ for (Map.Entry<Integer, LeAudioGroupDescriptor> entry : mGroupDescriptors.entrySet()) {
+ LeAudioGroupDescriptor descriptor = entry.getValue();
+ Integer group_id = entry.getKey();
+ if (descriptor.mIsActive) {
+ descriptor.mIsActive = false;
+ updateActiveDevices(group_id, descriptor.mActiveContexts,
+ ACTIVE_CONTEXTS_NONE, descriptor.mIsActive);
+ break;
+ }
+ }
+
// Cleanup native interfaces
mLeAudioNativeInterface.cleanup();
mLeAudioNativeInterface = null;
@@ -199,6 +317,7 @@
}
mDeviceGroupIdMap.clear();
+ mSetMemberAvailable.clear();
mGroupDescriptors.clear();
if (mStateMachinesThread != null) {
@@ -208,6 +327,10 @@
mAudioManager = null;
mAdapterService = null;
+ mAudioManager = null;
+
+ unbindCsisClientService();
+ unbindVolumeControlService();
return true;
}
@@ -235,6 +358,54 @@
sLeAudioService = instance;
}
+ private void bindVolumeControlService() {
+ synchronized (mVolumeControlProxyConnection) {
+ Intent intent = new Intent(IBluetoothVolumeControl.class.getName());
+ ComponentName comp = intent.resolveSystemService(getPackageManager(), 0);
+ intent.setComponent(comp);
+ if (comp == null || !bindService(intent, mVolumeControlProxyConnection, 0)) {
+ Log.wtf(TAG, "Could not bind to IBluetoothVolumeControl Service with " +
+ intent);
+ }
+ }
+ }
+ private void unbindVolumeControlService() {
+ synchronized (mVolumeControlProxyConnection) {
+ if (mVolumeControlProxy != null) {
+ if (DBG) {
+ Log.d(TAG, "Unbinding mVolumeControlProxyConnection");
+ }
+ mVolumeControlProxy = null;
+ // Synchronization should make sure unbind can be successful
+ unbindService(mVolumeControlProxyConnection);
+ }
+ }
+ }
+
+ private void bindCsisClientService() {
+ synchronized (mCsipSetCoordinatorProxyConnection) {
+ Intent intent = new Intent(IBluetoothCsipSetCoordinator.class.getName());
+ ComponentName comp = intent.resolveSystemService(getPackageManager(), 0);
+ intent.setComponent(comp);
+ if (comp == null || !bindService(intent, mCsipSetCoordinatorProxyConnection, 0)) {
+ Log.wtf(TAG, "Could not bind to IBluetoothCsisClient Service with " +
+ intent);
+ }
+ }
+ }
+ private void unbindCsisClientService() {
+ synchronized (mCsipSetCoordinatorProxyConnection) {
+ if (mCsipSetCoordinatorProxy != null) {
+ if (DBG) {
+ Log.d(TAG, "Unbinding CsisClientProxyConnection");
+ }
+ mCsipSetCoordinatorProxy = null;
+ // Synchronization should make sure unbind can be successful
+ unbindService(mCsipSetCoordinatorProxyConnection);
+ }
+ }
+ }
+
public boolean connect(BluetoothDevice device) {
if (DBG) {
Log.d(TAG, "connect(): " + device);
@@ -262,7 +433,7 @@
Log.e(TAG, "Ignored connect request for " + device + " : no state machine");
return false;
}
- sm.sendMessage(LeAudioStateMachine.CONNECT, groupId);
+ sm.sendMessage(LeAudioStateMachine.CONNECT);
}
// Connect other devices from this group
@@ -275,16 +446,17 @@
continue;
}
synchronized (mStateMachines) {
- LeAudioStateMachine sm = getOrCreateStateMachine(storedDevice);
- if (sm == null) {
- Log.e(TAG, "Ignored connect request for " + storedDevice
- + " : no state machine");
- continue;
- }
- sm.sendMessage(LeAudioStateMachine.CONNECT, groupId);
- }
- }
- }
+ LeAudioStateMachine sm = getOrCreateStateMachine(storedDevice);
+ if (sm == null) {
+ Log.e(TAG, "Ignored connect request for " + storedDevice
+ + " : no state machine");
+ continue;
+ }
+ sm.sendMessage(LeAudioStateMachine.CONNECT);
+ }
+ }
+ }
+
return true;
}
@@ -414,6 +586,55 @@
}
/**
+ * Add device to the given group.
+ * @param groupId group ID the device is being added to
+ * @param device the active device
+ * @return true on success, otherwise false
+ */
+ public boolean groupAddNode(int groupId, BluetoothDevice device) {
+ return mLeAudioNativeInterface.groupAddNode(groupId, device);
+ }
+
+ /**
+ * Remove device from a given group.
+ * @param groupId group ID the device is being removed from
+ * @param device the active device
+ * @return true on success, otherwise false
+ */
+ public boolean groupRemoveNode(int groupId, BluetoothDevice device) {
+ return mLeAudioNativeInterface.groupRemoveNode(groupId, device);
+ }
+
+ /**
+ * Checks if given group exists.
+ * @param group_id group Id to verify
+ * @return true given group exists, otherwise false
+ */
+ public boolean isValidDeviceGroup(int group_id) {
+ return (group_id != LE_AUDIO_GROUP_ID_INVALID) ?
+ mDeviceGroupIdMap.containsValue(group_id) :
+ false;
+ }
+
+ /**
+ * Get all the devices within a given group.
+ * @param group_id group Id to verify
+ * @return all devices within a given group or empty list
+ */
+ public List<BluetoothDevice> getGroupDevices(int group_id) {
+ List<BluetoothDevice> result = new ArrayList<>();
+
+ if (group_id != LE_AUDIO_GROUP_ID_INVALID) {
+ for (BluetoothDevice storedDevice : mDeviceGroupIdMap.keySet()) {
+ if (getGroupId(storedDevice) == group_id) {
+ result.add(storedDevice);
+ }
+ }
+ }
+ return result;
+ }
+
+ /**
* Get supported group audio direction from available context.
*
* @param activeContext bitset of active context to be matched with possible audio direction
@@ -528,7 +749,7 @@
if (device != null && mPreviousAudioOutDevice != null) {
int previousGroupId = getGroupId(mPreviousAudioOutDevice);
if (previousGroupId == groupId) {
- /* This is thes same group as aleady notified to the system.
+ /* This is the same group as already notified to the system.
* Therefore do not change the device we have connected to the group,
* unless, previous one is disconnected now
*/
@@ -755,14 +976,9 @@
} else if (stackEvent.type == LeAudioStackEvent.EVENT_TYPE_GROUP_STATUS_CHANGED) {
int group_id = stackEvent.valueInt1;
int group_status = stackEvent.valueInt2;
+ boolean send_intent = false;
switch (group_status) {
- case LeAudioStackEvent.GROUP_STATUS_IDLE:
- case LeAudioStackEvent.GROUP_STATUS_RECONFIGURED:
- case LeAudioStackEvent.GROUP_STATUS_DESTROYED:
- case LeAudioStackEvent.GROUP_STATUS_SUSPENDED:
- case LeAudioStackEvent.GROUP_STATUS_STREAMING:
- break;
case LeAudioStackEvent.GROUP_STATUS_ACTIVE: {
LeAudioGroupDescriptor descriptor = mGroupDescriptors.get(group_id);
if (descriptor != null) {
@@ -770,6 +986,7 @@
descriptor.mIsActive = true;
updateActiveDevices(group_id, ACTIVE_CONTEXTS_NONE,
descriptor.mActiveContexts, descriptor.mIsActive);
+ send_intent = true;
}
} else {
Log.e(TAG, "no descriptors for group: " + group_id);
@@ -783,6 +1000,7 @@
descriptor.mIsActive = false;
updateActiveDevices(group_id, descriptor.mActiveContexts,
ACTIVE_CONTEXTS_NONE, descriptor.mIsActive);
+ send_intent = true;
}
} else {
Log.e(TAG, "no descriptors for group: " + group_id);
@@ -793,10 +1011,11 @@
break;
}
- intent = new Intent(BluetoothLeAudio.ACTION_LE_AUDIO_GROUP_STATUS_CHANGED);
- intent.putExtra(BluetoothLeAudio.EXTRA_LE_AUDIO_GROUP_ID, group_id);
- intent.putExtra(BluetoothLeAudio.EXTRA_LE_AUDIO_GROUP_STATUS, group_status);
-
+ if (send_intent) {
+ intent = new Intent(BluetoothLeAudio.ACTION_LE_AUDIO_GROUP_STATUS_CHANGED);
+ intent.putExtra(BluetoothLeAudio.EXTRA_LE_AUDIO_GROUP_ID, group_id);
+ intent.putExtra(BluetoothLeAudio.EXTRA_LE_AUDIO_GROUP_STATUS, group_status);
+ }
}
if (intent != null) {
@@ -863,6 +1082,18 @@
if (bondState != BluetoothDevice.BOND_NONE) {
return;
}
+
+ /* Remove bonded set member from outstanding list */
+ if (mSetMemberAvailable.containsKey(device)) {
+ mSetMemberAvailable.remove(device);
+ }
+
+ int groupId = getGroupId(device);
+ if (groupId != LE_AUDIO_GROUP_ID_INVALID) {
+ /* In case device is still in the group, let's remove it */
+ mLeAudioNativeInterface.groupRemoveNode(groupId, device);
+ }
+
mDeviceGroupIdMap.remove(device);
synchronized (mStateMachines) {
LeAudioStateMachine sm = mStateMachines.get(device);
@@ -994,6 +1225,22 @@
}
}
+ synchronized void setMemberAvailable(BluetoothDevice device, int groupId) {
+ if (device == null) {
+ Log.e(TAG, "unexpected invocation. device=" + device);
+ return;
+ }
+
+ if (mSetMemberAvailable.containsKey(device)) {
+ if (DBG) {
+ Log.d(TAG, "Device " + device + " is already notified- drop it");
+ }
+ return;
+ }
+
+ mSetMemberAvailable.put(device, groupId);
+ }
+
/**
* Check whether can connect to a peer device.
* The check considers a number of factors during the evaluation.
@@ -1086,8 +1333,7 @@
if (device == null) {
return LE_AUDIO_GROUP_ID_INVALID;
}
- //TODO: implement
- return LE_AUDIO_GROUP_ID_INVALID;
+ return mDeviceGroupIdMap.getOrDefault(device, LE_AUDIO_GROUP_ID_INVALID);
}
/**
@@ -1098,6 +1344,23 @@
if (DBG) {
Log.d(TAG, "SetVolume " + volume);
}
+
+ if (mActiveDeviceGroupId == LE_AUDIO_GROUP_ID_INVALID) {
+ Log.e(TAG, "There is no active group ");
+ return;
+ }
+
+ if (mVolumeControlService == null) {
+ Log.e(TAG, "VolumeControl no available ");
+ return;
+ }
+
+ try {
+ mVolumeControlProxy.setVolumeGroup(mActiveDeviceGroupId, volume,
+ this.getAttributionSource());
+ } catch (RemoteException e) {
+ Log.e(TAG, "Set Volume failed: " + e);
+ }
}
/**
@@ -1221,6 +1484,26 @@
}
@Override
+ public boolean groupAddNode(int group_id, BluetoothDevice device,
+ AttributionSource source) {
+ LeAudioService service = getService(source);
+ if (service == null) {
+ return false;
+ }
+ return service.groupAddNode(group_id, device);
+ }
+
+ @Override
+ public boolean groupRemoveNode(int groupId, BluetoothDevice device,
+ AttributionSource source) {
+ LeAudioService service = getService(source);
+ if (service == null) {
+ return false;
+ }
+ return service.groupRemoveNode(groupId, device);
+ }
+
+ @Override
public void setVolume(int volume, AttributionSource source) {
LeAudioService service = getService(source);
if (service == null) {
@@ -1234,6 +1517,8 @@
@Override
public void dump(StringBuilder sb) {
super.dump(sb);
- // TODO: Dump all state machines
+ for (LeAudioStateMachine sm : mStateMachines.values()) {
+ sm.dump(sb);
+ }
}
}
diff --git a/src/com/android/bluetooth/le_audio/LeAudioStackEvent.java b/src/com/android/bluetooth/le_audio/LeAudioStackEvent.java
index 992afae..ab40c8b 100644
--- a/src/com/android/bluetooth/le_audio/LeAudioStackEvent.java
+++ b/src/com/android/bluetooth/le_audio/LeAudioStackEvent.java
@@ -40,13 +40,8 @@
static final int CONNECTION_STATE_CONNECTED = 2;
static final int CONNECTION_STATE_DISCONNECTING = 3;
- static final int GROUP_STATUS_IDLE = 0;
+ static final int GROUP_STATUS_INACTIVE = 0;
static final int GROUP_STATUS_ACTIVE = 1;
- static final int GROUP_STATUS_INACTIVE = 2;
- static final int GROUP_STATUS_STREAMING = 3;
- static final int GROUP_STATUS_SUSPENDED = 4;
- static final int GROUP_STATUS_RECONFIGURED = 5;
- static final int GROUP_STATUS_DESTROYED = 6;
static final int GROUP_NODE_ADDED = 1;
static final int GROUP_NODE_REMOVED = 2;
@@ -113,7 +108,7 @@
case EVENT_TYPE_GROUP_NODE_STATUS_CHANGED:
// same as EVENT_TYPE_GROUP_STATUS_CHANGED
case EVENT_TYPE_GROUP_STATUS_CHANGED:
- // same as EVENT_TYPE_GROUP_STATUS_CHANGED
+ return "{group_id:" + Integer.toString(value) + "}";
case EVENT_TYPE_AUDIO_CONF_CHANGED:
// FIXME: It should have proper direction names here
return "{direction:" + value + "}";
@@ -127,20 +122,10 @@
switch (type) {
case EVENT_TYPE_GROUP_STATUS_CHANGED:
switch (value) {
- case GROUP_STATUS_IDLE:
- return "GROUP_STATUS_IDLE";
case GROUP_STATUS_ACTIVE:
return "GROUP_STATUS_ACTIVE";
case GROUP_STATUS_INACTIVE:
return "GROUP_STATUS_INACTIVE";
- case GROUP_STATUS_STREAMING:
- return "GROUP_STATUS_STREAMING";
- case GROUP_STATUS_SUSPENDED:
- return "GROUP_STATUS_SUSPENDED";
- case GROUP_STATUS_RECONFIGURED:
- return "GROUP_STATUS_RECONFIGURED";
- case GROUP_STATUS_DESTROYED:
- return "GROUP_STATUS_DESTROYED";
default:
break;
}
@@ -164,8 +149,6 @@
private static String eventTypeValue3ToString(int type, int value) {
switch (type) {
- case EVENT_TYPE_GROUP_STATUS_CHANGED:
- return "{group_flags:" + Integer.toString(value) + "}";
case EVENT_TYPE_AUDIO_CONF_CHANGED:
// FIXME: It should have proper location names here
return "{snk_audio_loc:" + value + "}";
diff --git a/src/com/android/bluetooth/le_audio/LeAudioStateMachine.java b/src/com/android/bluetooth/le_audio/LeAudioStateMachine.java
index 1cfcd91..a838cbb 100644
--- a/src/com/android/bluetooth/le_audio/LeAudioStateMachine.java
+++ b/src/com/android/bluetooth/le_audio/LeAudioStateMachine.java
@@ -63,6 +63,11 @@
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 LeAudioStateMachine extends StateMachine {
private static final boolean DBG = false;
private static final String TAG = "LeAudioStateMachine";
@@ -157,8 +162,7 @@
switch (message.what) {
case CONNECT:
- int groupId = message.arg1;
- log("Connecting to " + mDevice + " group " + groupId);
+ log("Connecting to " + mDevice);
if (!mNativeInterface.connectLeAudio(mDevice)) {
Log.e(TAG, "Disconnected: error connecting to " + mDevice);
break;
@@ -184,7 +188,7 @@
}
switch (event.type) {
case LeAudioStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED:
- processConnectionEvent(event.valueInt1, event.valueInt2);
+ processConnectionEvent(event.valueInt1);
break;
default:
Log.e(TAG, "Disconnected: ignoring stack event: " + event);
@@ -198,7 +202,7 @@
}
// in Disconnected state
- private void processConnectionEvent(int state, int groupId) {
+ private void processConnectionEvent(int state) {
switch (state) {
case LeAudioStackEvent.CONNECTION_STATE_DISCONNECTED:
Log.w(TAG, "Ignore LeAudio DISCONNECTED event: " + mDevice);
@@ -285,7 +289,7 @@
}
switch (event.type) {
case LeAudioStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED:
- processConnectionEvent(event.valueInt1, event.valueInt2);
+ processConnectionEvent(event.valueInt1);
break;
default:
Log.e(TAG, "Connecting: ignoring stack event: " + event);
@@ -299,7 +303,7 @@
}
// in Connecting state
- private void processConnectionEvent(int state, int groupId) {
+ private void processConnectionEvent(int state) {
switch (state) {
case LeAudioStackEvent.CONNECTION_STATE_DISCONNECTED:
Log.w(TAG, "Connecting device disconnected: " + mDevice);
@@ -371,7 +375,7 @@
}
switch (event.type) {
case LeAudioStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED:
- processConnectionEvent(event.valueInt1, event.valueInt2);
+ processConnectionEvent(event.valueInt1);
break;
default:
Log.e(TAG, "Disconnecting: ignoring stack event: " + event);
@@ -385,7 +389,7 @@
}
// in Disconnecting state
- private void processConnectionEvent(int state, int groupId) {
+ private void processConnectionEvent(int state) {
switch (state) {
case LeAudioStackEvent.CONNECTION_STATE_DISCONNECTED:
Log.i(TAG, "Disconnected: " + mDevice);
@@ -465,7 +469,7 @@
}
switch (event.type) {
case LeAudioStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED:
- processConnectionEvent(event.valueInt1, event.valueInt2);
+ processConnectionEvent(event.valueInt1);
break;
default:
Log.e(TAG, "Connected: ignoring stack event: " + event);
@@ -479,7 +483,7 @@
}
// in Connected state
- private void processConnectionEvent(int state, int groupId) {
+ private void processConnectionEvent(int state) {
switch (state) {
case LeAudioStackEvent.CONNECTION_STATE_DISCONNECTED:
Log.i(TAG, "Disconnected from " + mDevice);
@@ -554,6 +558,24 @@
return Integer.toString(state);
}
+ 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) {
diff --git a/src/com/android/bluetooth/pbap/BluetoothPbapVcardManager.java b/src/com/android/bluetooth/pbap/BluetoothPbapVcardManager.java
index 8801c16..ed4eac9 100755
--- a/src/com/android/bluetooth/pbap/BluetoothPbapVcardManager.java
+++ b/src/com/android/bluetooth/pbap/BluetoothPbapVcardManager.java
@@ -352,18 +352,24 @@
mContext.getString(android.R.string.unknownName), contactCursor);
if (contactCursor != null) {
- if (!composer.initWithCallback(contactCursor,
- new EnterpriseRawContactEntitlesInfoCallback())) {
+ if (!composer.init(contactCursor)) {
return nameList;
}
+ int idColumn = contactCursor.getColumnIndex(Data.CONTACT_ID);
+ if (idColumn < 0) {
+ idColumn = contactCursor.getColumnIndex(Contacts._ID);
+ }
int i = 0;
contactCursor.moveToFirst();
- while (!composer.isAfterLast()) {
- String vcard = composer.createOneEntry();
+ while (!contactCursor.isAfterLast()) {
+ String vcard = composer.buildVCard(RawContactsEntity.queryRawContactEntity(
+ mResolver, contactCursor.getLong(idColumn)));
+ if (!contactCursor.moveToNext()) {
+ Log.e(TAG, "Cursor#moveToNext() returned false");
+ }
if (vcard == null) {
- Log.e(TAG, "Failed to read a contact. Error reason: "
- + composer.getErrorReason());
+ Log.e(TAG, "Failed to read a contact.");
return nameList;
} else if (vcard.isEmpty()) {
Log.i(TAG, "Contact may have been deleted during operation");
@@ -689,23 +695,6 @@
}
}
- /**
- * Handler enterprise contact id in VCardComposer
- */
- private static class EnterpriseRawContactEntitlesInfoCallback
- implements VCardComposer.RawContactEntitlesInfoCallback {
- @Override
- public VCardComposer.RawContactEntitlesInfo getRawContactEntitlesInfo(long contactId) {
- if (Contacts.isEnterpriseContactId(contactId)) {
- return new VCardComposer.RawContactEntitlesInfo(RawContactsEntity.CORP_CONTENT_URI,
- contactId - Contacts.ENTERPRISE_CONTACT_ID_BASE);
- } else {
- return new VCardComposer.RawContactEntitlesInfo(RawContactsEntity.CONTENT_URI,
- contactId);
- }
- }
- }
-
private int composeContactsAndSendVCards(Operation op, final Cursor contactIdCursor,
final boolean vcardType21, String ownerVCard, boolean ignorefilter, byte[] filter) {
long timestamp = 0;
@@ -754,21 +743,27 @@
});
buffer = new HandlerForStringBuffer(op, ownerVCard);
Log.v(TAG, "contactIdCursor size: " + contactIdCursor.getCount());
- if (!composer.initWithCallback(contactIdCursor,
- new EnterpriseRawContactEntitlesInfoCallback()) || !buffer.onInit(mContext)) {
+ if (!composer.init(contactIdCursor) || !buffer.onInit(mContext)) {
return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
}
+ int idColumn = contactIdCursor.getColumnIndex(Data.CONTACT_ID);
+ if (idColumn < 0) {
+ idColumn = contactIdCursor.getColumnIndex(Contacts._ID);
+ }
- while (!composer.isAfterLast()) {
+ while (!contactIdCursor.isAfterLast()) {
if (BluetoothPbapObexServer.sIsAborted) {
((ServerOperation) op).isAborted = true;
BluetoothPbapObexServer.sIsAborted = false;
break;
}
- String vcard = composer.createOneEntry();
+ String vcard = composer.buildVCard(RawContactsEntity.queryRawContactEntity(
+ mResolver, contactIdCursor.getLong(idColumn)));
+ if (!contactIdCursor.moveToNext()) {
+ Log.e(TAG, "Cursor#moveToNext() returned false");
+ }
if (vcard == null) {
- Log.e(TAG,
- "Failed to read a contact. Error reason: " + composer.getErrorReason());
+ Log.e(TAG, "Failed to read a contact.");
return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
} else if (vcard.isEmpty()) {
Log.i(TAG, "Contact may have been deleted during operation");
@@ -853,21 +848,27 @@
});
buffer = new HandlerForStringBuffer(op, ownerVCard);
Log.v(TAG, "contactIdCursor size: " + contactIdCursor.getCount());
- if (!composer.initWithCallback(contactIdCursor,
- new EnterpriseRawContactEntitlesInfoCallback()) || !buffer.onInit(mContext)) {
+ if (!composer.init(contactIdCursor) || !buffer.onInit(mContext)) {
return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
}
+ int idColumn = contactIdCursor.getColumnIndex(Data.CONTACT_ID);
+ if (idColumn < 0) {
+ idColumn = contactIdCursor.getColumnIndex(Contacts._ID);
+ }
- while (!composer.isAfterLast()) {
+ while (!contactIdCursor.isAfterLast()) {
if (BluetoothPbapObexServer.sIsAborted) {
((ServerOperation) op).isAborted = true;
BluetoothPbapObexServer.sIsAborted = false;
break;
}
- String vcard = composer.createOneEntry();
+ String vcard = composer.buildVCard(RawContactsEntity.queryRawContactEntity(
+ mResolver, contactIdCursor.getLong(idColumn)));
+ if (!contactIdCursor.moveToNext()) {
+ Log.e(TAG, "Cursor#moveToNext() returned false");
+ }
if (vcard == null) {
- Log.e(TAG,
- "Failed to read a contact. Error reason: " + composer.getErrorReason());
+ Log.e(TAG, "Failed to read a contact.");
return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
} else if (vcard.isEmpty()) {
Log.i(TAG, "Contact may have been deleted during operation");
diff --git a/src/com/android/bluetooth/telephony/BluetoothInCallService.java b/src/com/android/bluetooth/telephony/BluetoothInCallService.java
index 410dc09..dacfa06 100644
--- a/src/com/android/bluetooth/telephony/BluetoothInCallService.java
+++ b/src/com/android/bluetooth/telephony/BluetoothInCallService.java
@@ -26,6 +26,7 @@
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
+import android.media.AudioManager;
import android.net.Uri;
import android.os.Binder;
import android.os.Bundle;
@@ -55,6 +56,9 @@
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
/**
* Used to receive updates about calls from the Telecom component. This service is bound to Telecom
@@ -89,6 +93,8 @@
// Indicates that no BluetoothCall is ringing
private static final int DEFAULT_RINGING_ADDRESS_TYPE = 128;
+ private static final int DISCONNECT_TONE_TIMEOUT_SECONDS = 1;
+
private int mNumActiveCalls = 0;
private int mNumHeldCalls = 0;
private int mNumChildrenOfActiveCall = 0;
@@ -102,6 +108,13 @@
private static final Object LOCK = new Object();
private BluetoothHeadsetProxy mBluetoothHeadset;
+ private Semaphore mDisconnectionToneSemaphore = new Semaphore(0);
+ private int mAudioMode = AudioManager.MODE_INVALID;
+ private final Object mAudioModeLock = new Object();
+
+ @VisibleForTesting
+ public AudioManager mAudioManager;
+
@VisibleForTesting
public TelephonyManager mTelephonyManager;
@@ -291,6 +304,19 @@
}
}
+ class BluetoothOnModeChangedListener implements AudioManager.OnModeChangedListener {
+ @Override
+ public void onModeChanged(int mode) {
+ synchronized (mAudioModeLock) {
+ mAudioMode = mode;
+ }
+ if (mode == AudioManager.MODE_NORMAL) {
+ mDisconnectionToneSemaphore.release();
+ }
+ }
+ }
+ private BluetoothOnModeChangedListener mBluetoothOnModeChangedListener;
+
@Override
public IBinder onBind(Intent intent) {
Log.i(TAG, "onBind. Intent: " + intent);
@@ -525,6 +551,26 @@
return;
}
Log.d(TAG, "onCallRemoved");
+ BluetoothCall heldCall = mCallInfo.getHeldCall();
+ if (mCallInfo.isNullCall(heldCall)) {
+ // current call is the only call
+
+ mDisconnectionToneSemaphore.drainPermits();
+ boolean isAudioModeNormal = false;
+ synchronized (mAudioModeLock) {
+ isAudioModeNormal = (mAudioMode == AudioManager.MODE_NORMAL);
+ }
+ if (!isAudioModeNormal) {
+ Log.d(TAG, "Acquiring mDisconnectionToneSemaphore");
+ try {
+ boolean result = mDisconnectionToneSemaphore.tryAcquire(
+ DISCONNECT_TONE_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+ Log.d(TAG, "Acquiring mDisconnectionToneSemaphore result " + result);
+ } catch (InterruptedException e) {
+ Log.w(TAG, "Failed to acquire mDisconnectionToneSemaphore");
+ }
+ }
+ }
CallStateCallback callback = getCallback(call);
if (callback != null) {
call.unregisterCallback(callback);
@@ -565,11 +611,19 @@
mBluetoothAdapterReceiver = new BluetoothAdapterReceiver();
IntentFilter intentFilter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED);
registerReceiver(mBluetoothAdapterReceiver, intentFilter);
+ mBluetoothOnModeChangedListener = new BluetoothOnModeChangedListener();
+ mAudioManager = getSystemService(AudioManager.class);
+ mAudioManager.addOnModeChangedListener(
+ Executors.newSingleThreadExecutor(), mBluetoothOnModeChangedListener);
}
@Override
public void onDestroy() {
Log.d(TAG, "onDestroy");
+ if (mBluetoothOnModeChangedListener != null) {
+ mAudioManager.removeOnModeChangedListener(mBluetoothOnModeChangedListener);
+ mBluetoothOnModeChangedListener = null;
+ }
if (mBluetoothAdapterReceiver != null) {
unregisterReceiver(mBluetoothAdapterReceiver);
mBluetoothAdapterReceiver = null;
diff --git a/src/com/android/bluetooth/vc/VolumeControlService.java b/src/com/android/bluetooth/vc/VolumeControlService.java
index 8c55767..cb22e4d 100644
--- a/src/com/android/bluetooth/vc/VolumeControlService.java
+++ b/src/com/android/bluetooth/vc/VolumeControlService.java
@@ -426,14 +426,17 @@
}
void messageFromNative(VolumeControlStackEvent stackEvent) {
+
+ if (stackEvent.type == VolumeControlStackEvent.EVENT_TYPE_VOLUME_STATE_CHANGED) {
+ handleVolumeControlChanged(stackEvent.device, stackEvent.valueInt1,
+ stackEvent.valueInt2, stackEvent.valueBool1);
+ return;
+ }
+
Objects.requireNonNull(stackEvent.device,
"Device should never be null, event: " + stackEvent);
Intent intent = null;
- if (stackEvent.type == VolumeControlStackEvent.EVENT_TYPE_VOLUME_STATE_CHANGED) {
- handleVolumeControlChanged(stackEvent.device, stackEvent.valueInt1,
- stackEvent.valueInt2, stackEvent.valueBool1);
- }
if (intent != null) {
intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT
diff --git a/tests/unit/AndroidTest.xml b/tests/unit/AndroidTest.xml
index 6edd9a5..8cea43c 100644
--- a/tests/unit/AndroidTest.xml
+++ b/tests/unit/AndroidTest.xml
@@ -22,8 +22,8 @@
</target_preparer>
<target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
<option name="run-command" value="settings put global ble_scan_always_enabled 0" />
- <option name="run-command" value="svc bluetooth disable" />
- <option name="teardown-command" value="svc bluetooth enable" />
+ <option name="run-command" value="su u$(am get-current-user)_system svc bluetooth disable" />
+ <option name="teardown-command" value="su u$(am get-current-user)_system svc bluetooth enable" />
<option name="teardown-command" value="settings put global ble_scan_always_enabled 1" />
</target_preparer>
<target_preparer class="com.android.tradefed.targetprep.FolderSaver">
diff --git a/tests/unit/src/com/android/bluetooth/a2dpsink/A2dpSinkServiceTest.java b/tests/unit/src/com/android/bluetooth/a2dpsink/A2dpSinkServiceTest.java
index 6917e2f..d767156 100644
--- a/tests/unit/src/com/android/bluetooth/a2dpsink/A2dpSinkServiceTest.java
+++ b/tests/unit/src/com/android/bluetooth/a2dpsink/A2dpSinkServiceTest.java
@@ -15,12 +15,17 @@
*/
package com.android.bluetooth.a2dpsink;
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
import static org.mockito.Mockito.*;
import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothAudioConfig;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothProfile;
import android.content.Context;
+import android.media.AudioFormat;
import androidx.test.InstrumentationRegistry;
import androidx.test.filters.MediumTest;
@@ -33,7 +38,6 @@
import com.android.bluetooth.btservice.storage.DatabaseManager;
import org.junit.After;
-import org.junit.Assert;
import org.junit.Assume;
import org.junit.Before;
import org.junit.Rule;
@@ -42,6 +46,9 @@
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
+import java.util.ArrayList;
+import java.util.List;
+
@MediumTest
@RunWith(AndroidJUnit4.class)
public class A2dpSinkServiceTest {
@@ -53,6 +60,17 @@
@Mock private AdapterService mAdapterService;
@Mock private DatabaseManager mDatabaseManager;
+ @Mock private A2dpSinkNativeInterface mNativeInterface;
+
+ private BluetoothDevice mDevice1;
+ private BluetoothDevice mDevice2;
+ private BluetoothDevice mDevice3;
+ private BluetoothDevice mDevice4;
+ private BluetoothDevice mDevice5;
+ private BluetoothDevice mDevice6;
+
+ private static final int TEST_SAMPLE_RATE = 44;
+ private static final int TEST_CHANNEL_COUNT = 1;
@Before
public void setUp() throws Exception {
@@ -60,16 +78,33 @@
Assume.assumeTrue("Ignore test when A2dpSinkService is not enabled",
mTargetContext.getResources().getBoolean(R.bool.profile_supported_a2dp_sink));
MockitoAnnotations.initMocks(this);
+
+ mAdapter = BluetoothAdapter.getDefaultAdapter();
+ assertThat(mAdapter).isNotNull();
+ mDevice1 = makeBluetoothDevice("11:11:11:11:11:11");
+ mDevice2 = makeBluetoothDevice("22:22:22:22:22:22");
+ mDevice3 = makeBluetoothDevice("33:33:33:33:33:33");
+ mDevice4 = makeBluetoothDevice("44:44:44:44:44:44");
+ mDevice5 = makeBluetoothDevice("55:55:55:55:55:55");
+ mDevice6 = makeBluetoothDevice("66:66:66:66:66:66");
+ BluetoothDevice[] bondedDevices = new BluetoothDevice[]{
+ mDevice1, mDevice2, mDevice3, mDevice4, mDevice5, mDevice6
+ };
+
+ // Setup the adapter service and start our service under test
TestUtils.setAdapterService(mAdapterService);
doReturn(mDatabaseManager).when(mAdapterService).getDatabase();
doReturn(true, false).when(mAdapterService).isStartedProfile(anyString());
+ doReturn(bondedDevices).when(mAdapterService).getBondedDevices();
+ when(mDatabaseManager.setProfileConnectionPolicy(any(), anyInt(),
+ anyInt())).thenReturn(true);
setMaxConnectedAudioDevices(1);
TestUtils.startService(mServiceRule, A2dpSinkService.class);
mService = A2dpSinkService.getA2dpSinkService();
- Assert.assertNotNull(mService);
- // Try getting the Bluetooth adapter
- mAdapter = BluetoothAdapter.getDefaultAdapter();
- Assert.assertNotNull(mAdapter);
+ assertThat(mService).isNotNull();
+
+ mService.mNativeInterface = mNativeInterface;
+ doReturn(true).when(mNativeInterface).setActiveDevice(any());
}
@After
@@ -79,10 +114,31 @@
}
TestUtils.stopService(mServiceRule, A2dpSinkService.class);
mService = A2dpSinkService.getA2dpSinkService();
- Assert.assertNull(mService);
+ assertThat(mService).isNull();
TestUtils.clearAdapterService(mAdapterService);
}
+ private void setupDeviceConnection(BluetoothDevice device) {
+ assertThat(mService.getConnectionState(device)).isEqualTo(
+ BluetoothProfile.STATE_DISCONNECTED);
+ assertThat(mService.connect(device)).isTrue();
+ sendConnectionEvent(device, StackEvent.CONNECTION_STATE_CONNECTED);
+ waitForDeviceProcessing(device);
+ assertThat(mService.getConnectionState(device)).isEqualTo(
+ BluetoothProfile.STATE_CONNECTED);
+ }
+
+ private void sendConnectionEvent(BluetoothDevice device, int newState) {
+ StackEvent event = StackEvent.connectionStateChanged(device, newState);
+ mService.messageFromNative(event);
+ }
+
+ private void waitForDeviceProcessing(BluetoothDevice device) {
+ A2dpSinkStateMachine sm = mService.getStateMachineForDevice(device);
+ if (sm == null) return;
+ TestUtils.waitForLooperToFinishScheduledTask(sm.getHandler().getLooper());
+ }
+
private BluetoothDevice makeBluetoothDevice(String address) {
return mAdapter.getRemoteDevice(address);
}
@@ -105,29 +161,49 @@
.thenReturn(priority);
}
+ /**
+ * Test that initialization of the service completes and that we can get a instance
+ */
@Test
public void testInitialize() {
- Assert.assertNotNull(A2dpSinkService.getA2dpSinkService());
+ assertThat(A2dpSinkService.getA2dpSinkService()).isNotNull();
}
/**
- * Test that a PRIORITY_ON device is connected to
+ * Test that asking to connect with a null device fails
*/
@Test
- public void testConnect() {
- BluetoothDevice device = makeBluetoothDevice("11:11:11:11:11:11");
- mockDevicePriority(device, BluetoothProfile.CONNECTION_POLICY_ALLOWED);
- Assert.assertTrue(mService.connect(device));
+ public void testConnectNullDevice() {
+ assertThrows(IllegalArgumentException.class, () -> mService.connect(null));
}
/**
- * Test that a PRIORITY_OFF device is not connected to
+ * Test that a CONNECTION_POLICY_ALLOWED device can connected
*/
@Test
- public void testConnectPriorityOffDevice() {
- BluetoothDevice device = makeBluetoothDevice("11:11:11:11:11:11");
- mockDevicePriority(device, BluetoothProfile.CONNECTION_POLICY_FORBIDDEN);
- Assert.assertFalse(mService.connect(device));
+ public void testConnectPolicyAllowedDevice() {
+ mockDevicePriority(mDevice1, BluetoothProfile.CONNECTION_POLICY_ALLOWED);
+ setupDeviceConnection(mDevice1);
+ }
+
+ /**
+ * Test that a CONNECTION_POLICY_FORBIDDEN device is not allowed to connect
+ */
+ @Test
+ public void testConnectPolicyForbiddenDevice() {
+ mockDevicePriority(mDevice1, BluetoothProfile.CONNECTION_POLICY_FORBIDDEN);
+ assertThat(mService.connect(mDevice1)).isFalse();
+ assertThat(mService.getConnectionState(mDevice1)).isEqualTo(
+ BluetoothProfile.STATE_DISCONNECTED);
+ }
+
+ /**
+ * Test that a CONNECTION_POLICY_UNKNOWN device is allowed to connect
+ */
+ @Test
+ public void testConnectPolicyUnknownDevice() {
+ mockDevicePriority(mDevice1, BluetoothProfile.CONNECTION_POLICY_UNKNOWN);
+ setupDeviceConnection(mDevice1);
}
/**
@@ -137,22 +213,259 @@
public void testConnectMultipleDevices() {
setMaxConnectedAudioDevices(5);
- BluetoothDevice device1 = makeBluetoothDevice("11:11:11:11:11:11");
- BluetoothDevice device2 = makeBluetoothDevice("22:22:22:22:22:22");
- BluetoothDevice device3 = makeBluetoothDevice("33:33:33:33:33:33");
- BluetoothDevice device4 = makeBluetoothDevice("44:44:44:44:44:44");
- BluetoothDevice device5 = makeBluetoothDevice("55:55:55:55:55:55");
+ mockDevicePriority(mDevice1, BluetoothProfile.CONNECTION_POLICY_ALLOWED);
+ mockDevicePriority(mDevice2, BluetoothProfile.CONNECTION_POLICY_ALLOWED);
+ mockDevicePriority(mDevice3, BluetoothProfile.CONNECTION_POLICY_ALLOWED);
+ mockDevicePriority(mDevice4, BluetoothProfile.CONNECTION_POLICY_ALLOWED);
+ mockDevicePriority(mDevice5, BluetoothProfile.CONNECTION_POLICY_ALLOWED);
+ mockDevicePriority(mDevice6, BluetoothProfile.CONNECTION_POLICY_ALLOWED);
- mockDevicePriority(device1, BluetoothProfile.CONNECTION_POLICY_ALLOWED);
- mockDevicePriority(device2, BluetoothProfile.CONNECTION_POLICY_ALLOWED);
- mockDevicePriority(device3, BluetoothProfile.CONNECTION_POLICY_ALLOWED);
- mockDevicePriority(device4, BluetoothProfile.CONNECTION_POLICY_ALLOWED);
- mockDevicePriority(device5, BluetoothProfile.CONNECTION_POLICY_ALLOWED);
+ setupDeviceConnection(mDevice1);
+ setupDeviceConnection(mDevice2);
+ setupDeviceConnection(mDevice3);
+ setupDeviceConnection(mDevice4);
+ setupDeviceConnection(mDevice5);
+ }
- Assert.assertTrue(mService.connect(device1));
- Assert.assertTrue(mService.connect(device2));
- Assert.assertTrue(mService.connect(device3));
- Assert.assertTrue(mService.connect(device4));
- Assert.assertTrue(mService.connect(device5));
+ /**
+ * Test to make sure we can disconnect a connected device
+ */
+ @Test
+ public void testDisconnect() {
+ mockDevicePriority(mDevice1, BluetoothProfile.CONNECTION_POLICY_ALLOWED);
+ setupDeviceConnection(mDevice1);
+
+ assertThat(mService.disconnect(mDevice1)).isTrue();
+ waitForDeviceProcessing(mDevice1);
+ assertThat(mService.getConnectionState(mDevice1)).isEqualTo(
+ BluetoothProfile.STATE_DISCONNECTED);
+ }
+
+ /**
+ * Assure disconnect() fails with a device that's not connected
+ */
+ @Test
+ public void testDisconnectDeviceDoesNotExist() {
+ assertThat(mService.disconnect(mDevice1)).isFalse();
+ }
+
+ /**
+ * Assure disconnect() fails with an invalid device
+ */
+ @Test
+ public void testDisconnectNullDevice() {
+ assertThrows(IllegalArgumentException.class, () -> mService.disconnect(null));
+ }
+
+ /**
+ * Assure dump() returns something and does not crash
+ */
+ @Test
+ public void testDump() {
+ StringBuilder sb = new StringBuilder();
+ mService.dump(sb);
+ assertThat(sb.toString()).isNotNull();
+ }
+
+ /**
+ * Test that we can set the active device to a valid device and receive it back from
+ * GetActiveDevice()
+ */
+ @Test
+ public void testSetActiveDevice() {
+ mockDevicePriority(mDevice1, BluetoothProfile.CONNECTION_POLICY_ALLOWED);
+ assertThat(mService.getActiveDevice()).isNotEqualTo(mDevice1);
+ assertThat(mService.setActiveDevice(mDevice1)).isTrue();
+ assertThat(mService.getActiveDevice()).isEqualTo(mDevice1);
+ }
+
+ /**
+ * Test that calls to set a null active device succeed in unsetting the active device
+ */
+ @Test
+ public void testSetActiveDeviceNullDevice() {
+ assertThat(mService.setActiveDevice(null)).isTrue();
+ assertThat(mService.getActiveDevice()).isNull();
+ }
+
+ /**
+ * Make sure we can receive the set audio configuration
+ */
+ @Test
+ public void testGetAudioConfiguration() {
+ mockDevicePriority(mDevice1, BluetoothProfile.CONNECTION_POLICY_ALLOWED);
+ setupDeviceConnection(mDevice1);
+
+ StackEvent audioConfigChanged =
+ StackEvent.audioConfigChanged(mDevice1, TEST_SAMPLE_RATE, TEST_CHANNEL_COUNT);
+ mService.messageFromNative(audioConfigChanged);
+ waitForDeviceProcessing(mDevice1);
+
+ BluetoothAudioConfig expected = new BluetoothAudioConfig(TEST_SAMPLE_RATE,
+ TEST_CHANNEL_COUNT, AudioFormat.ENCODING_PCM_16BIT);
+ BluetoothAudioConfig config = mService.getAudioConfig(mDevice1);
+ assertThat(config).isEqualTo(expected);
+ }
+
+ /**
+ * Getting an audio config for a device that hasn't received one yet should return null
+ */
+ @Test
+ public void testGetAudioConfigWithConfigUnset() {
+ mockDevicePriority(mDevice1, BluetoothProfile.CONNECTION_POLICY_ALLOWED);
+ setupDeviceConnection(mDevice1);
+ assertThat(mService.getAudioConfig(mDevice1)).isNull();
+ }
+
+ /**
+ * Getting an audio config for a null device should return null
+ */
+ @Test
+ public void testGetAudioConfigNullDevice() {
+ assertThat(mService.getAudioConfig(null)).isNull();
+ }
+
+ /**
+ * Test that a newly connected device ends up in the set returned by
+ * getConnectedDevices
+ */
+ @Test
+ public void testGetConnectedDevices() {
+ ArrayList<BluetoothDevice> expected = new ArrayList<BluetoothDevice>();
+ expected.add(mDevice1);
+
+ mockDevicePriority(mDevice1, BluetoothProfile.CONNECTION_POLICY_ALLOWED);
+ setupDeviceConnection(mDevice1);
+
+ List<BluetoothDevice> devices = mService.getConnectedDevices();
+ assertThat(devices).isEqualTo(expected);
+ }
+
+ /**
+ * Test that a newly connected device ends up in the set returned by
+ * testGetDevicesMatchingConnectionStates
+ */
+ @Test
+ public void testGetDevicesMatchingConnectionStatesConnected() {
+ ArrayList<BluetoothDevice> expected = new ArrayList<BluetoothDevice>();
+ expected.add(mDevice1);
+ mockDevicePriority(mDevice1, BluetoothProfile.CONNECTION_POLICY_ALLOWED);
+ setupDeviceConnection(mDevice1);
+
+ List<BluetoothDevice> devices = mService.getDevicesMatchingConnectionStates(
+ new int[] {BluetoothProfile.STATE_CONNECTED});
+ assertThat(devices).isEqualTo(expected);
+ }
+
+ /**
+ * Test that a all bonded device end up in the set returned by
+ * testGetDevicesMatchingConnectionStates, even when they're disconnected
+ */
+ @Test
+ public void testGetDevicesMatchingConnectionStatesDisconnected() {
+ ArrayList<BluetoothDevice> expected = new ArrayList<BluetoothDevice>();
+ expected.add(mDevice1);
+ expected.add(mDevice2);
+ expected.add(mDevice3);
+ expected.add(mDevice4);
+ expected.add(mDevice5);
+ expected.add(mDevice6);
+
+ List<BluetoothDevice> devices = mService.getDevicesMatchingConnectionStates(
+ new int[] {BluetoothProfile.STATE_DISCONNECTED});
+ assertThat(devices).isEqualTo(expected);
+ }
+
+ /**
+ * Test that GetConnectionPolicy() can get a device with policy "Allowed"
+ */
+ @Test
+ public void testGetConnectionPolicyDeviceAllowed() {
+ mockDevicePriority(mDevice1, BluetoothProfile.CONNECTION_POLICY_ALLOWED);
+ assertThat(mService.getConnectionPolicy(mDevice1)).isEqualTo(
+ BluetoothProfile.CONNECTION_POLICY_ALLOWED);
+ }
+
+ /**
+ * Test that GetConnectionPolicy() can get a device with policy "Forbidden"
+ */
+ @Test
+ public void testGetConnectionPolicyDeviceForbidden() {
+ mockDevicePriority(mDevice1, BluetoothProfile.CONNECTION_POLICY_FORBIDDEN);
+ assertThat(mService.getConnectionPolicy(mDevice1)).isEqualTo(
+ BluetoothProfile.CONNECTION_POLICY_FORBIDDEN);
+ }
+
+ /**
+ * Test that GetConnectionPolicy() can get a device with policy "Unknown"
+ */
+ @Test
+ public void testGetConnectionPolicyDeviceUnknown() {
+ mockDevicePriority(mDevice1, BluetoothProfile.CONNECTION_POLICY_UNKNOWN);
+ assertThat(mService.getConnectionPolicy(mDevice1)).isEqualTo(
+ BluetoothProfile.CONNECTION_POLICY_UNKNOWN);
+ }
+
+ /**
+ * Test that SetConnectionPolicy() can change a device's policy to "Allowed"
+ */
+ @Test
+ public void testSetConnectionPolicyDeviceAllowed() {
+ assertThat(mService.setConnectionPolicy(mDevice1,
+ BluetoothProfile.CONNECTION_POLICY_ALLOWED)).isTrue();
+ verify(mDatabaseManager, times(1)).setProfileConnectionPolicy(mDevice1,
+ BluetoothProfile.A2DP_SINK, BluetoothProfile.CONNECTION_POLICY_ALLOWED);
+ }
+
+ /**
+ * Test that SetConnectionPolicy() can change a device's policy to "Forbidden"
+ */
+ @Test
+ public void testSetConnectionPolicyDeviceForbiddenWhileNotConnected() {
+ assertThat(mService.setConnectionPolicy(mDevice1,
+ BluetoothProfile.CONNECTION_POLICY_FORBIDDEN)).isTrue();
+ verify(mDatabaseManager, times(1)).setProfileConnectionPolicy(mDevice1,
+ BluetoothProfile.A2DP_SINK, BluetoothProfile.CONNECTION_POLICY_FORBIDDEN);
+ }
+
+ /**
+ * Test that SetConnectionPolicy() can change a connected device's policy to "Forbidden"
+ * and that the new "Forbidden" policy causes a disconnect of the device.
+ */
+ @Test
+ public void testSetConnectionPolicyDeviceForbiddenWhileConnected() {
+ mockDevicePriority(mDevice1, BluetoothProfile.CONNECTION_POLICY_ALLOWED);
+ setupDeviceConnection(mDevice1);
+
+ assertThat(mService.setConnectionPolicy(mDevice1,
+ BluetoothProfile.CONNECTION_POLICY_FORBIDDEN)).isTrue();
+ verify(mDatabaseManager, times(1)).setProfileConnectionPolicy(mDevice1,
+ BluetoothProfile.A2DP_SINK, BluetoothProfile.CONNECTION_POLICY_FORBIDDEN);
+
+ waitForDeviceProcessing(mDevice1);
+ assertThat(mService.getConnectionState(mDevice1)).isEqualTo(
+ BluetoothProfile.STATE_DISCONNECTED);
+ }
+
+ /**
+ * Test that SetConnectionPolicy() can change a device's policy to "Unknown"
+ */
+ @Test
+ public void testSetConnectionPolicyDeviceUnknown() {
+ assertThat(mService.setConnectionPolicy(mDevice1,
+ BluetoothProfile.CONNECTION_POLICY_UNKNOWN)).isTrue();
+ verify(mDatabaseManager, times(1)).setProfileConnectionPolicy(mDevice1,
+ BluetoothProfile.A2DP_SINK, BluetoothProfile.CONNECTION_POLICY_UNKNOWN);
+ }
+
+ /**
+ * Test that SetConnectionPolicy is robust to DatabaseManager failures
+ */
+ @Test
+ public void testSetConnectionPolicyDatabaseWriteFails() {
+ when(mDatabaseManager.setProfileConnectionPolicy(any(), anyInt(),
+ anyInt())).thenReturn(false);
+ assertThat(mService.setConnectionPolicy(mDevice1,
+ BluetoothProfile.CONNECTION_POLICY_ALLOWED)).isFalse();
}
}
diff --git a/tests/unit/src/com/android/bluetooth/a2dpsink/A2dpSinkStateMachineTest.java b/tests/unit/src/com/android/bluetooth/a2dpsink/A2dpSinkStateMachineTest.java
new file mode 100644
index 0000000..74d3e3c
--- /dev/null
+++ b/tests/unit/src/com/android/bluetooth/a2dpsink/A2dpSinkStateMachineTest.java
@@ -0,0 +1,370 @@
+/*
+ * 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.
+ */
+package com.android.bluetooth.a2dpsink;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothAudioConfig;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothProfile;
+import android.content.Context;
+import android.media.AudioFormat;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.bluetooth.R;
+import com.android.bluetooth.TestUtils;
+
+import org.junit.After;
+import org.junit.Assume;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@RunWith(AndroidJUnit4.class)
+public class A2dpSinkStateMachineTest {
+ private Context mTargetContext;
+
+ private BluetoothAdapter mAdapter;
+ private BluetoothDevice mDevice;
+ private final String mDeviceAddress = "11:11:11:11:11:11";
+ @Mock private A2dpSinkService mService;
+ @Mock private A2dpSinkNativeInterface mNativeInterface;
+
+ A2dpSinkStateMachine mStateMachine;
+ private static final int TIMEOUT_MS = 1000;
+ private static final int CONNECT_TIMEOUT_MS = 6000;
+ private static final int UNHANDLED_MESSAGE = 9999;
+
+ @Before
+ public void setUp() throws Exception {
+ mTargetContext = InstrumentationRegistry.getTargetContext();
+ Assume.assumeTrue("Ignore test when A2dpSinkService is not enabled",
+ mTargetContext.getResources().getBoolean(R.bool.profile_supported_a2dp_sink));
+ MockitoAnnotations.initMocks(this);
+
+ mAdapter = BluetoothAdapter.getDefaultAdapter();
+ assertThat(mAdapter).isNotNull();
+ mDevice = mAdapter.getRemoteDevice(mDeviceAddress);
+
+ doNothing().when(mService).removeStateMachine(any(A2dpSinkStateMachine.class));
+
+ mStateMachine = new A2dpSinkStateMachine(mDevice, mService, mNativeInterface);
+ mStateMachine.start();
+ assertThat(mStateMachine.getDevice()).isEqualTo(mDevice);
+ assertThat(mStateMachine.getAudioConfig()).isNull();
+ assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_DISCONNECTED);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ if (!mTargetContext.getResources().getBoolean(R.bool.profile_supported_a2dp_sink)) {
+ return;
+ }
+ mStateMachine = null;
+ mDevice = null;
+ mAdapter = null;
+ }
+
+ private void mockDeviceConnectionPolicy(BluetoothDevice device, int policy) {
+ doReturn(policy).when(mService).getConnectionPolicy(device);
+ }
+
+ private void sendConnectionEvent(int state) {
+ mStateMachine.sendMessage(A2dpSinkStateMachine.STACK_EVENT,
+ StackEvent.connectionStateChanged(mDevice, state));
+ }
+
+ private void sendAudioConfigChangedEvent(int sampleRate, int channelCount) {
+ mStateMachine.sendMessage(A2dpSinkStateMachine.STACK_EVENT,
+ StackEvent.audioConfigChanged(mDevice, sampleRate, channelCount));
+ }
+
+ /**********************************************************************************************
+ * DISCONNECTED STATE TESTS *
+ *********************************************************************************************/
+
+ @Test
+ public void testConnectInDisconnected() {
+ assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_DISCONNECTED);
+ mStateMachine.connect();
+ TestUtils.waitForLooperToFinishScheduledTask(mStateMachine.getHandler().getLooper());
+ verify(mNativeInterface, timeout(TIMEOUT_MS).times(1)).connectA2dpSink(mDevice);
+ assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_CONNECTING);
+ }
+
+ @Test
+ public void testDisconnectInDisconnected() {
+ assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_DISCONNECTED);
+ mStateMachine.disconnect();
+ TestUtils.waitForLooperToFinishScheduledTask(mStateMachine.getHandler().getLooper());
+ assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_DISCONNECTED);
+ }
+
+ @Test
+ public void testAudioConfigChangedInDisconnected() {
+ assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_DISCONNECTED);
+ sendAudioConfigChangedEvent(44, 1);
+ TestUtils.waitForLooperToFinishScheduledTask(mStateMachine.getHandler().getLooper());
+ assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_DISCONNECTED);
+ assertThat(mStateMachine.getAudioConfig()).isNull();
+ }
+
+ @Test
+ public void testIncomingConnectedInDisconnected() {
+ assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_DISCONNECTED);
+ sendConnectionEvent(BluetoothProfile.STATE_CONNECTED);
+ TestUtils.waitForLooperToFinishScheduledTask(mStateMachine.getHandler().getLooper());
+ assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_CONNECTED);
+ }
+
+ @Test
+ public void testAllowedIncomingConnectionInDisconnected() {
+ mockDeviceConnectionPolicy(mDevice, BluetoothProfile.CONNECTION_POLICY_ALLOWED);
+ assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_DISCONNECTED);
+ sendConnectionEvent(BluetoothProfile.STATE_CONNECTING);
+ TestUtils.waitForLooperToFinishScheduledTask(mStateMachine.getHandler().getLooper());
+ assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_CONNECTING);
+ verify(mNativeInterface, times(0)).connectA2dpSink(mDevice);
+ }
+
+ @Test
+ public void testForbiddenIncomingConnectionInDisconnected() {
+ mockDeviceConnectionPolicy(mDevice, BluetoothProfile.CONNECTION_POLICY_FORBIDDEN);
+ assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_DISCONNECTED);
+ sendConnectionEvent(BluetoothProfile.STATE_CONNECTING);
+ TestUtils.waitForLooperToFinishScheduledTask(mStateMachine.getHandler().getLooper());
+ verify(mNativeInterface, times(1)).disconnectA2dpSink(mDevice);
+ assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_DISCONNECTED);
+ }
+
+ @Test
+ public void testUnknownIncomingConnectionInDisconnected() {
+ mockDeviceConnectionPolicy(mDevice, BluetoothProfile.CONNECTION_POLICY_UNKNOWN);
+ assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_DISCONNECTED);
+ sendConnectionEvent(BluetoothProfile.STATE_CONNECTING);
+ TestUtils.waitForLooperToFinishScheduledTask(mStateMachine.getHandler().getLooper());
+ assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_CONNECTING);
+ verify(mNativeInterface, times(0)).connectA2dpSink(mDevice);
+ }
+
+ @Test
+ public void testIncomingDisconnectInDisconnected() {
+ assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_DISCONNECTED);
+ sendConnectionEvent(BluetoothProfile.STATE_DISCONNECTED);
+ TestUtils.waitForLooperToFinishScheduledTask(mStateMachine.getHandler().getLooper());
+ assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_DISCONNECTED);
+ verify(mService, timeout(TIMEOUT_MS).times(1)).removeStateMachine(mStateMachine);
+ }
+
+ @Test
+ public void testIncomingDisconnectingInDisconnected() {
+ assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_DISCONNECTED);
+ sendConnectionEvent(BluetoothProfile.STATE_DISCONNECTING);
+ TestUtils.waitForLooperToFinishScheduledTask(mStateMachine.getHandler().getLooper());
+ assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_DISCONNECTED);
+ verify(mService, times(0)).removeStateMachine(mStateMachine);
+ }
+
+ @Test
+ public void testIncomingConnectingInDisconnected() {
+ assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_DISCONNECTED);
+ sendConnectionEvent(BluetoothProfile.STATE_CONNECTING);
+ TestUtils.waitForLooperToFinishScheduledTask(mStateMachine.getHandler().getLooper());
+ assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_DISCONNECTED);
+ }
+
+ @Test
+ public void testUnhandledMessageInDisconnected() {
+ assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_DISCONNECTED);
+ mStateMachine.sendMessage(UNHANDLED_MESSAGE);
+ mStateMachine.sendMessage(UNHANDLED_MESSAGE, 0 /* arbitrary payload */);
+ }
+
+ /**********************************************************************************************
+ * CONNECTING STATE TESTS *
+ *********************************************************************************************/
+
+ @Test
+ public void testConnectedInConnecting() {
+ testConnectInDisconnected();
+ assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_CONNECTING);
+ sendConnectionEvent(BluetoothProfile.STATE_CONNECTED);
+ TestUtils.waitForLooperToFinishScheduledTask(mStateMachine.getHandler().getLooper());
+ assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_CONNECTED);
+ }
+
+ @Test
+ public void testConnectingInConnecting() {
+ testConnectInDisconnected();
+ assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_CONNECTING);
+ sendConnectionEvent(BluetoothProfile.STATE_CONNECTING);
+ TestUtils.waitForLooperToFinishScheduledTask(mStateMachine.getHandler().getLooper());
+ assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_CONNECTING);
+ }
+
+ @Test
+ public void testDisconnectingInConnecting() {
+ testConnectInDisconnected();
+ assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_CONNECTING);
+ sendConnectionEvent(BluetoothProfile.STATE_DISCONNECTING);
+ TestUtils.waitForLooperToFinishScheduledTask(mStateMachine.getHandler().getLooper());
+ assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_CONNECTING);
+ }
+
+ @Test
+ public void testDisconnectedInConnecting() {
+ testConnectInDisconnected();
+ assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_CONNECTING);
+ sendConnectionEvent(BluetoothProfile.STATE_DISCONNECTED);
+ TestUtils.waitForLooperToFinishScheduledTask(mStateMachine.getHandler().getLooper());
+ verify(mService, timeout(TIMEOUT_MS).times(1)).removeStateMachine(mStateMachine);
+ assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_DISCONNECTED);
+ }
+
+ @Test
+ public void testConnectionTimeoutInConnecting() {
+ testConnectInDisconnected();
+ assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_CONNECTING);
+ verify(mService, timeout(CONNECT_TIMEOUT_MS).times(1)).removeStateMachine(mStateMachine);
+ assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_DISCONNECTED);
+ }
+
+ @Test
+ public void testAudioStateChangeInConnecting() {
+ testConnectInDisconnected();
+ assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_CONNECTING);
+ sendAudioConfigChangedEvent(44, 1);
+ TestUtils.waitForLooperToFinishScheduledTask(mStateMachine.getHandler().getLooper());
+ assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_CONNECTING);
+ assertThat(mStateMachine.getAudioConfig()).isNull();
+ }
+
+ @Test
+ public void testConnectInConnecting() {
+ testConnectInDisconnected();
+ assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_CONNECTING);
+ mStateMachine.connect();
+ TestUtils.waitForLooperToFinishScheduledTask(mStateMachine.getHandler().getLooper());
+ assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_CONNECTING);
+ }
+
+ @Test
+ public void testDisconnectInConnecting() {
+ testConnectInDisconnected();
+ assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_CONNECTING);
+ mStateMachine.disconnect();
+ TestUtils.waitForLooperToFinishScheduledTask(mStateMachine.getHandler().getLooper());
+ assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_CONNECTING);
+ }
+
+ /**********************************************************************************************
+ * CONNECTED STATE TESTS *
+ *********************************************************************************************/
+
+ @Test
+ public void testConnectInConnected() {
+ testConnectedInConnecting();
+ assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_CONNECTED);
+ mStateMachine.connect();
+ TestUtils.waitForLooperToFinishScheduledTask(mStateMachine.getHandler().getLooper());
+ assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_CONNECTED);
+ }
+
+ @Test
+ public void testDisconnectInConnected() {
+ testConnectedInConnecting();
+ assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_CONNECTED);
+ mStateMachine.disconnect();
+ TestUtils.waitForLooperToFinishScheduledTask(mStateMachine.getHandler().getLooper());
+ verify(mNativeInterface, times(1)).disconnectA2dpSink(mDevice);
+ verify(mService, timeout(TIMEOUT_MS).times(1)).removeStateMachine(mStateMachine);
+ assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_DISCONNECTED);
+ }
+
+ @Test
+ public void testAudioStateChangeInConnected() {
+ testConnectedInConnecting();
+ assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_CONNECTED);
+ sendAudioConfigChangedEvent(44, 1);
+ TestUtils.waitForLooperToFinishScheduledTask(mStateMachine.getHandler().getLooper());
+ assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_CONNECTED);
+ BluetoothAudioConfig expected =
+ new BluetoothAudioConfig(44, 1, AudioFormat.ENCODING_PCM_16BIT);
+ BluetoothAudioConfig config = mStateMachine.getAudioConfig();
+ assertThat(config).isEqualTo(expected);
+ }
+
+ @Test
+ public void testConnectedInConnected() {
+ testConnectedInConnecting();
+ assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_CONNECTED);
+ sendConnectionEvent(BluetoothProfile.STATE_CONNECTED);
+ TestUtils.waitForLooperToFinishScheduledTask(mStateMachine.getHandler().getLooper());
+ assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_CONNECTED);
+ }
+
+ @Test
+ public void testConnectingInConnected() {
+ testConnectedInConnecting();
+ assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_CONNECTED);
+ sendConnectionEvent(BluetoothProfile.STATE_CONNECTING);
+ TestUtils.waitForLooperToFinishScheduledTask(mStateMachine.getHandler().getLooper());
+ assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_CONNECTED);
+ }
+
+ @Test
+ public void testDisconnectingInConnected() {
+ testConnectedInConnecting();
+ assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_CONNECTED);
+ sendConnectionEvent(BluetoothProfile.STATE_DISCONNECTING);
+ TestUtils.waitForLooperToFinishScheduledTask(mStateMachine.getHandler().getLooper());
+ verify(mService, timeout(TIMEOUT_MS).times(1)).removeStateMachine(mStateMachine);
+ assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_DISCONNECTED);
+ }
+
+ @Test
+ public void testDisconnectedInConnected() {
+ testConnectedInConnecting();
+ assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_CONNECTED);
+ sendConnectionEvent(BluetoothProfile.STATE_DISCONNECTED);
+ TestUtils.waitForLooperToFinishScheduledTask(mStateMachine.getHandler().getLooper());
+ verify(mService, timeout(TIMEOUT_MS).times(1)).removeStateMachine(mStateMachine);
+ assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_DISCONNECTED);
+ }
+
+ /**********************************************************************************************
+ * OTHER TESTS *
+ *********************************************************************************************/
+
+ @Test
+ public void testDump() {
+ StringBuilder sb = new StringBuilder();
+ mStateMachine.dump(sb);
+ assertThat(sb.toString()).isNotNull();
+ }
+}
diff --git a/tests/unit/src/com/android/bluetooth/a2dpsink/A2dpSinkStreamHandlerTest.java b/tests/unit/src/com/android/bluetooth/a2dpsink/A2dpSinkStreamHandlerTest.java
index 1a70021..9f3bd45 100644
--- a/tests/unit/src/com/android/bluetooth/a2dpsink/A2dpSinkStreamHandlerTest.java
+++ b/tests/unit/src/com/android/bluetooth/a2dpsink/A2dpSinkStreamHandlerTest.java
@@ -16,6 +16,8 @@
package com.android.bluetooth.a2dpsink;
+import static com.google.common.truth.Truth.assertThat;
+
import static org.mockito.Mockito.*;
import android.content.Context;
@@ -30,6 +32,7 @@
import androidx.test.runner.AndroidJUnit4;
import com.android.bluetooth.R;
+import com.android.bluetooth.TestUtils;
import org.junit.Assume;
import org.junit.Before;
@@ -46,10 +49,10 @@
private A2dpSinkStreamHandler mStreamHandler;
private Context mTargetContext;
- @Mock private Context mMockContext;
-
@Mock private A2dpSinkService mMockA2dpSink;
+ @Mock private A2dpSinkNativeInterface mMockNativeInterface;
+
@Mock private AudioManager mMockAudioManager;
@Mock private Resources mMockResources;
@@ -70,20 +73,21 @@
mHandlerThread = new HandlerThread("A2dpSinkStreamHandlerTest");
mHandlerThread.start();
- when(mMockContext.getSystemService(Context.AUDIO_SERVICE)).thenReturn(mMockAudioManager);
- when(mMockContext.getSystemServiceName(AudioManager.class))
+ when(mMockA2dpSink.getSystemService(Context.AUDIO_SERVICE)).thenReturn(mMockAudioManager);
+ when(mMockA2dpSink.getSystemServiceName(AudioManager.class))
.thenReturn(Context.AUDIO_SERVICE);
- when(mMockContext.getResources()).thenReturn(mMockResources);
+ when(mMockA2dpSink.getResources()).thenReturn(mMockResources);
when(mMockResources.getInteger(anyInt())).thenReturn(DUCK_PERCENT);
when(mMockAudioManager.requestAudioFocus(any())).thenReturn(
AudioManager.AUDIOFOCUS_REQUEST_GRANTED);
- when(mMockAudioManager.abandonAudioFocus(any())).thenReturn(AudioManager.AUDIOFOCUS_GAIN);
- doNothing().when(mMockA2dpSink).informAudioTrackGainNative(anyFloat());
- when(mMockContext.getMainLooper()).thenReturn(mHandlerThread.getLooper());
- when(mMockContext.getPackageManager()).thenReturn(mMockPackageManager);
+ when(mMockAudioManager.abandonAudioFocus(any())).thenReturn(
+ AudioManager.AUDIOFOCUS_REQUEST_GRANTED);
+ when(mMockAudioManager.generateAudioSessionId()).thenReturn(0);
+ when(mMockA2dpSink.getMainLooper()).thenReturn(mHandlerThread.getLooper());
+ when(mMockA2dpSink.getPackageManager()).thenReturn(mMockPackageManager);
when(mMockPackageManager.hasSystemFeature(any())).thenReturn(false);
- mStreamHandler = spy(new A2dpSinkStreamHandler(mMockA2dpSink, mMockContext));
+ mStreamHandler = spy(new A2dpSinkStreamHandler(mMockA2dpSink, mMockNativeInterface));
}
@Test
@@ -92,8 +96,9 @@
mStreamHandler.handleMessage(
mStreamHandler.obtainMessage(A2dpSinkStreamHandler.SRC_STR_START));
verify(mMockAudioManager, times(0)).requestAudioFocus(any());
- verify(mMockA2dpSink, times(0)).informAudioFocusStateNative(1);
- verify(mMockA2dpSink, times(0)).informAudioTrackGainNative(1.0f);
+ verify(mMockNativeInterface, times(0)).informAudioFocusState(1);
+ verify(mMockNativeInterface, times(0)).informAudioTrackGain(1.0f);
+ assertThat(mStreamHandler.isPlaying()).isFalse();
}
@Test
@@ -102,17 +107,19 @@
mStreamHandler.handleMessage(
mStreamHandler.obtainMessage(A2dpSinkStreamHandler.SRC_STR_STOP));
verify(mMockAudioManager, times(0)).requestAudioFocus(any());
- verify(mMockA2dpSink, times(0)).informAudioFocusStateNative(1);
- verify(mMockA2dpSink, times(0)).informAudioTrackGainNative(1.0f);
+ verify(mMockNativeInterface, times(0)).informAudioFocusState(1);
+ verify(mMockNativeInterface, times(0)).informAudioTrackGain(1.0f);
+ assertThat(mStreamHandler.isPlaying()).isFalse();
}
@Test
public void testSnkPlay() {
- // Play was pressed locally, expect streaming to start.
+ // Play was pressed locally, expect streaming to start soon.
mStreamHandler.handleMessage(mStreamHandler.obtainMessage(A2dpSinkStreamHandler.SNK_PLAY));
verify(mMockAudioManager, times(1)).requestAudioFocus(any());
- verify(mMockA2dpSink, times(1)).informAudioFocusStateNative(1);
- verify(mMockA2dpSink, times(1)).informAudioTrackGainNative(1.0f);
+ verify(mMockNativeInterface, times(1)).informAudioFocusState(1);
+ verify(mMockNativeInterface, times(1)).informAudioTrackGain(1.0f);
+ assertThat(mStreamHandler.isPlaying()).isFalse();
}
@Test
@@ -120,8 +127,9 @@
// Pause was pressed locally, expect streaming to stop.
mStreamHandler.handleMessage(mStreamHandler.obtainMessage(A2dpSinkStreamHandler.SNK_PAUSE));
verify(mMockAudioManager, times(0)).requestAudioFocus(any());
- verify(mMockA2dpSink, times(0)).informAudioFocusStateNative(1);
- verify(mMockA2dpSink, times(0)).informAudioTrackGainNative(1.0f);
+ verify(mMockNativeInterface, times(0)).informAudioFocusState(1);
+ verify(mMockNativeInterface, times(0)).informAudioTrackGain(1.0f);
+ assertThat(mStreamHandler.isPlaying()).isFalse();
}
@Test
@@ -131,7 +139,8 @@
mStreamHandler.handleMessage(
mStreamHandler.obtainMessage(A2dpSinkStreamHandler.DISCONNECT));
verify(mMockAudioManager, times(0)).abandonAudioFocus(any());
- verify(mMockA2dpSink, times(0)).informAudioFocusStateNative(0);
+ verify(mMockNativeInterface, times(0)).informAudioFocusState(0);
+ assertThat(mStreamHandler.isPlaying()).isFalse();
}
@Test
@@ -139,8 +148,9 @@
// Play was pressed remotely, expect no streaming due to lack of audio focus.
mStreamHandler.handleMessage(mStreamHandler.obtainMessage(A2dpSinkStreamHandler.SRC_PLAY));
verify(mMockAudioManager, times(0)).requestAudioFocus(any());
- verify(mMockA2dpSink, times(0)).informAudioFocusStateNative(1);
- verify(mMockA2dpSink, times(0)).informAudioTrackGainNative(1.0f);
+ verify(mMockNativeInterface, times(0)).informAudioFocusState(1);
+ verify(mMockNativeInterface, times(0)).informAudioTrackGain(1.0f);
+ assertThat(mStreamHandler.isPlaying()).isFalse();
}
@Test
@@ -149,8 +159,9 @@
when(mMockPackageManager.hasSystemFeature(any())).thenReturn(true);
mStreamHandler.handleMessage(mStreamHandler.obtainMessage(A2dpSinkStreamHandler.SRC_PLAY));
verify(mMockAudioManager, times(1)).requestAudioFocus(any());
- verify(mMockA2dpSink, times(1)).informAudioFocusStateNative(1);
- verify(mMockA2dpSink, times(1)).informAudioTrackGainNative(1.0f);
+ verify(mMockNativeInterface, times(1)).informAudioFocusState(1);
+ verify(mMockNativeInterface, times(1)).informAudioTrackGain(1.0f);
+ assertThat(mStreamHandler.isPlaying()).isTrue();
}
@Test
@@ -158,8 +169,9 @@
// Play was pressed locally, expect streaming to start.
mStreamHandler.handleMessage(mStreamHandler.obtainMessage(A2dpSinkStreamHandler.SRC_PLAY));
verify(mMockAudioManager, times(0)).requestAudioFocus(any());
- verify(mMockA2dpSink, times(0)).informAudioFocusStateNative(1);
- verify(mMockA2dpSink, times(0)).informAudioTrackGainNative(1.0f);
+ verify(mMockNativeInterface, times(0)).informAudioFocusState(1);
+ verify(mMockNativeInterface, times(0)).informAudioTrackGain(1.0f);
+ assertThat(mStreamHandler.isPlaying()).isFalse();
}
@Test
@@ -170,8 +182,11 @@
mStreamHandler.obtainMessage(A2dpSinkStreamHandler.AUDIO_FOCUS_CHANGE,
AudioManager.AUDIOFOCUS_GAIN));
verify(mMockAudioManager, times(1)).requestAudioFocus(any());
- verify(mMockA2dpSink, times(2)).informAudioFocusStateNative(1);
- verify(mMockA2dpSink, times(2)).informAudioTrackGainNative(1.0f);
+ verify(mMockNativeInterface, times(2)).informAudioFocusState(1);
+ verify(mMockNativeInterface, times(2)).informAudioTrackGain(1.0f);
+
+ TestUtils.waitForLooperToFinishScheduledTask(mHandlerThread.getLooper());
+ assertThat(mStreamHandler.getFocusState()).isEqualTo(AudioManager.AUDIOFOCUS_GAIN);
}
@Test
@@ -181,7 +196,11 @@
mStreamHandler.handleMessage(
mStreamHandler.obtainMessage(A2dpSinkStreamHandler.AUDIO_FOCUS_CHANGE,
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK));
- verify(mMockA2dpSink, times(1)).informAudioTrackGainNative(DUCK_PERCENT / 100.0f);
+ verify(mMockNativeInterface, times(1)).informAudioTrackGain(DUCK_PERCENT / 100.0f);
+
+ TestUtils.waitForLooperToFinishScheduledTask(mHandlerThread.getLooper());
+ assertThat(mStreamHandler.getFocusState()).isEqualTo(
+ AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK);
}
@Test
@@ -192,8 +211,12 @@
mStreamHandler.obtainMessage(A2dpSinkStreamHandler.AUDIO_FOCUS_CHANGE,
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT));
verify(mMockAudioManager, times(0)).abandonAudioFocus(any());
- verify(mMockA2dpSink, times(0)).informAudioFocusStateNative(0);
- verify(mMockA2dpSink, times(1)).informAudioTrackGainNative(0);
+ verify(mMockNativeInterface, times(0)).informAudioFocusState(0);
+ verify(mMockNativeInterface, times(1)).informAudioTrackGain(0);
+
+ TestUtils.waitForLooperToFinishScheduledTask(mHandlerThread.getLooper());
+ assertThat(mStreamHandler.getFocusState()).isEqualTo(
+ AudioManager.AUDIOFOCUS_LOSS_TRANSIENT);
}
@Test
@@ -204,8 +227,8 @@
mStreamHandler.obtainMessage(A2dpSinkStreamHandler.AUDIO_FOCUS_CHANGE,
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT));
verify(mMockAudioManager, times(0)).abandonAudioFocus(any());
- verify(mMockA2dpSink, times(0)).informAudioFocusStateNative(0);
- verify(mMockA2dpSink, times(1)).informAudioTrackGainNative(0);
+ verify(mMockNativeInterface, times(0)).informAudioFocusState(0);
+ verify(mMockNativeInterface, times(1)).informAudioTrackGain(0);
mStreamHandler.handleMessage(
mStreamHandler.obtainMessage(A2dpSinkStreamHandler.REQUEST_FOCUS, true));
verify(mMockAudioManager, times(2)).requestAudioFocus(any());
@@ -224,9 +247,12 @@
mStreamHandler.obtainMessage(A2dpSinkStreamHandler.AUDIO_FOCUS_CHANGE,
AudioManager.AUDIOFOCUS_GAIN));
verify(mMockAudioManager, times(0)).abandonAudioFocus(any());
- verify(mMockA2dpSink, times(0)).informAudioFocusStateNative(0);
- verify(mMockA2dpSink, times(1)).informAudioTrackGainNative(0);
- verify(mMockA2dpSink, times(2)).informAudioTrackGainNative(1.0f);
+ verify(mMockNativeInterface, times(0)).informAudioFocusState(0);
+ verify(mMockNativeInterface, times(1)).informAudioTrackGain(0);
+ verify(mMockNativeInterface, times(2)).informAudioTrackGain(1.0f);
+
+ TestUtils.waitForLooperToFinishScheduledTask(mHandlerThread.getLooper());
+ assertThat(mStreamHandler.getFocusState()).isEqualTo(AudioManager.AUDIOFOCUS_GAIN);
}
@Test
@@ -237,6 +263,10 @@
mStreamHandler.obtainMessage(A2dpSinkStreamHandler.AUDIO_FOCUS_CHANGE,
AudioManager.AUDIOFOCUS_LOSS));
verify(mMockAudioManager, times(1)).abandonAudioFocus(any());
- verify(mMockA2dpSink, times(1)).informAudioFocusStateNative(0);
+ verify(mMockNativeInterface, times(1)).informAudioFocusState(0);
+
+ TestUtils.waitForLooperToFinishScheduledTask(mHandlerThread.getLooper());
+ assertThat(mStreamHandler.getFocusState()).isEqualTo(AudioManager.AUDIOFOCUS_NONE);
+ assertThat(mStreamHandler.isPlaying()).isFalse();
}
}
diff --git a/tests/unit/src/com/android/bluetooth/a2dpsink/StackEventTest.java b/tests/unit/src/com/android/bluetooth/a2dpsink/StackEventTest.java
new file mode 100644
index 0000000..ac346d3
--- /dev/null
+++ b/tests/unit/src/com/android/bluetooth/a2dpsink/StackEventTest.java
@@ -0,0 +1,124 @@
+/*
+ * 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.
+ */
+package com.android.bluetooth.a2dpsink;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.content.Context;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.bluetooth.R;
+
+import org.junit.After;
+import org.junit.Assume;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class StackEventTest {
+ private Context mTargetContext = null;
+ private BluetoothAdapter mAdapter = null;
+ private BluetoothDevice mDevice = null;
+ private static final String TEST_ADDRESS = "11:11:11:11:11:11";
+
+ @Before
+ public void setUp() throws Exception {
+ mTargetContext = InstrumentationRegistry.getTargetContext();
+ Assume.assumeTrue("Ignore test when A2DP Sink is not enabled",
+ mTargetContext.getResources().getBoolean(R.bool.profile_supported_a2dp_sink));
+ mAdapter = BluetoothAdapter.getDefaultAdapter();
+ assertThat(mAdapter).isNotNull();
+ mDevice = mAdapter.getRemoteDevice(TEST_ADDRESS);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ mTargetContext = null;
+ mAdapter = null;
+ mDevice = null;
+ }
+
+ @Test
+ public void testCreateConnectionStateChangedDisconnectedEvent() {
+ testConnectionStateChangedBase(StackEvent.CONNECTION_STATE_DISCONNECTED);
+ }
+
+ @Test
+ public void testCreateConnectionStateChangedConnectingEvent() {
+ testConnectionStateChangedBase(StackEvent.CONNECTION_STATE_CONNECTING);
+ }
+
+ @Test
+ public void testCreateConnectionStateChangedConnectedEvent() {
+ testConnectionStateChangedBase(StackEvent.CONNECTION_STATE_CONNECTED);
+ }
+
+ @Test
+ public void testCreateConnectionStateChangedDisconnectingEvent() {
+ testConnectionStateChangedBase(StackEvent.CONNECTION_STATE_DISCONNECTING);
+ }
+
+ private void testConnectionStateChangedBase(int state) {
+ StackEvent event = StackEvent.connectionStateChanged(mDevice, state);
+ assertThat(event).isNotNull();
+ assertThat(event.mType).isEqualTo(StackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED);
+ assertThat(event.mDevice).isEqualTo(mDevice);
+ assertThat(event.mState).isEqualTo(state);
+ assertThat(event.toString()).isNotNull();
+ }
+
+ @Test
+ public void testCreateAudioStateStoppedEvent() {
+ testAudioStateChangedBase(StackEvent.AUDIO_STATE_STOPPED);
+ }
+
+ @Test
+ public void testCreateAudioStateStartedvent() {
+ testAudioStateChangedBase(StackEvent.AUDIO_STATE_STARTED);
+ }
+
+ @Test
+ public void testCreateAudioStateRemoteSuspendEvent() {
+ testAudioStateChangedBase(StackEvent.AUDIO_STATE_REMOTE_SUSPEND);
+ }
+
+ private void testAudioStateChangedBase(int state) {
+ StackEvent event = StackEvent.audioStateChanged(mDevice, state);
+ assertThat(event).isNotNull();
+ assertThat(event.mType).isEqualTo(StackEvent.EVENT_TYPE_AUDIO_STATE_CHANGED);
+ assertThat(event.mDevice).isEqualTo(mDevice);
+ assertThat(event.mState).isEqualTo(state);
+ assertThat(event.toString()).isNotNull();
+ }
+
+ @Test
+ public void testCreateAudioConfigurationChangedEvent() {
+ int sampleRate = 44000;
+ int channelCount = 1;
+ StackEvent event = StackEvent.audioConfigChanged(mDevice, sampleRate, channelCount);
+ assertThat(event).isNotNull();
+ assertThat(event.mType).isEqualTo(StackEvent.EVENT_TYPE_AUDIO_CONFIG_CHANGED);
+ assertThat(event.mDevice).isEqualTo(mDevice);
+ assertThat(event.mSampleRate).isEqualTo(sampleRate);
+ assertThat(event.mChannelCount).isEqualTo(channelCount);
+ assertThat(event.toString()).isNotNull();
+ }
+}
diff --git a/tests/unit/src/com/android/bluetooth/btservice/storage/DatabaseManagerTest.java b/tests/unit/src/com/android/bluetooth/btservice/storage/DatabaseManagerTest.java
index 90c3953..c0132aa 100644
--- a/tests/unit/src/com/android/bluetooth/btservice/storage/DatabaseManagerTest.java
+++ b/tests/unit/src/com/android/bluetooth/btservice/storage/DatabaseManagerTest.java
@@ -1070,7 +1070,7 @@
device.put("migrated", false);
assertThat(db.insert("metadata", SQLiteDatabase.CONFLICT_IGNORE, device),
CoreMatchers.not(-1));
- // Migrate database from 106 to 107
+ // Migrate database from 107 to 108
db.close();
db = testHelper.runMigrationsAndValidate(DB_NAME, 108, true,
MetadataDatabase.MIGRATION_107_108);
diff --git a/tests/unit/src/com/android/bluetooth/hfp/HeadsetPhoneStateTest.java b/tests/unit/src/com/android/bluetooth/hfp/HeadsetPhoneStateTest.java
index 99a9f34..130f457 100644
--- a/tests/unit/src/com/android/bluetooth/hfp/HeadsetPhoneStateTest.java
+++ b/tests/unit/src/com/android/bluetooth/hfp/HeadsetPhoneStateTest.java
@@ -137,9 +137,9 @@
public void testListenForPhoneState_ServiceAndSignalStrength() {
BluetoothDevice device1 = TestUtils.getTestDevice(mAdapter, 1);
mHeadsetPhoneState.listenForPhoneState(device1, PhoneStateListener.LISTEN_SERVICE_STATE
- | PhoneStateListener.LISTEN_ALWAYS_REPORTED_SIGNAL_STRENGTH);
+ | PhoneStateListener.LISTEN_SIGNAL_STRENGTHS);
verify(mTelephonyManager).listen(any(), eq(PhoneStateListener.LISTEN_SERVICE_STATE
- | PhoneStateListener.LISTEN_ALWAYS_REPORTED_SIGNAL_STRENGTH));
+ | PhoneStateListener.LISTEN_SIGNAL_STRENGTHS));
}
/**
@@ -150,9 +150,9 @@
public void testListenForPhoneState_ServiceAndSignalStrengthUpdateTurnOffSignalStrengh() {
BluetoothDevice device1 = TestUtils.getTestDevice(mAdapter, 1);
mHeadsetPhoneState.listenForPhoneState(device1, PhoneStateListener.LISTEN_SERVICE_STATE
- | PhoneStateListener.LISTEN_ALWAYS_REPORTED_SIGNAL_STRENGTH);
+ | PhoneStateListener.LISTEN_SIGNAL_STRENGTHS);
verify(mTelephonyManager).listen(any(), eq(PhoneStateListener.LISTEN_SERVICE_STATE
- | PhoneStateListener.LISTEN_ALWAYS_REPORTED_SIGNAL_STRENGTH));
+ | PhoneStateListener.LISTEN_SIGNAL_STRENGTHS));
mHeadsetPhoneState.listenForPhoneState(device1, PhoneStateListener.LISTEN_SERVICE_STATE);
verify(mTelephonyManager).listen(any(), eq(PhoneStateListener.LISTEN_NONE));
verify(mTelephonyManager).listen(any(), eq(PhoneStateListener.LISTEN_SERVICE_STATE));
@@ -165,9 +165,9 @@
public void testListenForPhoneState_ServiceAndSignalStrengthUpdateTurnOffAll() {
BluetoothDevice device1 = TestUtils.getTestDevice(mAdapter, 1);
mHeadsetPhoneState.listenForPhoneState(device1, PhoneStateListener.LISTEN_SERVICE_STATE
- | PhoneStateListener.LISTEN_ALWAYS_REPORTED_SIGNAL_STRENGTH);
+ | PhoneStateListener.LISTEN_SIGNAL_STRENGTHS);
verify(mTelephonyManager).listen(any(), eq(PhoneStateListener.LISTEN_SERVICE_STATE
- | PhoneStateListener.LISTEN_ALWAYS_REPORTED_SIGNAL_STRENGTH));
+ | PhoneStateListener.LISTEN_SIGNAL_STRENGTHS));
mHeadsetPhoneState.listenForPhoneState(device1, PhoneStateListener.LISTEN_NONE);
verify(mTelephonyManager).listen(any(), eq(PhoneStateListener.LISTEN_NONE));
}
@@ -183,12 +183,12 @@
BluetoothDevice device2 = TestUtils.getTestDevice(mAdapter, 2);
// Enabling updates from first device should trigger subscription
mHeadsetPhoneState.listenForPhoneState(device1, PhoneStateListener.LISTEN_SERVICE_STATE
- | PhoneStateListener.LISTEN_ALWAYS_REPORTED_SIGNAL_STRENGTH);
+ | PhoneStateListener.LISTEN_SIGNAL_STRENGTHS);
verify(mTelephonyManager).listen(any(), eq(PhoneStateListener.LISTEN_SERVICE_STATE
- | PhoneStateListener.LISTEN_ALWAYS_REPORTED_SIGNAL_STRENGTH));
+ | PhoneStateListener.LISTEN_SIGNAL_STRENGTHS));
// Enabling updates from second device should not trigger the same subscription
mHeadsetPhoneState.listenForPhoneState(device2, PhoneStateListener.LISTEN_SERVICE_STATE
- | PhoneStateListener.LISTEN_ALWAYS_REPORTED_SIGNAL_STRENGTH);
+ | PhoneStateListener.LISTEN_SIGNAL_STRENGTHS);
// Disabling updates from first device should not cancel subscription
mHeadsetPhoneState.listenForPhoneState(device1, PhoneStateListener.LISTEN_NONE);
// Disabling updates from second device should cancel subscription
@@ -211,15 +211,15 @@
verifyNoMoreInteractions(mTelephonyManager);
// Partially enabling updates from second device should trigger partial subscription
mHeadsetPhoneState.listenForPhoneState(device2,
- PhoneStateListener.LISTEN_ALWAYS_REPORTED_SIGNAL_STRENGTH);
+ PhoneStateListener.LISTEN_SIGNAL_STRENGTHS);
verify(mTelephonyManager).listen(any(), eq(PhoneStateListener.LISTEN_NONE));
verify(mTelephonyManager).listen(any(), eq(PhoneStateListener.LISTEN_SERVICE_STATE
- | PhoneStateListener.LISTEN_ALWAYS_REPORTED_SIGNAL_STRENGTH));
+ | PhoneStateListener.LISTEN_SIGNAL_STRENGTHS));
// Partially disabling updates from first device should not cancel all subscription
mHeadsetPhoneState.listenForPhoneState(device1, PhoneStateListener.LISTEN_NONE);
verify(mTelephonyManager, times(2)).listen(any(), eq(PhoneStateListener.LISTEN_NONE));
verify(mTelephonyManager).listen(
- any(), eq(PhoneStateListener.LISTEN_ALWAYS_REPORTED_SIGNAL_STRENGTH));
+ any(), eq(PhoneStateListener.LISTEN_SIGNAL_STRENGTHS));
// Partially disabling updates from second device should cancel subscription
mHeadsetPhoneState.listenForPhoneState(device2, PhoneStateListener.LISTEN_NONE);
verify(mTelephonyManager, times(3)).listen(any(), eq(PhoneStateListener.LISTEN_NONE));
diff --git a/tests/unit/src/com/android/bluetooth/hfp/HeadsetServiceAndStateMachineTest.java b/tests/unit/src/com/android/bluetooth/hfp/HeadsetServiceAndStateMachineTest.java
index 7ff5f1c..3ebcc05 100644
--- a/tests/unit/src/com/android/bluetooth/hfp/HeadsetServiceAndStateMachineTest.java
+++ b/tests/unit/src/com/android/bluetooth/hfp/HeadsetServiceAndStateMachineTest.java
@@ -954,8 +954,7 @@
mHeadsetService.startVoiceRecognition(deviceA);
verify(mNativeInterface, timeout(ASYNC_CALL_TIMEOUT_MILLIS)).atResponseCode(deviceA,
HeadsetHalConstants.AT_RESPONSE_OK, 0);
- verify(mAudioManager, timeout(ASYNC_CALL_TIMEOUT_MILLIS))
- .setParameters("A2dpSuspended=true");
+ verify(mAudioManager, timeout(ASYNC_CALL_TIMEOUT_MILLIS)).setA2dpSuspended(true);
verify(mNativeInterface, timeout(ASYNC_CALL_TIMEOUT_MILLIS)).connectAudio(deviceA);
verifyNoMoreInteractions(mNativeInterface);
}
@@ -1008,8 +1007,7 @@
// We still continue on the initiating HF
verify(mNativeInterface, timeout(ASYNC_CALL_TIMEOUT_MILLIS)).atResponseCode(deviceA,
HeadsetHalConstants.AT_RESPONSE_OK, 0);
- verify(mAudioManager, timeout(ASYNC_CALL_TIMEOUT_MILLIS))
- .setParameters("A2dpSuspended=true");
+ verify(mAudioManager, timeout(ASYNC_CALL_TIMEOUT_MILLIS)).setA2dpSuspended(true);
verify(mNativeInterface, timeout(ASYNC_CALL_TIMEOUT_MILLIS)).connectAudio(deviceA);
verifyNoMoreInteractions(mNativeInterface);
}
@@ -1084,8 +1082,7 @@
verify(mNativeInterface).setActiveDevice(deviceA);
Assert.assertEquals(deviceA, mHeadsetService.getActiveDevice());
verify(mNativeInterface, timeout(ASYNC_CALL_TIMEOUT_MILLIS)).startVoiceRecognition(deviceA);
- verify(mAudioManager, timeout(ASYNC_CALL_TIMEOUT_MILLIS))
- .setParameters("A2dpSuspended=true");
+ verify(mAudioManager, timeout(ASYNC_CALL_TIMEOUT_MILLIS)).setA2dpSuspended(true);
verify(mNativeInterface, timeout(ASYNC_CALL_TIMEOUT_MILLIS)).connectAudio(deviceA);
waitAndVerifyAudioStateIntent(ASYNC_CALL_TIMEOUT_MILLIS, deviceA,
BluetoothHeadset.STATE_AUDIO_CONNECTING, BluetoothHeadset.STATE_AUDIO_DISCONNECTED);
@@ -1133,8 +1130,7 @@
Assert.assertTrue(mHeadsetService.startVoiceRecognition(device));
verify(mNativeInterface, timeout(ASYNC_CALL_TIMEOUT_MILLIS)).atResponseCode(device,
HeadsetHalConstants.AT_RESPONSE_OK, 0);
- verify(mAudioManager, timeout(ASYNC_CALL_TIMEOUT_MILLIS))
- .setParameters("A2dpSuspended=true");
+ verify(mAudioManager, timeout(ASYNC_CALL_TIMEOUT_MILLIS)).setA2dpSuspended(true);
verify(mNativeInterface, timeout(ASYNC_CALL_TIMEOUT_MILLIS)).connectAudio(device);
waitAndVerifyAudioStateIntent(ASYNC_CALL_TIMEOUT_MILLIS, device,
BluetoothHeadset.STATE_AUDIO_CONNECTING, BluetoothHeadset.STATE_AUDIO_DISCONNECTED);
@@ -1151,8 +1147,7 @@
Assert.assertNotNull(device);
Assert.assertTrue(mHeadsetService.startVoiceRecognition(device));
verify(mNativeInterface, timeout(ASYNC_CALL_TIMEOUT_MILLIS)).startVoiceRecognition(device);
- verify(mAudioManager, timeout(ASYNC_CALL_TIMEOUT_MILLIS))
- .setParameters("A2dpSuspended=true");
+ verify(mAudioManager, timeout(ASYNC_CALL_TIMEOUT_MILLIS)).setA2dpSuspended(true);
verify(mNativeInterface, timeout(ASYNC_CALL_TIMEOUT_MILLIS)).connectAudio(device);
waitAndVerifyAudioStateIntent(ASYNC_CALL_TIMEOUT_MILLIS, device,
BluetoothHeadset.STATE_AUDIO_CONNECTING, BluetoothHeadset.STATE_AUDIO_DISCONNECTED);
diff --git a/tests/unit/src/com/android/bluetooth/hfp/HeadsetServiceTest.java b/tests/unit/src/com/android/bluetooth/hfp/HeadsetServiceTest.java
index 1a4e877..d1888bb 100644
--- a/tests/unit/src/com/android/bluetooth/hfp/HeadsetServiceTest.java
+++ b/tests/unit/src/com/android/bluetooth/hfp/HeadsetServiceTest.java
@@ -706,7 +706,7 @@
headsetCallState.mType, headsetCallState.mName, mAdapter.getAttributionSource());
TestUtils.waitForLooperToFinishScheduledTask(
mHeadsetService.getStateMachinesThreadLooper());
- verify(mAudioManager, never()).setParameters("A2dpSuspended=true");
+ verify(mAudioManager, never()).setA2dpSuspended(true);
HeadsetTestUtils.verifyPhoneStateChangeSetters(mPhoneState, headsetCallState,
ASYNC_CALL_TIMEOUT_MILLIS);
}
@@ -765,7 +765,7 @@
mHeadsetService.getStateMachinesThreadLooper());
// Should not ask Audio HAL to suspend A2DP without active device
- verify(mAudioManager, never()).setParameters("A2dpSuspended=true");
+ verify(mAudioManager, never()).setA2dpSuspended(true);
// Make sure we notify device about this change
verify(mStateMachines.get(mCurrentDevice)).sendMessage(
HeadsetStateMachine.CALL_STATE_CHANGED, headsetCallState);
@@ -783,7 +783,7 @@
TestUtils.waitForLooperToFinishScheduledTask(
mHeadsetService.getStateMachinesThreadLooper());
// Ask Audio HAL to suspend A2DP
- verify(mAudioManager).setParameters("A2dpSuspended=true");
+ verify(mAudioManager).setA2dpSuspended(true);
// Make sure state is updated
verify(mStateMachines.get(mCurrentDevice)).sendMessage(
HeadsetStateMachine.CALL_STATE_CHANGED, headsetCallState);
@@ -847,8 +847,7 @@
headsetCallState.mNumHeld, headsetCallState.mCallState, headsetCallState.mNumber,
headsetCallState.mType, headsetCallState.mName, mAdapter.getAttributionSource());
// Ask Audio HAL to suspend A2DP
- verify(mAudioManager, timeout(ASYNC_CALL_TIMEOUT_MILLIS))
- .setParameters("A2dpSuspended=true");
+ verify(mAudioManager, timeout(ASYNC_CALL_TIMEOUT_MILLIS)).setA2dpSuspended(true);
// Make sure we notify devices about this change
for (BluetoothDevice device : connectedDevices) {
verify(mStateMachines.get(device)).sendMessage(HeadsetStateMachine.CALL_STATE_CHANGED,
diff --git a/tests/unit/src/com/android/bluetooth/hfp/HeadsetStateMachineTest.java b/tests/unit/src/com/android/bluetooth/hfp/HeadsetStateMachineTest.java
index 34d228d..f927a61 100644
--- a/tests/unit/src/com/android/bluetooth/hfp/HeadsetStateMachineTest.java
+++ b/tests/unit/src/com/android/bluetooth/hfp/HeadsetStateMachineTest.java
@@ -17,6 +17,7 @@
package com.android.bluetooth.hfp;
import static android.Manifest.permission.BLUETOOTH_CONNECT;
+
import static org.mockito.Mockito.*;
import android.bluetooth.BluetoothAdapter;
@@ -69,6 +70,7 @@
private static final int CONNECT_TIMEOUT_TEST_WAIT_MILLIS = CONNECT_TIMEOUT_TEST_MILLIS * 3 / 2;
private static final int ASYNC_CALL_TIMEOUT_MILLIS = 250;
private static final String TEST_PHONE_NUMBER = "1234567890";
+ private static final int MAX_RETRY_DISCONNECT_AUDIO = 3;
private Context mTargetContext;
private BluetoothAdapter mAdapter;
private HandlerThread mHandlerThread;
@@ -742,23 +744,50 @@
}
/**
- * Test state transition from AudioDisconnecting to Connected state via
- * CONNECT_TIMEOUT message
+ * Test state transition from AudioDisconnecting to AudioOn state via CONNECT_TIMEOUT message
+ * until retry count is reached, then test transition to Disconnecting state.
*/
@Test
- public void testStateTransition_AudioDisconnectingToConnected_Timeout() {
+ public void testStateTransition_AudioDisconnectingToAudioOnAndDisconnecting_Timeout() {
int numBroadcastsSent = setUpAudioDisconnectingState();
// Wait for connection to timeout
numBroadcastsSent++;
- verify(mHeadsetService, timeout(CONNECT_TIMEOUT_TEST_WAIT_MILLIS).times(
- numBroadcastsSent)).sendBroadcastAsUser(mIntentArgument.capture(),
- eq(UserHandle.ALL), eq(BLUETOOTH_CONNECT),
- any(Bundle.class));
- HeadsetTestUtils.verifyAudioStateBroadcast(mTestDevice,
- BluetoothHeadset.STATE_AUDIO_DISCONNECTED, BluetoothHeadset.STATE_AUDIO_CONNECTED,
- mIntentArgument.getValue());
- Assert.assertThat(mHeadsetStateMachine.getCurrentState(),
- IsInstanceOf.instanceOf(HeadsetStateMachine.Connected.class));
+ for (int i = 0; i <= MAX_RETRY_DISCONNECT_AUDIO; i++) {
+ if (i > 0) { // Skip first AUDIO_DISCONNECTING init as it was setup before the loop
+ mHeadsetStateMachine.sendMessage(HeadsetStateMachine.DISCONNECT_AUDIO, mTestDevice);
+ // No new broadcast due to lack of AUDIO_DISCONNECTING intent variable
+ verify(mHeadsetService, after(ASYNC_CALL_TIMEOUT_MILLIS)
+ .times(numBroadcastsSent)).sendBroadcastAsUser(
+ any(Intent.class), eq(UserHandle.ALL), eq(BLUETOOTH_CONNECT),
+ any(Bundle.class));
+ Assert.assertThat(mHeadsetStateMachine.getCurrentState(),
+ IsInstanceOf.instanceOf(HeadsetStateMachine.AudioDisconnecting.class));
+ if (i == MAX_RETRY_DISCONNECT_AUDIO) {
+ // Increment twice numBroadcastsSent as DISCONNECT message is added on max retry
+ numBroadcastsSent += 2;
+ } else {
+ numBroadcastsSent++;
+ }
+ }
+ verify(mHeadsetService, timeout(CONNECT_TIMEOUT_TEST_WAIT_MILLIS).times(
+ numBroadcastsSent)).sendBroadcastAsUser(mIntentArgument.capture(),
+ eq(UserHandle.ALL), eq(BLUETOOTH_CONNECT), any(Bundle.class));
+ if (i < MAX_RETRY_DISCONNECT_AUDIO) { // Test if state is AudioOn before max retry
+ HeadsetTestUtils.verifyAudioStateBroadcast(mTestDevice,
+ BluetoothHeadset.STATE_AUDIO_CONNECTED,
+ BluetoothHeadset.STATE_AUDIO_CONNECTED,
+ mIntentArgument.getValue());
+ Assert.assertThat(mHeadsetStateMachine.getCurrentState(),
+ IsInstanceOf.instanceOf(HeadsetStateMachine.AudioOn.class));
+ } else { // Max retry count reached, test Disconnecting state
+ HeadsetTestUtils.verifyConnectionStateBroadcast(mTestDevice,
+ BluetoothHeadset.STATE_DISCONNECTING,
+ BluetoothHeadset.STATE_CONNECTED,
+ mIntentArgument.getValue());
+ Assert.assertThat(mHeadsetStateMachine.getCurrentState(),
+ IsInstanceOf.instanceOf(HeadsetStateMachine.Disconnecting.class));
+ }
+ }
}
/**
@@ -867,7 +896,7 @@
public void testAtBiaEvent_initialSubscriptionWithUpdates() {
setUpConnectedState();
verify(mPhoneState).listenForPhoneState(mTestDevice, PhoneStateListener.LISTEN_SERVICE_STATE
- | PhoneStateListener.LISTEN_ALWAYS_REPORTED_SIGNAL_STRENGTH);
+ | PhoneStateListener.LISTEN_SIGNAL_STRENGTHS);
mHeadsetStateMachine.sendMessage(HeadsetStateMachine.STACK_EVENT,
new HeadsetStackEvent(HeadsetStackEvent.EVENT_TYPE_BIA,
new HeadsetAgIndicatorEnableState(true, true, false, false), mTestDevice));
@@ -877,7 +906,7 @@
new HeadsetStackEvent(HeadsetStackEvent.EVENT_TYPE_BIA,
new HeadsetAgIndicatorEnableState(false, true, true, false), mTestDevice));
verify(mPhoneState, timeout(ASYNC_CALL_TIMEOUT_MILLIS)).listenForPhoneState(mTestDevice,
- PhoneStateListener.LISTEN_ALWAYS_REPORTED_SIGNAL_STRENGTH);
+ PhoneStateListener.LISTEN_SIGNAL_STRENGTHS);
mHeadsetStateMachine.sendMessage(HeadsetStateMachine.STACK_EVENT,
new HeadsetStackEvent(HeadsetStackEvent.EVENT_TYPE_BIA,
new HeadsetAgIndicatorEnableState(false, true, false, false), mTestDevice));
diff --git a/tests/unit/src/com/android/bluetooth/hfpclient/HeadsetClientStateMachineTest.java b/tests/unit/src/com/android/bluetooth/hfpclient/HeadsetClientStateMachineTest.java
index cb49e02..f7cb5c0 100644
--- a/tests/unit/src/com/android/bluetooth/hfpclient/HeadsetClientStateMachineTest.java
+++ b/tests/unit/src/com/android/bluetooth/hfpclient/HeadsetClientStateMachineTest.java
@@ -83,6 +83,8 @@
mAudioManager);
when(mHeadsetClientService.getResources()).thenReturn(mMockHfpResources);
when(mMockHfpResources.getBoolean(R.bool.hfp_clcc_poll_during_call)).thenReturn(true);
+ when(mMockHfpResources.getInteger(R.integer.hfp_clcc_poll_interval_during_call))
+ .thenReturn(2000);
mNativeInterface = spy(NativeInterface.getInstance());
// This line must be called to make sure relevant objects are initialized properly
diff --git a/tests/unit/src/com/android/bluetooth/hfpclient/connserv/HfpClientConnectionServiceTest.java b/tests/unit/src/com/android/bluetooth/hfpclient/connserv/HfpClientConnectionServiceTest.java
index c6d383f..8c4be97 100644
--- a/tests/unit/src/com/android/bluetooth/hfpclient/connserv/HfpClientConnectionServiceTest.java
+++ b/tests/unit/src/com/android/bluetooth/hfpclient/connserv/HfpClientConnectionServiceTest.java
@@ -17,6 +17,7 @@
package com.android.bluetooth.hfpclient.connserv;
import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
import static org.junit.Assume.assumeTrue;
import static org.mockito.ArgumentMatchers.any;
@@ -270,7 +271,16 @@
mServiceRule.startService(createServiceIntent());
InstrumentationRegistry.getTargetContext().sendBroadcast(
createDeviceConnectedIntent(device));
- assertThat(buildLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue();
+
+ buildLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS);
+ long startTime = System.currentTimeMillis();
+ while (mHfpClientConnectionService.findBlockForDevice(device) == null) {
+ if (System.currentTimeMillis() - startTime > TIMEOUT_MS) {
+ assertWithMessage(
+ "Timeout waiting for block to be added to HfpClientConnectionService")
+ .fail();
+ }
+ }
}
private HfpClientDeviceBlock.Factory createDeviceBlockFactoryForTest(
diff --git a/tests/unit/src/com/android/bluetooth/le_audio/LeAudioServiceTest.java b/tests/unit/src/com/android/bluetooth/le_audio/LeAudioServiceTest.java
index 94c2468..43ddfcc 100644
--- a/tests/unit/src/com/android/bluetooth/le_audio/LeAudioServiceTest.java
+++ b/tests/unit/src/com/android/bluetooth/le_audio/LeAudioServiceTest.java
@@ -21,9 +21,14 @@
import static com.google.common.truth.Truth.assertWithMessage;
import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyInt;
import static org.mockito.Mockito.anyString;
+import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.when;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.eq;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
@@ -54,7 +59,10 @@
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
+import java.util.Arrays;
+import java.util.Collections;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeoutException;
@@ -62,14 +70,21 @@
@MediumTest
@RunWith(AndroidJUnit4.class)
public class LeAudioServiceTest {
+ private static final int ASYNC_CALL_TIMEOUT_MILLIS = 250;
+ private static final int TIMEOUT_MS = 1000;
+ private static final int MAX_LE_AUDIO_CONNECTIONS = 5;
+ private static final int LE_AUDIO_GROUP_ID_INVALID = -1;
+
private BluetoothAdapter mAdapter;
private Context mTargetContext;
private LeAudioService mService;
private BluetoothDevice mLeftDevice;
private BluetoothDevice mRightDevice;
private BluetoothDevice mSingleDevice;
+ private HashSet<BluetoothDevice> mBondedDevices = new HashSet<>();
private HashMap<BluetoothDevice, LinkedBlockingQueue<Intent>> mDeviceQueueMap;
- private static final int TIMEOUT_MS = 1000;
+ private LinkedBlockingQueue<Intent> mGroupIntentQueue = new LinkedBlockingQueue<>();
+ private int testGroupId = 1;
private BroadcastReceiver mLeAudioIntentReceiver;
@@ -87,23 +102,37 @@
MockitoAnnotations.initMocks(this);
TestUtils.setAdapterService(mAdapterService);
+ doReturn(MAX_LE_AUDIO_CONNECTIONS).when(mAdapterService).getMaxConnectedAudioDevices();
+ doReturn(new ParcelUuid[]{BluetoothUuid.LE_AUDIO}).when(mAdapterService)
+ .getRemoteUuids(any(BluetoothDevice.class));
doReturn(mDatabaseManager).when(mAdapterService).getDatabase();
doReturn(true, false).when(mAdapterService).isStartedProfile(anyString());
mAdapter = BluetoothAdapter.getDefaultAdapter();
+ // Mock methods in AdapterService
+ doAnswer(invocation -> mBondedDevices.toArray(new BluetoothDevice[]{})).when(
+ mAdapterService).getBondedDevices();
startService();
mService.mLeAudioNativeInterface = mNativeInterface;
+ mService.mAudioManager = mAudioManager;
// Override the timeout value to speed up the test
LeAudioStateMachine.sConnectTimeoutMs = TIMEOUT_MS; // 1s
+ mGroupIntentQueue = new LinkedBlockingQueue<>();
+
// Set up the Connection State Changed receiver
IntentFilter filter = new IntentFilter();
filter.addAction(BluetoothLeAudio.ACTION_LE_AUDIO_CONNECTION_STATE_CHANGED);
+ filter.addAction(BluetoothLeAudio.ACTION_LE_AUDIO_CONF_CHANGED);
+ filter.addAction(BluetoothLeAudio.ACTION_LE_AUDIO_GROUP_STATUS_CHANGED);
mLeAudioIntentReceiver = new LeAudioIntentReceiver();
mTargetContext.registerReceiver(mLeAudioIntentReceiver, filter);
+ doAnswer(invocation -> mBondedDevices.toArray(new BluetoothDevice[]{})).when(
+ mAdapterService).getBondedDevices();
+
// Get a device for testing
mLeftDevice = TestUtils.getTestDevice(mAdapter, 0);
mRightDevice = TestUtils.getTestDevice(mAdapter, 1);
@@ -120,6 +149,8 @@
@After
public void tearDown() throws Exception {
+ mBondedDevices.clear();
+ mGroupIntentQueue.clear();
stopService();
mTargetContext.unregisterReceiver(mLeAudioIntentReceiver);
mDeviceQueueMap.clear();
@@ -155,6 +186,28 @@
+ e.getMessage()).fail();
}
}
+ if (BluetoothLeAudio.ACTION_LE_AUDIO_CONF_CHANGED.equals(intent.getAction())) {
+ try {
+ BluetoothDevice device = intent.getParcelableExtra(
+ BluetoothDevice.EXTRA_DEVICE);
+ assertThat(device).isNotNull();
+ LinkedBlockingQueue<Intent> queue = mDeviceQueueMap.get(device);
+ assertThat(queue).isNotNull();
+ queue.put(intent);
+ } catch (InterruptedException e) {
+ assertWithMessage("Cannot add Le Audio Intent to the Connection State queue: "
+ + e.getMessage()).fail();
+ }
+ }
+
+ if (BluetoothLeAudio.ACTION_LE_AUDIO_GROUP_STATUS_CHANGED.equals(intent.getAction())) {
+ try {
+ mGroupIntentQueue.put(intent);
+ } catch (InterruptedException e) {
+ assertWithMessage("Cannot add Le Audio Intent to the Connection State queue: "
+ + e.getMessage()).fail();
+ }
+ }
}
}
@@ -191,12 +244,6 @@
assertThat(mService.stop()).isTrue();
}
});
- // Try to restart the service. Note: must be done on the main thread
- InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
- public void run() {
- assertThat(mService.start()).isTrue();
- }
- });
}
/**
@@ -714,4 +761,269 @@
Intent intent = TestUtils.waitForNoIntent(timeoutMs, mDeviceQueueMap.get(device));
assertThat(intent).isNull();
}
+
+ /**
+ * Test setting connection policy
+ */
+ @Test
+ public void testSetConnectionPolicy() {
+ doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class));
+ doReturn(true).when(mNativeInterface).disconnectLeAudio(any(BluetoothDevice.class));
+ doReturn(true).when(mDatabaseManager).setProfileConnectionPolicy(any(BluetoothDevice.class),
+ anyInt(), anyInt());
+ when(mDatabaseManager.getProfileConnectionPolicy(mSingleDevice, BluetoothProfile.LE_AUDIO))
+ .thenReturn(BluetoothProfile.CONNECTION_POLICY_UNKNOWN);
+
+ assertThat(mService.setConnectionPolicy(mSingleDevice,
+ BluetoothProfile.CONNECTION_POLICY_ALLOWED)).isTrue();
+
+ // Verify the connection state broadcast, and that we are in Connecting state
+ verifyConnectionStateIntent(TIMEOUT_MS, mSingleDevice, BluetoothProfile.STATE_CONNECTING,
+ BluetoothProfile.STATE_DISCONNECTED);
+ assertThat(BluetoothProfile.STATE_CONNECTING)
+ .isEqualTo(mService.getConnectionState(mSingleDevice));
+
+ LeAudioStackEvent connCompletedEvent;
+ // Send a message to trigger connection completed
+ connCompletedEvent = new LeAudioStackEvent(
+ LeAudioStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED);
+ connCompletedEvent.device = mSingleDevice;
+ connCompletedEvent.valueInt1 = LeAudioStackEvent.CONNECTION_STATE_CONNECTED;
+ mService.messageFromNative(connCompletedEvent);
+
+ // Verify the connection state broadcast, and that we are in Connected state
+ verifyConnectionStateIntent(TIMEOUT_MS, mSingleDevice, BluetoothProfile.STATE_CONNECTED,
+ BluetoothProfile.STATE_CONNECTING);
+ assertThat(BluetoothProfile.STATE_CONNECTED)
+ .isEqualTo(mService.getConnectionState(mSingleDevice));
+
+ // Set connection policy to forbidden
+ assertThat(mService.setConnectionPolicy(mSingleDevice,
+ BluetoothProfile.CONNECTION_POLICY_FORBIDDEN)).isTrue();
+
+ // Verify the connection state broadcast, and that we are in Connecting state
+ verifyConnectionStateIntent(TIMEOUT_MS, mSingleDevice, BluetoothProfile.STATE_DISCONNECTING,
+ BluetoothProfile.STATE_CONNECTED);
+ assertThat(BluetoothProfile.STATE_DISCONNECTING)
+ .isEqualTo(mService.getConnectionState(mSingleDevice));
+
+ // Send a message to trigger disconnection completed
+ connCompletedEvent = new LeAudioStackEvent(
+ LeAudioStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED);
+ connCompletedEvent.device = mSingleDevice;
+ connCompletedEvent.valueInt1 = LeAudioStackEvent.CONNECTION_STATE_DISCONNECTED;
+ mService.messageFromNative(connCompletedEvent);
+
+ // Verify the connection state broadcast, and that we are in Disconnected state
+ verifyConnectionStateIntent(TIMEOUT_MS, mSingleDevice, BluetoothProfile.STATE_DISCONNECTED,
+ BluetoothProfile.STATE_DISCONNECTING);
+ assertThat(BluetoothProfile.STATE_DISCONNECTED)
+ .isEqualTo(mService.getConnectionState(mSingleDevice));
+ }
+
+ /**
+ * Helper function to connect Test device
+ *
+ * @param device test device
+ */
+ private void connectTestDevice(BluetoothDevice device, int GroupId) {
+ List<BluetoothDevice> prevConnectedDevices = mService.getConnectedDevices();
+
+ when(mDatabaseManager.getProfileConnectionPolicy(device, BluetoothProfile.LE_AUDIO))
+ .thenReturn(BluetoothProfile.CONNECTION_POLICY_UNKNOWN);
+ // Send a connect request
+ assertWithMessage("Connect failed").that(mService.connect(device)).isTrue();
+
+ // Make device bonded
+ mBondedDevices.add(device);
+
+ // Wait ASYNC_CALL_TIMEOUT_MILLIS for state to settle, timing is also tested here and
+ // 250ms for processing two messages should be way more than enough. Anything that breaks
+ // this indicate some breakage in other part of Android OS
+
+ verifyConnectionStateIntent(ASYNC_CALL_TIMEOUT_MILLIS, device,
+ BluetoothProfile.STATE_CONNECTING, BluetoothProfile.STATE_DISCONNECTED);
+ assertThat(BluetoothProfile.STATE_CONNECTING)
+ .isEqualTo(mService.getConnectionState(device));
+
+ // Use connected event to indicate that device is connected
+ LeAudioStackEvent connCompletedEvent =
+ new LeAudioStackEvent(LeAudioStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED);
+ connCompletedEvent.device = device;
+ connCompletedEvent.valueInt1 = LeAudioStackEvent.CONNECTION_STATE_CONNECTED;
+ mService.messageFromNative(connCompletedEvent);
+
+ verifyConnectionStateIntent(ASYNC_CALL_TIMEOUT_MILLIS, device,
+ BluetoothProfile.STATE_CONNECTED, BluetoothProfile.STATE_CONNECTING);
+
+ assertThat(BluetoothProfile.STATE_CONNECTED)
+ .isEqualTo(mService.getConnectionState(device));
+
+ LeAudioStackEvent nodeGroupAdded =
+ new LeAudioStackEvent(LeAudioStackEvent.EVENT_TYPE_GROUP_NODE_STATUS_CHANGED);
+ nodeGroupAdded.device = device;
+ nodeGroupAdded.valueInt1 = GroupId;
+ nodeGroupAdded.valueInt2 = LeAudioStackEvent.GROUP_NODE_ADDED;
+ mService.messageFromNative(nodeGroupAdded);
+
+ // Verify that the device is in the list of connected devices
+ assertThat(mService.getConnectedDevices().contains(device)).isTrue();
+ // Verify the list of previously connected devices
+ for (BluetoothDevice prevDevice : prevConnectedDevices) {
+ assertThat(mService.getConnectedDevices().contains(prevDevice)).isTrue();
+ }
+ }
+
+ /**
+ * Test matching connection state devices.
+ */
+ @Test
+ public void testGetDevicesMatchingConnectionState() {
+ // Update the device priority so okToConnect() returns true
+ doReturn(new ParcelUuid[]{BluetoothUuid.LE_AUDIO}).when(mAdapterService)
+ .getRemoteUuids(any(BluetoothDevice.class));
+ doReturn(new BluetoothDevice[]{mSingleDevice}).when(mAdapterService).getBondedDevices();
+ when(mDatabaseManager
+ .getProfileConnectionPolicy(mSingleDevice, BluetoothProfile.LE_AUDIO))
+ .thenReturn(BluetoothProfile.CONNECTION_POLICY_ALLOWED);
+ doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class));
+ doReturn(true).when(mNativeInterface).disconnectLeAudio(any(BluetoothDevice.class));
+
+ connectTestDevice(mSingleDevice, testGroupId);
+ }
+
+ /**
+ * Test adding node
+ */
+ @Test
+ public void testGroupAddRemoveNode() {
+ int groupId = 1;
+
+ doReturn(true).when(mNativeInterface).groupAddNode(groupId, mSingleDevice);
+ doReturn(true).when(mNativeInterface).groupRemoveNode(groupId, mSingleDevice);
+
+ assertThat(mService.groupAddNode(groupId, mSingleDevice)).isTrue();
+ assertThat(mService.groupRemoveNode(groupId, mSingleDevice)).isTrue();
+ }
+
+ /**
+ * Test setting active device group
+ */
+ @Test
+ public void testSetActiveDeviceGroup() {
+ int groupId = 1;
+
+ // Not connected device
+ assertThat(mService.setActiveDevice(mSingleDevice)).isFalse();
+
+ // Connected device
+ doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class));
+ connectTestDevice(mSingleDevice, testGroupId);
+ assertThat(mService.setActiveDevice(mSingleDevice)).isTrue();
+
+ // no active device
+ assertThat(mService.setActiveDevice(null)).isTrue();
+ }
+
+ /**
+ * Test getting active device
+ */
+ @Test
+ public void testGetActiveDevices() {
+ int groupId = 1;
+ int nodeStatus = LeAudioStackEvent.GROUP_NODE_ADDED;
+
+ // No active device
+ assertThat(mService.getActiveDevices().isEmpty()).isTrue();
+
+ // Single active device
+ doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class));
+ connectTestDevice(mSingleDevice, testGroupId);
+
+ // Add device to group
+ LeAudioStackEvent nodeStatusChangedEvent =
+ new LeAudioStackEvent(LeAudioStackEvent.EVENT_TYPE_GROUP_NODE_STATUS_CHANGED);
+ nodeStatusChangedEvent.device = mSingleDevice;
+ nodeStatusChangedEvent.valueInt1 = groupId;
+ nodeStatusChangedEvent.valueInt2 = nodeStatus;
+ mService.messageFromNative(nodeStatusChangedEvent);
+
+ assertThat(mService.setActiveDevice(mSingleDevice)).isTrue();
+ assertThat(mService.getActiveDevices().contains(mSingleDevice)).isTrue();
+ }
+
+ /**
+ * Test native interface audio configuration changed message handling
+ */
+ @Test
+ public void testMessageFromNativeAudioConfChanged() {
+ int direction = 1;
+ int groupId = 2;
+ int snkAudioLocation = 3;
+ int srcAudioLocation = 4;
+ int availableContexts = 5;
+ int eventType = LeAudioStackEvent.EVENT_TYPE_AUDIO_CONF_CHANGED;
+ String action = BluetoothLeAudio.ACTION_LE_AUDIO_CONF_CHANGED;
+
+ // Add device to group
+ LeAudioStackEvent audioConfChangedEvent = new LeAudioStackEvent(eventType);
+ audioConfChangedEvent.device = mSingleDevice;
+ audioConfChangedEvent.valueInt1 = direction;
+ audioConfChangedEvent.valueInt2 = groupId;
+ audioConfChangedEvent.valueInt3 = snkAudioLocation;
+ audioConfChangedEvent.valueInt4 = srcAudioLocation;
+ audioConfChangedEvent.valueInt5 = availableContexts;
+ mService.messageFromNative(audioConfChangedEvent);
+
+ Intent intent = TestUtils.waitForIntent(TIMEOUT_MS, mDeviceQueueMap.get(mSingleDevice));
+ assertThat(intent).isNotNull();
+ assertThat(action).isEqualTo(intent.getAction());
+ assertThat(mSingleDevice)
+ .isEqualTo(intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE));
+ assertThat(groupId)
+ .isEqualTo(intent.getIntExtra(BluetoothLeAudio.EXTRA_LE_AUDIO_GROUP_ID, -groupId));
+ assertThat(direction)
+ .isEqualTo(intent
+ .getIntExtra(BluetoothLeAudio.EXTRA_LE_AUDIO_DIRECTION, -direction));
+ assertThat(snkAudioLocation)
+ .isEqualTo(intent
+ .getIntExtra(BluetoothLeAudio.EXTRA_LE_AUDIO_SINK_LOCATION, -snkAudioLocation));
+ assertThat(srcAudioLocation)
+ .isEqualTo(intent
+ .getIntExtra(BluetoothLeAudio.EXTRA_LE_AUDIO_SOURCE_LOCATION, srcAudioLocation));
+ assertThat(availableContexts)
+ .isEqualTo(intent
+ .getIntExtra(BluetoothLeAudio.EXTRA_LE_AUDIO_AVAILABLE_CONTEXTS, availableContexts));
+ }
+
+ private void sendEventAndVerifyIntentForGroupStatusChanged(int groupId, int groupStatus) {
+ int eventType = LeAudioStackEvent.EVENT_TYPE_GROUP_STATUS_CHANGED;
+ String action = BluetoothLeAudio.ACTION_LE_AUDIO_GROUP_STATUS_CHANGED;
+
+ LeAudioStackEvent groupStatusChangedEvent = new LeAudioStackEvent(eventType);
+ groupStatusChangedEvent.valueInt1 = groupId;
+ groupStatusChangedEvent.valueInt2 = groupStatus;
+ mService.messageFromNative(groupStatusChangedEvent);
+
+ Intent intent = TestUtils.waitForIntent(TIMEOUT_MS, mGroupIntentQueue);
+ assertThat(intent).isNotNull();
+ assertThat(action).isEqualTo(intent.getAction());
+ assertThat(groupId)
+ .isEqualTo(intent.getIntExtra(BluetoothLeAudio.EXTRA_LE_AUDIO_GROUP_ID, -groupId));
+ assertThat(groupStatus)
+ .isEqualTo(intent
+ .getIntExtra(BluetoothLeAudio.EXTRA_LE_AUDIO_GROUP_STATUS, -groupStatus));
+ }
+
+ /**
+ * Test native interface group status message handling
+ */
+ @Test
+ public void testMessageFromNativeGroupStatusChanged() {
+ doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class));
+ connectTestDevice(mSingleDevice, testGroupId);
+
+ sendEventAndVerifyIntentForGroupStatusChanged(testGroupId, LeAudioStackEvent.GROUP_STATUS_ACTIVE);
+ sendEventAndVerifyIntentForGroupStatusChanged(testGroupId, LeAudioStackEvent.GROUP_STATUS_INACTIVE);
+ }
}