summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--TEST_MAPPING6
-rw-r--r--android/app/aidl/android/bluetooth/IBluetoothA2dp.aidl2
-rw-r--r--android/app/jni/com_android_bluetooth_btservice_AdapterService.cpp20
-rw-r--r--android/app/res/values/config.xml6
-rw-r--r--android/app/src/com/android/bluetooth/a2dp/A2dpService.java6
-rw-r--r--android/app/src/com/android/bluetooth/audio_util/MediaPlayerList.java30
-rw-r--r--android/app/src/com/android/bluetooth/audio_util/helpers/Image.java4
-rw-r--r--android/app/src/com/android/bluetooth/audio_util/helpers/Metadata.java17
-rw-r--r--android/app/src/com/android/bluetooth/audio_util/helpers/Util.java17
-rw-r--r--android/app/src/com/android/bluetooth/avrcp/AvrcpCoverArtService.java5
-rw-r--r--android/app/src/com/android/bluetooth/avrcp/AvrcpTargetService.java42
-rw-r--r--android/app/src/com/android/bluetooth/avrcp/helpers/AvrcpVersion.java10
-rw-r--r--android/app/src/com/android/bluetooth/avrcp/helpers/CoverArt.java30
-rw-r--r--android/app/src/com/android/bluetooth/bass_client/BassClientService.java49
-rw-r--r--android/app/src/com/android/bluetooth/btservice/AbstractionLayer.java1
-rw-r--r--android/app/src/com/android/bluetooth/btservice/AdapterNativeInterface.java10
-rw-r--r--android/app/src/com/android/bluetooth/btservice/AdapterService.java268
-rw-r--r--android/app/src/com/android/bluetooth/btservice/PhonePolicy.java25
-rw-r--r--android/app/src/com/android/bluetooth/btservice/RemoteDevices.java324
-rw-r--r--android/app/src/com/android/bluetooth/gatt/AdvertiseBinder.java197
-rw-r--r--android/app/src/com/android/bluetooth/gatt/AdvertiseBinder.kt175
-rw-r--r--android/app/src/com/android/bluetooth/gatt/AdvertiseManager.java248
-rw-r--r--android/app/src/com/android/bluetooth/gatt/AdvertiseManagerNativeInterface.java51
-rw-r--r--android/app/src/com/android/bluetooth/gatt/GattService.java44
-rw-r--r--android/app/src/com/android/bluetooth/hfp/HeadsetService.java9
-rw-r--r--android/app/src/com/android/bluetooth/hfp/HeadsetStateMachine.java9
-rw-r--r--android/app/src/com/android/bluetooth/le_audio/LeAudioService.java240
-rw-r--r--android/app/src/com/android/bluetooth/le_scan/ScanController.java19
-rw-r--r--android/app/src/com/android/bluetooth/le_scan/ScanManager.java96
-rw-r--r--android/app/src/com/android/bluetooth/opp/BluetoothOppService.java5
-rw-r--r--android/app/src/com/android/bluetooth/tbs/TbsGatt.java123
-rw-r--r--android/app/src/com/android/bluetooth/tbs/TbsGeneric.java92
-rw-r--r--android/app/src/com/android/bluetooth/tbs/TbsService.java31
-rw-r--r--android/app/src/com/com/android/bluetooth/le_scan/BatchScanThrottler.java112
-rw-r--r--android/app/tests/unit/Android.bp1
-rw-r--r--android/app/tests/unit/src/com/android/bluetooth/TestUtils.java20
-rw-r--r--android/app/tests/unit/src/com/android/bluetooth/a2dp/A2dpCodecConfigTest.java22
-rw-r--r--android/app/tests/unit/src/com/android/bluetooth/audio_util/BrowserPlayerWrapperTest.java6
-rw-r--r--android/app/tests/unit/src/com/android/bluetooth/audio_util/ImageTest.java18
-rw-r--r--android/app/tests/unit/src/com/android/bluetooth/audio_util/MediaPlayerWrapperTest.java14
-rw-r--r--android/app/tests/unit/src/com/android/bluetooth/audio_util/MetadataTest.java14
-rw-r--r--android/app/tests/unit/src/com/android/bluetooth/bass_client/BassClientServiceTest.java58
-rw-r--r--android/app/tests/unit/src/com/android/bluetooth/bass_client/BassClientStateMachineTest.java4
-rw-r--r--android/app/tests/unit/src/com/android/bluetooth/btservice/AdapterServiceTest.java343
-rw-r--r--android/app/tests/unit/src/com/android/bluetooth/btservice/BondStateMachineTest.java6
-rw-r--r--android/app/tests/unit/src/com/android/bluetooth/btservice/storage/DatabaseManagerTest.java11
-rw-r--r--android/app/tests/unit/src/com/android/bluetooth/gatt/AdvertiseBinderTest.java9
-rw-r--r--android/app/tests/unit/src/com/android/bluetooth/gatt/AdvertiseManagerTest.java41
-rw-r--r--android/app/tests/unit/src/com/android/bluetooth/gatt/GattServiceTest.java113
-rw-r--r--android/app/tests/unit/src/com/android/bluetooth/hfp/HeadsetServiceTest.java76
-rw-r--r--android/app/tests/unit/src/com/android/bluetooth/hfp/HeadsetStateMachineTest.java22
-rw-r--r--android/app/tests/unit/src/com/android/bluetooth/le_audio/ContentControlIdKeeperTest.java5
-rw-r--r--android/app/tests/unit/src/com/android/bluetooth/le_audio/LeAudioBroadcastServiceTest.java161
-rw-r--r--android/app/tests/unit/src/com/android/bluetooth/le_audio/LeAudioServiceTest.java554
-rw-r--r--android/app/tests/unit/src/com/android/bluetooth/le_scan/BatchScanThrottlerTest.java202
-rw-r--r--android/app/tests/unit/src/com/android/bluetooth/le_scan/ScanControllerTest.java49
-rw-r--r--android/app/tests/unit/src/com/android/bluetooth/le_scan/ScanManagerTest.java10
-rw-r--r--android/app/tests/unit/src/com/android/bluetooth/mcp/McpServiceTest.java3
-rw-r--r--android/app/tests/unit/src/com/android/bluetooth/mcp/MediaControlProfileTest.java6
-rw-r--r--android/app/tests/unit/src/com/android/bluetooth/tbs/TbsGattTest.java212
-rw-r--r--android/app/tests/unit/src/com/android/bluetooth/tbs/TbsGenericTest.java170
-rw-r--r--android/app/tests/unit/src/com/android/bluetooth/vc/VolumeControlServiceTest.java5
-rw-r--r--android/pandora/mmi2grpc/mmi2grpc/_rootcanal.py1
-rw-r--r--android/pandora/mmi2grpc/mmi2grpc/avrcp.py71
-rw-r--r--android/pandora/server/configs/pts_bot_tests_config.json101
-rw-r--r--android/pandora/server/configs/pts_bot_tests_config_auto.json64
-rw-r--r--android/pandora/server/src/A2dp.kt9
-rw-r--r--android/pandora/server/src/MediaPlayer.kt14
-rw-r--r--android/pandora/server/src/MediaPlayerBrowserService.kt20
-rw-r--r--android/pandora/test/a2dp/signaling_channel.py200
-rw-r--r--android/pandora/test/a2dp/signaling_channel_test.py378
-rw-r--r--android/pandora/test/a2dp_test.py93
-rw-r--r--flags/Android.bp1
-rw-r--r--flags/BUILD.gn1
-rw-r--r--flags/active_device_manager.aconfig10
-rw-r--r--flags/bta_dm.aconfig7
-rw-r--r--flags/framework.aconfig10
-rw-r--r--flags/gap.aconfig20
-rw-r--r--flags/gatt.aconfig11
-rw-r--r--flags/hfp.aconfig10
-rw-r--r--flags/le_scanning.aconfig12
-rw-r--r--flags/leaudio.aconfig10
-rw-r--r--flags/security.aconfig10
-rw-r--r--framework/api/system-current.txt2
-rw-r--r--framework/java/android/bluetooth/BluetoothA2dp.java15
-rw-r--r--framework/java/android/bluetooth/BluetoothGatt.java195
-rw-r--r--framework/java/android/bluetooth/BluetoothSocket.java3
-rw-r--r--framework/tests/bumble/src/android/bluetooth/DckL2capTest.kt45
-rw-r--r--framework/tests/bumble/src/android/bluetooth/GattClientTest.java38
-rw-r--r--framework/tests/bumble/src/android/bluetooth/LeScanningTest.java3
-rw-r--r--framework/tests/bumble/src/android/bluetooth/pairing/PairingTest.java669
-rw-r--r--framework/tests/bumble/src/android/bluetooth/pairing/utils/IntentReceiver.java400
-rw-r--r--offload/hal/Android.bp48
-rw-r--r--offload/hal/ffi.rs279
-rw-r--r--offload/hal/include/hal/ffi.h62
-rw-r--r--offload/hal/lib.rs22
-rw-r--r--offload/hal/service.rs245
-rw-r--r--offload/hci/Android.bp53
-rw-r--r--offload/hci/command.rs544
-rw-r--r--offload/hci/data.rs204
-rw-r--r--offload/hci/derive/enum_data.rs95
-rw-r--r--offload/hci/derive/lib.rs89
-rw-r--r--offload/hci/derive/return_parameters.rs116
-rw-r--r--offload/hci/derive/struct_data.rs178
-rw-r--r--offload/hci/event.rs343
-rw-r--r--offload/hci/lib.rs78
-rw-r--r--offload/hci/reader.rs99
-rw-r--r--offload/hci/status.rs95
-rw-r--r--offload/hci/writer.rs103
-rw-r--r--offload/leaudio/aidl/Android.bp33
-rw-r--r--offload/leaudio/aidl/android/hardware/bluetooth/offload/leaudio/IHciProxy.aidl35
-rw-r--r--offload/leaudio/aidl/android/hardware/bluetooth/offload/leaudio/IHciProxyCallbacks.aidl35
-rw-r--r--offload/leaudio/aidl/android/hardware/bluetooth/offload/leaudio/StreamConfiguration.aidl48
-rw-r--r--offload/leaudio/hci/Android.bp46
-rw-r--r--offload/leaudio/hci/arbiter.rs154
-rw-r--r--offload/leaudio/hci/lib.rs42
-rw-r--r--offload/leaudio/hci/proxy.rs381
-rw-r--r--offload/leaudio/hci/service.rs126
-rw-r--r--offload/leaudio/hci/tests.rs914
-rw-r--r--pandora/interfaces/pandora_experimental/mediaplayer.proto2
-rw-r--r--sysprop/a2dp.sysprop7
-rw-r--r--system/bta/Android.bp89
-rw-r--r--system/bta/av/bta_av_aact.cc83
-rw-r--r--system/bta/dm/bta_dm_disc.cc5
-rw-r--r--system/bta/hh/bta_hh_headtracker.cc5
-rw-r--r--system/bta/include/bta_ras_api.h9
-rw-r--r--system/bta/jv/bta_jv_act.cc95
-rw-r--r--system/bta/le_audio/client.cc19
-rw-r--r--system/bta/le_audio/le_audio_client_test.cc24
-rw-r--r--system/bta/le_audio/state_machine.cc4
-rw-r--r--system/bta/le_audio/state_machine_test.cc6
-rw-r--r--system/bta/ras/ras_client.cc43
-rw-r--r--system/bta/ras/ras_utils_test.cc179
-rw-r--r--system/bta/test/bta_disc_test.cc56
-rw-r--r--system/btif/include/btif_dm.h2
-rw-r--r--system/btif/include/btif_storage.h1
-rw-r--r--system/btif/src/bluetooth.cc11
-rw-r--r--system/btif/src/btif_a2dp_source.cc12
-rw-r--r--system/btif/src/btif_core.cc21
-rw-r--r--system/btif/src/btif_dm.cc182
-rw-r--r--system/btif/src/btif_sock_rfc.cc85
-rw-r--r--system/btif/src/btif_storage.cc74
-rw-r--r--system/btif/src/btif_util.cc1
-rw-r--r--system/btif/test/btif_core_test.cc1
-rw-r--r--system/gd/hci/distance_measurement_manager.cc24
-rw-r--r--system/gd/hci/distance_measurement_manager.h4
-rw-r--r--system/gd/hci/le_advertising_manager.h4
-rw-r--r--system/gd/rust/topshim/le_audio/le_audio_shim.cc2
-rw-r--r--system/gd/rust/topshim/src/profiles/le_audio.rs23
-rw-r--r--system/gd/storage/config_keys.h1
-rw-r--r--system/include/hardware/bluetooth.h15
-rw-r--r--system/include/hardware/bt_le_audio.h1
-rw-r--r--system/main/shim/distance_measurement_manager.cc5
-rw-r--r--system/rust/src/core/shared_box.rs6
-rw-r--r--system/stack/Android.bp6
-rw-r--r--system/stack/avrc/avrc_api.cc8
-rw-r--r--system/stack/btm/btm_ble_gap.cc2
-rw-r--r--system/stack/fuzzers/avrc_fuzzer.cc4
-rw-r--r--system/stack/fuzzers/bnep_fuzzer.cc4
-rw-r--r--system/stack/fuzzers/gatt_fuzzer.cc5
-rw-r--r--system/stack/fuzzers/l2cap_fuzzer.cc3
-rw-r--r--system/stack/fuzzers/rfcomm_fuzzer.cc4
-rw-r--r--system/stack/fuzzers/sdp_fuzzer.cc4
-rw-r--r--system/stack/fuzzers/smp_fuzzer.cc5
-rw-r--r--system/stack/include/avrc_api.h4
-rw-r--r--system/stack/include/ble_appearance.h446
-rw-r--r--system/stack/include/bt_dev_class.h232
-rw-r--r--system/stack/include/btm_ble_api_types.h119
-rw-r--r--system/stack/include/smp_status.h28
-rw-r--r--system/stack/rfcomm/port_rfc.cc2
-rw-r--r--system/stack/rfcomm/rfc_port_fsm.cc30
-rw-r--r--system/stack/rfcomm/rfc_port_if.cc7
-rw-r--r--system/stack/smp/smp_act.cc17
-rw-r--r--system/stack/smp/smp_api.cc4
-rw-r--r--system/stack/smp/smp_utils.cc2
-rw-r--r--system/stack/test/stack_smp_test.cc6
-rw-r--r--system/test/headless/property.cc4
-rw-r--r--system/test/headless/property.h1
-rw-r--r--tools/rootcanal/hal/bluetooth_hci.cc18
-rw-r--r--tools/rootcanal/hal/bluetooth_hci.h20
-rw-r--r--tools/rootcanal/model/controller/controller_properties.cc34
-rw-r--r--tools/rootcanal/model/controller/dual_mode_controller.cc9
-rw-r--r--tools/rootcanal/model/controller/dual_mode_controller.h1
-rw-r--r--tools/rootcanal/model/setup/test_command_handler.cc2
-rw-r--r--tools/rootcanal/packets/hci_packets.pdl1
-rw-r--r--tools/rootcanal/proto/rootcanal/configuration.proto2
-rw-r--r--tools/rootcanal/rust/src/lmp/procedure/mod.rs4
-rwxr-xr-xtools/rootcanal/scripts/controller_info.py62
188 files changed, 11143 insertions, 2426 deletions
diff --git a/TEST_MAPPING b/TEST_MAPPING
index 5fc31de514..562f9717c8 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -79,6 +79,9 @@
"name": "bluetooth_le_audio_test"
},
{
+ "name": "bluetooth_ras_test"
+ },
+ {
"name": "bluetooth_packet_parser_test"
},
{
@@ -275,6 +278,9 @@
"name": "bluetooth_le_audio_test"
},
{
+ "name": "bluetooth_ras_test"
+ },
+ {
"name": "bluetooth_packet_parser_test"
},
{
diff --git a/android/app/aidl/android/bluetooth/IBluetoothA2dp.aidl b/android/app/aidl/android/bluetooth/IBluetoothA2dp.aidl
index bcfc95585d..fa5d1362ac 100644
--- a/android/app/aidl/android/bluetooth/IBluetoothA2dp.aidl
+++ b/android/app/aidl/android/bluetooth/IBluetoothA2dp.aidl
@@ -53,7 +53,7 @@ interface IBluetoothA2dp {
boolean isA2dpPlaying(in BluetoothDevice device, in AttributionSource attributionSource);
@JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)")
List<BluetoothCodecType> getSupportedCodecTypes();
- @JavaPassthrough(annotation="@android.annotation.RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_CONNECT,android.Manifest.permission.BLUETOOTH_PRIVILEGED})")
+ @JavaPassthrough(annotation="@android.annotation.RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_CONNECT,android.Manifest.permission.BLUETOOTH_PRIVILEGED}, conditional=true)")
BluetoothCodecStatus getCodecStatus(in BluetoothDevice device, in AttributionSource attributionSource);
@JavaPassthrough(annotation="@android.annotation.RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_CONNECT,android.Manifest.permission.BLUETOOTH_PRIVILEGED}, conditional=true)")
oneway void setCodecConfigPreference(in BluetoothDevice device, in BluetoothCodecConfig codecConfig, in AttributionSource attributionSource);
diff --git a/android/app/jni/com_android_bluetooth_btservice_AdapterService.cpp b/android/app/jni/com_android_bluetooth_btservice_AdapterService.cpp
index 805e22b0df..6df96370b1 100644
--- a/android/app/jni/com_android_bluetooth_btservice_AdapterService.cpp
+++ b/android/app/jni/com_android_bluetooth_btservice_AdapterService.cpp
@@ -2228,6 +2228,25 @@ static jboolean disconnectAllAclsNative(JNIEnv* /* env */, jobject /* obj */) {
return (ret == BT_STATUS_SUCCESS) ? JNI_TRUE : JNI_FALSE;
}
+static jboolean disconnectAclNative(JNIEnv* env, jobject /* obj */, jbyteArray address,
+ jint transport) {
+ log::verbose("");
+
+ if (!sBluetoothInterface) {
+ return JNI_FALSE;
+ }
+
+ jbyte* addr = env->GetByteArrayElements(address, nullptr);
+ if (addr == nullptr) {
+ jniThrowIOException(env, EINVAL);
+ return JNI_FALSE;
+ }
+ RawAddress addr_obj = {};
+ addr_obj.FromOctets(reinterpret_cast<uint8_t*>(addr));
+
+ return sBluetoothInterface->disconnect_acl(addr_obj, transport);
+}
+
static jboolean allowWakeByHidNative(JNIEnv* /* env */, jobject /* obj */) {
log::verbose("");
@@ -2320,6 +2339,7 @@ int register_com_android_bluetooth_btservice_AdapterService(JNIEnv* env) {
{"clearFilterAcceptListNative", "()Z",
reinterpret_cast<void*>(clearFilterAcceptListNative)},
{"disconnectAllAclsNative", "()Z", reinterpret_cast<void*>(disconnectAllAclsNative)},
+ {"disconnectAclNative", "([BI)Z", reinterpret_cast<void*>(disconnectAclNative)},
{"allowWakeByHidNative", "()Z", reinterpret_cast<void*>(allowWakeByHidNative)},
{"restoreFilterAcceptListNative", "()Z",
reinterpret_cast<void*>(restoreFilterAcceptListNative)},
diff --git a/android/app/res/values/config.xml b/android/app/res/values/config.xml
index 7418000e8e..0bb72b5ca8 100644
--- a/android/app/res/values/config.xml
+++ b/android/app/res/values/config.xml
@@ -138,12 +138,6 @@
<integer name="a2dp_source_codec_priority_lc3">6001</integer>
<integer name="a2dp_source_codec_priority_opus">7001</integer>
- <!-- For enabling the AVRCP Target Cover Artowrk feature-->
- <bool name="avrcp_target_enable_cover_art">true</bool>
-
- <!-- Enable support for URI based images. Off by default due to increased memory usage -->
- <bool name="avrcp_target_cover_art_uri_images">false</bool>
-
<!-- Package that is responsible for user interaction on pairing request,
success or cancel.
Receives:
diff --git a/android/app/src/com/android/bluetooth/a2dp/A2dpService.java b/android/app/src/com/android/bluetooth/a2dp/A2dpService.java
index 136a0d4621..100076127b 100644
--- a/android/app/src/com/android/bluetooth/a2dp/A2dpService.java
+++ b/android/app/src/com/android/bluetooth/a2dp/A2dpService.java
@@ -50,7 +50,6 @@ import android.media.BluetoothProfileConnectionInfo;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
-import android.os.HandlerThread;
import android.os.Looper;
import android.sysprop.BluetoothProperties;
import android.util.Log;
@@ -1515,12 +1514,14 @@ public class A2dpService extends ProfileService {
@Override
public BluetoothCodecStatus getCodecStatus(
BluetoothDevice device, AttributionSource source) {
+ requireNonNull(device);
A2dpService service = getServiceAndEnforceConnect(source);
if (service == null) {
return null;
}
- service.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null);
+ Utils.enforceCdmAssociationIfNotBluetoothPrivileged(
+ service, service.mCompanionDeviceManager, source, device);
return service.getCodecStatus(device);
}
@@ -1530,6 +1531,7 @@ public class A2dpService extends ProfileService {
BluetoothDevice device,
BluetoothCodecConfig codecConfig,
AttributionSource source) {
+ requireNonNull(device);
A2dpService service = getServiceAndEnforceConnect(source);
if (service == null) {
return;
diff --git a/android/app/src/com/android/bluetooth/audio_util/MediaPlayerList.java b/android/app/src/com/android/bluetooth/audio_util/MediaPlayerList.java
index b04f684f34..e062814766 100644
--- a/android/app/src/com/android/bluetooth/audio_util/MediaPlayerList.java
+++ b/android/app/src/com/android/bluetooth/audio_util/MediaPlayerList.java
@@ -784,6 +784,10 @@ public class MediaPlayerList {
}
}
+ public boolean isVfsCoverArtEnabled() {
+ return Util.areUriImagesSupported();
+ }
+
/**
* Adds a {@link MediaController} to the {@link #mMediaPlayers} map and returns its ID.
*
@@ -985,6 +989,19 @@ public class MediaPlayerList {
mActivePlayerId = playerId;
+ if (Utils.isPtsTestMode()) {
+ sendFolderUpdate(true, true, false);
+ } else if (Flags.setAddressedPlayer() && Flags.browsingRefactor()) {
+ // If the browsing refactor flag is not active, addressed player should always be 0.
+ // If the new active player has been set by Addressed player key event
+ // We don't send an addressed player update.
+ if (mActivePlayerId != mAddressedPlayerId) {
+ mAddressedPlayerId = mActivePlayerId;
+ Log.d(TAG, "setActivePlayer AddressedPlayer changed to " + mAddressedPlayerId);
+ sendFolderUpdate(false, true, false);
+ }
+ }
+
MediaPlayerWrapper player = getActivePlayer();
if (player == null) return;
@@ -1002,19 +1019,6 @@ public class MediaPlayerList {
return;
}
- if (Utils.isPtsTestMode()) {
- sendFolderUpdate(true, true, false);
- } else if (Flags.setAddressedPlayer() && Flags.browsingRefactor()) {
- // If the browsing refactor flag is not active, addressed player should always be 0.
- // If the new active player has been set by Addressed player key event
- // We don't send an addressed player update.
- if (mActivePlayerId != mAddressedPlayerId) {
- mAddressedPlayerId = mActivePlayerId;
- Log.d(TAG, "setActivePlayer AddressedPlayer changed to " + mAddressedPlayerId);
- sendFolderUpdate(false, true, false);
- }
- }
-
MediaData data = player.getCurrentMediaData();
if (mAudioPlaybackIsActive) {
data.state = mCurrMediaData.state;
diff --git a/android/app/src/com/android/bluetooth/audio_util/helpers/Image.java b/android/app/src/com/android/bluetooth/audio_util/helpers/Image.java
index fc106e9a95..d89a6afddf 100644
--- a/android/app/src/com/android/bluetooth/audio_util/helpers/Image.java
+++ b/android/app/src/com/android/bluetooth/audio_util/helpers/Image.java
@@ -60,7 +60,7 @@ public class Image {
Bitmap bmp_album_art = metadata.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART);
Bitmap bmp_icon = metadata.getBitmap(MediaMetadata.METADATA_KEY_DISPLAY_ICON);
- if (mContext != null && Util.areUriImagesSupported(mContext)) {
+ if (Util.areUriImagesSupported()) {
uri_art = metadata.getString(MediaMetadata.METADATA_KEY_ART_URI);
uri_album_art = metadata.getString(MediaMetadata.METADATA_KEY_ALBUM_ART_URI);
uri_icon = metadata.getString(MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI);
@@ -92,7 +92,7 @@ public class Image {
Bitmap bmp_album_art = bundle.getParcelable(MediaMetadata.METADATA_KEY_ALBUM_ART);
Bitmap bmp_icon = bundle.getParcelable(MediaMetadata.METADATA_KEY_DISPLAY_ICON);
- if (mContext != null && Util.areUriImagesSupported(mContext)) {
+ if (Util.areUriImagesSupported()) {
uri_art = bundle.getString(MediaMetadata.METADATA_KEY_ART_URI);
uri_album_art = bundle.getString(MediaMetadata.METADATA_KEY_ALBUM_ART_URI);
uri_icon = bundle.getString(MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI);
diff --git a/android/app/src/com/android/bluetooth/audio_util/helpers/Metadata.java b/android/app/src/com/android/bluetooth/audio_util/helpers/Metadata.java
index 02ca4fbd5c..63791acb29 100644
--- a/android/app/src/com/android/bluetooth/audio_util/helpers/Metadata.java
+++ b/android/app/src/com/android/bluetooth/audio_util/helpers/Metadata.java
@@ -23,8 +23,6 @@ import android.media.browse.MediaBrowser.MediaItem;
import android.media.session.MediaSession;
import android.os.Bundle;
-import com.android.bluetooth.R;
-
import java.util.Objects;
public class Metadata implements Cloneable {
@@ -201,7 +199,7 @@ public class Metadata implements Cloneable {
mMetadata.duration = "" + data.getLong(MediaMetadata.METADATA_KEY_DURATION);
}
if ((mContext != null
- && Util.areUriImagesSupported(mContext)
+ && Util.areUriImagesSupported()
&& (data.containsKey(MediaMetadata.METADATA_KEY_ART_URI)
|| data.containsKey(MediaMetadata.METADATA_KEY_ALBUM_ART_URI)
|| data.containsKey(
@@ -233,7 +231,7 @@ public class Metadata implements Cloneable {
if (desc.getIconBitmap() != null) {
mMetadata.image = new Image(mContext, desc.getIconBitmap());
} else if (mContext != null
- && Util.areUriImagesSupported(mContext)
+ && Util.areUriImagesSupported()
&& desc.getIconUri() != null) {
mMetadata.image = new Image(mContext, desc.getIconUri());
}
@@ -281,7 +279,7 @@ public class Metadata implements Cloneable {
mMetadata.duration = "" + bundle.getLong(MediaMetadata.METADATA_KEY_DURATION);
}
if ((mContext != null
- && Util.areUriImagesSupported(mContext)
+ && Util.areUriImagesSupported()
&& (bundle.containsKey(MediaMetadata.METADATA_KEY_ART_URI)
|| bundle.containsKey(MediaMetadata.METADATA_KEY_ALBUM_ART_URI)
|| bundle.containsKey(
@@ -296,13 +294,8 @@ public class Metadata implements Cloneable {
/** Elect to use default values in the Metadata in place of any missing values */
public Builder useDefaults() {
- if (mMetadata.mediaId == null) {
- mMetadata.mediaId = EMPTY_MEDIA_ID;
- }
- if (mMetadata.title == null) {
- mMetadata.title =
- mContext != null ? mContext.getString(R.string.not_provided) : EMPTY_TITLE;
- }
+ if (mMetadata.mediaId == null) mMetadata.mediaId = EMPTY_MEDIA_ID;
+ if (mMetadata.title == null) mMetadata.title = EMPTY_TITLE;
if (mMetadata.artist == null) mMetadata.artist = EMPTY_ARTIST;
if (mMetadata.album == null) mMetadata.album = EMPTY_ALBUM;
if (mMetadata.trackNum == null) mMetadata.trackNum = EMPTY_TRACK_NUM;
diff --git a/android/app/src/com/android/bluetooth/audio_util/helpers/Util.java b/android/app/src/com/android/bluetooth/audio_util/helpers/Util.java
index 32597edc75..877a315c88 100644
--- a/android/app/src/com/android/bluetooth/audio_util/helpers/Util.java
+++ b/android/app/src/com/android/bluetooth/audio_util/helpers/Util.java
@@ -21,9 +21,10 @@ import android.content.pm.PackageManager;
import android.media.MediaMetadata;
import android.media.browse.MediaBrowser.MediaItem;
import android.media.session.MediaSession;
+import android.os.SystemProperties;
import android.util.Log;
-import com.android.bluetooth.R;
+import androidx.annotation.VisibleForTesting;
import java.util.ArrayList;
import java.util.List;
@@ -31,6 +32,13 @@ import java.util.List;
class Util {
public static String TAG = "audio_util.Util";
+ private static final String VFS_COVER_ART_ENABLED_PROPERTY =
+ "bluetooth.profile.avrcp.target.vfs_coverart.enabled";
+
+ @VisibleForTesting
+ static Boolean sUriImagesSupport =
+ SystemProperties.getBoolean(VFS_COVER_ART_ENABLED_PROPERTY, false);
+
// TODO (apanicke): Remove this prefix later, for now it makes debugging easier.
public static final String NOW_PLAYING_PREFIX = "NowPlayingId";
@@ -49,13 +57,12 @@ class Util {
}
/**
- * Get whether or not Bluetooth is configured to support URI images or not.
+ * Get whether or not Bluetooth is configured to support URI images.
*
* <p>Note that creating URI images will dramatically increase memory usage.
*/
- public static boolean areUriImagesSupported(Context context) {
- if (context == null) return false;
- return context.getResources().getBoolean(R.bool.avrcp_target_cover_art_uri_images);
+ public static boolean areUriImagesSupported() {
+ return sUriImagesSupport.booleanValue();
}
/** Translate a MediaItem to audio_util's Metadata */
diff --git a/android/app/src/com/android/bluetooth/avrcp/AvrcpCoverArtService.java b/android/app/src/com/android/bluetooth/avrcp/AvrcpCoverArtService.java
index 26d7d3e48d..46805f404c 100644
--- a/android/app/src/com/android/bluetooth/avrcp/AvrcpCoverArtService.java
+++ b/android/app/src/com/android/bluetooth/avrcp/AvrcpCoverArtService.java
@@ -62,8 +62,9 @@ public class AvrcpCoverArtService {
// Native interface
private AvrcpNativeInterface mNativeInterface;
- public AvrcpCoverArtService() {
- mNativeInterface = AvrcpNativeInterface.getInstance();
+ // The native interface must be a parameter here in order to be able to mock AvrcpTargetService
+ public AvrcpCoverArtService(AvrcpNativeInterface nativeInterface) {
+ mNativeInterface = nativeInterface;
mAcceptThread = new SocketAcceptor();
mStorage = new AvrcpCoverArtStorage(COVER_ART_STORAGE_MAX_ITEMS);
}
diff --git a/android/app/src/com/android/bluetooth/avrcp/AvrcpTargetService.java b/android/app/src/com/android/bluetooth/avrcp/AvrcpTargetService.java
index b6aa7cf8b6..068db0f024 100644
--- a/android/app/src/com/android/bluetooth/avrcp/AvrcpTargetService.java
+++ b/android/app/src/com/android/bluetooth/avrcp/AvrcpTargetService.java
@@ -36,8 +36,8 @@ import android.view.KeyEvent;
import com.android.bluetooth.BluetoothEventLogger;
import com.android.bluetooth.BluetoothMetricsProto;
-import com.android.bluetooth.R;
import com.android.bluetooth.a2dp.A2dpService;
+import com.android.bluetooth.audio_util.ListItem;
import com.android.bluetooth.audio_util.MediaData;
import com.android.bluetooth.audio_util.MediaPlayerList;
import com.android.bluetooth.audio_util.MediaPlayerWrapper;
@@ -98,6 +98,8 @@ public class AvrcpTargetService extends ProfileService {
private static AvrcpTargetService sInstance = null;
+ private final boolean mIsVfsCoverArtEnabled;
+
public AvrcpTargetService(AdapterService adapterService) {
this(
requireNonNull(adapterService),
@@ -142,13 +144,11 @@ public class AvrcpTargetService extends ProfileService {
mMediaPlayerList.init(new ListCallback());
}
- if (!getResources().getBoolean(R.bool.avrcp_target_enable_cover_art)) {
- mAvrcpCoverArtService = null;
- } else if (!mAvrcpVersion.isAtleastVersion(AvrcpVersion.AVRCP_VERSION_1_6)) {
+ if (!mAvrcpVersion.isAtleastVersion(AvrcpVersion.AVRCP_VERSION_1_6)) {
Log.e(TAG, "Please use AVRCP version 1.6 to enable cover art");
mAvrcpCoverArtService = null;
} else {
- AvrcpCoverArtService coverArtService = new AvrcpCoverArtService();
+ AvrcpCoverArtService coverArtService = new AvrcpCoverArtService(mNativeInterface);
if (coverArtService.start()) {
mAvrcpCoverArtService = coverArtService;
} else {
@@ -157,6 +157,8 @@ public class AvrcpTargetService extends ProfileService {
}
}
+ mIsVfsCoverArtEnabled = mMediaPlayerList.isVfsCoverArtEnabled();
+
mReceiver = new AvrcpBroadcastReceiver();
IntentFilter filter = new IntentFilter();
filter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
@@ -399,8 +401,7 @@ public class AvrcpTargetService extends ProfileService {
Metadata getCurrentSongInfo() {
Metadata metadata = mMediaPlayerList.getCurrentSongInfo();
if (mAvrcpCoverArtService != null && metadata.image != null) {
- String imageHandle = mAvrcpCoverArtService.storeImage(metadata.image);
- if (imageHandle != null) metadata.image.setImageHandle(imageHandle);
+ metadata.image.setImageHandle(mAvrcpCoverArtService.storeImage(metadata.image));
}
return metadata;
}
@@ -433,26 +434,20 @@ public class AvrcpTargetService extends ProfileService {
List<Metadata> getNowPlayingList() {
String currentMediaId = getCurrentMediaId();
Metadata currentTrack = null;
- String imageHandle = null;
List<Metadata> nowPlayingList = mMediaPlayerList.getNowPlayingList();
if (mAvrcpCoverArtService != null) {
for (Metadata metadata : nowPlayingList) {
if (TextUtils.equals(metadata.mediaId, currentMediaId)) {
currentTrack = metadata;
} else if (metadata.image != null) {
- imageHandle = mAvrcpCoverArtService.storeImage(metadata.image);
- if (imageHandle != null) {
- metadata.image.setImageHandle(imageHandle);
- }
+ metadata.image.setImageHandle(mAvrcpCoverArtService.storeImage(metadata.image));
}
}
// Always store the current item from the queue last so we know the image is in storage
if (currentTrack != null) {
- imageHandle = mAvrcpCoverArtService.storeImage(currentTrack.image);
- if (imageHandle != null) {
- currentTrack.image.setImageHandle(imageHandle);
- }
+ currentTrack.image.setImageHandle(
+ mAvrcpCoverArtService.storeImage(currentTrack.image));
}
}
return nowPlayingList;
@@ -491,7 +486,20 @@ public class AvrcpTargetService extends ProfileService {
/** See {@link MediaPlayerList#getFolderItems}. */
void getFolderItems(int playerId, String mediaId, MediaPlayerList.GetFolderItemsCallback cb) {
- mMediaPlayerList.getFolderItems(playerId, mediaId, cb);
+ mMediaPlayerList.getFolderItems(
+ playerId,
+ mediaId,
+ (id, results) -> {
+ if (mIsVfsCoverArtEnabled && mAvrcpCoverArtService != null) {
+ for (ListItem item : results) {
+ if (item != null && item.song != null && item.song.image != null) {
+ item.song.image.setImageHandle(
+ mAvrcpCoverArtService.storeImage(item.song.image));
+ }
+ }
+ }
+ cb.run(id, results);
+ });
}
/** See {@link MediaPlayerList#playItem}. */
diff --git a/android/app/src/com/android/bluetooth/avrcp/helpers/AvrcpVersion.java b/android/app/src/com/android/bluetooth/avrcp/helpers/AvrcpVersion.java
index a3ab9192e9..c475db1ef1 100644
--- a/android/app/src/com/android/bluetooth/avrcp/helpers/AvrcpVersion.java
+++ b/android/app/src/com/android/bluetooth/avrcp/helpers/AvrcpVersion.java
@@ -18,6 +18,8 @@ package com.android.bluetooth.avrcp;
import android.os.SystemProperties;
+import com.android.bluetooth.flags.Flags;
+
/** A class to represent an AVRCP version */
final class AvrcpVersion {
public static final AvrcpVersion AVRCP_VERSION_1_3 = new AvrcpVersion(1, 3);
@@ -36,8 +38,12 @@ final class AvrcpVersion {
public int minor;
public static AvrcpVersion getCurrentSystemPropertiesValue() {
- // Make sure this default version agrees with avrc_api.h's "AVRC_DEFAULT_VERSION"
- String version = SystemProperties.get(AVRCP_VERSION_PROPERTY, AVRCP_VERSION_1_5_STRING);
+ // Make sure this default version agrees with AVRC_GetProfileVersion
+
+ String defaultVersion =
+ Flags.avrcp16Default() ? AVRCP_VERSION_1_6_STRING : AVRCP_VERSION_1_5_STRING;
+ String version = SystemProperties.get(AVRCP_VERSION_PROPERTY, defaultVersion);
+
switch (version) {
case AVRCP_VERSION_1_3_STRING:
return AVRCP_VERSION_1_3;
diff --git a/android/app/src/com/android/bluetooth/avrcp/helpers/CoverArt.java b/android/app/src/com/android/bluetooth/avrcp/helpers/CoverArt.java
index 95388baf78..dd720077a0 100644
--- a/android/app/src/com/android/bluetooth/avrcp/helpers/CoverArt.java
+++ b/android/app/src/com/android/bluetooth/avrcp/helpers/CoverArt.java
@@ -40,7 +40,12 @@ import java.security.NoSuchAlgorithmException;
*/
public class CoverArt {
private static final String TAG = CoverArt.class.getSimpleName();
- private static final BipPixel PIXEL_THUMBNAIL = BipPixel.createFixed(200, 200);
+
+ // The size in pixels of the thumbnail sides.
+ private static final int THUMBNAIL_SIZE = 200;
+
+ private static final BipPixel PIXEL_THUMBNAIL =
+ BipPixel.createFixed(THUMBNAIL_SIZE, THUMBNAIL_SIZE);
private String mImageHandle = null;
private Bitmap mImage = null;
@@ -50,7 +55,7 @@ public class CoverArt {
// Create a scaled version of the image for now, as consumers don't need
// anything larger than this at the moment. Also makes each image gathered
// the same dimensions for hashing purposes.
- mImage = Bitmap.createScaledBitmap(image.getImage(), 200, 200, false);
+ mImage = Bitmap.createScaledBitmap(image.getImage(), THUMBNAIL_SIZE, THUMBNAIL_SIZE, false);
}
/**
@@ -133,13 +138,15 @@ public class CoverArt {
BipEncoding encoding = descriptor.getEncoding();
BipPixel pixel = descriptor.getPixel();
- if (encoding.getType() == BipEncoding.JPEG && PIXEL_THUMBNAIL.equals(pixel)) {
+ int encodingType = encoding.getType();
+ if ((encodingType == BipEncoding.JPEG || encodingType == BipEncoding.PNG)
+ && PIXEL_THUMBNAIL.equals(pixel)) {
return true;
}
return false;
}
- /** Get the cover artwork image bytes as a 200 x 200 JPEG thumbnail */
+ /** Get the cover artwork image bytes as a THUMBNAIL_SIZE x THUMBNAIL_SIZE JPEG thumbnail */
public byte[] getThumbnail() {
debug("GetImageThumbnail()");
if (mImage == null) return null;
@@ -160,12 +167,19 @@ public class CoverArt {
return null;
}
BipImageProperties.Builder builder = new BipImageProperties.Builder();
- BipEncoding encoding = new BipEncoding(BipEncoding.JPEG);
- BipPixel pixel = BipPixel.createFixed(200, 200);
- BipImageFormat format = BipImageFormat.createNative(encoding, pixel, -1);
+
+ BipEncoding jpgEncoding = new BipEncoding(BipEncoding.JPEG);
+ BipEncoding pngEncoding = new BipEncoding(BipEncoding.PNG);
+ BipPixel jpgPixel = BipPixel.createFixed(THUMBNAIL_SIZE, THUMBNAIL_SIZE);
+ BipPixel pngPixel = BipPixel.createFixed(THUMBNAIL_SIZE, THUMBNAIL_SIZE);
+
+ BipImageFormat jpgNativeFormat = BipImageFormat.createNative(jpgEncoding, jpgPixel, -1);
+ BipImageFormat pngVariantFormat =
+ BipImageFormat.createVariant(pngEncoding, pngPixel, THUMBNAIL_SIZE, null);
builder.setImageHandle(mImageHandle);
- builder.addNativeFormat(format);
+ builder.addNativeFormat(jpgNativeFormat);
+ builder.addVariantFormat(pngVariantFormat);
BipImageProperties properties = builder.build();
return properties;
diff --git a/android/app/src/com/android/bluetooth/bass_client/BassClientService.java b/android/app/src/com/android/bluetooth/bass_client/BassClientService.java
index 53657b16a6..0daef0175a 100644
--- a/android/app/src/com/android/bluetooth/bass_client/BassClientService.java
+++ b/android/app/src/com/android/bluetooth/bass_client/BassClientService.java
@@ -2499,6 +2499,28 @@ public class BassClientService extends ProfileService {
BluetoothDevice srcDevice = getDeviceForSyncHandle(syncHandle);
mSyncHandleToDeviceMap.remove(syncHandle);
int broadcastId = getBroadcastIdForSyncHandle(syncHandle);
+ if (leaudioMonitorUnicastSourceWhenManagedByBroadcastDelegator()) {
+ synchronized (mPendingSourcesToAdd) {
+ Iterator<AddSourceData> iterator = mPendingSourcesToAdd.iterator();
+ while (iterator.hasNext()) {
+ AddSourceData pendingSourcesToAdd = iterator.next();
+ if (pendingSourcesToAdd.mSourceMetadata.getBroadcastId() == broadcastId) {
+ iterator.remove();
+ }
+ }
+ }
+ synchronized (mSinksWaitingForPast) {
+ Iterator<Map.Entry<BluetoothDevice, Pair<Integer, Integer>>> iterator =
+ mSinksWaitingForPast.entrySet().iterator();
+ while (iterator.hasNext()) {
+ Map.Entry<BluetoothDevice, Pair<Integer, Integer>> entry = iterator.next();
+ int broadcastIdForPast = entry.getValue().first;
+ if (broadcastId == broadcastIdForPast) {
+ iterator.remove();
+ }
+ }
+ }
+ }
mSyncHandleToBroadcastIdMap.remove(syncHandle);
if (srcDevice != null) {
mPeriodicAdvertisementResultMap.get(srcDevice).remove(broadcastId);
@@ -4054,7 +4076,10 @@ public class BassClientService extends ProfileService {
mUnicastSourceStreamStatus = Optional.of(status);
if (status == STATUS_LOCAL_STREAM_REQUESTED) {
- if (areReceiversReceivingOnlyExternalBroadcast(getConnectedDevices())) {
+ if ((leaudioMonitorUnicastSourceWhenManagedByBroadcastDelegator()
+ && hasPrimaryDeviceManagedExternalBroadcast())
+ || (!leaudioMonitorUnicastSourceWhenManagedByBroadcastDelegator()
+ && areReceiversReceivingOnlyExternalBroadcast(getConnectedDevices()))) {
if (leaudioBroadcastAssistantPeripheralEntrustment()) {
cacheSuspendingSources(BassConstants.INVALID_BROADCAST_ID);
List<Pair<BluetoothLeBroadcastReceiveState, BluetoothDevice>> sourcesToStop =
@@ -4067,11 +4092,13 @@ public class BassClientService extends ProfileService {
suspendAllReceiversSourceSynchronization();
}
}
- for (Map.Entry<Integer, PauseType> entry : mPausedBroadcastIds.entrySet()) {
- Integer broadcastId = entry.getKey();
- PauseType pauseType = entry.getValue();
- if (pauseType != PauseType.HOST_INTENTIONAL) {
- suspendReceiversSourceSynchronization(broadcastId);
+ if (!leaudioMonitorUnicastSourceWhenManagedByBroadcastDelegator()) {
+ for (Map.Entry<Integer, PauseType> entry : mPausedBroadcastIds.entrySet()) {
+ Integer broadcastId = entry.getKey();
+ PauseType pauseType = entry.getValue();
+ if (pauseType != PauseType.HOST_INTENTIONAL) {
+ suspendReceiversSourceSynchronization(broadcastId);
+ }
}
}
} else if (status == STATUS_LOCAL_STREAM_SUSPENDED) {
@@ -4235,6 +4262,16 @@ public class BassClientService extends ProfileService {
return activeSinks;
}
+ /** Get sink devices synced to the broadcasts by broadcast id */
+ public List<BluetoothDevice> getSyncedBroadcastSinks(int broadcastId) {
+ return getConnectedDevices().stream()
+ .filter(
+ device ->
+ getAllSources(device).stream()
+ .anyMatch(rs -> rs.getBroadcastId() == broadcastId))
+ .toList();
+ }
+
private boolean isSyncedToBroadcastStream(Long syncState) {
return syncState != BassConstants.BCAST_RCVR_STATE_BIS_SYNC_NOT_SYNC_TO_BIS
&& syncState != BassConstants.BCAST_RCVR_STATE_BIS_SYNC_FAILED_SYNC_TO_BIG;
diff --git a/android/app/src/com/android/bluetooth/btservice/AbstractionLayer.java b/android/app/src/com/android/bluetooth/btservice/AbstractionLayer.java
index 981b2db0c7..9ebe10d181 100644
--- a/android/app/src/com/android/bluetooth/btservice/AbstractionLayer.java
+++ b/android/app/src/com/android/bluetooth/btservice/AbstractionLayer.java
@@ -50,6 +50,7 @@ public final class AbstractionLayer {
static final int BT_PROPERTY_REMOTE_ASHA_TRUNCATED_HISYNCID = 0X16;
static final int BT_PROPERTY_REMOTE_MODEL_NUM = 0x17;
static final int BT_PROPERTY_LPP_OFFLOAD_FEATURES = 0x1B;
+ static final int BT_PROPERTY_UUIDS_LE = 0x1C;
public static final int BT_DEVICE_TYPE_BREDR = 0x01;
public static final int BT_DEVICE_TYPE_BLE = 0x02;
diff --git a/android/app/src/com/android/bluetooth/btservice/AdapterNativeInterface.java b/android/app/src/com/android/bluetooth/btservice/AdapterNativeInterface.java
index 64b5a0dfae..244e132a66 100644
--- a/android/app/src/com/android/bluetooth/btservice/AdapterNativeInterface.java
+++ b/android/app/src/com/android/bluetooth/btservice/AdapterNativeInterface.java
@@ -316,6 +316,14 @@ public class AdapterNativeInterface {
return disconnectAllAclsNative();
}
+ boolean disconnectAllAcls(BluetoothDevice device) {
+ return disconnectAcl(device, BluetoothDevice.TRANSPORT_AUTO);
+ }
+
+ boolean disconnectAcl(BluetoothDevice device, int transport) {
+ return disconnectAclNative(Utils.getBytesFromAddress(device.getAddress()), transport);
+ }
+
boolean allowWakeByHid() {
return allowWakeByHidNative();
}
@@ -463,6 +471,8 @@ public class AdapterNativeInterface {
private native boolean disconnectAllAclsNative();
+ private native boolean disconnectAclNative(byte[] address, int transport);
+
private native boolean allowWakeByHidNative();
private native boolean restoreFilterAcceptListNative();
diff --git a/android/app/src/com/android/bluetooth/btservice/AdapterService.java b/android/app/src/com/android/bluetooth/btservice/AdapterService.java
index fe9cca3409..cba0af9ae6 100644
--- a/android/app/src/com/android/bluetooth/btservice/AdapterService.java
+++ b/android/app/src/com/android/bluetooth/btservice/AdapterService.java
@@ -28,6 +28,7 @@ import static android.bluetooth.BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERA
import static android.bluetooth.BluetoothAdapter.SCAN_MODE_NONE;
import static android.bluetooth.BluetoothDevice.BATTERY_LEVEL_UNKNOWN;
import static android.bluetooth.BluetoothDevice.TRANSPORT_AUTO;
+import static android.bluetooth.BluetoothProfile.getProfileName;
import static android.bluetooth.IBluetoothLeAudio.LE_AUDIO_GROUP_ID_INVALID;
import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
import static android.text.format.DateUtils.SECOND_IN_MILLIS;
@@ -59,6 +60,7 @@ import android.bluetooth.BluetoothAdapter.ActiveDeviceUse;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothDevice.BluetoothAddress;
import android.bluetooth.BluetoothFrameworkInitializer;
+import android.bluetooth.BluetoothLeAudio;
import android.bluetooth.BluetoothMap;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothProtoEnums;
@@ -116,6 +118,7 @@ import android.sysprop.BluetoothProperties;
import android.text.TextUtils;
import android.util.Base64;
import android.util.Log;
+import android.util.Pair;
import android.util.SparseArray;
import com.android.bluetooth.BluetoothMetricsProto;
@@ -181,6 +184,7 @@ import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.time.Duration;
+import java.time.Instant;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
@@ -290,6 +294,11 @@ public class AdapterService extends Service {
private long mEnergyUsedTotalVoltAmpSecMicro;
private HashSet<String> mLeAudioAllowDevices = new HashSet<>();
+ /* List of pairs of gatt clients which controls AutoActiveMode on the device.*/
+ @VisibleForTesting
+ final List<Pair<Integer, BluetoothDevice>> mLeGattClientsControllingAutoActiveMode =
+ new ArrayList<>();
+
private BluetoothAdapter mAdapter;
@VisibleForTesting AdapterProperties mAdapterProperties;
private AdapterState mAdapterStateMachine;
@@ -1546,46 +1555,45 @@ public class AdapterService extends Service {
@VisibleForTesting
void setProfileServiceState(int profileId, int state) {
+ Instant start = Instant.now();
+ String logHdr = "setProfileServiceState(" + getProfileName(profileId) + ", " + state + "):";
+
if (state == BluetoothAdapter.STATE_ON) {
- if (!mStartedProfiles.containsKey(profileId)) {
- ProfileService profileService = PROFILE_CONSTRUCTORS.get(profileId).apply(this);
- mStartedProfiles.put(profileId, profileService);
- addProfile(profileService);
- profileService.start();
- profileService.setAvailable(true);
- // With `Flags.scanManagerRefactor()` GattService initialization is pushed back to
- // `ON` state instead of `BLE_ON`. Here we ensure mGattService is set prior
- // to other Profiles using it.
- if (profileId == BluetoothProfile.GATT && Flags.scanManagerRefactor()) {
- mGattService = GattService.getGattService();
- }
- onProfileServiceStateChanged(profileService, BluetoothAdapter.STATE_ON);
- } else {
- Log.e(
- TAG,
- "setProfileServiceState("
- + BluetoothProfile.getProfileName(profileId)
- + ", STATE_ON): profile is already started");
+ if (mStartedProfiles.containsKey(profileId)) {
+ Log.wtf(TAG, logHdr + " profile is already started");
+ return;
}
+ Log.d(TAG, logHdr + " starting profile");
+ ProfileService profileService = PROFILE_CONSTRUCTORS.get(profileId).apply(this);
+ mStartedProfiles.put(profileId, profileService);
+ addProfile(profileService);
+ profileService.start();
+ profileService.setAvailable(true);
+ // With `Flags.scanManagerRefactor()` GattService initialization is pushed back to
+ // `ON` state instead of `BLE_ON`. Here we ensure mGattService is set prior
+ // to other Profiles using it.
+ if (profileId == BluetoothProfile.GATT && Flags.scanManagerRefactor()) {
+ mGattService = GattService.getGattService();
+ }
+ onProfileServiceStateChanged(profileService, BluetoothAdapter.STATE_ON);
} else if (state == BluetoothAdapter.STATE_OFF) {
ProfileService profileService = mStartedProfiles.remove(profileId);
- if (profileService != null) {
- profileService.setAvailable(false);
- onProfileServiceStateChanged(profileService, BluetoothAdapter.STATE_OFF);
- profileService.stop();
- removeProfile(profileService);
- profileService.cleanup();
- if (profileService.getBinder() != null) {
- profileService.getBinder().cleanup();
- }
- } else {
- Log.e(
- TAG,
- "setProfileServiceState("
- + BluetoothProfile.getProfileName(profileId)
- + ", STATE_OFF): profile is already stopped");
+ if (profileService == null) {
+ Log.wtf(TAG, logHdr + " profile is already stopped");
+ return;
+ }
+ Log.d(TAG, logHdr + " stopping profile");
+ profileService.setAvailable(false);
+ onProfileServiceStateChanged(profileService, BluetoothAdapter.STATE_OFF);
+ profileService.stop();
+ removeProfile(profileService);
+ profileService.cleanup();
+ if (profileService.getBinder() != null) {
+ profileService.getBinder().cleanup();
}
}
+ Instant end = Instant.now();
+ Log.d(TAG, logHdr + " completed in " + Duration.between(start, end).toMillis() + "ms");
}
private void setAllProfileServiceStates(int[] profileIds, int state) {
@@ -5153,6 +5161,192 @@ public class AdapterService extends Service {
return getConnectionState(device) != BluetoothDevice.CONNECTION_STATE_DISCONNECTED;
}
+ private void addGattClientToControlAutoActiveMode(int clientIf, BluetoothDevice device) {
+ if (!Flags.allowGattConnectFromTheAppsWithoutMakingLeaudioDeviceActive()) {
+ Log.i(
+ TAG,
+ "flag: allowGattConnectFromTheAppsWithoutMakingLeaudioDeviceActive is not"
+ + " enabled");
+ return;
+ }
+
+ /* When GATT client is connecting to LeAudio device, stack should not assume that
+ * LeAudio device should be automatically connected to Audio Framework.
+ * e.g. given LeAudio device might be busy with audio streaming from another device.
+ * LeAudio shall be automatically connected to Audio Framework when
+ * 1. Remote device expects that - Targeted Announcements are used
+ * 2. User is connecting device from Settings application.
+ *
+ * Above conditions are tracked by LeAudioService. In here, there is need to notify
+ * LeAudioService that connection is made for GATT purposes, so LeAudioService can
+ * disable AutoActiveMode and make sure to not make device Active just after connection
+ * is created.
+ *
+ * Note: AutoActiveMode is by default set to true and it means that LeAudio device is ready
+ * to streaming just after connection is created. That implies that device will be connected
+ * to Audio Framework (is made Active) when connection is created.
+ */
+
+ int groupId = mLeAudioService.getGroupId(device);
+ if (groupId == BluetoothLeAudio.GROUP_ID_INVALID) {
+ /* If this is not a LeAudio device, there is nothing to do here. */
+ return;
+ }
+
+ if (mLeAudioService.getConnectionPolicy(device)
+ != BluetoothProfile.CONNECTION_POLICY_ALLOWED) {
+ Log.d(
+ TAG,
+ "addGattClientToControlAutoActiveMode: "
+ + device
+ + " LeAudio connection policy is not allowed");
+ return;
+ }
+
+ Log.i(
+ TAG,
+ "addGattClientToControlAutoActiveMode: clientIf: "
+ + clientIf
+ + ", "
+ + device
+ + ", groupId: "
+ + groupId);
+
+ synchronized (mLeGattClientsControllingAutoActiveMode) {
+ Pair newPair = new Pair<>(clientIf, device);
+ if (mLeGattClientsControllingAutoActiveMode.contains(newPair)) {
+ return;
+ }
+
+ for (Pair<Integer, BluetoothDevice> pair : mLeGattClientsControllingAutoActiveMode) {
+ if (pair.second.equals(device)
+ || groupId == mLeAudioService.getGroupId(pair.second)) {
+ Log.i(TAG, "addGattClientToControlAutoActiveMode: adding new client");
+ mLeGattClientsControllingAutoActiveMode.add(newPair);
+ return;
+ }
+ }
+
+ if (mLeAudioService.setAutoActiveModeState(mLeAudioService.getGroupId(device), false)) {
+ Log.i(
+ TAG,
+ "addGattClientToControlAutoActiveMode: adding new client and notifying"
+ + " leAudioService");
+ mLeGattClientsControllingAutoActiveMode.add(newPair);
+ }
+ }
+ }
+
+ /**
+ * When this is called, AdapterService is aware of user doing GATT connection over LE. Adapter
+ * service will use this information to manage internal GATT services if needed. For now,
+ * AdapterService is using this information to control Auto Active Mode for LeAudio devices.
+ *
+ * @param clientIf clientIf ClientIf which was doing GATT connection attempt
+ * @param device device Remote device to connect
+ */
+ public void notifyDirectLeGattClientConnect(int clientIf, BluetoothDevice device) {
+ if (mLeAudioService != null) {
+ addGattClientToControlAutoActiveMode(clientIf, device);
+ }
+ }
+
+ private void removeGattClientFromControlAutoActiveMode(int clientIf, BluetoothDevice device) {
+ if (mLeGattClientsControllingAutoActiveMode.isEmpty()) {
+ return;
+ }
+
+ int groupId = mLeAudioService.getGroupId(device);
+ if (groupId == BluetoothLeAudio.GROUP_ID_INVALID) {
+ /* If this is not a LeAudio device, there is nothing to do here. */
+ return;
+ }
+
+ /* Remember if auto active mode is still disabled.
+ * If it is disabled, it means, that either User or remote device did not make an
+ * action to make LeAudio device Active.
+ * That means, AdapterService should disconnect ACL when all the clients are disconnected
+ * from the group to which the device belongs.
+ */
+ boolean isAutoActiveModeDisabled = !mLeAudioService.isAutoActiveModeEnabled(groupId);
+
+ synchronized (mLeGattClientsControllingAutoActiveMode) {
+ Log.d(
+ TAG,
+ "removeGattClientFromControlAutoActiveMode: removing clientIf:"
+ + clientIf
+ + ", "
+ + device
+ + ", groupId: "
+ + groupId);
+
+ mLeGattClientsControllingAutoActiveMode.remove(new Pair<>(clientIf, device));
+
+ if (!mLeGattClientsControllingAutoActiveMode.isEmpty()) {
+ for (Pair<Integer, BluetoothDevice> pair :
+ mLeGattClientsControllingAutoActiveMode) {
+ if (pair.second.equals(device)
+ || groupId == mLeAudioService.getGroupId(pair.second)) {
+ Log.d(
+ TAG,
+ "removeGattClientFromControlAutoActiveMode:"
+ + device
+ + " or groupId: "
+ + groupId
+ + " is still in use by clientif: "
+ + pair.first);
+ return;
+ }
+ }
+ }
+
+ /* Back auto active mode to default. */
+ mLeAudioService.setAutoActiveModeState(groupId, true);
+ }
+
+ int leConnectedState =
+ BluetoothDevice.CONNECTION_STATE_ENCRYPTED_LE
+ | BluetoothDevice.CONNECTION_STATE_CONNECTED;
+
+ /* If auto active mode was disabled for the given group and is still connected
+ * make sure to disconnected all the devices from the group
+ */
+ if (isAutoActiveModeDisabled && ((getConnectionState(device) & leConnectedState) != 0)) {
+ for (BluetoothDevice dev : mLeAudioService.getGroupDevices(groupId)) {
+ /* Need to disconnect all the devices from the group as those might be connected
+ * as well especially those which migh keep the connection
+ */
+ if ((getConnectionState(dev) & leConnectedState) != 0) {
+ mNativeInterface.disconnectAcl(dev, BluetoothDevice.TRANSPORT_LE);
+ }
+ }
+ }
+ }
+
+ /**
+ * Notify AdapterService about failed GATT connection attempt.
+ *
+ * @param clientIf ClientIf which was doing GATT connection attempt
+ * @param device Remote device to which connection attpemt failed
+ */
+ public void notifyGattClientConnectFailed(int clientIf, BluetoothDevice device) {
+ if (mLeAudioService != null) {
+ removeGattClientFromControlAutoActiveMode(clientIf, device);
+ }
+ }
+
+ /**
+ * Notify AdapterService about GATT connection being disconnecting or disconnected.
+ *
+ * @param clientIf ClientIf which is disconnecting or is already disconnected
+ * @param device Remote device which is disconnecting or is disconnected
+ */
+ public void notifyGattClientDisconnect(int clientIf, BluetoothDevice device) {
+ if (mLeAudioService != null) {
+ removeGattClientFromControlAutoActiveMode(clientIf, device);
+ }
+ }
+
public int getConnectionState(BluetoothDevice device) {
final String address = device.getAddress();
if (Flags.apiGetConnectionStateUsingIdentityAddress()) {
@@ -6577,6 +6771,12 @@ public class AdapterService extends Service {
}
writer.println();
+ writer.println("LE Gatt clients controlling AutoActiveMode:");
+ for (Pair<Integer, BluetoothDevice> pair : mLeGattClientsControllingAutoActiveMode) {
+ writer.println(" clientIf:" + pair.first + " " + pair.second);
+ }
+ writer.println();
+
mAdapterStateMachine.dump(fd, writer, args);
StringBuilder sb = new StringBuilder();
diff --git a/android/app/src/com/android/bluetooth/btservice/PhonePolicy.java b/android/app/src/com/android/bluetooth/btservice/PhonePolicy.java
index 475da0dba1..47a5d9991f 100644
--- a/android/app/src/com/android/bluetooth/btservice/PhonePolicy.java
+++ b/android/app/src/com/android/bluetooth/btservice/PhonePolicy.java
@@ -274,6 +274,27 @@ public class PhonePolicy implements AdapterService.BluetoothStateCallback {
&& hap.getConnectionPolicy(device) != CONNECTION_POLICY_FORBIDDEN;
}
+ private boolean shouldBlockBroadcastForHapDevice(BluetoothDevice device, ParcelUuid[] uuids) {
+ if (!Flags.leaudioDisableBroadcastForHapDevice()) {
+ Log.i(TAG, "disableBroadcastForHapDevice: Flag is disabled");
+ return false;
+ }
+
+ HapClientService hap = mFactory.getHapClientService();
+ if (hap == null) {
+ Log.e(TAG, "shouldBlockBroadcastForHapDevice: No HapClientService");
+ return false;
+ }
+
+ if (!SystemProperties.getBoolean(SYSPROP_HAP_ENABLED, true)) {
+ Log.i(TAG, "shouldBlockBroadcastForHapDevice: SystemProperty is overridden to false");
+ return false;
+ }
+
+ return Utils.arrayContains(uuids, BluetoothUuid.HAS)
+ && hap.getConnectionPolicy(device) == CONNECTION_POLICY_ALLOWED;
+ }
+
// Policy implementation, all functions MUST be private
private void processInitProfilePriorities(BluetoothDevice device, ParcelUuid[] uuids) {
String log = "processInitProfilePriorities(" + device + "): ";
@@ -518,7 +539,7 @@ public class PhonePolicy implements AdapterService.BluetoothStateCallback {
if ((bcService != null)
&& Utils.arrayContains(uuids, BluetoothUuid.BASS)
&& (bcService.getConnectionPolicy(device) == CONNECTION_POLICY_UNKNOWN)) {
- if (isLeAudioProfileAllowed) {
+ if (isLeAudioProfileAllowed && !shouldBlockBroadcastForHapDevice(device, uuids)) {
Log.d(TAG, log + "Setting BASS priority");
if (mAutoConnectProfilesSupported) {
bcService.setConnectionPolicy(device, CONNECTION_POLICY_ALLOWED);
@@ -531,7 +552,7 @@ public class PhonePolicy implements AdapterService.BluetoothStateCallback {
CONNECTION_POLICY_ALLOWED);
}
} else {
- Log.d(TAG, log + "LE_AUDIO is not allowed: Clear BASS priority");
+ Log.d(TAG, log + "LE_AUDIO Broadcast is not allowed: Clear BASS priority");
mAdapterService
.getDatabase()
.setProfileConnectionPolicy(
diff --git a/android/app/src/com/android/bluetooth/btservice/RemoteDevices.java b/android/app/src/com/android/bluetooth/btservice/RemoteDevices.java
index f0bad17e64..f297d905ac 100644
--- a/android/app/src/com/android/bluetooth/btservice/RemoteDevices.java
+++ b/android/app/src/com/android/bluetooth/btservice/RemoteDevices.java
@@ -345,7 +345,8 @@ public class RemoteDevices {
private String mModelName;
@VisibleForTesting int mBondState;
@VisibleForTesting int mDeviceType;
- @VisibleForTesting ParcelUuid[] mUuids;
+ @VisibleForTesting ParcelUuid[] mUuidsBrEdr;
+ @VisibleForTesting ParcelUuid[] mUuidsLe;
private BluetoothSinkAudioPolicy mAudioPolicy;
DeviceProperties() {
@@ -506,20 +507,71 @@ public class RemoteDevices {
}
/**
- * @return the mUuids
+ * @return the UUIDs on LE and Classic transport
*/
ParcelUuid[] getUuids() {
synchronized (mObject) {
- return mUuids;
+ /* When we bond dual mode device, and discover LE and Classic services, stack would
+ * return LE and Classic UUIDs separately, but Java apps expect them merged.
+ */
+ int combinedUuidsLength =
+ (mUuidsBrEdr != null ? mUuidsBrEdr.length : 0)
+ + (mUuidsLe != null ? mUuidsLe.length : 0);
+ if (!Flags.separateServiceStorage() || combinedUuidsLength == 0) {
+ return mUuidsBrEdr;
+ }
+
+ java.util.LinkedHashSet<ParcelUuid> result =
+ new java.util.LinkedHashSet<ParcelUuid>();
+ if (mUuidsBrEdr != null) {
+ for (ParcelUuid uuid : mUuidsBrEdr) {
+ result.add(uuid);
+ }
+ }
+
+ if (mUuidsLe != null) {
+ for (ParcelUuid uuid : mUuidsLe) {
+ result.add(uuid);
+ }
+ }
+
+ return result.toArray(new ParcelUuid[combinedUuidsLength]);
+ }
+ }
+
+ /**
+ * @return just classic transport UUIDS
+ */
+ ParcelUuid[] getUuidsBrEdr() {
+ synchronized (mObject) {
+ return mUuidsBrEdr;
+ }
+ }
+
+ /**
+ * @param uuids the mUuidsBrEdr to set
+ */
+ void setUuidsBrEdr(ParcelUuid[] uuids) {
+ synchronized (mObject) {
+ this.mUuidsBrEdr = uuids;
+ }
+ }
+
+ /**
+ * @return the mUuidsLe
+ */
+ ParcelUuid[] getUuidsLe() {
+ synchronized (mObject) {
+ return mUuidsLe;
}
}
/**
- * @param uuids the mUuids to set
+ * @param uuids the mUuidsLe to set
*/
- void setUuids(ParcelUuid[] uuids) {
+ void setUuidsLe(ParcelUuid[] uuids) {
synchronized (mObject) {
- this.mUuids = uuids;
+ this.mUuidsLe = uuids;
}
}
@@ -636,7 +688,8 @@ public class RemoteDevices {
cachedBluetoothDevice issued a connect using the local cached copy of uuids,
without waiting for the ACTION_UUID intent.
This was resulting in multiple calls to connect().*/
- mUuids = null;
+ mUuidsBrEdr = null;
+ mUuidsLe = null;
mAlias = null;
}
}
@@ -988,147 +1041,168 @@ public class RemoteDevices {
return;
}
+ boolean uuids_updated = false;
+
for (int j = 0; j < types.length; j++) {
type = types[j];
val = values[j];
- if (val.length > 0) {
- synchronized (mObject) {
- debugLog("Update property, device=" + bdDevice + ", type: " + type);
- switch (type) {
- case AbstractionLayer.BT_PROPERTY_BDNAME:
- final String newName = new String(val);
- if (newName.equals(deviceProperties.getName())) {
- debugLog("Skip name update for " + bdDevice);
- break;
- }
- deviceProperties.setName(newName);
- List<String> wordBreakdownList =
- MetricsLogger.getInstance().getWordBreakdownList(newName);
- if (SdkLevel.isAtLeastU()) {
- MetricsLogger.getInstance()
- .uploadRestrictedBluetothDeviceName(wordBreakdownList);
- }
- intent = new Intent(BluetoothDevice.ACTION_NAME_CHANGED);
- intent.putExtra(BluetoothDevice.EXTRA_DEVICE, bdDevice);
- intent.putExtra(BluetoothDevice.EXTRA_NAME, deviceProperties.getName());
- intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
- mAdapterService.sendBroadcast(
- intent,
- BLUETOOTH_CONNECT,
- Utils.getTempBroadcastOptions().toBundle());
- debugLog("Remote device name is: " + deviceProperties.getName());
- break;
- case AbstractionLayer.BT_PROPERTY_REMOTE_FRIENDLY_NAME:
- deviceProperties.setAlias(bdDevice, new String(val));
- debugLog("Remote device alias is: " + deviceProperties.getAlias());
- break;
- case AbstractionLayer.BT_PROPERTY_BDADDR:
- deviceProperties.setAddress(val);
- debugLog(
- "Remote Address is:"
- + Utils.getRedactedAddressStringFromByte(val));
+ if (val.length == 0) {
+ continue;
+ }
+
+ synchronized (mObject) {
+ debugLog("Update property, device=" + bdDevice + ", type: " + type);
+ switch (type) {
+ case AbstractionLayer.BT_PROPERTY_BDNAME:
+ final String newName = new String(val);
+ if (newName.equals(deviceProperties.getName())) {
+ debugLog("Skip name update for " + bdDevice);
break;
- case AbstractionLayer.BT_PROPERTY_CLASS_OF_DEVICE:
- final int newBluetoothClass = Utils.byteArrayToInt(val);
- if (newBluetoothClass == deviceProperties.getBluetoothClass()) {
- debugLog(
- "Skip class update, device="
- + bdDevice
- + ", cod=0x"
- + Integer.toHexString(newBluetoothClass));
- break;
- }
- deviceProperties.setBluetoothClass(newBluetoothClass);
- intent = new Intent(BluetoothDevice.ACTION_CLASS_CHANGED);
- intent.putExtra(BluetoothDevice.EXTRA_DEVICE, bdDevice);
- intent.putExtra(
- BluetoothDevice.EXTRA_CLASS,
- new BluetoothClass(deviceProperties.getBluetoothClass()));
- intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
- mAdapterService.sendBroadcast(
- intent,
- BLUETOOTH_CONNECT,
- Utils.getTempBroadcastOptions().toBundle());
+ }
+ deviceProperties.setName(newName);
+ List<String> wordBreakdownList =
+ MetricsLogger.getInstance().getWordBreakdownList(newName);
+ if (SdkLevel.isAtLeastU()) {
+ MetricsLogger.getInstance()
+ .uploadRestrictedBluetothDeviceName(wordBreakdownList);
+ }
+ intent = new Intent(BluetoothDevice.ACTION_NAME_CHANGED);
+ intent.putExtra(BluetoothDevice.EXTRA_DEVICE, bdDevice);
+ intent.putExtra(BluetoothDevice.EXTRA_NAME, deviceProperties.getName());
+ intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
+ mAdapterService.sendBroadcast(
+ intent,
+ BLUETOOTH_CONNECT,
+ Utils.getTempBroadcastOptions().toBundle());
+ debugLog("Remote device name is: " + deviceProperties.getName());
+ break;
+ case AbstractionLayer.BT_PROPERTY_REMOTE_FRIENDLY_NAME:
+ deviceProperties.setAlias(bdDevice, new String(val));
+ debugLog("Remote device alias is: " + deviceProperties.getAlias());
+ break;
+ case AbstractionLayer.BT_PROPERTY_BDADDR:
+ deviceProperties.setAddress(val);
+ debugLog(
+ "Remote Address is:" + Utils.getRedactedAddressStringFromByte(val));
+ break;
+ case AbstractionLayer.BT_PROPERTY_CLASS_OF_DEVICE:
+ final int newBluetoothClass = Utils.byteArrayToInt(val);
+ if (newBluetoothClass == deviceProperties.getBluetoothClass()) {
debugLog(
- "Remote class update, device="
+ "Skip class update, device="
+ bdDevice
+ ", cod=0x"
+ Integer.toHexString(newBluetoothClass));
break;
- case AbstractionLayer.BT_PROPERTY_UUIDS:
+ }
+ deviceProperties.setBluetoothClass(newBluetoothClass);
+ intent = new Intent(BluetoothDevice.ACTION_CLASS_CHANGED);
+ intent.putExtra(BluetoothDevice.EXTRA_DEVICE, bdDevice);
+ intent.putExtra(
+ BluetoothDevice.EXTRA_CLASS,
+ new BluetoothClass(deviceProperties.getBluetoothClass()));
+ intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
+ mAdapterService.sendBroadcast(
+ intent,
+ BLUETOOTH_CONNECT,
+ Utils.getTempBroadcastOptions().toBundle());
+ debugLog(
+ "Remote class update, device="
+ + bdDevice
+ + ", cod=0x"
+ + Integer.toHexString(newBluetoothClass));
+ break;
+ case AbstractionLayer.BT_PROPERTY_UUIDS:
+ case AbstractionLayer.BT_PROPERTY_UUIDS_LE:
+ if (type == AbstractionLayer.BT_PROPERTY_UUIDS) {
final ParcelUuid[] newUuids = Utils.byteArrayToUuid(val);
- if (areUuidsEqual(newUuids, deviceProperties.getUuids())) {
+ if (areUuidsEqual(newUuids, deviceProperties.getUuidsBrEdr())) {
// SDP Skip adding UUIDs to property cache if equal
debugLog("Skip uuids update for " + bdDevice.getAddress());
MetricsLogger.getInstance()
.cacheCount(BluetoothProtoEnums.SDP_UUIDS_EQUAL_SKIP, 1);
break;
}
- deviceProperties.setUuids(newUuids);
- if (mAdapterService.getState() == BluetoothAdapter.STATE_ON) {
- // SDP Adding UUIDs to property cache and sending intent
- MetricsLogger.getInstance()
- .cacheCount(
- BluetoothProtoEnums.SDP_ADD_UUID_WITH_INTENT, 1);
- mAdapterService.deviceUuidUpdated(bdDevice);
- sendUuidIntent(bdDevice, deviceProperties, true);
- } else if (mAdapterService.getState()
- == BluetoothAdapter.STATE_BLE_ON) {
- // SDP Adding UUIDs to property cache but with no intent
- MetricsLogger.getInstance()
- .cacheCount(
- BluetoothProtoEnums.SDP_ADD_UUID_WITH_NO_INTENT, 1);
- mAdapterService.deviceUuidUpdated(bdDevice);
- } else {
- // SDP Silently dropping UUIDs and with no intent
+ deviceProperties.setUuidsBrEdr(newUuids);
+ } else if (type == AbstractionLayer.BT_PROPERTY_UUIDS_LE) {
+ final ParcelUuid[] newUuidsLe = Utils.byteArrayToUuid(val);
+ if (areUuidsEqual(newUuidsLe, deviceProperties.getUuidsLe())) {
+ // SDP Skip adding UUIDs to property cache if equal
+ debugLog("Skip LE uuids update for " + bdDevice.getAddress());
MetricsLogger.getInstance()
- .cacheCount(BluetoothProtoEnums.SDP_DROP_UUID, 1);
- }
- break;
- case AbstractionLayer.BT_PROPERTY_TYPE_OF_DEVICE:
- if (deviceProperties.isConsolidated()) {
+ .cacheCount(BluetoothProtoEnums.SDP_UUIDS_EQUAL_SKIP, 1);
break;
}
- // The device type from hal layer, defined in bluetooth.h,
- // matches the type defined in BluetoothDevice.java
- deviceProperties.setDeviceType(Utils.byteArrayToInt(val));
- break;
- case AbstractionLayer.BT_PROPERTY_REMOTE_RSSI:
- // RSSI from hal is in one byte
- deviceProperties.setRssi(val[0]);
- break;
- case AbstractionLayer.BT_PROPERTY_REMOTE_IS_COORDINATED_SET_MEMBER:
- deviceProperties.setIsCoordinatedSetMember(val[0] != 0);
- break;
- case AbstractionLayer.BT_PROPERTY_REMOTE_ASHA_CAPABILITY:
- deviceProperties.setAshaCapability(val[0]);
- break;
- case AbstractionLayer.BT_PROPERTY_REMOTE_ASHA_TRUNCATED_HISYNCID:
- deviceProperties.setAshaTruncatedHiSyncId(val[0]);
- break;
- case AbstractionLayer.BT_PROPERTY_REMOTE_MODEL_NUM:
- final String modelName = new String(val);
- debugLog("Remote device model name: " + modelName);
- deviceProperties.setModelName(modelName);
- BluetoothStatsLog.write(
- BluetoothStatsLog.BLUETOOTH_DEVICE_INFO_REPORTED,
- mAdapterService.obfuscateAddress(bdDevice),
- BluetoothProtoEnums.DEVICE_INFO_INTERNAL,
- LOG_SOURCE_DIS,
- null,
- modelName,
- null,
- null,
- mAdapterService.getMetricId(bdDevice),
- bdDevice.getAddressType(),
- 0,
- 0,
- 0);
+ deviceProperties.setUuidsLe(newUuidsLe);
+ }
+ uuids_updated = true;
+ break;
+ case AbstractionLayer.BT_PROPERTY_TYPE_OF_DEVICE:
+ if (deviceProperties.isConsolidated()) {
break;
- }
+ }
+ // The device type from hal layer, defined in bluetooth.h,
+ // matches the type defined in BluetoothDevice.java
+ deviceProperties.setDeviceType(Utils.byteArrayToInt(val));
+ break;
+ case AbstractionLayer.BT_PROPERTY_REMOTE_RSSI:
+ // RSSI from hal is in one byte
+ deviceProperties.setRssi(val[0]);
+ break;
+ case AbstractionLayer.BT_PROPERTY_REMOTE_IS_COORDINATED_SET_MEMBER:
+ deviceProperties.setIsCoordinatedSetMember(val[0] != 0);
+ break;
+ case AbstractionLayer.BT_PROPERTY_REMOTE_ASHA_CAPABILITY:
+ deviceProperties.setAshaCapability(val[0]);
+ break;
+ case AbstractionLayer.BT_PROPERTY_REMOTE_ASHA_TRUNCATED_HISYNCID:
+ deviceProperties.setAshaTruncatedHiSyncId(val[0]);
+ break;
+ case AbstractionLayer.BT_PROPERTY_REMOTE_MODEL_NUM:
+ final String modelName = new String(val);
+ debugLog("Remote device model name: " + modelName);
+ deviceProperties.setModelName(modelName);
+ BluetoothStatsLog.write(
+ BluetoothStatsLog.BLUETOOTH_DEVICE_INFO_REPORTED,
+ mAdapterService.obfuscateAddress(bdDevice),
+ BluetoothProtoEnums.DEVICE_INFO_INTERNAL,
+ LOG_SOURCE_DIS,
+ null,
+ modelName,
+ null,
+ null,
+ mAdapterService.getMetricId(bdDevice),
+ bdDevice.getAddressType(),
+ 0,
+ 0,
+ 0);
+ break;
}
}
}
+
+ if (!uuids_updated) {
+ return;
+ }
+
+ /* uuids_updated == true
+ * We might have received LE and BREDR UUIDS separately, ensure that UUID intent is sent
+ * just once */
+
+ if (mAdapterService.getState() == BluetoothAdapter.STATE_ON) {
+ // SDP Adding UUIDs to property cache and sending intent
+ MetricsLogger.getInstance().cacheCount(BluetoothProtoEnums.SDP_ADD_UUID_WITH_INTENT, 1);
+ mAdapterService.deviceUuidUpdated(bdDevice);
+ sendUuidIntent(bdDevice, deviceProperties, true);
+ } else if (mAdapterService.getState() == BluetoothAdapter.STATE_BLE_ON) {
+ // SDP Adding UUIDs to property cache but with no intent
+ MetricsLogger.getInstance()
+ .cacheCount(BluetoothProtoEnums.SDP_ADD_UUID_WITH_NO_INTENT, 1);
+ mAdapterService.deviceUuidUpdated(bdDevice);
+ } else {
+ // SDP Silently dropping UUIDs and with no intent
+ MetricsLogger.getInstance().cacheCount(BluetoothProtoEnums.SDP_DROP_UUID, 1);
+ }
}
void deviceFoundCallback(byte[] address) {
diff --git a/android/app/src/com/android/bluetooth/gatt/AdvertiseBinder.java b/android/app/src/com/android/bluetooth/gatt/AdvertiseBinder.java
deleted file mode 100644
index 9e4d281bda..0000000000
--- a/android/app/src/com/android/bluetooth/gatt/AdvertiseBinder.java
+++ /dev/null
@@ -1,197 +0,0 @@
-/*
- * Copyright 2025 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.gatt;
-
-import static android.Manifest.permission.BLUETOOTH_ADVERTISE;
-import static android.Manifest.permission.BLUETOOTH_PRIVILEGED;
-
-import static java.util.Objects.requireNonNull;
-
-import android.annotation.Nullable;
-import android.annotation.RequiresPermission;
-import android.bluetooth.IBluetoothAdvertise;
-import android.bluetooth.le.AdvertiseData;
-import android.bluetooth.le.AdvertisingSetParameters;
-import android.bluetooth.le.IAdvertisingSetCallback;
-import android.bluetooth.le.PeriodicAdvertisingParameters;
-import android.content.AttributionSource;
-import android.content.Context;
-
-import com.android.bluetooth.Utils;
-
-class AdvertiseBinder extends IBluetoothAdvertise.Stub {
- private final Context mContext;
- private final AdvertiseManager mAdvertiseManager;
- private volatile boolean mIsAvailable = true;
-
- AdvertiseBinder(Context context, AdvertiseManager manager) {
- mContext = context;
- mAdvertiseManager = manager;
- }
-
- void cleanup() {
- mIsAvailable = false;
- }
-
- @RequiresPermission(BLUETOOTH_ADVERTISE)
- @Nullable
- private AdvertiseManager getManager(AttributionSource source) {
- requireNonNull(source);
- if (!Utils.checkAdvertisePermissionForDataDelivery(
- mContext, source, "AdvertiseManager startAdvertisingSet")) {
- return null;
- }
- return mIsAvailable ? mAdvertiseManager : null;
- }
-
- @Override
- public void startAdvertisingSet(
- AdvertisingSetParameters parameters,
- @Nullable AdvertiseData advertiseData,
- @Nullable AdvertiseData scanResponse,
- @Nullable PeriodicAdvertisingParameters periodicParameters,
- @Nullable AdvertiseData periodicData,
- int duration,
- int maxExtAdvEvents,
- int serverIf,
- IAdvertisingSetCallback callback,
- AttributionSource source) {
- requireNonNull(parameters);
- requireNonNull(callback);
- AdvertiseManager manager = getManager(source);
- if (manager == null) {
- return;
- }
- if (parameters.getOwnAddressType() != AdvertisingSetParameters.ADDRESS_TYPE_DEFAULT
- || serverIf != 0
- || parameters.isDirected()) {
- mContext.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null);
- }
- manager.startAdvertisingSet(
- parameters,
- advertiseData,
- scanResponse,
- periodicParameters,
- periodicData,
- duration,
- maxExtAdvEvents,
- serverIf,
- callback,
- source);
- }
-
- @Override
- public void stopAdvertisingSet(IAdvertisingSetCallback callback, AttributionSource source) {
- requireNonNull(callback);
- AdvertiseManager manager = getManager(source);
- if (manager == null) {
- return;
- }
- manager.stopAdvertisingSet(callback);
- }
-
- @Override
- public void getOwnAddress(int advertiserId, AttributionSource source) {
- AdvertiseManager manager = getManager(source);
- if (manager == null) {
- return;
- }
- mContext.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null);
- manager.getOwnAddress(advertiserId);
- }
-
- @Override
- public void enableAdvertisingSet(
- int advertiserId,
- boolean enable,
- int duration,
- int maxExtAdvEvents,
- AttributionSource source) {
- AdvertiseManager manager = getManager(source);
- if (manager == null) {
- return;
- }
- manager.enableAdvertisingSet(advertiserId, enable, duration, maxExtAdvEvents);
- }
-
- @Override
- public void setAdvertisingData(
- int advertiserId, @Nullable AdvertiseData data, AttributionSource source) {
- AdvertiseManager manager = getManager(source);
- if (manager == null) {
- return;
- }
- manager.setAdvertisingData(advertiserId, data);
- }
-
- @Override
- public void setScanResponseData(
- int advertiserId, @Nullable AdvertiseData data, AttributionSource source) {
- AdvertiseManager manager = getManager(source);
- if (manager == null) {
- return;
- }
- manager.setScanResponseData(advertiserId, data);
- }
-
- @Override
- public void setAdvertisingParameters(
- int advertiserId, AdvertisingSetParameters parameters, AttributionSource source) {
- requireNonNull(parameters);
- AdvertiseManager manager = getManager(source);
- if (manager == null) {
- return;
- }
- if (parameters.getOwnAddressType() != AdvertisingSetParameters.ADDRESS_TYPE_DEFAULT
- || parameters.isDirected()) {
- mContext.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null);
- }
- manager.setAdvertisingParameters(advertiserId, parameters);
- }
-
- @Override
- public void setPeriodicAdvertisingParameters(
- int advertiserId,
- @Nullable PeriodicAdvertisingParameters parameters,
- AttributionSource source) {
- AdvertiseManager manager = getManager(source);
- if (manager == null) {
- return;
- }
- manager.setPeriodicAdvertisingParameters(advertiserId, parameters);
- }
-
- @Override
- public void setPeriodicAdvertisingData(
- int advertiserId, @Nullable AdvertiseData data, AttributionSource source) {
- AdvertiseManager manager = getManager(source);
- if (manager == null) {
- return;
- }
- manager.setPeriodicAdvertisingData(advertiserId, data);
- }
-
- @Override
- public void setPeriodicAdvertisingEnable(
- int advertiserId, boolean enable, AttributionSource source) {
- AdvertiseManager manager = getManager(source);
- if (manager == null) {
- return;
- }
- manager.setPeriodicAdvertisingEnable(advertiserId, enable);
- }
-}
diff --git a/android/app/src/com/android/bluetooth/gatt/AdvertiseBinder.kt b/android/app/src/com/android/bluetooth/gatt/AdvertiseBinder.kt
new file mode 100644
index 0000000000..66415cd82f
--- /dev/null
+++ b/android/app/src/com/android/bluetooth/gatt/AdvertiseBinder.kt
@@ -0,0 +1,175 @@
+/*
+ * Copyright 2025 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.gatt
+
+import android.Manifest.permission.BLUETOOTH_ADVERTISE
+import android.Manifest.permission.BLUETOOTH_PRIVILEGED
+import android.annotation.RequiresPermission
+import android.bluetooth.IBluetoothAdvertise
+import android.bluetooth.le.AdvertiseData
+import android.bluetooth.le.AdvertisingSetParameters
+import android.bluetooth.le.IAdvertisingSetCallback
+import android.bluetooth.le.PeriodicAdvertisingParameters
+import android.content.AttributionSource
+import android.content.Context
+import com.android.bluetooth.Utils
+
+class AdvertiseBinder(
+ private val mContext: Context,
+ private val mAdvertiseManager: AdvertiseManager,
+) : IBluetoothAdvertise.Stub() {
+ @Volatile private var mIsAvailable = true
+
+ fun cleanup() {
+ mIsAvailable = false
+ }
+
+ @RequiresPermission(BLUETOOTH_ADVERTISE)
+ private fun getManager(source: AttributionSource): AdvertiseManager? {
+ if (!Utils.checkAdvertisePermissionForDataDelivery(mContext, source, "AdvertiseManager")) {
+ return null
+ }
+ return if (mIsAvailable) mAdvertiseManager else null
+ }
+
+ override fun startAdvertisingSet(
+ parameters: AdvertisingSetParameters,
+ advertiseData: AdvertiseData?,
+ scanResponse: AdvertiseData?,
+ periodicParameters: PeriodicAdvertisingParameters?,
+ periodicData: AdvertiseData?,
+ duration: Int,
+ maxExtAdvEvents: Int,
+ serverIf: Int,
+ callback: IAdvertisingSetCallback,
+ source: AttributionSource,
+ ) {
+ if (
+ parameters.ownAddressType != AdvertisingSetParameters.ADDRESS_TYPE_DEFAULT ||
+ serverIf != 0 ||
+ parameters.isDirected
+ ) {
+ mContext.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null)
+ }
+
+ getManager(source)?.let {
+ it.doOnAdvertiseThread {
+ it.startAdvertisingSet(
+ parameters,
+ advertiseData,
+ scanResponse,
+ periodicParameters,
+ periodicData,
+ duration,
+ maxExtAdvEvents,
+ serverIf,
+ callback,
+ source,
+ )
+ }
+ }
+ }
+
+ override fun stopAdvertisingSet(callback: IAdvertisingSetCallback, source: AttributionSource) {
+ getManager(source)?.let { it.doOnAdvertiseThread { it.stopAdvertisingSet(callback) } }
+ }
+
+ override fun getOwnAddress(advertiserId: Int, source: AttributionSource) {
+ mContext.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null)
+ getManager(source)?.let { it.doOnAdvertiseThread { it.getOwnAddress(advertiserId) } }
+ }
+
+ override fun enableAdvertisingSet(
+ advertiserId: Int,
+ enable: Boolean,
+ duration: Int,
+ maxExtAdvEvents: Int,
+ source: AttributionSource,
+ ) {
+ getManager(source)?.let {
+ it.doOnAdvertiseThread {
+ it.enableAdvertisingSet(advertiserId, enable, duration, maxExtAdvEvents)
+ }
+ }
+ }
+
+ override fun setAdvertisingData(
+ advertiserId: Int,
+ data: AdvertiseData?,
+ source: AttributionSource,
+ ) {
+ getManager(source)?.let {
+ it.doOnAdvertiseThread { it.setAdvertisingData(advertiserId, data) }
+ }
+ }
+
+ override fun setScanResponseData(
+ advertiserId: Int,
+ data: AdvertiseData?,
+ source: AttributionSource,
+ ) {
+ getManager(source)?.let {
+ it.doOnAdvertiseThread { it.setScanResponseData(advertiserId, data) }
+ }
+ }
+
+ override fun setAdvertisingParameters(
+ advertiserId: Int,
+ parameters: AdvertisingSetParameters,
+ source: AttributionSource,
+ ) {
+ if (
+ parameters.ownAddressType != AdvertisingSetParameters.ADDRESS_TYPE_DEFAULT ||
+ parameters.isDirected
+ ) {
+ mContext.enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, null)
+ }
+ getManager(source)?.let {
+ it.doOnAdvertiseThread { it.setAdvertisingParameters(advertiserId, parameters) }
+ }
+ }
+
+ override fun setPeriodicAdvertisingParameters(
+ advertiserId: Int,
+ parameters: PeriodicAdvertisingParameters?,
+ source: AttributionSource,
+ ) {
+ getManager(source)?.let {
+ it.doOnAdvertiseThread { it.setPeriodicAdvertisingParameters(advertiserId, parameters) }
+ }
+ }
+
+ override fun setPeriodicAdvertisingData(
+ advertiserId: Int,
+ data: AdvertiseData?,
+ source: AttributionSource,
+ ) {
+ getManager(source)?.let {
+ it.doOnAdvertiseThread { it.setPeriodicAdvertisingData(advertiserId, data) }
+ }
+ }
+
+ override fun setPeriodicAdvertisingEnable(
+ advertiserId: Int,
+ enable: Boolean,
+ source: AttributionSource,
+ ) {
+ getManager(source)?.let {
+ it.doOnAdvertiseThread { it.setPeriodicAdvertisingEnable(advertiserId, enable) }
+ }
+ }
+}
diff --git a/android/app/src/com/android/bluetooth/gatt/AdvertiseManager.java b/android/app/src/com/android/bluetooth/gatt/AdvertiseManager.java
index 7e89f99055..6ed719cedc 100644
--- a/android/app/src/com/android/bluetooth/gatt/AdvertiseManager.java
+++ b/android/app/src/com/android/bluetooth/gatt/AdvertiseManager.java
@@ -24,45 +24,52 @@ import android.bluetooth.le.PeriodicAdvertisingParameters;
import android.content.AttributionSource;
import android.os.Binder;
import android.os.Handler;
-import android.os.HandlerThread;
import android.os.IBinder;
-import android.os.IInterface;
import android.os.Looper;
import android.os.RemoteException;
import android.util.Log;
+import com.android.bluetooth.Utils;
import com.android.bluetooth.btservice.AdapterService;
-import com.android.internal.annotations.GuardedBy;
+import com.android.bluetooth.flags.Flags;
import com.android.internal.annotations.VisibleForTesting;
import java.util.HashMap;
import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
-/**
- * Manages Bluetooth LE advertising operations and interacts with bluedroid stack. TODO: add tests.
- */
-@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+/** Manages Bluetooth LE advertising operations. */
public class AdvertiseManager {
private static final String TAG = GattServiceConfig.TAG_PREFIX + "AdvertiseManager";
- private final GattService mService;
+ private static final long RUN_SYNC_WAIT_TIME_MS = 2000L;
+
+ private final AdapterService mService;
private final AdvertiseManagerNativeInterface mNativeInterface;
private final AdvertiseBinder mAdvertiseBinder;
private final AdvertiserMap mAdvertiserMap;
- @GuardedBy("itself")
private final Map<IBinder, AdvertiserInfo> mAdvertisers = new HashMap<>();
- private Handler mHandler;
- static int sTempRegistrationId = -1;
+ private final Handler mHandler;
+ private volatile boolean mIsAvailable = true;
+ @VisibleForTesting int mTempRegistrationId = -1;
- AdvertiseManager(GattService service) {
- this(service, AdvertiseManagerNativeInterface.getInstance(), new AdvertiserMap());
+ AdvertiseManager(AdapterService service, Looper advertiseLooper) {
+ this(
+ service,
+ advertiseLooper,
+ AdvertiseManagerNativeInterface.getInstance(),
+ new AdvertiserMap());
}
@VisibleForTesting
AdvertiseManager(
- GattService service,
+ AdapterService service,
+ Looper advertiseLooper,
AdvertiseManagerNativeInterface nativeInterface,
AdvertiserMap advertiserMap) {
Log.d(TAG, "advertise manager created");
@@ -70,42 +77,26 @@ public class AdvertiseManager {
mNativeInterface = nativeInterface;
mAdvertiserMap = advertiserMap;
- // Start a HandlerThread that handles advertising operations
mNativeInterface.init(this);
- HandlerThread thread = new HandlerThread("BluetoothAdvertiseManager");
- thread.start();
- mHandler = new Handler(thread.getLooper());
+ mHandler = new Handler(advertiseLooper);
mAdvertiseBinder = new AdvertiseBinder(service, this);
}
- // TODO(b/327849650): We shouldn't need this, it should be safe to do in the cleanup method. But
- // it would be a logic change.
- void clear() {
- mAdvertiserMap.clear();
- }
-
void cleanup() {
Log.d(TAG, "cleanup()");
- mAdvertiseBinder.cleanup();
- mNativeInterface.cleanup();
- synchronized (mAdvertisers) {
- mAdvertisers.clear();
- }
- sTempRegistrationId = -1;
-
- if (mHandler != null) {
- // Shut down the thread
- mHandler.removeCallbacksAndMessages(null);
- Looper looper = mHandler.getLooper();
- if (looper != null) {
- looper.quit();
- }
- mHandler = null;
- }
+ mIsAvailable = false;
+ mHandler.removeCallbacksAndMessages(null);
+ forceRunSyncOnAdvertiseThread(
+ () -> {
+ mAdvertiserMap.clear();
+ mAdvertiseBinder.cleanup();
+ mNativeInterface.cleanup();
+ mAdvertisers.clear();
+ });
}
void dump(StringBuilder sb) {
- mAdvertiserMap.dump(sb);
+ forceRunSyncOnAdvertiseThread(() -> mAdvertiserMap.dump(sb));
}
AdvertiseBinder getBinder() {
@@ -129,8 +120,12 @@ public class AdvertiseManager {
}
}
+ private interface CallbackWrapper {
+ void call() throws RemoteException;
+ }
+
IBinder toBinder(IAdvertisingSetCallback e) {
- return ((IInterface) e).asBinder();
+ return e.asBinder();
}
class AdvertisingSetDeathRecipient implements IBinder.DeathRecipient {
@@ -145,25 +140,22 @@ public class AdvertiseManager {
@Override
public void binderDied() {
Log.d(TAG, "Binder is dead - unregistering advertising set (" + mPackageName + ")!");
- stopAdvertisingSet(callback);
+ doOnAdvertiseThread(() -> stopAdvertisingSet(callback));
}
}
- Map.Entry<IBinder, AdvertiserInfo> findAdvertiser(int advertiserId) {
+ private Map.Entry<IBinder, AdvertiserInfo> findAdvertiser(int advertiserId) {
Map.Entry<IBinder, AdvertiserInfo> entry = null;
- synchronized (mAdvertisers) {
- for (Map.Entry<IBinder, AdvertiserInfo> e : mAdvertisers.entrySet()) {
- if (e.getValue().id == advertiserId) {
- entry = e;
- break;
- }
+ for (Map.Entry<IBinder, AdvertiserInfo> e : mAdvertisers.entrySet()) {
+ if (e.getValue().id == advertiserId) {
+ entry = e;
+ break;
}
}
return entry;
}
- void onAdvertisingSetStarted(int regId, int advertiserId, int txPower, int status)
- throws Exception {
+ void onAdvertisingSetStarted(int regId, int advertiserId, int txPower, int status) {
Log.d(
TAG,
"onAdvertisingSetStarted() - regId="
@@ -172,6 +164,7 @@ public class AdvertiseManager {
+ advertiserId
+ ", status="
+ status);
+ checkThread();
Map.Entry<IBinder, AdvertiserInfo> entry = findAdvertiser(regId);
@@ -191,26 +184,24 @@ public class AdvertiseManager {
} else {
IBinder binder = entry.getKey();
binder.unlinkToDeath(entry.getValue().deathRecipient, 0);
- synchronized (mAdvertisers) {
- mAdvertisers.remove(binder);
- }
+ mAdvertisers.remove(binder);
AppAdvertiseStats stats = mAdvertiserMap.getAppAdvertiseStatsById(regId);
if (stats != null) {
- int instanceCount;
- synchronized (mAdvertisers) {
- instanceCount = mAdvertisers.size();
- }
- stats.recordAdvertiseStop(instanceCount);
+ stats.recordAdvertiseStop(mAdvertisers.size());
stats.recordAdvertiseErrorCount(status);
}
mAdvertiserMap.removeAppAdvertiseStats(regId);
}
- callback.onAdvertisingSetStarted(mAdvertiseBinder, advertiserId, txPower, status);
+ sendToCallback(
+ advertiserId,
+ () ->
+ callback.onAdvertisingSetStarted(
+ mAdvertiseBinder, advertiserId, txPower, status));
}
- void onAdvertisingEnabled(int advertiserId, boolean enable, int status) throws Exception {
+ void onAdvertisingEnabled(int advertiserId, boolean enable, int status) {
Log.d(
TAG,
"onAdvertisingSetEnabled() - advertiserId="
@@ -219,6 +210,7 @@ public class AdvertiseManager {
+ enable
+ ", status="
+ status);
+ checkThread();
Map.Entry<IBinder, AdvertiserInfo> entry = findAdvertiser(advertiserId);
if (entry == null) {
@@ -230,16 +222,13 @@ public class AdvertiseManager {
}
IAdvertisingSetCallback callback = entry.getValue().callback;
- callback.onAdvertisingEnabled(advertiserId, enable, status);
+ sendToCallback(
+ advertiserId, () -> callback.onAdvertisingEnabled(advertiserId, enable, status));
if (!enable && status != 0) {
AppAdvertiseStats stats = mAdvertiserMap.getAppAdvertiseStatsById(advertiserId);
if (stats != null) {
- int instanceCount;
- synchronized (mAdvertisers) {
- instanceCount = mAdvertisers.size();
- }
- stats.recordAdvertiseStop(instanceCount);
+ stats.recordAdvertiseStop(mAdvertisers.size());
}
}
}
@@ -255,6 +244,7 @@ public class AdvertiseManager {
int serverIf,
IAdvertisingSetCallback callback,
AttributionSource attrSource) {
+ checkThread();
// If we are using an isolated server, force usage of an NRPA
if (serverIf != 0
&& parameters.getOwnAddressType()
@@ -289,7 +279,7 @@ public class AdvertiseManager {
throw new IllegalArgumentException("Can't link to advertiser's death");
}
- String deviceName = AdapterService.getAdapterService().getName();
+ String deviceName = mService.getName();
try {
byte[] advDataBytes = AdvertiseHelper.advertiseDataToBytes(advertiseData, deviceName);
byte[] scanResponseBytes =
@@ -297,10 +287,8 @@ public class AdvertiseManager {
byte[] periodicDataBytes =
AdvertiseHelper.advertiseDataToBytes(periodicData, deviceName);
- int cbId = --sTempRegistrationId;
- synchronized (mAdvertisers) {
- mAdvertisers.put(binder, new AdvertiserInfo(cbId, deathRecipient, callback));
- }
+ int cbId = --mTempRegistrationId;
+ mAdvertisers.put(binder, new AdvertiserInfo(cbId, deathRecipient, callback));
Log.d(TAG, "startAdvertisingSet() - reg_id=" + cbId + ", callback: " + binder);
@@ -340,9 +328,9 @@ public class AdvertiseManager {
}
}
- void onOwnAddressRead(int advertiserId, int addressType, String address)
- throws RemoteException {
+ void onOwnAddressRead(int advertiserId, int addressType, String address) {
Log.d(TAG, "onOwnAddressRead() advertiserId=" + advertiserId);
+ checkThread();
Map.Entry<IBinder, AdvertiserInfo> entry = findAdvertiser(advertiserId);
if (entry == null) {
@@ -351,10 +339,12 @@ public class AdvertiseManager {
}
IAdvertisingSetCallback callback = entry.getValue().callback;
- callback.onOwnAddressRead(advertiserId, addressType, address);
+ sendToCallback(
+ advertiserId, () -> callback.onOwnAddressRead(advertiserId, addressType, address));
}
void getOwnAddress(int advertiserId) {
+ checkThread();
Map.Entry<IBinder, AdvertiserInfo> entry = findAdvertiser(advertiserId);
if (entry == null) {
Log.w(TAG, "getOwnAddress() - bad advertiserId " + advertiserId);
@@ -364,13 +354,11 @@ public class AdvertiseManager {
}
void stopAdvertisingSet(IAdvertisingSetCallback callback) {
+ checkThread();
IBinder binder = toBinder(callback);
Log.d(TAG, "stopAdvertisingSet() " + binder);
- AdvertiserInfo adv;
- synchronized (mAdvertisers) {
- adv = mAdvertisers.remove(binder);
- }
+ AdvertiserInfo adv = mAdvertisers.remove(binder);
if (adv == null) {
Log.e(TAG, "stopAdvertisingSet() - no client found for callback");
return;
@@ -397,6 +385,7 @@ public class AdvertiseManager {
}
void enableAdvertisingSet(int advertiserId, boolean enable, int duration, int maxExtAdvEvents) {
+ checkThread();
Map.Entry<IBinder, AdvertiserInfo> entry = findAdvertiser(advertiserId);
if (entry == null) {
Log.w(TAG, "enableAdvertisingSet() - bad advertiserId " + advertiserId);
@@ -408,12 +397,13 @@ public class AdvertiseManager {
}
void setAdvertisingData(int advertiserId, AdvertiseData data) {
+ checkThread();
Map.Entry<IBinder, AdvertiserInfo> entry = findAdvertiser(advertiserId);
if (entry == null) {
Log.w(TAG, "setAdvertisingData() - bad advertiserId " + advertiserId);
return;
}
- String deviceName = AdapterService.getAdapterService().getName();
+ String deviceName = mService.getName();
try {
mNativeInterface.setAdvertisingData(
advertiserId, AdvertiseHelper.advertiseDataToBytes(data, deviceName));
@@ -430,12 +420,13 @@ public class AdvertiseManager {
}
void setScanResponseData(int advertiserId, AdvertiseData data) {
+ checkThread();
Map.Entry<IBinder, AdvertiserInfo> entry = findAdvertiser(advertiserId);
if (entry == null) {
Log.w(TAG, "setScanResponseData() - bad advertiserId " + advertiserId);
return;
}
- String deviceName = AdapterService.getAdapterService().getName();
+ String deviceName = mService.getName();
try {
mNativeInterface.setScanResponseData(
advertiserId, AdvertiseHelper.advertiseDataToBytes(data, deviceName));
@@ -452,6 +443,7 @@ public class AdvertiseManager {
}
void setAdvertisingParameters(int advertiserId, AdvertisingSetParameters parameters) {
+ checkThread();
Map.Entry<IBinder, AdvertiserInfo> entry = findAdvertiser(advertiserId);
if (entry == null) {
Log.w(TAG, "setAdvertisingParameters() - bad advertiserId " + advertiserId);
@@ -464,6 +456,7 @@ public class AdvertiseManager {
void setPeriodicAdvertisingParameters(
int advertiserId, PeriodicAdvertisingParameters parameters) {
+ checkThread();
Map.Entry<IBinder, AdvertiserInfo> entry = findAdvertiser(advertiserId);
if (entry == null) {
Log.w(TAG, "setPeriodicAdvertisingParameters() - bad advertiserId " + advertiserId);
@@ -475,12 +468,13 @@ public class AdvertiseManager {
}
void setPeriodicAdvertisingData(int advertiserId, AdvertiseData data) {
+ checkThread();
Map.Entry<IBinder, AdvertiserInfo> entry = findAdvertiser(advertiserId);
if (entry == null) {
Log.w(TAG, "setPeriodicAdvertisingData() - bad advertiserId " + advertiserId);
return;
}
- String deviceName = AdapterService.getAdapterService().getName();
+ String deviceName = mService.getName();
try {
mNativeInterface.setPeriodicAdvertisingData(
advertiserId, AdvertiseHelper.advertiseDataToBytes(data, deviceName));
@@ -497,6 +491,7 @@ public class AdvertiseManager {
}
void setPeriodicAdvertisingEnable(int advertiserId, boolean enable) {
+ checkThread();
Map.Entry<IBinder, AdvertiserInfo> entry = findAdvertiser(advertiserId);
if (entry == null) {
Log.w(TAG, "setPeriodicAdvertisingEnable() - bad advertiserId " + advertiserId);
@@ -505,7 +500,8 @@ public class AdvertiseManager {
mNativeInterface.setPeriodicAdvertisingEnable(advertiserId, enable);
}
- void onAdvertisingDataSet(int advertiserId, int status) throws Exception {
+ void onAdvertisingDataSet(int advertiserId, int status) {
+ checkThread();
Log.d(TAG, "onAdvertisingDataSet() advertiserId=" + advertiserId + ", status=" + status);
Map.Entry<IBinder, AdvertiserInfo> entry = findAdvertiser(advertiserId);
@@ -515,10 +511,11 @@ public class AdvertiseManager {
}
IAdvertisingSetCallback callback = entry.getValue().callback;
- callback.onAdvertisingDataSet(advertiserId, status);
+ sendToCallback(advertiserId, () -> callback.onAdvertisingDataSet(advertiserId, status));
}
- void onScanResponseDataSet(int advertiserId, int status) throws Exception {
+ void onScanResponseDataSet(int advertiserId, int status) {
+ checkThread();
Log.d(TAG, "onScanResponseDataSet() advertiserId=" + advertiserId + ", status=" + status);
Map.Entry<IBinder, AdvertiserInfo> entry = findAdvertiser(advertiserId);
@@ -528,11 +525,10 @@ public class AdvertiseManager {
}
IAdvertisingSetCallback callback = entry.getValue().callback;
- callback.onScanResponseDataSet(advertiserId, status);
+ sendToCallback(advertiserId, () -> callback.onScanResponseDataSet(advertiserId, status));
}
- void onAdvertisingParametersUpdated(int advertiserId, int txPower, int status)
- throws Exception {
+ void onAdvertisingParametersUpdated(int advertiserId, int txPower, int status) {
Log.d(
TAG,
"onAdvertisingParametersUpdated() advertiserId="
@@ -541,6 +537,7 @@ public class AdvertiseManager {
+ txPower
+ ", status="
+ status);
+ checkThread();
Map.Entry<IBinder, AdvertiserInfo> entry = findAdvertiser(advertiserId);
if (entry == null) {
@@ -549,16 +546,19 @@ public class AdvertiseManager {
}
IAdvertisingSetCallback callback = entry.getValue().callback;
- callback.onAdvertisingParametersUpdated(advertiserId, txPower, status);
+ sendToCallback(
+ advertiserId,
+ () -> callback.onAdvertisingParametersUpdated(advertiserId, txPower, status));
}
- void onPeriodicAdvertisingParametersUpdated(int advertiserId, int status) throws Exception {
+ void onPeriodicAdvertisingParametersUpdated(int advertiserId, int status) {
Log.d(
TAG,
"onPeriodicAdvertisingParametersUpdated() advertiserId="
+ advertiserId
+ ", status="
+ status);
+ checkThread();
Map.Entry<IBinder, AdvertiserInfo> entry = findAdvertiser(advertiserId);
if (entry == null) {
@@ -569,16 +569,19 @@ public class AdvertiseManager {
}
IAdvertisingSetCallback callback = entry.getValue().callback;
- callback.onPeriodicAdvertisingParametersUpdated(advertiserId, status);
+ sendToCallback(
+ advertiserId,
+ () -> callback.onPeriodicAdvertisingParametersUpdated(advertiserId, status));
}
- void onPeriodicAdvertisingDataSet(int advertiserId, int status) throws Exception {
+ void onPeriodicAdvertisingDataSet(int advertiserId, int status) {
Log.d(
TAG,
"onPeriodicAdvertisingDataSet() advertiserId="
+ advertiserId
+ ", status="
+ status);
+ checkThread();
Map.Entry<IBinder, AdvertiserInfo> entry = findAdvertiser(advertiserId);
if (entry == null) {
@@ -587,11 +590,11 @@ public class AdvertiseManager {
}
IAdvertisingSetCallback callback = entry.getValue().callback;
- callback.onPeriodicAdvertisingDataSet(advertiserId, status);
+ sendToCallback(
+ advertiserId, () -> callback.onPeriodicAdvertisingDataSet(advertiserId, status));
}
- void onPeriodicAdvertisingEnabled(int advertiserId, boolean enable, int status)
- throws Exception {
+ void onPeriodicAdvertisingEnabled(int advertiserId, boolean enable, int status) {
Log.d(
TAG,
"onPeriodicAdvertisingEnabled() advertiserId="
@@ -604,9 +607,12 @@ public class AdvertiseManager {
Log.i(TAG, "onAdvertisingSetEnable() - bad advertiserId " + advertiserId);
return;
}
+ checkThread();
IAdvertisingSetCallback callback = entry.getValue().callback;
- callback.onPeriodicAdvertisingEnabled(advertiserId, enable, status);
+ sendToCallback(
+ advertiserId,
+ () -> callback.onPeriodicAdvertisingEnabled(advertiserId, enable, status));
AppAdvertiseStats stats = mAdvertiserMap.getAppAdvertiseStatsById(advertiserId);
if (stats != null) {
@@ -614,4 +620,52 @@ public class AdvertiseManager {
}
}
+ void doOnAdvertiseThread(Runnable r) {
+ if (mIsAvailable) {
+ if (Flags.advertiseThread()) {
+ mHandler.post(
+ () -> {
+ if (mIsAvailable) {
+ r.run();
+ }
+ });
+ } else {
+ r.run();
+ }
+ }
+ }
+
+ private void forceRunSyncOnAdvertiseThread(Runnable r) {
+ if (!Flags.advertiseThread()) {
+ r.run();
+ return;
+ }
+ final CompletableFuture<Void> future = new CompletableFuture<>();
+ mHandler.postAtFrontOfQueue(
+ () -> {
+ r.run();
+ future.complete(null);
+ });
+ try {
+ future.get(RUN_SYNC_WAIT_TIME_MS, TimeUnit.MILLISECONDS);
+ } catch (InterruptedException | TimeoutException | ExecutionException e) {
+ Log.w(TAG, "Unable to complete sync task: " + e);
+ }
+ }
+
+ private void checkThread() {
+ if (Flags.advertiseThread()
+ && !mHandler.getLooper().isCurrentThread()
+ && !Utils.isInstrumentationTestMode()) {
+ throw new IllegalStateException("Not on advertise thread");
+ }
+ }
+
+ private void sendToCallback(int advertiserId, CallbackWrapper wrapper) {
+ try {
+ wrapper.call();
+ } catch (RemoteException e) {
+ Log.i(TAG, "RemoteException in callback for advertiserId: " + advertiserId);
+ }
+ }
}
diff --git a/android/app/src/com/android/bluetooth/gatt/AdvertiseManagerNativeInterface.java b/android/app/src/com/android/bluetooth/gatt/AdvertiseManagerNativeInterface.java
index 215c709970..6cdd47f97e 100644
--- a/android/app/src/com/android/bluetooth/gatt/AdvertiseManagerNativeInterface.java
+++ b/android/app/src/com/android/bluetooth/gatt/AdvertiseManagerNativeInterface.java
@@ -19,11 +19,12 @@ package com.android.bluetooth.gatt;
import android.bluetooth.le.AdvertisingSetParameters;
import android.bluetooth.le.PeriodicAdvertisingParameters;
+import androidx.annotation.VisibleForTesting;
+
import com.android.internal.annotations.GuardedBy;
-import com.android.internal.annotations.VisibleForTesting;
/** Native interface for AdvertiseManager */
-@VisibleForTesting
+@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
public class AdvertiseManagerNativeInterface {
private static final String TAG = AdvertiseManagerNativeInterface.class.getSimpleName();
@@ -121,43 +122,47 @@ public class AdvertiseManagerNativeInterface {
setPeriodicAdvertisingEnableNative(advertiserId, enable);
}
- void onAdvertisingSetStarted(int regId, int advertiserId, int txPower, int status)
- throws Exception {
- mManager.onAdvertisingSetStarted(regId, advertiserId, txPower, status);
+ void onAdvertisingSetStarted(int regId, int advertiserId, int txPower, int status) {
+ mManager.doOnAdvertiseThread(
+ () -> mManager.onAdvertisingSetStarted(regId, advertiserId, txPower, status));
}
- void onOwnAddressRead(int advertiserId, int addressType, String address) throws Exception {
- mManager.onOwnAddressRead(advertiserId, addressType, address);
+ void onOwnAddressRead(int advertiserId, int addressType, String address) {
+ mManager.doOnAdvertiseThread(
+ () -> mManager.onOwnAddressRead(advertiserId, addressType, address));
}
- void onAdvertisingEnabled(int advertiserId, boolean enable, int status) throws Exception {
- mManager.onAdvertisingEnabled(advertiserId, enable, status);
+ void onAdvertisingEnabled(int advertiserId, boolean enable, int status) {
+ mManager.doOnAdvertiseThread(
+ () -> mManager.onAdvertisingEnabled(advertiserId, enable, status));
}
- void onAdvertisingDataSet(int advertiserId, int status) throws Exception {
- mManager.onAdvertisingDataSet(advertiserId, status);
+ void onAdvertisingDataSet(int advertiserId, int status) {
+ mManager.doOnAdvertiseThread(() -> mManager.onAdvertisingDataSet(advertiserId, status));
}
- void onScanResponseDataSet(int advertiserId, int status) throws Exception {
- mManager.onScanResponseDataSet(advertiserId, status);
+ void onScanResponseDataSet(int advertiserId, int status) {
+ mManager.doOnAdvertiseThread(() -> mManager.onScanResponseDataSet(advertiserId, status));
}
- void onAdvertisingParametersUpdated(int advertiserId, int txPower, int status)
- throws Exception {
- mManager.onAdvertisingParametersUpdated(advertiserId, txPower, status);
+ void onAdvertisingParametersUpdated(int advertiserId, int txPower, int status) {
+ mManager.doOnAdvertiseThread(
+ () -> mManager.onAdvertisingParametersUpdated(advertiserId, txPower, status));
}
- void onPeriodicAdvertisingParametersUpdated(int advertiserId, int status) throws Exception {
- mManager.onPeriodicAdvertisingParametersUpdated(advertiserId, status);
+ void onPeriodicAdvertisingParametersUpdated(int advertiserId, int status) {
+ mManager.doOnAdvertiseThread(
+ () -> mManager.onPeriodicAdvertisingParametersUpdated(advertiserId, status));
}
- void onPeriodicAdvertisingDataSet(int advertiserId, int status) throws Exception {
- mManager.onPeriodicAdvertisingDataSet(advertiserId, status);
+ void onPeriodicAdvertisingDataSet(int advertiserId, int status) {
+ mManager.doOnAdvertiseThread(
+ () -> mManager.onPeriodicAdvertisingDataSet(advertiserId, status));
}
- void onPeriodicAdvertisingEnabled(int advertiserId, boolean enable, int status)
- throws Exception {
- mManager.onPeriodicAdvertisingEnabled(advertiserId, enable, status);
+ void onPeriodicAdvertisingEnabled(int advertiserId, boolean enable, int status) {
+ mManager.doOnAdvertiseThread(
+ () -> mManager.onPeriodicAdvertisingEnabled(advertiserId, enable, status));
}
private native void initializeNative();
diff --git a/android/app/src/com/android/bluetooth/gatt/GattService.java b/android/app/src/com/android/bluetooth/gatt/GattService.java
index 7773ff9f43..e9563f8908 100644
--- a/android/app/src/com/android/bluetooth/gatt/GattService.java
+++ b/android/app/src/com/android/bluetooth/gatt/GattService.java
@@ -51,6 +51,7 @@ import android.content.pm.PackageManager.PackageInfoFlags;
import android.content.res.Resources;
import android.os.Binder;
import android.os.Build;
+import android.os.HandlerThread;
import android.os.IBinder;
import android.os.ParcelUuid;
import android.os.RemoteException;
@@ -119,6 +120,9 @@ public class GattService extends ProfileService {
private static final Map<String, Integer> EARLY_MTU_EXCHANGE_PACKAGES =
Map.of("com.teslamotors", GATT_MTU_MAX);
+ private static final Map<String, String> GATT_CLIENTS_NOTIFY_TO_ADAPTER_PACKAGES =
+ Map.of("com.google.android.gms", "com.google.android.gms.findmydevice");
+
@VisibleForTesting static final int GATT_CLIENT_LIMIT_PER_APP = 32;
@Nullable public final ScanController mScanController;
@@ -153,6 +157,7 @@ public class GattService extends ProfileService {
private final DistanceMeasurementManager mDistanceMeasurementManager;
private final ActivityManager mActivityManager;
private final PackageManager mPackageManager;
+ private final HandlerThread mHandlerThread;
public GattService(AdapterService adapterService) {
super(requireNonNull(adapterService));
@@ -166,7 +171,12 @@ public class GattService extends ProfileService {
mNativeInterface = GattObjectsFactory.getInstance().getNativeInterface();
mNativeInterface.init(this);
- mAdvertiseManager = new AdvertiseManager(this);
+
+ // Create a thread to handle LE operations
+ mHandlerThread = new HandlerThread("Bluetooth LE");
+ mHandlerThread.start();
+
+ mAdvertiseManager = new AdvertiseManager(mAdapterService, mHandlerThread.getLooper());
if (!Flags.scanManagerRefactor()) {
mScanController = new ScanController(adapterService);
@@ -206,7 +216,6 @@ public class GattService extends ProfileService {
if (mScanController != null) {
mScanController.stop();
}
- mAdvertiseManager.clear();
mClientMap.clear();
mRestrictedHandles.clear();
mServerMap.clear();
@@ -221,6 +230,7 @@ public class GattService extends ProfileService {
mNativeInterface.cleanup();
mAdvertiseManager.cleanup();
mDistanceMeasurementManager.cleanup();
+ mHandlerThread.quit();
}
/** This is only used when Flags.scanManagerRefactor() is true. */
@@ -857,7 +867,9 @@ public class GattService extends ProfileService {
+ ", connId="
+ connId
+ ", address="
- + BluetoothUtils.toAnonymizedAddress(address));
+ + BluetoothUtils.toAnonymizedAddress(address)
+ + ", status="
+ + status);
int connectionState = BluetoothProtoEnums.CONNECTION_STATE_DISCONNECTED;
if (status == 0) {
mClientMap.addConnection(clientIf, connId, address);
@@ -871,7 +883,10 @@ public class GattService extends ProfileService {
mPermits.putIfAbsent(address, -1);
}
connectionState = BluetoothProtoEnums.CONNECTION_STATE_CONNECTED;
+ } else {
+ mAdapterService.notifyGattClientConnectFailed(clientIf, getDevice(address));
}
+
ContextMap<IBluetoothGattCallback>.App app = mClientMap.getById(clientIf);
if (app != null) {
app.callback.onClientConnectionState(
@@ -900,6 +915,7 @@ public class GattService extends ProfileService {
+ BluetoothUtils.toAnonymizedAddress(address));
mClientMap.removeConnection(clientIf, connId);
+ mAdapterService.notifyGattClientDisconnect(clientIf, getDevice(address));
ContextMap<IBluetoothGattCallback>.App app = mClientMap.getById(clientIf);
mRestrictedHandles.remove(connId);
@@ -1496,6 +1512,10 @@ public class GattService extends ProfileService {
unregisterClient(
appId, attributionSource, ContextMap.RemoveReason.REASON_UNREGISTER_ALL);
}
+ for (Integer appId : mServerMap.getAllAppsIds()) {
+ Log.d(TAG, "unreg:" + appId);
+ unregisterServer(appId, attributionSource);
+ }
}
/**************************************************************************
@@ -1631,6 +1651,21 @@ public class GattService extends ProfileService {
}
}
+ if (transport != BluetoothDevice.TRANSPORT_BREDR && isDirect && !opportunistic) {
+ String attributionTag = getLastAttributionTag(attributionSource);
+ if (packageName != null && attributionTag != null) {
+ for (Map.Entry<String, String> entry :
+ GATT_CLIENTS_NOTIFY_TO_ADAPTER_PACKAGES.entrySet()) {
+ if (packageName.contains(entry.getKey())
+ && attributionTag.contains(entry.getValue())) {
+ mAdapterService.notifyDirectLeGattClientConnect(
+ clientIf, getDevice(address));
+ break;
+ }
+ }
+ }
+ }
+
mNativeInterface.gattClientConnect(
clientIf,
address,
@@ -1669,6 +1704,9 @@ public class GattService extends ProfileService {
.BLUETOOTH_CROSS_LAYER_EVENT_REPORTED__EVENT_TYPE__GATT_DISCONNECT_JAVA,
BluetoothStatsLog.BLUETOOTH_CROSS_LAYER_EVENT_REPORTED__STATE__START,
attributionSource.getUid());
+
+ mAdapterService.notifyGattClientDisconnect(clientIf, getDevice(address));
+
mNativeInterface.gattClientDisconnect(clientIf, address, connId != null ? connId : 0);
}
diff --git a/android/app/src/com/android/bluetooth/hfp/HeadsetService.java b/android/app/src/com/android/bluetooth/hfp/HeadsetService.java
index 949db48483..179b7fafb0 100644
--- a/android/app/src/com/android/bluetooth/hfp/HeadsetService.java
+++ b/android/app/src/com/android/bluetooth/hfp/HeadsetService.java
@@ -1502,9 +1502,6 @@ public class HeadsetService extends ProfileService {
} else {
broadcastActiveDevice(mActiveDevice);
}
- if (Flags.updateActiveDeviceInBandRingtone()) {
- updateInbandRinging(device, true);
- }
} else if (shouldPersistAudio()) {
if (Utils.isScoManagedByAudioEnabled()) {
// tell Audio Framework that active device changed
@@ -1546,9 +1543,9 @@ public class HeadsetService extends ProfileService {
} else {
broadcastActiveDevice(mActiveDevice);
}
- if (Flags.updateActiveDeviceInBandRingtone()) {
- updateInbandRinging(device, true);
- }
+ }
+ if (Flags.updateActiveDeviceInBandRingtone()) {
+ updateInbandRinging(device, true);
}
}
return true;
diff --git a/android/app/src/com/android/bluetooth/hfp/HeadsetStateMachine.java b/android/app/src/com/android/bluetooth/hfp/HeadsetStateMachine.java
index fd56faa613..fb0ed9d2f0 100644
--- a/android/app/src/com/android/bluetooth/hfp/HeadsetStateMachine.java
+++ b/android/app/src/com/android/bluetooth/hfp/HeadsetStateMachine.java
@@ -55,7 +55,6 @@ import com.android.bluetooth.btservice.AdapterService;
import com.android.bluetooth.btservice.MetricsLogger;
import com.android.bluetooth.btservice.ProfileService;
import com.android.bluetooth.btservice.storage.DatabaseManager;
-import com.android.bluetooth.flags.Flags;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.State;
import com.android.internal.util.StateMachine;
@@ -1122,12 +1121,6 @@ class HeadsetStateMachine extends StateMachine {
}
}
break;
- case INTENT_SCO_VOLUME_CHANGED:
- if (Flags.hfpAllowVolumeChangeWithoutSco()) {
- // when flag is removed, remove INTENT_SCO_VOLUME_CHANGED case in AudioOn
- processIntentScoVolume((Intent) message.obj, mDevice);
- }
- break;
case INTENT_CONNECTION_ACCESS_REPLY:
handleAccessPermissionResult((Intent) message.obj);
break;
@@ -1630,8 +1623,6 @@ class HeadsetStateMachine extends StateMachine {
break;
}
case INTENT_SCO_VOLUME_CHANGED:
- // TODO: b/362313390 Remove this case once the fix is in place because this
- // message will be handled by the ConnectedBase state.
processIntentScoVolume((Intent) message.obj, mDevice);
break;
case STACK_EVENT:
diff --git a/android/app/src/com/android/bluetooth/le_audio/LeAudioService.java b/android/app/src/com/android/bluetooth/le_audio/LeAudioService.java
index 8c9ca8735b..bb5346fe6e 100644
--- a/android/app/src/com/android/bluetooth/le_audio/LeAudioService.java
+++ b/android/app/src/com/android/bluetooth/le_audio/LeAudioService.java
@@ -102,6 +102,7 @@ import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
+import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -142,6 +143,9 @@ public class LeAudioService extends ProfileService {
/** Indicates group is active */
private static final int ACTIVE_STATE_ACTIVE = 0x02;
+ /** Filter for Targeted Announcements */
+ static final byte[] CAP_TARGETED_ANNOUNCEMENT_PAYLOAD = new byte[] {0x01};
+
/** This is used by application read-only for checking the fallback active group id. */
public static final String BLUETOOTH_LE_BROADCAST_FALLBACK_ACTIVE_GROUP_ID =
"bluetooth_le_broadcast_fallback_active_group_id";
@@ -255,6 +259,7 @@ public class LeAudioService extends ProfileService {
mInputSelectableConfig = new ArrayList<>();
mOutputSelectableConfig = new ArrayList<>();
mInactivatedDueToContextType = false;
+ mAutoActiveModeEnabled = true;
}
Integer mGroupId;
@@ -270,6 +275,7 @@ public class LeAudioService extends ProfileService {
List<BluetoothLeAudioCodecConfig> mInputSelectableConfig;
List<BluetoothLeAudioCodecConfig> mOutputSelectableConfig;
Boolean mInactivatedDueToContextType;
+ Boolean mAutoActiveModeEnabled;
private Integer mActiveState;
private Integer mAllowedSinkContexts;
@@ -816,7 +822,7 @@ public class LeAudioService extends ProfileService {
}
@VisibleForTesting
- static synchronized void setLeAudioService(LeAudioService instance) {
+ public static synchronized void setLeAudioService(LeAudioService instance) {
Log.d(TAG, "setLeAudioService(): set to: " + instance);
sLeAudioService = instance;
}
@@ -1650,6 +1656,31 @@ public class LeAudioService extends ProfileService {
&& groupId == mUnicastGroupIdDeactivatedForBroadcastTransition;
}
+ /** Get local broadcast receiving devices */
+ public Set<BluetoothDevice> getLocalBroadcastReceivers() {
+ if (mBroadcastDescriptors == null) {
+ Log.e(TAG, "getLocalBroadcastReceivers: Invalid Broadcast Descriptors");
+ return Collections.emptySet();
+ }
+
+ BassClientService bassClientService = getBassClientService();
+ if (bassClientService == null) {
+ Log.e(TAG, "getLocalBroadcastReceivers: Bass service not available");
+ return Collections.emptySet();
+ }
+
+ Set<BluetoothDevice> deviceList = new HashSet<>();
+ for (Map.Entry<Integer, LeAudioBroadcastDescriptor> entry :
+ mBroadcastDescriptors.entrySet()) {
+ if (!entry.getValue().mState.equals(LeAudioStackEvent.BROADCAST_STATE_STOPPED)) {
+ List<BluetoothDevice> devices =
+ bassClientService.getSyncedBroadcastSinks(entry.getKey());
+ deviceList.addAll(devices);
+ }
+ }
+ return deviceList;
+ }
+
private boolean areBroadcastsAllStopped() {
if (mBroadcastDescriptors == null) {
Log.e(TAG, "areBroadcastsAllStopped: Invalid Broadcast Descriptors");
@@ -1987,12 +2018,37 @@ public class LeAudioService extends ProfileService {
mExposedActiveDevice = device;
}
+ boolean isAnyGroupDisabledFromAutoActiveMode() {
+ mGroupReadLock.lock();
+ try {
+ for (Map.Entry<Integer, LeAudioGroupDescriptor> groupEntry :
+ mGroupDescriptorsView.entrySet()) {
+ LeAudioGroupDescriptor groupDescriptor = groupEntry.getValue();
+ if (!groupDescriptor.mAutoActiveModeEnabled) {
+ Log.d(
+ TAG,
+ "isAnyGroupDisabledFromAutoActiveMode: disabled groupId "
+ + groupEntry.getKey());
+ return true;
+ }
+ }
+ } finally {
+ mGroupReadLock.unlock();
+ }
+ return false;
+ }
+
boolean isScannerNeeded() {
if (mDeviceDescriptors.isEmpty() || !mBluetoothEnabled) {
Log.d(TAG, "isScannerNeeded: false, mBluetoothEnabled: " + mBluetoothEnabled);
return false;
}
+ if (isAnyGroupDisabledFromAutoActiveMode()) {
+ Log.d(TAG, "isScannerNeeded true, some group has disabled Auto Active Mode");
+ return true;
+ }
+
if (allLeAudioDevicesConnected()) {
Log.d(TAG, "isScannerNeeded: all devices connected, scanner not needed");
return false;
@@ -2060,9 +2116,7 @@ public class LeAudioService extends ProfileService {
.getBluetoothScanController()
.stopScanInternal(mScannerId);
- mAdapterService
- .getBluetoothScanController()
- .unregisterScannerInternal(mScannerId);
+ mAdapterService.getBluetoothScanController().unregisterScannerInternal(mScannerId);
mScannerId = SCANNER_NOT_INITIALIZED;
}
@@ -2075,18 +2129,15 @@ public class LeAudioService extends ProfileService {
}
mScannerId = scannerId;
- /* Filter we are building here will not match to anything.
- * Eventually we should be able to start scan from native when
- * b/276350722 is done
- */
ScanFilter filter =
new ScanFilter.Builder()
- .setServiceData(BluetoothUuid.LE_AUDIO, new byte[] {0x11})
+ .setServiceData(BluetoothUuid.CAP, CAP_TARGETED_ANNOUNCEMENT_PAYLOAD)
.build();
ScanSettings settings =
new ScanSettings.Builder()
.setLegacy(false)
+ .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES)
.setScanMode(ScanSettings.SCAN_MODE_BALANCED)
.setPhy(BluetoothDevice.PHY_LE_1M)
.build();
@@ -2096,10 +2147,28 @@ public class LeAudioService extends ProfileService {
.startScanInternal(scannerId, settings, List.of(filter));
}
- // Eventually we should be able to start scan from native when b/276350722 is done
- // All the result returned here are ignored
@Override
- public void onScanResult(ScanResult scanResult) {}
+ public void onScanResult(ScanResult scanResult) {
+ Log.d(TAG, "onScanResult: " + scanResult.getDevice());
+ BluetoothDevice device = scanResult.getDevice();
+ if (device == null) {
+ return;
+ }
+
+ int groupId = getGroupId(device);
+ LeAudioGroupDescriptor descriptor = getGroupDescriptor(groupId);
+ if (descriptor == null) {
+ return;
+ }
+
+ if (!descriptor.mAutoActiveModeEnabled) {
+ Log.i(TAG, "onScanResult: GroupId: " + groupId + " is getting Active");
+ descriptor.mAutoActiveModeEnabled = true;
+ if (!getConnectedPeerDevices(groupId).isEmpty()) {
+ setActiveDevice(device);
+ }
+ }
+ }
@Override
public void onBatchScanResults(List<ScanResult> batchResults) {}
@@ -2398,6 +2467,25 @@ public class LeAudioService extends ProfileService {
}
}
+ private void clearAutoActiveModeToDefault() {
+ mGroupReadLock.lock();
+ try {
+ for (Map.Entry<Integer, LeAudioGroupDescriptor> groupEntry :
+ mGroupDescriptorsView.entrySet()) {
+ LeAudioGroupDescriptor groupDescriptor = groupEntry.getValue();
+ if (!groupDescriptor.mAutoActiveModeEnabled) {
+ Log.d(
+ TAG,
+ "mAutoActiveModeEnabled back to default for groupId: "
+ + groupEntry.getKey());
+ groupDescriptor.mAutoActiveModeEnabled = true;
+ }
+ }
+ } finally {
+ mGroupReadLock.unlock();
+ }
+ }
+
/**
* Set the active device group.
*
@@ -2416,6 +2504,9 @@ public class LeAudioService extends ProfileService {
groupId = descriptor.mGroupId;
+ /* User force device being active, clear the flag */
+ clearAutoActiveModeToDefault();
+
if (!isGroupAvailableForStream(groupId)) {
Log.e(
TAG,
@@ -2442,11 +2533,10 @@ public class LeAudioService extends ProfileService {
+ ", mExposedActiveDevice: "
+ mExposedActiveDevice);
- if (!Flags.leaudioBroadcastPrimaryGroupSelection()
- && isBroadcastActive()
- && currentlyActiveGroupId == LE_AUDIO_GROUP_ID_INVALID
- && mUnicastGroupIdDeactivatedForBroadcastTransition != LE_AUDIO_GROUP_ID_INVALID) {
-
+ /* Replace fallback unicast and monitoring input device if device is active local
+ * broadcaster.
+ */
+ if (isAnyBroadcastInStreamingState()) {
LeAudioGroupDescriptor fallbackGroupDescriptor = getGroupDescriptor(groupId);
// If broadcast is ongoing and need to update unicast fallback active group
@@ -3125,12 +3215,16 @@ public class LeAudioService extends ProfileService {
}
}
+ private boolean isAnyBroadcastInStreamingState() {
+ return mBroadcastDescriptors.values().stream()
+ .anyMatch(d -> d.mState.equals(LeAudioStackEvent.BROADCAST_STATE_STREAMING));
+ }
+
void transitionFromBroadcastToUnicast() {
if (mUnicastGroupIdDeactivatedForBroadcastTransition == LE_AUDIO_GROUP_ID_INVALID) {
Log.d(TAG, "No deactivated group due for broadcast transmission");
// Notify audio manager
- if (mBroadcastDescriptors.values().stream()
- .noneMatch(d -> d.mState.equals(LeAudioStackEvent.BROADCAST_STATE_STREAMING))) {
+ if (!isAnyBroadcastInStreamingState()) {
updateBroadcastActiveDevice(null, mActiveBroadcastAudioDevice, false);
}
return;
@@ -3896,11 +3990,7 @@ public class LeAudioService extends ProfileService {
}
// Notify audio manager
- if (mBroadcastDescriptors.values().stream()
- .anyMatch(
- d ->
- d.mState.equals(
- LeAudioStackEvent.BROADCAST_STATE_STREAMING))) {
+ if (isAnyBroadcastInStreamingState()) {
if (!Objects.equals(device, mActiveBroadcastAudioDevice)) {
updateBroadcastActiveDevice(device, mActiveBroadcastAudioDevice, true);
}
@@ -4189,6 +4279,7 @@ public class LeAudioService extends ProfileService {
if (getConnectedPeerDevices(groupId).isEmpty()) {
descriptor.mIsConnected = false;
+ descriptor.mAutoActiveModeEnabled = true;
descriptor.mAvailableContexts = Flags.leaudioUnicastNoAvailableContexts() ? null : 0;
if (descriptor.isActive()) {
/* Notify Native layer */
@@ -4531,6 +4622,91 @@ public class LeAudioService extends ProfileService {
}
/**
+ * Set auto active mode state.
+ *
+ * <p>Auto Active Mode by default is set to true and it means, that after ACL connection is
+ * created to LeAudio device which is part of the group, can be connected to Audio Framework
+ * (set as Active).
+ *
+ * <p>If Auto Active Mode is set to false, it means that after LeAudio device is connected for
+ * given group, the function isGroupAvailableForStream(groupId) will return false and
+ * ActiveDeviceManager will not make this group active.
+ *
+ * <p>This mode can change internally when two things happen: 1. LeAudioService detects Targeted
+ * Announcements from the device which belongs to the group.
+ * 2. @BluetoothLeAudio.setActiveDevice() is called with a device which belongs to the group.
+ *
+ * <p>Note: Auto Active Mode can be disabled only when all devices from the group are
+ * disconnected.
+ *
+ * @param groupId LeAudio group id which Auto Active Mode should be changed.
+ * @param enabled true when Auto Active Mode should be enabled (default value), false otherwise.
+ * @return true when Auto Active Mode is set, false otherwise
+ */
+ public boolean setAutoActiveModeState(int groupId, boolean enabled) {
+ Log.d(TAG, "setAutoActiveModeState: groupId: " + groupId + " enabled: " + enabled);
+
+ mGroupReadLock.lock();
+ try {
+ LeAudioGroupDescriptor descriptor = getGroupDescriptor(groupId);
+ if (descriptor == null) {
+ Log.i(
+ TAG,
+ "setAutoActiveModeState: groupId: "
+ + groupId
+ + " is not known LeAudio group");
+ return false;
+ }
+
+ /* Disabling Auto Active Mode is allowed only when all the devices from the group
+ * are disconnected */
+ if (!enabled && descriptor.mIsConnected) {
+ Log.i(
+ TAG,
+ "setAutoActiveModeState: GroupId: " + groupId + " is already connected ");
+ return false;
+ }
+
+ Log.i(TAG, "setAutoActiveModeState: groupId: " + groupId + ", enabled: " + enabled);
+ descriptor.mAutoActiveModeEnabled = enabled;
+ return true;
+ } finally {
+ mGroupReadLock.unlock();
+ }
+ }
+
+ /**
+ * Is Auto Active Mode enabled
+ *
+ * @param groupId LeAudio group id which Auto Active Mode should be taken.
+ * @return true when Auto Active Mode is enabled, false otherwise
+ */
+ public boolean isAutoActiveModeEnabled(int groupId) {
+ mGroupReadLock.lock();
+ try {
+ LeAudioGroupDescriptor descriptor = getGroupDescriptor(groupId);
+ if (descriptor == null) {
+ Log.i(
+ TAG,
+ "isAutoActiveModeEnabled: groupId: "
+ + groupId
+ + " is not known LeAudio group");
+ return false;
+ }
+ Log.i(
+ TAG,
+ "isAutoActiveModeEnabled: groupId: "
+ + groupId
+ + ", mAutoActiveModeEnabled: "
+ + descriptor.mAutoActiveModeEnabled);
+ return descriptor.mAutoActiveModeEnabled;
+
+ } finally {
+ mGroupReadLock.unlock();
+ }
+ }
+
+ /**
* Check if group is available for streaming. If there is no available context types then group
* is not available for streaming.
*
@@ -4542,9 +4718,21 @@ public class LeAudioService extends ProfileService {
try {
LeAudioGroupDescriptor descriptor = getGroupDescriptor(groupId);
if (descriptor == null) {
- Log.e(TAG, "getGroupId: No valid descriptor for groupId: " + groupId);
+ Log.e(
+ TAG,
+ "isGroupAvailableForStream: No valid descriptor for groupId: " + groupId);
return false;
}
+
+ if (!descriptor.mAutoActiveModeEnabled) {
+ Log.e(
+ TAG,
+ "isGroupAvailableForStream: Auto Active Mode is disabled for groupId: "
+ + groupId);
+ return false;
+ }
+
+ Log.i(TAG, " descriptor.mAvailableContexts: " + descriptor.mAvailableContexts);
return descriptor.mAvailableContexts != null && descriptor.mAvailableContexts != 0;
} finally {
mGroupReadLock.unlock();
@@ -6056,6 +6244,8 @@ public class LeAudioService extends ProfileService {
sb,
"mInactivatedDueToContextType: "
+ groupDescriptor.mInactivatedDueToContextType);
+ ProfileService.println(
+ sb, "mAutoActiveModeEnabled: " + groupDescriptor.mAutoActiveModeEnabled);
for (Map.Entry<BluetoothDevice, LeAudioDeviceDescriptor> deviceEntry :
mDeviceDescriptors.entrySet()) {
diff --git a/android/app/src/com/android/bluetooth/le_scan/ScanController.java b/android/app/src/com/android/bluetooth/le_scan/ScanController.java
index a8f5a4025e..7f39fdaaf1 100644
--- a/android/app/src/com/android/bluetooth/le_scan/ScanController.java
+++ b/android/app/src/com/android/bluetooth/le_scan/ScanController.java
@@ -91,7 +91,7 @@ public class ScanController {
private static final int TRUNCATED_RESULT_SIZE = 11;
/** The default floor value for LE batch scan report delays greater than 0 */
- @VisibleForTesting static final long DEFAULT_REPORT_DELAY_FLOOR = 5000;
+ static final long DEFAULT_REPORT_DELAY_FLOOR = 5000L;
private static final int NUM_SCAN_EVENTS_KEPT = 20;
@@ -759,17 +759,19 @@ public class ScanController {
}
}
}
- if (permittedResults.isEmpty()) {
- return;
- }
}
if (client.hasDisavowedLocation) {
permittedResults.removeIf(mLocationDenylistPredicate);
}
+ if (permittedResults.isEmpty()) {
+ mScanManager.callbackDone(scannerId, status);
+ return;
+ }
if (app.mCallback != null) {
app.mCallback.onBatchScanResults(permittedResults);
+ mScanManager.batchScanResultDelivered();
} else {
// PendingIntent based
try {
@@ -791,6 +793,9 @@ public class ScanController {
@SuppressWarnings("NonApiType")
private void sendBatchScanResults(
ScannerMap.ScannerApp app, ScanClient client, ArrayList<ScanResult> results) {
+ if (results.isEmpty()) {
+ return;
+ }
try {
if (app.mCallback != null) {
if (mScanManager.isAutoBatchScanClientEnabled(client)) {
@@ -811,6 +816,7 @@ public class ScanController {
Log.e(TAG, "Exception: " + e);
handleDeadScanClient(client);
}
+ mScanManager.batchScanResultDelivered();
}
// Check and deliver scan results for different scan clients.
@@ -833,14 +839,11 @@ public class ScanController {
}
}
}
- if (permittedResults.isEmpty()) {
- return;
- }
}
if (client.filters == null || client.filters.isEmpty()) {
sendBatchScanResults(app, client, permittedResults);
- // TODO: Question to reviewer: Shouldn't there be a return here?
+ return;
}
// Reconstruct the scan results.
ArrayList<ScanResult> results = new ArrayList<ScanResult>();
diff --git a/android/app/src/com/android/bluetooth/le_scan/ScanManager.java b/android/app/src/com/android/bluetooth/le_scan/ScanManager.java
index b228bf5fc8..e946b3afcf 100644
--- a/android/app/src/com/android/bluetooth/le_scan/ScanManager.java
+++ b/android/app/src/com/android/bluetooth/le_scan/ScanManager.java
@@ -146,6 +146,7 @@ public class ScanManager {
@VisibleForTesting boolean mIsConnecting;
@VisibleForTesting int mProfilesConnecting;
private int mProfilesConnected, mProfilesDisconnecting;
+ private final BatchScanThrottler mBatchScanThrottler;
@VisibleForTesting
static class UidImportance {
@@ -158,7 +159,7 @@ public class ScanManager {
}
}
- public ScanManager(
+ ScanManager(
AdapterService adapterService,
ScanController scanController,
BluetoothAdapterProxy bluetoothAdapterProxy,
@@ -202,13 +203,13 @@ public class ScanManager {
IntentFilter locationIntentFilter = new IntentFilter(LocationManager.MODE_CHANGED_ACTION);
locationIntentFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
mAdapterService.registerReceiver(mLocationReceiver, locationIntentFilter);
+ mBatchScanThrottler = new BatchScanThrottler(timeProvider, mScreenOn);
}
- public void cleanup() {
+ void cleanup() {
mRegularScanClients.clear();
mBatchClients.clear();
mSuspendedScanClients.clear();
- mScanNative.cleanup();
if (mActivityManager != null) {
try {
@@ -225,6 +226,8 @@ public class ScanManager {
// Shut down the thread
mHandler.removeCallbacksAndMessages(null);
+ mScanNative.cleanup();
+
try {
mAdapterService.unregisterReceiver(mLocationReceiver);
} catch (IllegalArgumentException e) {
@@ -232,16 +235,16 @@ public class ScanManager {
}
}
- public void registerScanner(UUID uuid) {
+ void registerScanner(UUID uuid) {
mScanNative.registerScanner(uuid.getLeastSignificantBits(), uuid.getMostSignificantBits());
}
- public void unregisterScanner(int scannerId) {
+ void unregisterScanner(int scannerId) {
mScanNative.unregisterScanner(scannerId);
}
/** Returns the regular scan queue. */
- public Set<ScanClient> getRegularScanQueue() {
+ Set<ScanClient> getRegularScanQueue() {
return mRegularScanClients;
}
@@ -251,12 +254,12 @@ public class ScanManager {
}
/** Returns batch scan queue. */
- public Set<ScanClient> getBatchScanQueue() {
+ Set<ScanClient> getBatchScanQueue() {
return mBatchClients;
}
/** Returns a set of full batch scan clients. */
- public Set<ScanClient> getFullBatchScanQueue() {
+ Set<ScanClient> getFullBatchScanQueue() {
// TODO: split full batch scan clients and truncated batch clients so we don't need to
// construct this every time.
Set<ScanClient> fullBatchClients = new HashSet<ScanClient>();
@@ -268,12 +271,12 @@ public class ScanManager {
return fullBatchClients;
}
- public void startScan(ScanClient client) {
+ void startScan(ScanClient client) {
Log.d(TAG, "startScan() " + client);
sendMessage(MSG_START_BLE_SCAN, client);
}
- public void stopScan(int scannerId) {
+ void stopScan(int scannerId) {
ScanClient client = mScanNative.getBatchScanClient(scannerId);
if (client == null) {
client = mScanNative.getRegularScanClient(scannerId);
@@ -284,14 +287,18 @@ public class ScanManager {
sendMessage(MSG_STOP_BLE_SCAN, client);
}
- public void flushBatchScanResults(ScanClient client) {
+ void flushBatchScanResults(ScanClient client) {
sendMessage(MSG_FLUSH_BATCH_RESULTS, client);
}
- public void callbackDone(int scannerId, int status) {
+ void callbackDone(int scannerId, int status) {
mScanNative.callbackDone(scannerId, status);
}
+ void batchScanResultDelivered() {
+ mBatchScanThrottler.resetBackoff();
+ }
+
private void sendMessage(int what, ScanClient client) {
mHandler.obtainMessage(what, client).sendToTarget();
}
@@ -304,10 +311,16 @@ public class ScanManager {
return mBluetoothAdapterProxy.isOffloadedScanFilteringSupported();
}
- public boolean isAutoBatchScanClientEnabled(ScanClient client) {
+ boolean isAutoBatchScanClientEnabled(ScanClient client) {
return mScanNative.isAutoBatchScanClientEnabled(client);
}
+ int getCurrentUsedTrackingAdvertisement() {
+ synchronized (mCurUsedTrackableAdvertisementsLock) {
+ return mCurUsedTrackableAdvertisements;
+ }
+ }
+
// Handler class that handles BLE scan operations.
@VisibleForTesting
class ClientHandler extends Handler {
@@ -533,6 +546,7 @@ public class ScanManager {
}
mScreenOn = false;
Log.d(TAG, "handleScreenOff()");
+ mBatchScanThrottler.onScreenOn(false);
handleSuspendScans();
updateRegularScanClientsScreenOff();
updateRegularScanToBatchScanClients();
@@ -863,6 +877,7 @@ public class ScanManager {
}
mScreenOn = true;
Log.d(TAG, "handleScreenOn()");
+ mBatchScanThrottler.onScreenOn(true);
updateBatchScanToRegularScanClients();
handleResumeScans();
updateRegularScanClientsScreenOn();
@@ -920,14 +935,14 @@ public class ScanManager {
/** Parameters for batch scans. */
static class BatchScanParams {
- public int scanMode;
- public int fullScanscannerId;
- public int truncatedScanscannerId;
+ @VisibleForTesting int mScanMode;
+ private int mFullScanscannerId;
+ private int mTruncatedScanscannerId;
BatchScanParams() {
- scanMode = -1;
- fullScanscannerId = -1;
- truncatedScanscannerId = -1;
+ mScanMode = -1;
+ mFullScanscannerId = -1;
+ mTruncatedScanscannerId = -1;
}
@Override
@@ -938,20 +953,14 @@ public class ScanManager {
if (!(obj instanceof BatchScanParams other)) {
return false;
}
- return scanMode == other.scanMode
- && fullScanscannerId == other.fullScanscannerId
- && truncatedScanscannerId == other.truncatedScanscannerId;
+ return mScanMode == other.mScanMode
+ && mFullScanscannerId == other.mFullScanscannerId
+ && mTruncatedScanscannerId == other.mTruncatedScanscannerId;
}
@Override
public int hashCode() {
- return Objects.hash(scanMode, fullScanscannerId, truncatedScanscannerId);
- }
- }
-
- public int getCurrentUsedTrackingAdvertisement() {
- synchronized (mCurUsedTrackableAdvertisementsLock) {
- return mCurUsedTrackableAdvertisements;
+ return Objects.hash(mScanMode, mFullScanscannerId, mTruncatedScanscannerId);
}
}
@@ -1261,9 +1270,9 @@ public class ScanManager {
waitForCallback();
resetCountDownLatch();
int scanInterval =
- Utils.millsToUnit(getBatchScanIntervalMillis(batchScanParams.scanMode));
+ Utils.millsToUnit(getBatchScanIntervalMillis(batchScanParams.mScanMode));
int scanWindow =
- Utils.millsToUnit(getBatchScanWindowMillis(batchScanParams.scanMode));
+ Utils.millsToUnit(getBatchScanWindowMillis(batchScanParams.mScanMode));
mNativeInterface.gattClientStartBatchScan(
scannerId,
resultType,
@@ -1297,15 +1306,15 @@ public class ScanManager {
BatchScanParams params = new BatchScanParams();
ScanClient winner = getAggressiveClient(mBatchClients);
if (winner != null) {
- params.scanMode = winner.settings.getScanMode();
+ params.mScanMode = winner.settings.getScanMode();
}
// TODO: split full batch scan results and truncated batch scan results to different
// collections.
for (ScanClient client : mBatchClients) {
if (client.settings.getScanResultType() == ScanSettings.SCAN_RESULT_TYPE_FULL) {
- params.fullScanscannerId = client.scannerId;
+ params.mFullScanscannerId = client.scannerId;
} else {
- params.truncatedScanscannerId = client.scannerId;
+ params.mTruncatedScanscannerId = client.scannerId;
}
}
return params;
@@ -1358,7 +1367,10 @@ public class ScanManager {
if (mBatchClients.isEmpty()) {
return;
}
- long batchTriggerIntervalMillis = getBatchTriggerIntervalMillis();
+ long batchTriggerIntervalMillis =
+ Flags.batchScanOptimization()
+ ? mBatchScanThrottler.getBatchTriggerIntervalMillis(mBatchClients)
+ : getBatchTriggerIntervalMillis();
// Allows the alarm to be triggered within
// [batchTriggerIntervalMillis, 1.1 * batchTriggerIntervalMillis]
long windowLengthMillis = batchTriggerIntervalMillis / 10;
@@ -1493,16 +1505,16 @@ public class ScanManager {
void flushBatchResults(int scannerId) {
Log.d(TAG, "flushPendingBatchResults - scannerId = " + scannerId);
- if (mBatchScanParams.fullScanscannerId != -1) {
+ if (mBatchScanParams.mFullScanscannerId != -1) {
resetCountDownLatch();
mNativeInterface.gattClientReadScanReports(
- mBatchScanParams.fullScanscannerId, SCAN_RESULT_TYPE_FULL);
+ mBatchScanParams.mFullScanscannerId, SCAN_RESULT_TYPE_FULL);
waitForCallback();
}
- if (mBatchScanParams.truncatedScanscannerId != -1) {
+ if (mBatchScanParams.mTruncatedScanscannerId != -1) {
resetCountDownLatch();
mNativeInterface.gattClientReadScanReports(
- mBatchScanParams.truncatedScanscannerId, SCAN_RESULT_TYPE_TRUNCATED);
+ mBatchScanParams.mTruncatedScanscannerId, SCAN_RESULT_TYPE_TRUNCATED);
waitForCallback();
}
setBatchAlarm();
@@ -1661,13 +1673,13 @@ public class ScanManager {
/** Return batch scan result type value defined in bt stack. */
private int getResultType(BatchScanParams params) {
- if (params.fullScanscannerId != -1 && params.truncatedScanscannerId != -1) {
+ if (params.mFullScanscannerId != -1 && params.mTruncatedScanscannerId != -1) {
return SCAN_RESULT_TYPE_BOTH;
}
- if (params.truncatedScanscannerId != -1) {
+ if (params.mTruncatedScanscannerId != -1) {
return SCAN_RESULT_TYPE_TRUNCATED;
}
- if (params.fullScanscannerId != -1) {
+ if (params.mFullScanscannerId != -1) {
return SCAN_RESULT_TYPE_FULL;
}
return -1;
diff --git a/android/app/src/com/android/bluetooth/opp/BluetoothOppService.java b/android/app/src/com/android/bluetooth/opp/BluetoothOppService.java
index 4bf981a0a5..b912e8e966 100644
--- a/android/app/src/com/android/bluetooth/opp/BluetoothOppService.java
+++ b/android/app/src/com/android/bluetooth/opp/BluetoothOppService.java
@@ -724,8 +724,9 @@ public class BluetoothOppService extends ProfileService implements IObexConnecti
mPendingUpdate = false;
}
Cursor cursor =
- getContentResolver()
- .query(
+ BluetoothMethodProxy.getInstance()
+ .contentResolverQuery(
+ getContentResolver(),
BluetoothShare.CONTENT_URI,
null,
null,
diff --git a/android/app/src/com/android/bluetooth/tbs/TbsGatt.java b/android/app/src/com/android/bluetooth/tbs/TbsGatt.java
index 803b249a3d..9403b5fc93 100644
--- a/android/app/src/com/android/bluetooth/tbs/TbsGatt.java
+++ b/android/app/src/com/android/bluetooth/tbs/TbsGatt.java
@@ -19,6 +19,8 @@ package com.android.bluetooth.tbs;
import static android.bluetooth.BluetoothDevice.METADATA_GTBS_CCCD;
+import static java.util.Objects.requireNonNull;
+
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
@@ -27,7 +29,6 @@ import android.bluetooth.BluetoothGattDescriptor;
import android.bluetooth.BluetoothGattServerCallback;
import android.bluetooth.BluetoothGattService;
import android.bluetooth.BluetoothProfile;
-import android.content.Context;
import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
@@ -48,12 +49,10 @@ import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
-import java.util.Objects;
import java.util.UUID;
public class TbsGatt {
-
- private static final String TAG = "TbsGatt";
+ private static final String TAG = TbsGatt.class.getSimpleName();
private static final String UUID_PREFIX = "0000";
private static final String UUID_SUFFIX = "-0000-1000-8000-00805f9b34fb";
@@ -66,51 +65,50 @@ public class TbsGatt {
@VisibleForTesting static final UUID UUID_BEARER_TECHNOLOGY = makeUuid("2BB5");
@VisibleForTesting static final UUID UUID_BEARER_URI_SCHEMES_SUPPORTED_LIST = makeUuid("2BB6");
@VisibleForTesting static final UUID UUID_BEARER_LIST_CURRENT_CALLS = makeUuid("2BB9");
- @VisibleForTesting static final UUID UUID_CONTENT_CONTROL_ID = makeUuid("2BBA");
+ private static final UUID UUID_CONTENT_CONTROL_ID = makeUuid("2BBA");
@VisibleForTesting static final UUID UUID_STATUS_FLAGS = makeUuid("2BBB");
@VisibleForTesting static final UUID UUID_CALL_STATE = makeUuid("2BBD");
@VisibleForTesting static final UUID UUID_CALL_CONTROL_POINT = makeUuid("2BBE");
-
- @VisibleForTesting
- static final UUID UUID_CALL_CONTROL_POINT_OPTIONAL_OPCODES = makeUuid("2BBF");
-
+ private static final UUID UUID_CALL_CONTROL_POINT_OPTIONAL_OPCODES = makeUuid("2BBF");
@VisibleForTesting static final UUID UUID_TERMINATION_REASON = makeUuid("2BC0");
@VisibleForTesting static final UUID UUID_INCOMING_CALL = makeUuid("2BC1");
@VisibleForTesting static final UUID UUID_CALL_FRIENDLY_NAME = makeUuid("2BC2");
-
@VisibleForTesting
static final UUID UUID_CLIENT_CHARACTERISTIC_CONFIGURATION = makeUuid("2902");
@VisibleForTesting static final int STATUS_FLAG_INBAND_RINGTONE_ENABLED = 0x0001;
@VisibleForTesting static final int STATUS_FLAG_SILENT_MODE_ENABLED = 0x0002;
- @VisibleForTesting static final int CALL_CONTROL_POINT_OPTIONAL_OPCODE_LOCAL_HOLD = 0x0001;
- @VisibleForTesting static final int CALL_CONTROL_POINT_OPTIONAL_OPCODE_JOIN = 0x0002;
-
- @VisibleForTesting public static final int CALL_CONTROL_POINT_OPCODE_ACCEPT = 0x00;
- @VisibleForTesting public static final int CALL_CONTROL_POINT_OPCODE_TERMINATE = 0x01;
- @VisibleForTesting public static final int CALL_CONTROL_POINT_OPCODE_LOCAL_HOLD = 0x02;
- @VisibleForTesting public static final int CALL_CONTROL_POINT_OPCODE_LOCAL_RETRIEVE = 0x03;
- @VisibleForTesting public static final int CALL_CONTROL_POINT_OPCODE_ORIGINATE = 0x04;
- @VisibleForTesting public static final int CALL_CONTROL_POINT_OPCODE_JOIN = 0x05;
+ private static final int CALL_CONTROL_POINT_OPTIONAL_OPCODE_LOCAL_HOLD = 0x0001;
+ private static final int CALL_CONTROL_POINT_OPTIONAL_OPCODE_JOIN = 0x0002;
- @VisibleForTesting public static final int CALL_CONTROL_POINT_RESULT_SUCCESS = 0x00;
+ static final int CALL_CONTROL_POINT_OPCODE_ACCEPT = 0x00;
+ static final int CALL_CONTROL_POINT_OPCODE_TERMINATE = 0x01;
+ static final int CALL_CONTROL_POINT_OPCODE_LOCAL_HOLD = 0x02;
+ static final int CALL_CONTROL_POINT_OPCODE_LOCAL_RETRIEVE = 0x03;
+ static final int CALL_CONTROL_POINT_OPCODE_ORIGINATE = 0x04;
+ static final int CALL_CONTROL_POINT_OPCODE_JOIN = 0x05;
- @VisibleForTesting
- public static final int CALL_CONTROL_POINT_RESULT_OPCODE_NOT_SUPPORTED = 0x01;
+ static final int CALL_CONTROL_POINT_RESULT_SUCCESS = 0x00;
+ static final int CALL_CONTROL_POINT_RESULT_OPCODE_NOT_SUPPORTED = 0x01;
+ static final int CALL_CONTROL_POINT_RESULT_OPERATION_NOT_POSSIBLE = 0x02;
+ static final int CALL_CONTROL_POINT_RESULT_INVALID_CALL_INDEX = 0x03;
+ static final int CALL_CONTROL_POINT_RESULT_STATE_MISMATCH = 0x04;
+ static final int CALL_CONTROL_POINT_RESULT_INVALID_OUTGOING_URI = 0x06;
- @VisibleForTesting
- public static final int CALL_CONTROL_POINT_RESULT_OPERATION_NOT_POSSIBLE = 0x02;
+ private final Object mPendingGattOperationsLock = new Object();
+ private final Map<BluetoothDevice, Integer> mStatusFlagValue = new HashMap<>();
- @VisibleForTesting public static final int CALL_CONTROL_POINT_RESULT_INVALID_CALL_INDEX = 0x03;
- @VisibleForTesting public static final int CALL_CONTROL_POINT_RESULT_STATE_MISMATCH = 0x04;
- @VisibleForTesting public static final int CALL_CONTROL_POINT_RESULT_LACK_OF_RESOURCES = 0x05;
+ @GuardedBy("mPendingGattOperationsLock")
+ private final Map<BluetoothDevice, List<GattOpContext>> mPendingGattOperations =
+ new HashMap<>();
- @VisibleForTesting
- public static final int CALL_CONTROL_POINT_RESULT_INVALID_OUTGOING_URI = 0x06;
+ private final Map<BluetoothDevice, HashMap<UUID, Short>> mCccDescriptorValues = new HashMap<>();
- private final Object mPendingGattOperationsLock = new Object();
- private final Context mContext;
+ private final AdapterService mAdapterService;
+ private final TbsService mTbsService;
+ private final Handler mHandler;
+ private final BluetoothGattServerProxy mBluetoothGattServer;
private final GattCharacteristic mBearerProviderNameCharacteristic;
private final GattCharacteristic mBearerUciCharacteristic;
private final GattCharacteristic mBearerTechnologyCharacteristic;
@@ -124,18 +122,10 @@ public class TbsGatt {
private final GattCharacteristic mTerminationReasonCharacteristic;
private final GattCharacteristic mIncomingCallCharacteristic;
private final GattCharacteristic mCallFriendlyNameCharacteristic;
- private boolean mSilentMode = false;
- private Map<BluetoothDevice, Integer> mStatusFlagValue = new HashMap<>();
-
- @GuardedBy("mPendingGattOperationsLock")
- private Map<BluetoothDevice, List<GattOpContext>> mPendingGattOperations = new HashMap<>();
- private BluetoothGattServerProxy mBluetoothGattServer;
- private Handler mHandler;
private Callback mCallback;
- private AdapterService mAdapterService;
- private HashMap<BluetoothDevice, HashMap<UUID, Short>> mCccDescriptorValues;
- private TbsService mTbsService;
+
+ private boolean mSilentMode = false;
private static final int LOG_NB_EVENTS = 200;
private BluetoothEventLogger mEventLogger = null;
@@ -242,12 +232,19 @@ public class TbsGatt {
public byte[] mValue;
}
- TbsGatt(TbsService tbsService) {
- mContext = tbsService;
- mAdapterService =
- Objects.requireNonNull(
- AdapterService.getAdapterService(),
- "AdapterService shouldn't be null when creating TbsGatt");
+ TbsGatt(AdapterService adapterService, TbsService tbsService) {
+ this(adapterService, tbsService, new BluetoothGattServerProxy(adapterService));
+ }
+
+ @VisibleForTesting
+ TbsGatt(
+ AdapterService adapterService,
+ TbsService tbsService,
+ BluetoothGattServerProxy gattServerProxy) {
+ mTbsService = requireNonNull(tbsService);
+ mAdapterService = requireNonNull(adapterService);
+ mBluetoothGattServer = requireNonNull(gattServerProxy);
+ mHandler = new Handler(Looper.getMainLooper());
mBearerProviderNameCharacteristic =
new GattCharacteristic(
@@ -317,13 +314,6 @@ public class TbsGatt {
| BluetoothGattCharacteristic.PROPERTY_NOTIFY,
BluetoothGattCharacteristic.PERMISSION_READ_ENCRYPTED);
- mTbsService = tbsService;
- mBluetoothGattServer = null;
- }
-
- @VisibleForTesting
- void setBluetoothGattServerForTesting(BluetoothGattServerProxy proxy) {
- mBluetoothGattServer = proxy;
}
public boolean init(
@@ -335,7 +325,6 @@ public class TbsGatt {
String providerName,
int technology,
Callback callback) {
- mCccDescriptorValues = new HashMap<>();
mBearerProviderNameCharacteristic.setValue(providerName);
mBearerTechnologyCharacteristic.setValue(new byte[] {(byte) (technology & 0xFF)});
mBearerUciCharacteristic.setValue(uci);
@@ -344,11 +333,6 @@ public class TbsGatt {
setCallControlPointOptionalOpcodes(isLocalHoldOpcodeSupported, isJoinOpcodeSupported);
mStatusFlagsCharacteristic.setValue(0, BluetoothGattCharacteristic.FORMAT_UINT16, 0);
mCallback = callback;
- mHandler = new Handler(Looper.getMainLooper());
-
- if (mBluetoothGattServer == null) {
- mBluetoothGattServer = new BluetoothGattServerProxy(mContext);
- }
if (!mBluetoothGattServer.open(mGattServerCallback)) {
Log.e(TAG, " Could not open Gatt server");
@@ -381,22 +365,14 @@ public class TbsGatt {
mEventLogger.add("Initialized");
mAdapterService.registerBluetoothStateCallback(
- mContext.getMainExecutor(), mBluetoothStateChangeCallback);
+ mAdapterService.getMainExecutor(), mBluetoothStateChangeCallback);
return true;
}
public void cleanup() {
mAdapterService.unregisterBluetoothStateCallback(mBluetoothStateChangeCallback);
- if (mBluetoothGattServer == null) {
- return;
- }
mBluetoothGattServer.close();
- mBluetoothGattServer = null;
- }
-
- public Context getContext() {
- return mContext;
}
private void removeUuidFromMetadata(ParcelUuid charUuid, BluetoothDevice device) {
@@ -506,19 +482,14 @@ public class TbsGatt {
BluetoothDevice device, BluetoothGattCharacteristic characteristic, byte[] value) {
if (getDeviceAuthorization(device) != BluetoothDevice.ACCESS_ALLOWED) return;
if (value == null) return;
- if (mBluetoothGattServer != null) {
- mBluetoothGattServer.notifyCharacteristicChanged(
- device, characteristic, false, value);
- }
+ mBluetoothGattServer.notifyCharacteristicChanged(device, characteristic, false, value);
}
private void notifyCharacteristicChanged(
BluetoothDevice device, BluetoothGattCharacteristic characteristic) {
if (getDeviceAuthorization(device) != BluetoothDevice.ACCESS_ALLOWED) return;
- if (mBluetoothGattServer != null) {
- mBluetoothGattServer.notifyCharacteristicChanged(device, characteristic, false);
- }
+ mBluetoothGattServer.notifyCharacteristicChanged(device, characteristic, false);
}
public void notifyWithValue(
diff --git a/android/app/src/com/android/bluetooth/tbs/TbsGeneric.java b/android/app/src/com/android/bluetooth/tbs/TbsGeneric.java
index 957e1e2b69..b49eee0a03 100644
--- a/android/app/src/com/android/bluetooth/tbs/TbsGeneric.java
+++ b/android/app/src/com/android/bluetooth/tbs/TbsGeneric.java
@@ -17,6 +17,8 @@
package com.android.bluetooth.tbs;
+import static java.util.Objects.requireNonNull;
+
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothLeAudio;
import android.bluetooth.BluetoothLeCall;
@@ -50,8 +52,7 @@ import java.util.UUID;
/** Container class to store TBS instances */
public class TbsGeneric {
-
- private static final String TAG = "TbsGeneric";
+ private static final String TAG = TbsGeneric.class.getSimpleName();
private static final String UCI = "GTBS";
private static final String DEFAULT_PROVIDER_NAME = "none";
@@ -121,17 +122,20 @@ public class TbsGeneric {
}
}
- private boolean mIsInitialized = false;
- private TbsGatt mTbsGatt = null;
- private List<Bearer> mBearerList = new ArrayList<>();
+ private final List<Bearer> mBearerList = new ArrayList<>();
+ private final Map<Integer, TbsCall> mCurrentCallsList = new TreeMap<>();
+ private final Receiver mReceiver = new Receiver();
+ private final ServiceFactory mFactory = new ServiceFactory();
+
+ private final TbsGatt mTbsGatt;
+ private final Context mContext;
+
+ private boolean mIsInitialized;
private int mLastIndexAssigned = TbsCall.INDEX_UNASSIGNED;
- private Map<Integer, TbsCall> mCurrentCallsList = new TreeMap<>();
private Bearer mForegroundBearer = null;
private int mLastRequestIdAssigned = 0;
private List<String> mUriSchemes = new ArrayList<>(Arrays.asList("tel"));
- private Receiver mReceiver = null;
private int mStoredRingerMode = -1;
- private final ServiceFactory mFactory = new ServiceFactory();
private LeAudioService mLeAudioService;
private final class Receiver extends BroadcastReceiver {
@@ -162,9 +166,9 @@ public class TbsGeneric {
}
;
- public synchronized boolean init(TbsGatt tbsGatt) {
- Log.d(TAG, "init");
- mTbsGatt = tbsGatt;
+ TbsGeneric(Context ctx, TbsGatt tbsGatt) {
+ mTbsGatt = requireNonNull(tbsGatt);
+ mContext = requireNonNull(ctx);
int ccid =
ContentControlIdKeeper.acquireCcid(
@@ -173,7 +177,7 @@ public class TbsGeneric {
if (!isCcidValid(ccid)) {
Log.e(TAG, " CCID is not valid");
cleanup();
- return false;
+ return;
}
if (!mTbsGatt.init(
@@ -187,15 +191,10 @@ public class TbsGeneric {
mTbsGattCallback)) {
Log.e(TAG, " TbsGatt init failed");
cleanup();
- return false;
+ return;
}
- AudioManager audioManager = mTbsGatt.getContext().getSystemService(AudioManager.class);
- if (audioManager == null) {
- Log.w(TAG, " AudioManager is not available");
- cleanup();
- return false;
- }
+ AudioManager audioManager = requireNonNull(mContext.getSystemService(AudioManager.class));
// read initial value of ringer mode
mStoredRingerMode = audioManager.getRingerMode();
@@ -206,25 +205,20 @@ public class TbsGeneric {
mTbsGatt.clearSilentModeFlag();
}
- mReceiver = new Receiver();
IntentFilter filter = new IntentFilter(AudioManager.RINGER_MODE_CHANGED_ACTION);
filter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
- mTbsGatt.getContext().registerReceiver(mReceiver, filter);
+ mContext.registerReceiver(mReceiver, filter);
mIsInitialized = true;
- return true;
}
public synchronized void cleanup() {
Log.d(TAG, "cleanup");
- if (mTbsGatt != null) {
- if (mReceiver != null) {
- mTbsGatt.getContext().unregisterReceiver(mReceiver);
- }
- mTbsGatt.cleanup();
- mTbsGatt = null;
+ if (mIsInitialized) {
+ mContext.unregisterReceiver(mReceiver);
}
+ mTbsGatt.cleanup();
mIsInitialized = false;
}
@@ -236,9 +230,7 @@ public class TbsGeneric {
*/
public synchronized void onDeviceAuthorizationSet(BluetoothDevice device) {
// Notify TBS GATT service instance in case of pending operations
- if (mTbsGatt != null) {
- mTbsGatt.onDeviceAuthorizationSet(device);
- }
+ mTbsGatt.onDeviceAuthorizationSet(device);
}
/**
@@ -247,10 +239,6 @@ public class TbsGeneric {
* @param device device for which inband ringtone has been set
*/
public synchronized void setInbandRingtoneSupport(BluetoothDevice device) {
- if (mTbsGatt == null) {
- Log.w(TAG, "setInbandRingtoneSupport, mTbsGatt is null");
- return;
- }
mTbsGatt.setInbandRingtoneFlag(device);
}
@@ -260,10 +248,6 @@ public class TbsGeneric {
* @param device device for which inband ringtone has been cleared
*/
public synchronized void clearInbandRingtoneSupport(BluetoothDevice device) {
- if (mTbsGatt == null) {
- Log.w(TAG, "setInbandRingtoneSupport, mTbsGatt is null");
- return;
- }
mTbsGatt.clearInbandRingtoneFlag(device);
}
@@ -767,7 +751,7 @@ public class TbsGeneric {
Log.i(TAG, "originate uri=" + uri);
Intent intent = new Intent(Intent.ACTION_CALL_PRIVILEGED, Uri.parse(uri));
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- mTbsGatt.getContext().startActivity(intent);
+ mContext.startActivity(intent);
mTbsGatt.setCallControlPointResult(
device,
TbsGatt.CALL_CONTROL_POINT_OPCODE_ORIGINATE,
@@ -844,6 +828,20 @@ public class TbsGeneric {
return;
}
+ if (shouldBlockTbsForBroadcastReceiver(device)) {
+ Log.w(
+ TAG,
+ "Blocking TBS operation for non-primary device in broadcast,"
+ + " opcode = "
+ + callControlRequestOpcodeStr(opcode));
+ mTbsGatt.setCallControlPointResult(
+ device,
+ opcode,
+ 0,
+ TbsGatt.CALL_CONTROL_POINT_RESULT_OPERATION_NOT_POSSIBLE);
+ return;
+ }
+
int result;
switch (opcode) {
@@ -1245,6 +1243,20 @@ public class TbsGeneric {
return false;
}
+ private boolean shouldBlockTbsForBroadcastReceiver(BluetoothDevice device) {
+ if (device == null) {
+ Log.w(TAG, "shouldBlockTbsForBroadcastReceiver: Ignore null device");
+ return false;
+ }
+ if (!isLeAudioServiceAvailable()) {
+ Log.w(TAG, "shouldBlockTbsForBroadcastReceiver: LeAudioService is not available");
+ return false;
+ }
+
+ return mLeAudioService.getLocalBroadcastReceivers().contains(device)
+ && !mLeAudioService.isPrimaryDevice(device);
+ }
+
/**
* Dump status of TBS service along with related objects
*
diff --git a/android/app/src/com/android/bluetooth/tbs/TbsService.java b/android/app/src/com/android/bluetooth/tbs/TbsService.java
index dc3819eec1..263f765e53 100644
--- a/android/app/src/com/android/bluetooth/tbs/TbsService.java
+++ b/android/app/src/com/android/bluetooth/tbs/TbsService.java
@@ -20,6 +20,8 @@ package com.android.bluetooth.tbs;
import static android.Manifest.permission.BLUETOOTH_CONNECT;
import static android.Manifest.permission.BLUETOOTH_PRIVILEGED;
+import static java.util.Objects.requireNonNull;
+
import android.annotation.RequiresPermission;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothLeCall;
@@ -27,13 +29,13 @@ import android.bluetooth.BluetoothProfile;
import android.bluetooth.IBluetoothLeCallControl;
import android.bluetooth.IBluetoothLeCallControlCallback;
import android.content.AttributionSource;
-import android.content.Context;
import android.os.ParcelUuid;
import android.os.RemoteException;
import android.sysprop.BluetoothProperties;
import android.util.Log;
import com.android.bluetooth.Utils;
+import com.android.bluetooth.btservice.AdapterService;
import com.android.bluetooth.btservice.ProfileService;
import com.android.bluetooth.le_audio.LeAudioService;
import com.android.internal.annotations.VisibleForTesting;
@@ -44,16 +46,20 @@ import java.util.Map;
import java.util.UUID;
public class TbsService extends ProfileService {
-
- private static final String TAG = "TbsService";
+ private static final String TAG = TbsService.class.getSimpleName();
private static TbsService sTbsService;
+
private final Map<BluetoothDevice, Integer> mDeviceAuthorizations = new HashMap<>();
+ private final TbsGeneric mTbsGeneric;
- private final TbsGeneric mTbsGeneric = new TbsGeneric();
+ public TbsService(AdapterService adapterService) {
+ super(requireNonNull(adapterService));
- public TbsService(Context ctx) {
- super(ctx);
+ // Mark service as started
+ setTbsService(this);
+
+ mTbsGeneric = new TbsGeneric(adapterService, new TbsGatt(adapterService, this));
}
public static boolean isEnabled() {
@@ -66,19 +72,6 @@ public class TbsService extends ProfileService {
}
@Override
- public void start() {
- Log.d(TAG, "start()");
- if (sTbsService != null) {
- throw new IllegalStateException("start() called twice");
- }
-
- // Mark service as started
- setTbsService(this);
-
- mTbsGeneric.init(new TbsGatt(this));
- }
-
- @Override
public void stop() {
Log.d(TAG, "stop()");
if (sTbsService == null) {
diff --git a/android/app/src/com/com/android/bluetooth/le_scan/BatchScanThrottler.java b/android/app/src/com/com/android/bluetooth/le_scan/BatchScanThrottler.java
new file mode 100644
index 0000000000..4fd324dcf9
--- /dev/null
+++ b/android/app/src/com/com/android/bluetooth/le_scan/BatchScanThrottler.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright 2025 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.le_scan;
+
+import static com.android.bluetooth.le_scan.ScanController.DEFAULT_REPORT_DELAY_FLOOR;
+
+import android.provider.DeviceConfig;
+
+import com.android.bluetooth.Utils.TimeProvider;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.Set;
+
+/**
+ * Throttler to reduce the number of times the Bluetooth process wakes up to check for pending batch
+ * scan results. The wake-up intervals are increased when no matching results are found and are
+ * longer when the screen is off.
+ */
+class BatchScanThrottler {
+ // Minimum batch trigger interval to check for batched results when the screen is off
+ @VisibleForTesting static final long SCREEN_OFF_MINIMUM_DELAY_FLOOR_MS = 20000L;
+ // Adjusted minimum report delay for unfiltered batch scan clients
+ @VisibleForTesting static final long UNFILTERED_DELAY_FLOOR_MS = 20000L;
+ // Adjusted minimum report delay for unfiltered batch scan clients when the screen is off
+ @VisibleForTesting static final long UNFILTERED_SCREEN_OFF_DELAY_FLOOR_MS = 60000L;
+ // Backoff stages used as multipliers for the minimum delay floor (standard or screen-off)
+ @VisibleForTesting static final int[] BACKOFF_MULTIPLIERS = {1, 1, 2, 2, 4};
+ // Start screen-off trigger interval throttling after the screen has been off for this period
+ // of time. This allows the screen-on intervals to be used for a short period of time after the
+ // screen has gone off, and avoids too much flipping between screen-off and screen-on backoffs
+ // when the screen is off for a short period of time
+ @VisibleForTesting static final long SCREEN_OFF_DELAY_MS = 60000L;
+ private final TimeProvider mTimeProvider;
+ private final long mDelayFloor;
+ private final long mScreenOffDelayFloor;
+ private int mBackoffStage = 0;
+ private long mScreenOffTriggerTime = 0L;
+ private boolean mScreenOffThrottling = false;
+
+ BatchScanThrottler(TimeProvider timeProvider, boolean screenOn) {
+ mTimeProvider = timeProvider;
+ mDelayFloor =
+ DeviceConfig.getLong(
+ DeviceConfig.NAMESPACE_BLUETOOTH,
+ "report_delay",
+ DEFAULT_REPORT_DELAY_FLOOR);
+ mScreenOffDelayFloor = Math.max(mDelayFloor, SCREEN_OFF_MINIMUM_DELAY_FLOOR_MS);
+ onScreenOn(screenOn);
+ }
+
+ void resetBackoff() {
+ mBackoffStage = 0;
+ }
+
+ void onScreenOn(boolean screenOn) {
+ if (screenOn) {
+ mScreenOffTriggerTime = 0L;
+ mScreenOffThrottling = false;
+ resetBackoff();
+ } else {
+ // Screen-off intervals to be used after the trigger time
+ mScreenOffTriggerTime = mTimeProvider.elapsedRealtime() + SCREEN_OFF_DELAY_MS;
+ }
+ }
+
+ long getBatchTriggerIntervalMillis(Set<ScanClient> batchClients) {
+ // Check if we're past the screen-off time and should be using screen-off backoff values
+ if (!mScreenOffThrottling
+ && mScreenOffTriggerTime != 0
+ && mTimeProvider.elapsedRealtime() >= mScreenOffTriggerTime) {
+ mScreenOffThrottling = true;
+ resetBackoff();
+ }
+ long unfilteredFloor =
+ mScreenOffThrottling
+ ? UNFILTERED_SCREEN_OFF_DELAY_FLOOR_MS
+ : UNFILTERED_DELAY_FLOOR_MS;
+ long intervalMillis = Long.MAX_VALUE;
+ for (ScanClient client : batchClients) {
+ if (client.settings.getReportDelayMillis() > 0) {
+ long clientIntervalMillis = client.settings.getReportDelayMillis();
+ if ((client.filters == null || client.filters.isEmpty())
+ && clientIntervalMillis < unfilteredFloor) {
+ clientIntervalMillis = unfilteredFloor;
+ }
+ intervalMillis = Math.min(intervalMillis, clientIntervalMillis);
+ }
+ }
+ int backoffIndex =
+ mBackoffStage >= BACKOFF_MULTIPLIERS.length
+ ? BACKOFF_MULTIPLIERS.length - 1
+ : mBackoffStage++;
+ return Math.max(
+ intervalMillis,
+ (mScreenOffThrottling ? mScreenOffDelayFloor : mDelayFloor)
+ * BACKOFF_MULTIPLIERS[backoffIndex]);
+ }
+}
diff --git a/android/app/tests/unit/Android.bp b/android/app/tests/unit/Android.bp
index 346c57fffa..eae8e4a89f 100644
--- a/android/app/tests/unit/Android.bp
+++ b/android/app/tests/unit/Android.bp
@@ -22,6 +22,7 @@ java_defaults {
static_libs: [
"PlatformProperties",
+ "TestParameterInjector",
"android.media.audio-aconfig-exported-java",
"androidx.media_media",
"androidx.room_room-migration",
diff --git a/android/app/tests/unit/src/com/android/bluetooth/TestUtils.java b/android/app/tests/unit/src/com/android/bluetooth/TestUtils.java
index 877df75ef2..78d1e66db1 100644
--- a/android/app/tests/unit/src/com/android/bluetooth/TestUtils.java
+++ b/android/app/tests/unit/src/com/android/bluetooth/TestUtils.java
@@ -41,7 +41,6 @@ import androidx.test.uiautomator.UiDevice;
import com.android.bluetooth.avrcpcontroller.BluetoothMediaBrowserService;
import com.android.bluetooth.btservice.AdapterService;
-import org.junit.Assert;
import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
@@ -112,11 +111,11 @@ public class TestUtils {
* TestUtils#setAdapterService(AdapterService)}
*/
public static void clearAdapterService(AdapterService adapterService) {
- Assert.assertSame(
- "AdapterService.getAdapterService() must return the same object as the"
- + " supplied adapterService in this method",
- adapterService,
- AdapterService.getAdapterService());
+ assertWithMessage(
+ "AdapterService.getAdapterService() must return the same object as the"
+ + " supplied adapterService in this method")
+ .that(adapterService)
+ .isSameInstanceAs(AdapterService.getAdapterService());
assertThat(adapterService).isNotNull();
AdapterService.clearAdapterService(adapterService);
}
@@ -157,10 +156,7 @@ public class TestUtils {
return context.getPackageManager()
.getResourcesForApplication("com.android.bluetooth.tests");
} catch (PackageManager.NameNotFoundException e) {
- assertWithMessage(
- "Setup Failure: Unable to get test application resources"
- + e.toString())
- .fail();
+ assertWithMessage("Unable to get test application resources: " + e.toString()).fail();
return null;
}
}
@@ -178,7 +174,7 @@ public class TestUtils {
assertThat(intent).isNotNull();
return intent;
} catch (InterruptedException e) {
- Assert.fail("Cannot obtain an Intent from the queue: " + e.getMessage());
+ assertWithMessage("Cannot obtain an Intent from the queue: " + e.toString()).fail();
}
return null;
}
@@ -194,7 +190,7 @@ public class TestUtils {
Intent intent = queue.poll(timeoutMs, TimeUnit.MILLISECONDS);
assertThat(intent).isNull();
} catch (InterruptedException e) {
- Assert.fail("Cannot obtain an Intent from the queue: " + e.getMessage());
+ assertWithMessage("Cannot obtain an Intent from the queue: " + e.toString()).fail();
}
}
diff --git a/android/app/tests/unit/src/com/android/bluetooth/a2dp/A2dpCodecConfigTest.java b/android/app/tests/unit/src/com/android/bluetooth/a2dp/A2dpCodecConfigTest.java
index 156151e9b0..a481cef2e0 100644
--- a/android/app/tests/unit/src/com/android/bluetooth/a2dp/A2dpCodecConfigTest.java
+++ b/android/app/tests/unit/src/com/android/bluetooth/a2dp/A2dpCodecConfigTest.java
@@ -17,6 +17,7 @@
package com.android.bluetooth.a2dp;
import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
import static org.mockito.Mockito.*;
@@ -33,7 +34,6 @@ import androidx.test.runner.AndroidJUnit4;
import com.android.bluetooth.R;
import org.junit.After;
-import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
@@ -787,10 +787,12 @@ public class A2dpCodecConfigTest {
codecConfig.getCodecSpecific3(),
codecConfig.getCodecSpecific4());
}
- Assert.fail(
- "getDefaultCodecConfigByType: No such codecType="
- + codecType
- + " in sDefaultCodecConfigs");
+ assertWithMessage(
+ "Default codec ("
+ + Arrays.toString(sDefaultCodecConfigs)
+ + ") does not contains "
+ + codecType)
+ .fail();
return null;
}
@@ -810,10 +812,12 @@ public class A2dpCodecConfigTest {
codecCapabilities.getCodecSpecific3(),
codecCapabilities.getCodecSpecific4());
}
- Assert.fail(
- "getCodecCapabilitiesByType: No such codecType="
- + codecType
- + " in sCodecCapabilities");
+ assertWithMessage(
+ "Codec capabilities ("
+ + Arrays.toString(sCodecCapabilities)
+ + ") does not contains "
+ + codecType)
+ .fail();
return null;
}
diff --git a/android/app/tests/unit/src/com/android/bluetooth/audio_util/BrowserPlayerWrapperTest.java b/android/app/tests/unit/src/com/android/bluetooth/audio_util/BrowserPlayerWrapperTest.java
index cd9ad4dd9a..b3d61e33af 100644
--- a/android/app/tests/unit/src/com/android/bluetooth/audio_util/BrowserPlayerWrapperTest.java
+++ b/android/app/tests/unit/src/com/android/bluetooth/audio_util/BrowserPlayerWrapperTest.java
@@ -40,7 +40,6 @@ import androidx.test.InstrumentationRegistry;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
-import com.android.bluetooth.R;
import com.android.bluetooth.TestUtils;
import org.junit.After;
@@ -75,7 +74,6 @@ public class BrowserPlayerWrapperTest {
private HandlerThread mThread;
@Mock Context mMockContext;
- @Mock Resources mMockResources;
private Context mTargetContext;
private Resources mTestResources;
private MockContentResolver mTestContentResolver;
@@ -116,8 +114,7 @@ public class BrowserPlayerWrapperTest {
});
when(mMockContext.getContentResolver()).thenReturn(mTestContentResolver);
- when(mMockResources.getBoolean(R.bool.avrcp_target_cover_art_uri_images)).thenReturn(true);
- when(mMockContext.getResources()).thenReturn(mMockResources);
+ Util.sUriImagesSupport = true;
// Set up Looper thread for the timeout handler
mThread = new HandlerThread("MediaPlayerWrapperTestThread");
@@ -138,6 +135,7 @@ public class BrowserPlayerWrapperTest {
mTestBitmap = null;
mTestResources = null;
mTargetContext = null;
+ Util.sUriImagesSupport = false;
}
private Bitmap loadImage(int resId) {
diff --git a/android/app/tests/unit/src/com/android/bluetooth/audio_util/ImageTest.java b/android/app/tests/unit/src/com/android/bluetooth/audio_util/ImageTest.java
index 60ca5a5aef..6c6e6e78fc 100644
--- a/android/app/tests/unit/src/com/android/bluetooth/audio_util/ImageTest.java
+++ b/android/app/tests/unit/src/com/android/bluetooth/audio_util/ImageTest.java
@@ -36,7 +36,6 @@ import android.test.mock.MockContentResolver;
import androidx.test.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;
-import com.android.bluetooth.R;
import com.android.bluetooth.TestUtils;
import org.junit.After;
@@ -57,7 +56,6 @@ public class ImageTest {
@Rule public MockitoRule mockitoRule = MockitoJUnit.rule();
private @Mock Context mMockContext;
- private @Mock Resources mMockResources;
private Resources mTestResources;
private MockContentResolver mTestContentResolver;
@@ -108,8 +106,7 @@ public class ImageTest {
});
when(mMockContext.getContentResolver()).thenReturn(mTestContentResolver);
- when(mMockContext.getResources()).thenReturn(mMockResources);
- when(mMockResources.getBoolean(R.bool.avrcp_target_cover_art_uri_images)).thenReturn(true);
+ Util.sUriImagesSupport = true;
}
@After
@@ -119,6 +116,7 @@ public class ImageTest {
mTestResources = null;
mTargetContext = null;
mMockContext = null;
+ Util.sUriImagesSupport = false;
}
private Bitmap loadImage(int resId) {
@@ -287,7 +285,7 @@ public class ImageTest {
*/
@Test
public void testCreateImageFromMediaMetadataWithArtUriDisabled() {
- when(mMockResources.getBoolean(R.bool.avrcp_target_cover_art_uri_images)).thenReturn(false);
+ Util.sUriImagesSupport = false;
MediaMetadata metadata =
getMediaMetadataWithUri(MediaMetadata.METADATA_KEY_ART_URI, IMAGE_STRING_1);
Image artwork = new Image(mMockContext, metadata);
@@ -302,7 +300,7 @@ public class ImageTest {
*/
@Test
public void testCreateImageFromMediaMetadataWithAlbumArtUriDisabled() {
- when(mMockResources.getBoolean(R.bool.avrcp_target_cover_art_uri_images)).thenReturn(false);
+ Util.sUriImagesSupport = false;
MediaMetadata metadata =
getMediaMetadataWithUri(MediaMetadata.METADATA_KEY_ALBUM_ART_URI, IMAGE_STRING_1);
Image artwork = new Image(mMockContext, metadata);
@@ -317,7 +315,7 @@ public class ImageTest {
*/
@Test
public void testCreateImageFromMediaMetadataWithDisplayIconUriDisabled() {
- when(mMockResources.getBoolean(R.bool.avrcp_target_cover_art_uri_images)).thenReturn(false);
+ Util.sUriImagesSupport = false;
MediaMetadata metadata =
getMediaMetadataWithUri(
MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI, IMAGE_STRING_1);
@@ -459,7 +457,7 @@ public class ImageTest {
*/
@Test
public void testCreateImageFromBundleWithArtUriDisabled() {
- when(mMockResources.getBoolean(R.bool.avrcp_target_cover_art_uri_images)).thenReturn(false);
+ Util.sUriImagesSupport = false;
Bundle bundle = getBundleWithUri(MediaMetadata.METADATA_KEY_ART_URI, IMAGE_STRING_1);
Image artwork = new Image(mMockContext, bundle);
assertThat(artwork.getImage()).isNull();
@@ -473,7 +471,7 @@ public class ImageTest {
*/
@Test
public void testCreateImageFromBundleWithAlbumArtUriDisabled() {
- when(mMockResources.getBoolean(R.bool.avrcp_target_cover_art_uri_images)).thenReturn(false);
+ Util.sUriImagesSupport = false;
Bundle bundle = getBundleWithUri(MediaMetadata.METADATA_KEY_ALBUM_ART_URI, IMAGE_STRING_1);
Image artwork = new Image(mMockContext, bundle);
assertThat(artwork.getImage()).isNull();
@@ -487,7 +485,7 @@ public class ImageTest {
*/
@Test
public void testCreateImageFromBundleWithDisplayIconUriDisabled() {
- when(mMockResources.getBoolean(R.bool.avrcp_target_cover_art_uri_images)).thenReturn(false);
+ Util.sUriImagesSupport = false;
Bundle bundle =
getBundleWithUri(MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI, IMAGE_STRING_1);
Image artwork = new Image(mMockContext, bundle);
diff --git a/android/app/tests/unit/src/com/android/bluetooth/audio_util/MediaPlayerWrapperTest.java b/android/app/tests/unit/src/com/android/bluetooth/audio_util/MediaPlayerWrapperTest.java
index 2b323d9810..794a1200b3 100644
--- a/android/app/tests/unit/src/com/android/bluetooth/audio_util/MediaPlayerWrapperTest.java
+++ b/android/app/tests/unit/src/com/android/bluetooth/audio_util/MediaPlayerWrapperTest.java
@@ -36,9 +36,9 @@ import androidx.test.filters.SmallTest;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;
-import com.android.bluetooth.R;
import com.android.bluetooth.TestUtils;
+import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
@@ -75,7 +75,6 @@ public class MediaPlayerWrapperTest {
@Mock MediaController mMockController;
@Mock MediaPlayerWrapper.Callback mTestCbs;
@Mock Context mMockContext;
- @Mock Resources mMockResources;
List<MediaSession.QueueItem> getQueueFromDescriptions(
List<MediaDescription.Builder> descriptions) {
@@ -98,8 +97,7 @@ public class MediaPlayerWrapperTest {
InstrumentationRegistry.getInstrumentation().getTargetContext());
mTestBitmap = loadImage(com.android.bluetooth.tests.R.raw.image_200_200);
- when(mMockResources.getBoolean(R.bool.avrcp_target_cover_art_uri_images)).thenReturn(true);
- when(mMockContext.getResources()).thenReturn(mMockResources);
+ Util.sUriImagesSupport = true;
// Set failure handler to capture Log.wtf messages
Log.setWtfHandler(mFailHandler);
@@ -160,6 +158,14 @@ public class MediaPlayerWrapperTest {
MediaPlayerWrapper.sTesting = true;
}
+ @After
+ public void tearDown() {
+ if (mThread != null) {
+ mThread.quitSafely();
+ }
+ Util.sUriImagesSupport = false;
+ }
+
private Bitmap loadImage(int resId) {
InputStream imageInputStream = mTestResources.openRawResource(resId);
return BitmapFactory.decodeStream(imageInputStream);
diff --git a/android/app/tests/unit/src/com/android/bluetooth/audio_util/MetadataTest.java b/android/app/tests/unit/src/com/android/bluetooth/audio_util/MetadataTest.java
index 5ba6e0f4e5..4cc12f763d 100644
--- a/android/app/tests/unit/src/com/android/bluetooth/audio_util/MetadataTest.java
+++ b/android/app/tests/unit/src/com/android/bluetooth/audio_util/MetadataTest.java
@@ -38,7 +38,6 @@ import android.test.mock.MockContentResolver;
import androidx.test.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;
-import com.android.bluetooth.R;
import com.android.bluetooth.TestUtils;
import org.junit.After;
@@ -59,7 +58,6 @@ public class MetadataTest {
@Rule public MockitoRule mockitoRule = MockitoJUnit.rule();
private @Mock Context mMockContext;
- private @Mock Resources mMockResources;
private Resources mTestResources;
private MockContentResolver mTestContentResolver;
@@ -111,8 +109,7 @@ public class MetadataTest {
});
when(mMockContext.getContentResolver()).thenReturn(mTestContentResolver);
- when(mMockResources.getBoolean(R.bool.avrcp_target_cover_art_uri_images)).thenReturn(true);
- when(mMockContext.getResources()).thenReturn(mMockResources);
+ Util.sUriImagesSupport = true;
mSongImage = new Image(mMockContext, mTestBitmap);
}
@@ -125,6 +122,7 @@ public class MetadataTest {
mTestResources = null;
mTargetContext = null;
mMockContext = null;
+ Util.sUriImagesSupport = false;
}
private Bitmap loadImage(int resId) {
@@ -427,7 +425,7 @@ public class MetadataTest {
*/
@Test
public void testBuildMetadataFromMediaMetadataWithUriAndUrisDisabled() {
- when(mMockResources.getBoolean(R.bool.avrcp_target_cover_art_uri_images)).thenReturn(false);
+ Util.sUriImagesSupport = false;
MediaMetadata m = getMediaMetadataWithUri(MediaMetadata.METADATA_KEY_ART_URI, IMAGE_URI_1);
Metadata metadata =
new Metadata.Builder().useContext(mMockContext).fromMediaMetadata(m).build();
@@ -737,7 +735,7 @@ public class MetadataTest {
*/
@Test
public void testBuildMetadataFromBundleWithUriAndUrisDisabled() {
- when(mMockResources.getBoolean(R.bool.avrcp_target_cover_art_uri_images)).thenReturn(false);
+ Util.sUriImagesSupport = false;
Bundle bundle = getBundleWithUri(MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI, IMAGE_URI_1);
Metadata metadata =
new Metadata.Builder().useContext(mMockContext).fromBundle(bundle).build();
@@ -852,7 +850,7 @@ public class MetadataTest {
*/
@Test
public void testBuildMetadataFromMediaItemWithIconUriAndUrisDisabled() {
- when(mMockResources.getBoolean(R.bool.avrcp_target_cover_art_uri_images)).thenReturn(false);
+ Util.sUriImagesSupport = false;
MediaDescription description = getMediaDescription(null, IMAGE_URI_1, null);
MediaItem item = getMediaItem(description);
Metadata metadata =
@@ -980,7 +978,7 @@ public class MetadataTest {
*/
@Test
public void testBuildMetadataFromQueueItemWithIconUriandUrisDisabled() {
- when(mMockResources.getBoolean(R.bool.avrcp_target_cover_art_uri_images)).thenReturn(false);
+ Util.sUriImagesSupport = false;
MediaDescription description = getMediaDescription(null, IMAGE_URI_1, null);
QueueItem queueItem = getQueueItem(description);
Metadata metadata =
diff --git a/android/app/tests/unit/src/com/android/bluetooth/bass_client/BassClientServiceTest.java b/android/app/tests/unit/src/com/android/bluetooth/bass_client/BassClientServiceTest.java
index 2749dd7e09..c1c7734f2e 100644
--- a/android/app/tests/unit/src/com/android/bluetooth/bass_client/BassClientServiceTest.java
+++ b/android/app/tests/unit/src/com/android/bluetooth/bass_client/BassClientServiceTest.java
@@ -4472,6 +4472,8 @@ public class BassClientServiceTest {
// Verify getSyncedBroadcastSinks returns empty device list if no broadcst ID
assertThat(mBassClientService.getSyncedBroadcastSinks().isEmpty()).isTrue();
+ assertThat(mBassClientService.getSyncedBroadcastSinks(TEST_BROADCAST_ID).isEmpty())
+ .isTrue();
// Update receiver state with broadcast ID
injectRemoteSourceStateChanged(meta, true, false);
@@ -4487,6 +4489,16 @@ public class BassClientServiceTest {
assertThat(mBassClientService.getSyncedBroadcastSinks().isEmpty()).isTrue();
}
+ activeSinks.clear();
+ // Verify getSyncedBroadcastSinks by broadcast id
+ activeSinks = mBassClientService.getSyncedBroadcastSinks(TEST_BROADCAST_ID);
+ if (Flags.leaudioBigDependsOnAudioState()) {
+ // Verify getSyncedBroadcastSinks returns correct device list if no BIS synced
+ assertThat(activeSinks.size()).isEqualTo(2);
+ assertThat(activeSinks.contains(mCurrentDevice)).isTrue();
+ assertThat(activeSinks.contains(mCurrentDevice1)).isTrue();
+ }
+
// Update receiver state with BIS sync
injectRemoteSourceStateChanged(meta, true, true);
@@ -6348,7 +6360,8 @@ public class BassClientServiceTest {
@Test
@EnableFlags({
Flags.FLAG_LEAUDIO_BROADCAST_RESYNC_HELPER,
- Flags.FLAG_LEAUDIO_BROADCAST_EXTRACT_PERIODIC_SCANNER_FROM_STATE_MACHINE
+ Flags.FLAG_LEAUDIO_BROADCAST_EXTRACT_PERIODIC_SCANNER_FROM_STATE_MACHINE,
+ Flags.FLAG_LEAUDIO_MONITOR_UNICAST_SOURCE_WHEN_MANAGED_BY_BROADCAST_DELEGATOR
})
public void sinkUnintentional_handleUnicastSourceStreamStatusChange_withoutScanning() {
sinkUnintentionalWithoutScanning();
@@ -6357,7 +6370,6 @@ public class BassClientServiceTest {
mBassClientService.handleUnicastSourceStreamStatusChange(
0 /* STATUS_LOCAL_STREAM_REQUESTED */);
verifyStopBigMonitoringWithUnsync();
- verifyRemoveMessageAndInjectSourceRemoval();
checkNoResumeSynchronizationByBig();
/* Unicast finished streaming */
@@ -6370,7 +6382,8 @@ public class BassClientServiceTest {
@Test
@EnableFlags({
Flags.FLAG_LEAUDIO_BROADCAST_RESYNC_HELPER,
- Flags.FLAG_LEAUDIO_BROADCAST_EXTRACT_PERIODIC_SCANNER_FROM_STATE_MACHINE
+ Flags.FLAG_LEAUDIO_BROADCAST_EXTRACT_PERIODIC_SCANNER_FROM_STATE_MACHINE,
+ Flags.FLAG_LEAUDIO_MONITOR_UNICAST_SOURCE_WHEN_MANAGED_BY_BROADCAST_DELEGATOR
})
public void sinkUnintentional_handleUnicastSourceStreamStatusChange_duringScanning() {
sinkUnintentionalDuringScanning();
@@ -6379,7 +6392,6 @@ public class BassClientServiceTest {
mBassClientService.handleUnicastSourceStreamStatusChange(
0 /* STATUS_LOCAL_STREAM_REQUESTED */);
verifyStopBigMonitoringWithoutUnsync();
- verifyRemoveMessageAndInjectSourceRemoval();
checkNoResumeSynchronizationByBig();
/* Unicast finished streaming */
@@ -6642,6 +6654,44 @@ public class BassClientServiceTest {
@Test
@EnableFlags({
Flags.FLAG_LEAUDIO_BROADCAST_RESYNC_HELPER,
+ Flags.FLAG_LEAUDIO_BROADCAST_EXTRACT_PERIODIC_SCANNER_FROM_STATE_MACHINE,
+ Flags.FLAG_LEAUDIO_BROADCAST_ASSISTANT_PERIPHERAL_ENTRUSTMENT,
+ Flags.FLAG_LEAUDIO_MONITOR_UNICAST_SOURCE_WHEN_MANAGED_BY_BROADCAST_DELEGATOR
+ })
+ public void hostIntentional_handleUnicastSourceStreamStatusChange_beforeResumeCompleted() {
+ prepareSynchronizedPairAndStopSearching();
+
+ /* Unicast would like to stream */
+ mBassClientService.handleUnicastSourceStreamStatusChange(
+ 0 /* STATUS_LOCAL_STREAM_REQUESTED */);
+ checkNoSinkPause();
+
+ /* Unicast finished streaming */
+ mBassClientService.handleUnicastSourceStreamStatusChange(
+ 2 /* STATUS_LOCAL_STREAM_SUSPENDED */);
+ mInOrderMethodProxy
+ .verify(mMethodProxy)
+ .periodicAdvertisingManagerRegisterSync(
+ any(), any(), anyInt(), anyInt(), any(), any());
+
+ /* Unicast would like to stream again before previous resume was complete*/
+ mBassClientService.handleUnicastSourceStreamStatusChange(
+ 0 /* STATUS_LOCAL_STREAM_REQUESTED */);
+
+ /* Unicast finished streaming */
+ mBassClientService.handleUnicastSourceStreamStatusChange(
+ 2 /* STATUS_LOCAL_STREAM_SUSPENDED */);
+ mInOrderMethodProxy
+ .verify(mMethodProxy)
+ .periodicAdvertisingManagerRegisterSync(
+ any(), any(), anyInt(), anyInt(), any(), any());
+ onSyncEstablished(mSourceDevice, TEST_SYNC_HANDLE); // In case of add source to inactive
+ verifyAllGroupMembersGettingUpdateOrAddSource(createBroadcastMetadata(TEST_BROADCAST_ID));
+ }
+
+ @Test
+ @EnableFlags({
+ Flags.FLAG_LEAUDIO_BROADCAST_RESYNC_HELPER,
Flags.FLAG_LEAUDIO_BROADCAST_EXTRACT_PERIODIC_SCANNER_FROM_STATE_MACHINE
})
public void hostIntentional_handleUnicastSourceStreamStatusChangeNoContext_withoutScanning() {
diff --git a/android/app/tests/unit/src/com/android/bluetooth/bass_client/BassClientStateMachineTest.java b/android/app/tests/unit/src/com/android/bluetooth/bass_client/BassClientStateMachineTest.java
index f3854445e2..e6b2d5f1d6 100644
--- a/android/app/tests/unit/src/com/android/bluetooth/bass_client/BassClientStateMachineTest.java
+++ b/android/app/tests/unit/src/com/android/bluetooth/bass_client/BassClientStateMachineTest.java
@@ -48,6 +48,7 @@ import static com.android.bluetooth.bass_client.BassClientStateMachine.UPDATE_BC
import static com.android.bluetooth.bass_client.BassConstants.CLIENT_CHARACTERISTIC_CONFIG;
import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
@@ -111,7 +112,6 @@ import com.google.common.primitives.Bytes;
import org.hamcrest.Matcher;
import org.hamcrest.core.AllOf;
import org.junit.After;
-import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
@@ -210,7 +210,7 @@ public class BassClientStateMachineTest {
|| type == BassClientStateMachine.ConnectedProcessing.class) {
return BluetoothProfile.STATE_CONNECTED;
} else {
- Assert.fail("Invalid class type given: " + type);
+ assertWithMessage("Invalid class type given: " + type).fail();
return 0;
}
}
diff --git a/android/app/tests/unit/src/com/android/bluetooth/btservice/AdapterServiceTest.java b/android/app/tests/unit/src/com/android/bluetooth/btservice/AdapterServiceTest.java
index 44956086f3..f3dab9d9be 100644
--- a/android/app/tests/unit/src/com/android/bluetooth/btservice/AdapterServiceTest.java
+++ b/android/app/tests/unit/src/com/android/bluetooth/btservice/AdapterServiceTest.java
@@ -81,6 +81,7 @@ import com.android.bluetooth.flags.Flags;
import com.android.bluetooth.gatt.AdvertiseManagerNativeInterface;
import com.android.bluetooth.gatt.DistanceMeasurementNativeInterface;
import com.android.bluetooth.gatt.GattNativeInterface;
+import com.android.bluetooth.le_audio.LeAudioService;
import com.android.bluetooth.le_scan.PeriodicScanNativeInterface;
import com.android.bluetooth.le_scan.ScanNativeInterface;
import com.android.bluetooth.sdp.SdpManagerNativeInterface;
@@ -93,6 +94,7 @@ import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
@@ -144,6 +146,7 @@ public class AdapterServiceTest {
private @Mock Context mMockContext;
private @Mock ApplicationInfo mMockApplicationInfo;
+ private @Mock LeAudioService mMockLeAudioService;
private @Mock Resources mMockResources;
private @Mock ProfileService mMockGattService;
private @Mock ProfileService mMockService;
@@ -185,6 +188,10 @@ public class AdapterServiceTest {
private int mForegroundUserId;
private TestLooper mLooper;
+ private final BluetoothAdapter mAdapter = BluetoothAdapter.getDefaultAdapter();
+ private final BluetoothDevice mDevice = TestUtils.getTestDevice(mAdapter, 0);
+ private final BluetoothDevice mDeviceTwo = TestUtils.getTestDevice(mAdapter, 2);
+
static void configureEnabledProfiles() {
Log.e(TAG, "configureEnabledProfiles");
@@ -224,6 +231,12 @@ public class AdapterServiceTest {
doReturn(mJniCallbacks).when(mNativeInterface).getCallbacks();
+ doReturn(true).when(mMockLeAudioService).isAvailable();
+ LeAudioService.setLeAudioService(mMockLeAudioService);
+ doReturn(BluetoothProfile.CONNECTION_POLICY_ALLOWED)
+ .when(mMockLeAudioService)
+ .getConnectionPolicy(any());
+
AdapterNativeInterface.setInstance(mNativeInterface);
BluetoothKeystoreNativeInterface.setInstance(mKeystoreNativeInterface);
BluetoothQualityReportNativeInterface.setInstance(mQualityNativeInterface);
@@ -351,6 +364,7 @@ public class AdapterServiceTest {
// Restores the foregroundUserId to the ID prior to the test setup
Utils.setForegroundUserId(mForegroundUserId);
+ LeAudioService.setLeAudioService(null);
mAdapterService.cleanup();
mAdapterService.unregisterRemoteCallback(mIBluetoothCallback);
AdapterNativeInterface.setInstance(null);
@@ -1121,4 +1135,333 @@ public class AdapterServiceTest {
}
assertThat(mLooper.nextMessage()).isNull();
}
+
+ InOrder prepareLeAudioWithConnectedDevices(
+ List<BluetoothDevice> devices,
+ int groupId,
+ boolean returnOnSetAutoActiveModeState,
+ int returnOnGetConnectionStateLeAudio,
+ int returnOnGetConnectionStateAdapter) {
+ doEnable(false);
+
+ doReturn(groupId).when(mMockLeAudioService).getGroupId(any());
+
+ doReturn(returnOnGetConnectionStateLeAudio)
+ .when(mMockLeAudioService)
+ .getConnectionState(any());
+ doReturn(returnOnGetConnectionStateAdapter)
+ .when(mNativeInterface)
+ .getConnectionState(any());
+
+ doReturn(returnOnSetAutoActiveModeState)
+ .when(mMockLeAudioService)
+ .setAutoActiveModeState(groupId, false);
+ doReturn(devices).when(mMockLeAudioService).getGroupDevices(groupId);
+
+ return inOrder(mMockLeAudioService);
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ALLOW_GATT_CONNECT_FROM_THE_APPS_WITHOUT_MAKING_LEAUDIO_DEVICE_ACTIVE)
+ public void testGattConnectionToLeAudioDevice_whenDeviceIsNotConnected_success() {
+ int groupId = 1;
+ int getConnectionState_LeAudioService = BluetoothProfile.STATE_CONNECTED;
+ int getConnectionState_AdapterService =
+ BluetoothDevice.CONNECTION_STATE_ENCRYPTED_LE
+ | BluetoothDevice.CONNECTION_STATE_CONNECTED;
+ InOrder order =
+ prepareLeAudioWithConnectedDevices(
+ List.of(mDevice),
+ groupId,
+ true,
+ getConnectionState_LeAudioService,
+ getConnectionState_AdapterService);
+
+ mAdapterService.notifyDirectLeGattClientConnect(1, mDevice);
+
+ order.verify(mMockLeAudioService).setAutoActiveModeState(groupId, false);
+ assertThat(mAdapterService.mLeGattClientsControllingAutoActiveMode.size()).isEqualTo(1);
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ALLOW_GATT_CONNECT_FROM_THE_APPS_WITHOUT_MAKING_LEAUDIO_DEVICE_ACTIVE)
+ public void testGattConnectionToLeAudioDevice_whenDeviceIsConnected_ignore() {
+ int groupId = 1;
+ int getConnectionState_LeAudioService = BluetoothProfile.STATE_CONNECTED;
+ int getConnectionState_AdapterService =
+ BluetoothDevice.CONNECTION_STATE_ENCRYPTED_LE
+ | BluetoothDevice.CONNECTION_STATE_CONNECTED;
+ InOrder order =
+ prepareLeAudioWithConnectedDevices(
+ List.of(mDevice),
+ groupId,
+ false,
+ getConnectionState_LeAudioService,
+ getConnectionState_AdapterService);
+
+ mAdapterService.notifyDirectLeGattClientConnect(1, mDevice);
+
+ order.verify(mMockLeAudioService).setAutoActiveModeState(groupId, false);
+ assertThat(mAdapterService.mLeGattClientsControllingAutoActiveMode).isEmpty();
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ALLOW_GATT_CONNECT_FROM_THE_APPS_WITHOUT_MAKING_LEAUDIO_DEVICE_ACTIVE)
+ public void testGattConnectionToLeAudioDevice_whenLeAudioIsNotAllowed_ignore() {
+ int groupId = 1;
+ int getConnectionState_LeAudioService = BluetoothProfile.STATE_DISCONNECTED;
+ int getConnectionState_AdapterService =
+ BluetoothDevice.CONNECTION_STATE_ENCRYPTED_LE
+ | BluetoothDevice.CONNECTION_STATE_CONNECTED;
+ InOrder order =
+ prepareLeAudioWithConnectedDevices(
+ List.of(mDevice),
+ groupId,
+ false,
+ getConnectionState_LeAudioService,
+ getConnectionState_AdapterService);
+
+ doReturn(BluetoothProfile.CONNECTION_POLICY_FORBIDDEN)
+ .when(mMockLeAudioService)
+ .getConnectionPolicy(any());
+ mAdapterService.notifyDirectLeGattClientConnect(1, mDevice);
+
+ order.verify(mMockLeAudioService, never()).setAutoActiveModeState(groupId, false);
+ assertThat(mAdapterService.mLeGattClientsControllingAutoActiveMode).isEmpty();
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ALLOW_GATT_CONNECT_FROM_THE_APPS_WITHOUT_MAKING_LEAUDIO_DEVICE_ACTIVE)
+ public void testGattConnectionToLeAudioDevice_failedToConnect() {
+ int groupId = 1;
+ int clientIf = 1;
+
+ int getConnectionState_LeAudioService = BluetoothProfile.STATE_CONNECTED;
+ int getConnectionState_AdapterService =
+ BluetoothDevice.CONNECTION_STATE_ENCRYPTED_LE
+ | BluetoothDevice.CONNECTION_STATE_CONNECTED;
+ InOrder order =
+ prepareLeAudioWithConnectedDevices(
+ List.of(mDevice),
+ groupId,
+ true,
+ getConnectionState_LeAudioService,
+ getConnectionState_AdapterService);
+
+ mAdapterService.notifyDirectLeGattClientConnect(clientIf, mDevice);
+
+ order.verify(mMockLeAudioService).setAutoActiveModeState(groupId, false);
+ assertThat(mAdapterService.mLeGattClientsControllingAutoActiveMode.size()).isEqualTo(1);
+
+ mAdapterService.notifyGattClientConnectFailed(clientIf, mDevice);
+ order.verify(mMockLeAudioService).setAutoActiveModeState(groupId, true);
+ assertThat(mAdapterService.mLeGattClientsControllingAutoActiveMode).isEmpty();
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ALLOW_GATT_CONNECT_FROM_THE_APPS_WITHOUT_MAKING_LEAUDIO_DEVICE_ACTIVE)
+ public void testGattConnectionToLeAudioDevice_triggerDisconnected() {
+ int groupId = 1;
+ int clientIf = 1;
+
+ int getConnectionState_LeAudioService = BluetoothProfile.STATE_DISCONNECTED;
+ int getConnectionState_AdapterService = BluetoothDevice.CONNECTION_STATE_DISCONNECTED;
+ InOrder order =
+ prepareLeAudioWithConnectedDevices(
+ List.of(mDevice),
+ groupId,
+ true,
+ getConnectionState_LeAudioService,
+ getConnectionState_AdapterService);
+ InOrder orderNative = inOrder(mNativeInterface);
+
+ mAdapterService.notifyDirectLeGattClientConnect(clientIf, mDevice);
+
+ order.verify(mMockLeAudioService).setAutoActiveModeState(groupId, false);
+ assertThat(mAdapterService.mLeGattClientsControllingAutoActiveMode.size()).isEqualTo(1);
+
+ mAdapterService.notifyGattClientDisconnect(clientIf, mDevice);
+ orderNative.verify(mNativeInterface, never()).disconnectAcl(any(), anyInt());
+ order.verify(mMockLeAudioService).setAutoActiveModeState(groupId, true);
+ assertThat(mAdapterService.mLeGattClientsControllingAutoActiveMode).isEmpty();
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ALLOW_GATT_CONNECT_FROM_THE_APPS_WITHOUT_MAKING_LEAUDIO_DEVICE_ACTIVE)
+ public void testGattConnectionToLeAudioDevice_triggerDisconnecting() {
+ int groupId = 1;
+ int clientIf = 1;
+ int getConnectionState_LeAudioService = BluetoothProfile.STATE_CONNECTED;
+ int getConnectionState_AdapterService =
+ BluetoothDevice.CONNECTION_STATE_ENCRYPTED_LE
+ | BluetoothDevice.CONNECTION_STATE_CONNECTED;
+ InOrder order =
+ prepareLeAudioWithConnectedDevices(
+ List.of(mDevice),
+ groupId,
+ true,
+ getConnectionState_LeAudioService,
+ getConnectionState_AdapterService);
+
+ InOrder orderNative = inOrder(mNativeInterface);
+
+ mAdapterService.notifyDirectLeGattClientConnect(clientIf, mDevice);
+
+ order.verify(mMockLeAudioService).setAutoActiveModeState(groupId, false);
+ assertThat(mAdapterService.mLeGattClientsControllingAutoActiveMode.size()).isEqualTo(1);
+
+ mAdapterService.notifyGattClientDisconnect(clientIf, mDevice);
+ order.verify(mMockLeAudioService).setAutoActiveModeState(groupId, true);
+ orderNative.verify(mNativeInterface).disconnectAcl(any(), eq(BluetoothDevice.TRANSPORT_LE));
+ assertThat(mAdapterService.mLeGattClientsControllingAutoActiveMode).isEmpty();
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ALLOW_GATT_CONNECT_FROM_THE_APPS_WITHOUT_MAKING_LEAUDIO_DEVICE_ACTIVE)
+ public void testGattConnectionToLeAudioDevice_connectingMultipleClients() {
+ int groupId = 1;
+ int clientIf = 1;
+ int clientIfTwo = 2;
+
+ int getConnectionState_LeAudioService = BluetoothProfile.STATE_CONNECTED;
+ int getConnectionState_AdapterService =
+ BluetoothDevice.CONNECTION_STATE_ENCRYPTED_LE
+ | BluetoothDevice.CONNECTION_STATE_CONNECTED;
+ InOrder order =
+ prepareLeAudioWithConnectedDevices(
+ List.of(mDevice),
+ groupId,
+ true,
+ getConnectionState_LeAudioService,
+ getConnectionState_AdapterService);
+
+ InOrder orderNative = inOrder(mNativeInterface);
+
+ // Connect first client to device
+ mAdapterService.notifyDirectLeGattClientConnect(clientIf, mDevice);
+
+ order.verify(mMockLeAudioService).setAutoActiveModeState(groupId, false);
+ assertThat(mAdapterService.mLeGattClientsControllingAutoActiveMode.size()).isEqualTo(1);
+
+ // Connect second client to device
+ mAdapterService.notifyDirectLeGattClientConnect(clientIfTwo, mDevice);
+
+ order.verify(mMockLeAudioService, never()).setAutoActiveModeState(groupId, false);
+ assertThat(mAdapterService.mLeGattClientsControllingAutoActiveMode.size()).isEqualTo(2);
+
+ // Disconnect first client to device
+ mAdapterService.notifyGattClientDisconnect(clientIf, mDevice);
+ order.verify(mMockLeAudioService, never()).setAutoActiveModeState(groupId, true);
+ orderNative.verify(mNativeInterface, never()).disconnectAcl(any(), anyInt());
+ assertThat(mAdapterService.mLeGattClientsControllingAutoActiveMode.size()).isEqualTo(1);
+
+ // Disconnect second client to device
+ mAdapterService.notifyGattClientDisconnect(clientIfTwo, mDevice);
+ order.verify(mMockLeAudioService).setAutoActiveModeState(groupId, true);
+ orderNative
+ .verify(mNativeInterface, times(1))
+ .disconnectAcl(any(), eq(BluetoothDevice.TRANSPORT_LE));
+ assertThat(mAdapterService.mLeGattClientsControllingAutoActiveMode).isEmpty();
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ALLOW_GATT_CONNECT_FROM_THE_APPS_WITHOUT_MAKING_LEAUDIO_DEVICE_ACTIVE)
+ public void testGattConnectionToLeAudioDevice_connectingMultipleDevicesInSameGroup() {
+ int groupId = 1;
+ int clientIf = 1;
+ int clientIfTwo = 2;
+
+ int getConnectionState_LeAudioService = BluetoothProfile.STATE_CONNECTED;
+ int getConnectionState_AdapterService =
+ BluetoothDevice.CONNECTION_STATE_ENCRYPTED_LE
+ | BluetoothDevice.CONNECTION_STATE_CONNECTED;
+ InOrder order =
+ prepareLeAudioWithConnectedDevices(
+ List.of(mDevice, mDeviceTwo),
+ groupId,
+ true,
+ getConnectionState_LeAudioService,
+ getConnectionState_AdapterService);
+
+ InOrder orderNative = inOrder(mNativeInterface);
+
+ // Connecting device one
+ when(mMockLeAudioService.setAutoActiveModeState(groupId, false)).thenReturn(true);
+ mAdapterService.notifyDirectLeGattClientConnect(clientIf, mDevice);
+
+ order.verify(mMockLeAudioService).setAutoActiveModeState(groupId, false);
+ assertThat(mAdapterService.mLeGattClientsControllingAutoActiveMode.size()).isEqualTo(1);
+
+ // Connecting device two
+ mAdapterService.notifyDirectLeGattClientConnect(clientIfTwo, mDeviceTwo);
+
+ order.verify(mMockLeAudioService, never()).setAutoActiveModeState(groupId, false);
+ assertThat(mAdapterService.mLeGattClientsControllingAutoActiveMode.size()).isEqualTo(2);
+
+ // Disconnect first device
+ mAdapterService.notifyGattClientDisconnect(clientIf, mDevice);
+ order.verify(mMockLeAudioService, never()).setAutoActiveModeState(groupId, true);
+ orderNative.verify(mNativeInterface, never()).disconnectAcl(any(), anyInt());
+ assertThat(mAdapterService.mLeGattClientsControllingAutoActiveMode.size()).isEqualTo(1);
+
+ // Disconnect second device
+ mAdapterService.notifyGattClientDisconnect(clientIfTwo, mDeviceTwo);
+ order.verify(mMockLeAudioService).setAutoActiveModeState(groupId, true);
+ orderNative
+ .verify(mNativeInterface, times(2))
+ .disconnectAcl(any(), eq(BluetoothDevice.TRANSPORT_LE));
+ assertThat(mAdapterService.mLeGattClientsControllingAutoActiveMode).isEmpty();
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ALLOW_GATT_CONNECT_FROM_THE_APPS_WITHOUT_MAKING_LEAUDIO_DEVICE_ACTIVE)
+ public void testGattConnectionToLeAudioDevice_remoteSwitchesToActiveBeforeDisconnect() {
+ int groupId = 1;
+ int clientIf = 1;
+ int clientIfTwo = 2;
+
+ int getConnectionState_LeAudioService = BluetoothProfile.STATE_CONNECTED;
+ int getConnectionState_AdapterService =
+ BluetoothDevice.CONNECTION_STATE_ENCRYPTED_LE
+ | BluetoothDevice.CONNECTION_STATE_CONNECTED;
+ InOrder order =
+ prepareLeAudioWithConnectedDevices(
+ List.of(mDevice, mDeviceTwo),
+ groupId,
+ true,
+ getConnectionState_LeAudioService,
+ getConnectionState_AdapterService);
+
+ InOrder orderNative = inOrder(mNativeInterface);
+
+ // Connecting device one
+ when(mMockLeAudioService.setAutoActiveModeState(groupId, false)).thenReturn(true);
+ mAdapterService.notifyDirectLeGattClientConnect(clientIf, mDevice);
+
+ order.verify(mMockLeAudioService).setAutoActiveModeState(groupId, false);
+ assertThat(mAdapterService.mLeGattClientsControllingAutoActiveMode.size()).isEqualTo(1);
+
+ // Connecting device two
+ mAdapterService.notifyDirectLeGattClientConnect(clientIfTwo, mDeviceTwo);
+
+ order.verify(mMockLeAudioService, never()).setAutoActiveModeState(groupId, false);
+ assertThat(mAdapterService.mLeGattClientsControllingAutoActiveMode.size()).isEqualTo(2);
+
+ // Remote switches to Active
+ when(mMockLeAudioService.isAutoActiveModeEnabled(groupId)).thenReturn(true);
+
+ // Disconnect first device
+ mAdapterService.notifyGattClientDisconnect(clientIf, mDevice);
+ order.verify(mMockLeAudioService, never()).setAutoActiveModeState(groupId, true);
+ orderNative.verify(mNativeInterface, never()).disconnectAcl(any(), anyInt());
+ assertThat(mAdapterService.mLeGattClientsControllingAutoActiveMode.size()).isEqualTo(1);
+
+ // Disconnect second device
+ mAdapterService.notifyGattClientDisconnect(clientIfTwo, mDeviceTwo);
+
+ // Verify devices will not be disconnected
+ order.verify(mMockLeAudioService).setAutoActiveModeState(groupId, true);
+ orderNative.verify(mNativeInterface, never()).disconnectAcl(any(), anyInt());
+ assertThat(mAdapterService.mLeGattClientsControllingAutoActiveMode).isEmpty();
+ }
}
diff --git a/android/app/tests/unit/src/com/android/bluetooth/btservice/BondStateMachineTest.java b/android/app/tests/unit/src/com/android/bluetooth/btservice/BondStateMachineTest.java
index cc40c6af72..e274ac6eed 100644
--- a/android/app/tests/unit/src/com/android/bluetooth/btservice/BondStateMachineTest.java
+++ b/android/app/tests/unit/src/com/android/bluetooth/btservice/BondStateMachineTest.java
@@ -218,7 +218,7 @@ public class BondStateMachineTest {
RemoteDevices.DeviceProperties testDeviceProperties =
mRemoteDevices.addDeviceProperties(TEST_BT_ADDR_BYTES);
- testDeviceProperties.mUuids = TEST_UUIDS;
+ testDeviceProperties.mUuidsBrEdr = TEST_UUIDS;
BluetoothDevice testDevice = testDeviceProperties.getDevice();
assertThat(testDevice).isNotNull();
@@ -228,7 +228,7 @@ public class BondStateMachineTest {
bondingMsg.arg2 = AbstractionLayer.BT_STATUS_RMT_DEV_DOWN;
mBondStateMachine.sendMessage(bondingMsg);
- pendingDeviceProperties.mUuids = TEST_UUIDS;
+ pendingDeviceProperties.mUuidsBrEdr = TEST_UUIDS;
Message uuidUpdateMsg = mBondStateMachine.obtainMessage(BondStateMachine.UUID_UPDATE);
uuidUpdateMsg.obj = pendingDevice;
@@ -634,7 +634,7 @@ public class BondStateMachineTest {
}
if (uuids != null) {
// Add dummy UUID for the device.
- mDeviceProperties.mUuids = TEST_UUIDS;
+ mDeviceProperties.mUuidsBrEdr = TEST_UUIDS;
}
testSendIntentCase(
oldState,
diff --git a/android/app/tests/unit/src/com/android/bluetooth/btservice/storage/DatabaseManagerTest.java b/android/app/tests/unit/src/com/android/bluetooth/btservice/storage/DatabaseManagerTest.java
index b2c68e4cfa..2753ee7b17 100644
--- a/android/app/tests/unit/src/com/android/bluetooth/btservice/storage/DatabaseManagerTest.java
+++ b/android/app/tests/unit/src/com/android/bluetooth/btservice/storage/DatabaseManagerTest.java
@@ -18,6 +18,7 @@ package com.android.bluetooth.btservice.storage;
import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.doAnswer;
@@ -788,16 +789,16 @@ public final class DatabaseManagerTest {
preferences.putInt(BluetoothAdapter.AUDIO_MODE_DUPLEX, BluetoothProfile.LE_AUDIO);
// TEST 1: If input is invalid, throws the right Exception
- Assert.assertThrows(
+ assertThrows(
NullPointerException.class,
() -> mDatabaseManager.setPreferredAudioProfiles(null, preferences));
- Assert.assertThrows(
+ assertThrows(
NullPointerException.class,
() -> mDatabaseManager.setPreferredAudioProfiles(new ArrayList<>(), null));
- Assert.assertThrows(
+ assertThrows(
IllegalArgumentException.class,
() -> mDatabaseManager.setPreferredAudioProfiles(new ArrayList<>(), preferences));
- Assert.assertThrows(
+ assertThrows(
IllegalArgumentException.class,
() -> mDatabaseManager.getPreferredAudioProfiles(null));
@@ -1748,7 +1749,7 @@ public final class DatabaseManagerTest {
// Check whether the value is saved in database
restartDatabaseManagerHelper();
- Assert.assertArrayEquals(value, mDatabaseManager.getCustomMeta(mTestDevice, key));
+ assertThat(mDatabaseManager.getCustomMeta(mTestDevice, key)).isEqualTo(value);
mDatabaseManager.factoryReset();
mDatabaseManager.mMetadataCache.clear();
diff --git a/android/app/tests/unit/src/com/android/bluetooth/gatt/AdvertiseBinderTest.java b/android/app/tests/unit/src/com/android/bluetooth/gatt/AdvertiseBinderTest.java
index 3a0abb2870..1eca1e2483 100644
--- a/android/app/tests/unit/src/com/android/bluetooth/gatt/AdvertiseBinderTest.java
+++ b/android/app/tests/unit/src/com/android/bluetooth/gatt/AdvertiseBinderTest.java
@@ -16,6 +16,8 @@
package com.android.bluetooth.gatt;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
@@ -54,6 +56,13 @@ public class AdvertiseBinderTest {
@Before
public void setUp() {
+ doAnswer(
+ invocation -> {
+ ((Runnable) invocation.getArgument(0)).run();
+ return null;
+ })
+ .when(mAdvertiseManager)
+ .doOnAdvertiseThread(any());
mBinder = new AdvertiseBinder(mAdapterService, mAdvertiseManager);
}
diff --git a/android/app/tests/unit/src/com/android/bluetooth/gatt/AdvertiseManagerTest.java b/android/app/tests/unit/src/com/android/bluetooth/gatt/AdvertiseManagerTest.java
index 6621a43331..4cd5423ce9 100644
--- a/android/app/tests/unit/src/com/android/bluetooth/gatt/AdvertiseManagerTest.java
+++ b/android/app/tests/unit/src/com/android/bluetooth/gatt/AdvertiseManagerTest.java
@@ -27,15 +27,16 @@ import android.bluetooth.le.AdvertisingSetParameters;
import android.bluetooth.le.IAdvertisingSetCallback;
import android.bluetooth.le.PeriodicAdvertisingParameters;
import android.os.IBinder;
+import android.os.test.TestLooper;
+import android.platform.test.flag.junit.FlagsParameterization;
+import android.platform.test.flag.junit.SetFlagsRule;
import androidx.test.InstrumentationRegistry;
import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
-import com.android.bluetooth.TestUtils;
import com.android.bluetooth.btservice.AdapterService;
+import com.android.bluetooth.flags.Flags;
-import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
@@ -44,17 +45,21 @@ import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4;
+import platform.test.runner.parameterized.Parameters;
+
+import java.util.List;
+
/** Test cases for {@link AdvertiseManager}. */
@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(ParameterizedAndroidJunit4.class)
public class AdvertiseManagerTest {
@Rule public MockitoRule mockitoRule = MockitoJUnit.rule();
+ @Rule public final SetFlagsRule mSetFlagsRule;
@Mock private AdapterService mAdapterService;
- @Mock private GattService mService;
-
@Mock private AdvertiserMap mAdvertiserMap;
@Mock private AdvertiseManagerNativeInterface mNativeInterface;
@@ -66,10 +71,23 @@ public class AdvertiseManagerTest {
private AdvertiseManager mAdvertiseManager;
private int mAdvertiserId;
+ @Parameters(name = "{0}")
+ public static List<FlagsParameterization> getParams() {
+ return FlagsParameterization.allCombinationsOf(Flags.FLAG_ADVERTISE_THREAD);
+ }
+
+ public AdvertiseManagerTest(FlagsParameterization flags) {
+ mSetFlagsRule = new SetFlagsRule(flags);
+ }
+
@Before
public void setUp() throws Exception {
- TestUtils.setAdapterService(mAdapterService);
- mAdvertiseManager = new AdvertiseManager(mService, mNativeInterface, mAdvertiserMap);
+ mAdvertiseManager =
+ new AdvertiseManager(
+ mAdapterService,
+ new TestLooper().getLooper(),
+ mNativeInterface,
+ mAdvertiserMap);
AdvertisingSetParameters parameters = new AdvertisingSetParameters.Builder().build();
AdvertiseData advertiseData = new AdvertiseData.Builder().build();
@@ -95,12 +113,7 @@ public class AdvertiseManagerTest {
mCallback,
InstrumentationRegistry.getTargetContext().getAttributionSource());
- mAdvertiserId = AdvertiseManager.sTempRegistrationId;
- }
-
- @After
- public void tearDown() throws Exception {
- TestUtils.clearAdapterService(mAdapterService);
+ mAdvertiserId = mAdvertiseManager.mTempRegistrationId;
}
@Test
diff --git a/android/app/tests/unit/src/com/android/bluetooth/gatt/GattServiceTest.java b/android/app/tests/unit/src/com/android/bluetooth/gatt/GattServiceTest.java
index f64e934955..2ff148fa41 100644
--- a/android/app/tests/unit/src/com/android/bluetooth/gatt/GattServiceTest.java
+++ b/android/app/tests/unit/src/com/android/bluetooth/gatt/GattServiceTest.java
@@ -36,6 +36,7 @@ import android.content.Context;
import android.content.res.Resources;
import android.location.LocationManager;
import android.os.Bundle;
+import android.os.Process;
import android.platform.test.flag.junit.SetFlagsRule;
import android.provider.Settings;
import android.test.mock.MockContentProvider;
@@ -269,6 +270,118 @@ public class GattServiceTest {
}
@Test
+ public void clientConnectOverLeFailed() throws Exception {
+ int clientIf = 1;
+ String address = REMOTE_DEVICE_ADDRESS;
+ int addressType = BluetoothDevice.ADDRESS_TYPE_RANDOM;
+ boolean isDirect = true;
+ int transport = BluetoothDevice.TRANSPORT_LE;
+ boolean opportunistic = false;
+ int phy = 3;
+
+ AttributionSource testAttributeSource =
+ new AttributionSource.Builder(Process.SYSTEM_UID)
+ .setPid(Process.myPid())
+ .setDeviceId(Context.DEVICE_ID_DEFAULT)
+ .setPackageName("com.google.android.gms")
+ .setAttributionTag("com.google.android.gms.findmydevice")
+ .build();
+
+ mService.clientConnect(
+ clientIf,
+ address,
+ addressType,
+ isDirect,
+ transport,
+ opportunistic,
+ phy,
+ testAttributeSource);
+
+ verify(mAdapterService).notifyDirectLeGattClientConnect(anyInt(), any());
+ verify(mNativeInterface)
+ .gattClientConnect(
+ clientIf, address, addressType, isDirect, transport, opportunistic, phy, 0);
+ mService.onConnected(clientIf, 0, BluetoothGatt.GATT_CONNECTION_TIMEOUT, address);
+ verify(mAdapterService).notifyGattClientConnectFailed(anyInt(), any());
+ }
+
+ @Test
+ public void clientConnectDisconnectOverLe() throws Exception {
+ int clientIf = 1;
+ String address = REMOTE_DEVICE_ADDRESS;
+ int addressType = BluetoothDevice.ADDRESS_TYPE_RANDOM;
+ boolean isDirect = true;
+ int transport = BluetoothDevice.TRANSPORT_LE;
+ boolean opportunistic = false;
+ int phy = 3;
+
+ AttributionSource testAttributeSource =
+ new AttributionSource.Builder(Process.SYSTEM_UID)
+ .setPid(Process.myPid())
+ .setDeviceId(Context.DEVICE_ID_DEFAULT)
+ .setPackageName("com.google.android.gms")
+ .setAttributionTag("com.google.android.gms.findmydevice")
+ .build();
+
+ mService.clientConnect(
+ clientIf,
+ address,
+ addressType,
+ isDirect,
+ transport,
+ opportunistic,
+ phy,
+ testAttributeSource);
+
+ verify(mAdapterService).notifyDirectLeGattClientConnect(anyInt(), any());
+ verify(mNativeInterface)
+ .gattClientConnect(
+ clientIf, address, addressType, isDirect, transport, opportunistic, phy, 0);
+ mService.onConnected(clientIf, 15, BluetoothGatt.GATT_SUCCESS, address);
+ mService.clientDisconnect(clientIf, address, mAttributionSource);
+
+ verify(mAdapterService).notifyGattClientDisconnect(anyInt(), any());
+ }
+
+ @Test
+ public void clientConnectOverLeDisconnectedByRemote() throws Exception {
+ int clientIf = 1;
+ String address = REMOTE_DEVICE_ADDRESS;
+ int addressType = BluetoothDevice.ADDRESS_TYPE_RANDOM;
+ boolean isDirect = true;
+ int transport = BluetoothDevice.TRANSPORT_LE;
+ boolean opportunistic = false;
+ int phy = 3;
+
+ AttributionSource testAttributeSource =
+ new AttributionSource.Builder(Process.SYSTEM_UID)
+ .setPid(Process.myPid())
+ .setDeviceId(Context.DEVICE_ID_DEFAULT)
+ .setPackageName("com.google.android.gms")
+ .setAttributionTag("com.google.android.gms.findmydevice")
+ .build();
+
+ mService.clientConnect(
+ clientIf,
+ address,
+ addressType,
+ isDirect,
+ transport,
+ opportunistic,
+ phy,
+ testAttributeSource);
+
+ verify(mAdapterService).notifyDirectLeGattClientConnect(anyInt(), any());
+ verify(mNativeInterface)
+ .gattClientConnect(
+ clientIf, address, addressType, isDirect, transport, opportunistic, phy, 0);
+ mService.onConnected(clientIf, 15, BluetoothGatt.GATT_SUCCESS, address);
+ mService.onDisconnected(clientIf, 15, 1, address);
+
+ verify(mAdapterService).notifyGattClientDisconnect(anyInt(), any());
+ }
+
+ @Test
public void disconnectAll() {
Map<Integer, String> connMap = new HashMap<>();
int clientIf = 1;
diff --git a/android/app/tests/unit/src/com/android/bluetooth/hfp/HeadsetServiceTest.java b/android/app/tests/unit/src/com/android/bluetooth/hfp/HeadsetServiceTest.java
index 0ff583d158..b7a9a49116 100644
--- a/android/app/tests/unit/src/com/android/bluetooth/hfp/HeadsetServiceTest.java
+++ b/android/app/tests/unit/src/com/android/bluetooth/hfp/HeadsetServiceTest.java
@@ -18,9 +18,11 @@ package com.android.bluetooth.hfp;
import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.anyBoolean;
import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.atLeast;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn;
@@ -46,6 +48,7 @@ import android.media.AudioManager;
import android.os.ParcelUuid;
import android.os.RemoteException;
import android.os.SystemClock;
+import android.platform.test.annotations.EnableFlags;
import androidx.test.InstrumentationRegistry;
import androidx.test.filters.MediumTest;
@@ -60,7 +63,6 @@ import com.android.bluetooth.btservice.storage.DatabaseManager;
import com.android.bluetooth.flags.Flags;
import org.junit.After;
-import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
@@ -397,12 +399,9 @@ public class HeadsetServiceTest {
HeadsetStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED,
HeadsetHalConstants.CONNECTION_STATE_DISCONNECTED,
mCurrentDevice);
- try {
- mHeadsetService.messageFromNative(connectingEvent);
- Assert.fail("Expect an IllegalStateException");
- } catch (IllegalStateException exception) {
- // Do nothing
- }
+ assertThrows(
+ IllegalStateException.class,
+ () -> mHeadsetService.messageFromNative(connectingEvent));
verifyNoMoreInteractions(mObjectsFactory);
}
@@ -1299,6 +1298,54 @@ public class HeadsetServiceTest {
assertThat(mHeadsetService.isInbandRingingEnabled()).isFalse();
}
+ @Test
+ @EnableFlags({Flags.FLAG_UPDATE_ACTIVE_DEVICE_IN_BAND_RINGTONE})
+ public void testIncomingCallDeviceConnect_InbandRingStatus() {
+ when(mDatabaseManager.getProfileConnectionPolicy(
+ any(BluetoothDevice.class), eq(BluetoothProfile.HEADSET)))
+ .thenReturn(BluetoothProfile.CONNECTION_POLICY_UNKNOWN);
+ mCurrentDevice = TestUtils.getTestDevice(mAdapter, 0);
+ connectDeviceHelper(mCurrentDevice);
+
+ when(mStateMachines.get(mCurrentDevice).getDevice()).thenReturn(mCurrentDevice);
+ when(mStateMachines.get(mCurrentDevice).getConnectionState())
+ .thenReturn(BluetoothProfile.STATE_CONNECTED);
+
+ when(mSystemInterface.isRinging()).thenReturn(true);
+ mHeadsetService.setActiveDevice(mCurrentDevice);
+
+ verify(mNativeInterface).setActiveDevice(mCurrentDevice);
+ verify(mStateMachines.get(mCurrentDevice))
+ .sendMessage(HeadsetStateMachine.CONNECT_AUDIO, mCurrentDevice);
+ verify(mStateMachines.get(mCurrentDevice))
+ .sendMessage(eq(HeadsetStateMachine.SEND_BSIR), eq(1));
+ }
+
+ @Test
+ @EnableFlags({Flags.FLAG_UPDATE_ACTIVE_DEVICE_IN_BAND_RINGTONE})
+ public void testIncomingCallWithDeviceAudioConnected() {
+ ArrayList<BluetoothDevice> connectedDevices = new ArrayList<>();
+ when(mDatabaseManager.getProfileConnectionPolicy(
+ any(BluetoothDevice.class), eq(BluetoothProfile.HEADSET)))
+ .thenReturn(BluetoothProfile.CONNECTION_POLICY_UNKNOWN);
+ for (int i = 2; i >= 0; i--) {
+ mCurrentDevice = TestUtils.getTestDevice(mAdapter, i);
+ connectDeviceHelper(mCurrentDevice);
+ connectedDevices.add(mCurrentDevice);
+ }
+
+ mHeadsetService.setActiveDevice(connectedDevices.get(1));
+ when(mStateMachines.get(connectedDevices.get(1)).getAudioState())
+ .thenReturn(BluetoothHeadset.STATE_AUDIO_CONNECTED);
+
+ when(mSystemInterface.isRinging()).thenReturn(true);
+ mHeadsetService.setActiveDevice(connectedDevices.get(2));
+
+ verify(mNativeInterface).setActiveDevice(connectedDevices.get(2));
+ verify(mStateMachines.get(connectedDevices.get(2)), atLeast(1))
+ .sendMessage(eq(HeadsetStateMachine.SEND_BSIR), eq(0));
+ }
+
private void addConnectedDeviceHelper(BluetoothDevice device) {
mCurrentDevice = device;
when(mDatabaseManager.getProfileConnectionPolicy(
@@ -1332,4 +1379,19 @@ public class HeadsetServiceTest {
.thenReturn(priority);
assertThat(mHeadsetService.okToAcceptConnection(device, false)).isEqualTo(expected);
}
+
+ private void connectDeviceHelper(BluetoothDevice device) {
+ assertThat(mHeadsetService.connect(device)).isTrue();
+ verify(mObjectsFactory)
+ .makeStateMachine(
+ device,
+ mHeadsetService.getStateMachinesThreadLooper(),
+ mHeadsetService,
+ mAdapterService,
+ mNativeInterface,
+ mSystemInterface);
+ when(mStateMachines.get(device).getDevice()).thenReturn(device);
+ when(mStateMachines.get(device).getConnectionState())
+ .thenReturn(BluetoothProfile.STATE_CONNECTED);
+ }
}
diff --git a/android/app/tests/unit/src/com/android/bluetooth/hfp/HeadsetStateMachineTest.java b/android/app/tests/unit/src/com/android/bluetooth/hfp/HeadsetStateMachineTest.java
index e594c924a8..0b42a50d92 100644
--- a/android/app/tests/unit/src/com/android/bluetooth/hfp/HeadsetStateMachineTest.java
+++ b/android/app/tests/unit/src/com/android/bluetooth/hfp/HeadsetStateMachineTest.java
@@ -1793,28 +1793,6 @@ public class HeadsetStateMachineTest {
}
@Test
- @EnableFlags({Flags.FLAG_HFP_ALLOW_VOLUME_CHANGE_WITHOUT_SCO})
- public void testVolumeChangeEvent_fromIntentWhenConnected() {
- setUpConnectedState();
- int originalVolume = mHeadsetStateMachine.mSpeakerVolume;
- mHeadsetStateMachine.mSpeakerVolume = 0;
- int vol = 10;
-
- // Send INTENT_SCO_VOLUME_CHANGED message
- Intent volumeChange = new Intent(AudioManager.ACTION_VOLUME_CHANGED);
- volumeChange.putExtra(AudioManager.EXTRA_VOLUME_STREAM_VALUE, vol);
-
- mHeadsetStateMachine.sendMessage(
- HeadsetStateMachine.INTENT_SCO_VOLUME_CHANGED, volumeChange);
- TestUtils.waitForLooperToFinishScheduledTask(mHandlerThread.getLooper());
-
- // verify volume processed
- verify(mNativeInterface).setVolume(mTestDevice, HeadsetHalConstants.VOLUME_TYPE_SPK, vol);
-
- mHeadsetStateMachine.mSpeakerVolume = originalVolume;
- }
-
- @Test
public void testVolumeChangeEvent_fromIntentWhenAudioOn() {
setUpAudioOnState();
int originalVolume = mHeadsetStateMachine.mSpeakerVolume;
diff --git a/android/app/tests/unit/src/com/android/bluetooth/le_audio/ContentControlIdKeeperTest.java b/android/app/tests/unit/src/com/android/bluetooth/le_audio/ContentControlIdKeeperTest.java
index 823456b5a2..500e697502 100644
--- a/android/app/tests/unit/src/com/android/bluetooth/le_audio/ContentControlIdKeeperTest.java
+++ b/android/app/tests/unit/src/com/android/bluetooth/le_audio/ContentControlIdKeeperTest.java
@@ -30,7 +30,6 @@ import androidx.test.runner.AndroidJUnit4;
import com.android.bluetooth.btservice.ServiceFactory;
import org.junit.After;
-import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
@@ -64,7 +63,7 @@ public class ContentControlIdKeeperTest {
public int testCcidAcquire(ParcelUuid uuid, int context, int expectedListSize) {
int ccid = ContentControlIdKeeper.acquireCcid(uuid, context);
- Assert.assertNotEquals(ccid, ContentControlIdKeeper.CCID_INVALID);
+ assertThat(ccid).isNotEqualTo(ContentControlIdKeeper.CCID_INVALID);
verify(mLeAudioServiceMock).setCcidInformation(eq(uuid), eq(ccid), eq(context));
Map<ParcelUuid, Pair<Integer, Integer>> uuidToCcidContextPair =
@@ -98,7 +97,7 @@ public class ContentControlIdKeeperTest {
int ccid_one = testCcidAcquire(uuid_one, BluetoothLeAudio.CONTEXT_TYPE_MEDIA, 1);
int ccid_two = testCcidAcquire(uuid_two, BluetoothLeAudio.CONTEXT_TYPE_RINGTONE, 2);
- Assert.assertNotEquals(ccid_one, ccid_two);
+ assertThat(ccid_one).isNotEqualTo(ccid_two);
testCcidRelease(uuid_one, ccid_one, 1);
testCcidRelease(uuid_two, ccid_two, 0);
diff --git a/android/app/tests/unit/src/com/android/bluetooth/le_audio/LeAudioBroadcastServiceTest.java b/android/app/tests/unit/src/com/android/bluetooth/le_audio/LeAudioBroadcastServiceTest.java
index 401e0c6844..8433d1398c 100644
--- a/android/app/tests/unit/src/com/android/bluetooth/le_audio/LeAudioBroadcastServiceTest.java
+++ b/android/app/tests/unit/src/com/android/bluetooth/le_audio/LeAudioBroadcastServiceTest.java
@@ -22,6 +22,7 @@ import static android.bluetooth.IBluetoothLeAudio.LE_AUDIO_GROUP_ID_INVALID;
import static com.android.bluetooth.bass_client.BassConstants.INVALID_BROADCAST_ID;
import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
import static org.mockito.Mockito.*;
@@ -57,7 +58,6 @@ import com.android.bluetooth.flags.Flags;
import com.android.bluetooth.tbs.TbsService;
import org.junit.After;
-import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
@@ -91,7 +91,6 @@ public class LeAudioBroadcastServiceTest {
private LeAudioService mService;
private LeAudioIntentReceiver mLeAudioIntentReceiver;
private LinkedBlockingQueue<Intent> mIntentQueue;
- private boolean onBroadcastToUnicastFallbackGroupChangedCallbackCalled = false;
@Rule public MockitoRule mockitoRule = MockitoJUnit.rule();
@Mock private ActiveDeviceManager mActiveDeviceManager;
@@ -105,6 +104,7 @@ public class LeAudioBroadcastServiceTest {
@Mock private TbsService mTbsService;
@Mock private MetricsLogger mMetricsLogger;
@Mock private IBluetoothLeBroadcastCallback mCallbacks;
+ @Mock private IBluetoothLeAudioCallback mLeAudioCallbacks;
@Mock private IBinder mBinder;
@Spy private LeAudioObjectsFactory mObjectsFactory = LeAudioObjectsFactory.getInstance();
@@ -155,6 +155,7 @@ public class LeAudioBroadcastServiceTest {
mTargetContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
doReturn(mBinder).when(mCallbacks).asBinder();
+ doReturn(mBinder).when(mLeAudioCallbacks).asBinder();
doNothing().when(mBinder).linkToDeath(any(), eq(0));
// Use spied objects factory
@@ -750,10 +751,8 @@ public class LeAudioBroadcastServiceTest {
TestUtils.waitForLooperToFinishScheduledTask(mService.getMainLooper());
- List<BluetoothLeBroadcastMetadata> meta_list = mService.getAllBroadcastMetadata();
- assertThat(meta_list).isNotNull();
- Assert.assertNotEquals(meta_list.size(), 0);
- assertThat(meta_list.get(0)).isEqualTo(state_event.broadcastMetadata);
+ assertThat(mService.getAllBroadcastMetadata())
+ .containsExactly(state_event.broadcastMetadata);
}
@Test
@@ -1674,7 +1673,7 @@ public class LeAudioBroadcastServiceTest {
reset(mAudioManager);
- Assert.assertTrue(mService.setActiveDevice(mDevice2));
+ assertThat(mService.setActiveDevice(mDevice2)).isTrue();
if (!Flags.leaudioUseAudioRecordingListener()) {
/* Update fallback active device (only input is active) */
@@ -1707,34 +1706,8 @@ public class LeAudioBroadcastServiceTest {
when(mDatabaseManager.getMostRecentlyConnectedDevices()).thenReturn(devices);
- onBroadcastToUnicastFallbackGroupChangedCallbackCalled = false;
-
- IBluetoothLeAudioCallback leAudioCallbacks =
- new IBluetoothLeAudioCallback.Stub() {
- @Override
- public void onCodecConfigChanged(int gid, BluetoothLeAudioCodecStatus status) {}
-
- @Override
- public void onGroupStatusChanged(int gid, int gStatus) {}
-
- @Override
- public void onGroupNodeAdded(BluetoothDevice device, int gid) {}
-
- @Override
- public void onGroupNodeRemoved(BluetoothDevice device, int gid) {}
-
- @Override
- public void onGroupStreamStatusChanged(int groupId, int groupStreamStatus) {}
-
- @Override
- public void onBroadcastToUnicastFallbackGroupChanged(int groupId) {
- onBroadcastToUnicastFallbackGroupChangedCallbackCalled = true;
- assertThat(groupId2).isEqualTo(groupId);
- }
- };
-
synchronized (mService.mLeAudioCallbacks) {
- mService.mLeAudioCallbacks.register(leAudioCallbacks);
+ mService.mLeAudioCallbacks.register(mLeAudioCallbacks);
}
initializeNative();
@@ -1745,21 +1718,49 @@ public class LeAudioBroadcastServiceTest {
TestUtils.waitForLooperToFinishScheduledTask(mService.getMainLooper());
assertThat(mService.mUnicastGroupIdDeactivatedForBroadcastTransition).isEqualTo(groupId2);
- assertThat(onBroadcastToUnicastFallbackGroupChangedCallbackCalled).isTrue();
- onBroadcastToUnicastFallbackGroupChangedCallbackCalled = false;
+ try {
+ verify(mLeAudioCallbacks).onBroadcastToUnicastFallbackGroupChanged(groupId2);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+
synchronized (mService.mLeAudioCallbacks) {
- mService.mLeAudioCallbacks.unregister(leAudioCallbacks);
+ mService.mLeAudioCallbacks.unregister(mLeAudioCallbacks);
}
}
+ @Test
+ public void testGetLocalBroadcastReceivers() {
+ int broadcastId = 243;
+ byte[] code = {0x00, 0x01, 0x00, 0x02};
+
+ synchronized (mService.mBroadcastCallbacks) {
+ mService.mBroadcastCallbacks.register(mCallbacks);
+ }
+
+ BluetoothLeAudioContentMetadata.Builder meta_builder =
+ new BluetoothLeAudioContentMetadata.Builder();
+ meta_builder.setLanguage("deu");
+ meta_builder.setProgramInfo("Subgroup broadcast info");
+ BluetoothLeAudioContentMetadata meta = meta_builder.build();
+
+ verifyBroadcastStarted(broadcastId, buildBroadcastSettingsFromMetadata(meta, code, 1));
+ when(mBassClientService.getSyncedBroadcastSinks(broadcastId)).thenReturn(List.of(mDevice));
+ assertThat(mService.getLocalBroadcastReceivers().size()).isEqualTo(1);
+ assertThat(mService.getLocalBroadcastReceivers()).containsExactly(mDevice);
+
+ verifyBroadcastStopped(broadcastId);
+ assertThat(mService.getLocalBroadcastReceivers()).isEmpty();
+ }
+
private class LeAudioIntentReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
try {
mIntentQueue.put(intent);
} catch (InterruptedException e) {
- Assert.fail("Cannot add Intent to the queue: " + e.getMessage());
+ assertWithMessage("Cannot add Intent to the queue: " + e.toString()).fail();
}
}
}
@@ -1802,11 +1803,11 @@ public class LeAudioBroadcastServiceTest {
eq(mDevice2), eq(mDevice), connectionInfoArgumentCaptor.capture());
List<BluetoothProfileConnectionInfo> connInfos =
connectionInfoArgumentCaptor.getAllValues();
- Assert.assertEquals(connInfos.size(), 1);
+ assertThat(connInfos.size()).isEqualTo(1);
assertThat(connInfos.get(0).isLeOutput()).isFalse();
}
- Assert.assertEquals(mService.getBroadcastToUnicastFallbackGroup(), groupId2);
+ assertThat(mService.getBroadcastToUnicastFallbackGroup()).isEqualTo(groupId2);
}
private void disconnectDevice(BluetoothDevice device) {
@@ -1819,8 +1820,8 @@ public class LeAudioBroadcastServiceTest {
device,
BluetoothProfile.STATE_DISCONNECTING,
BluetoothProfile.STATE_CONNECTED);
- Assert.assertEquals(
- BluetoothProfile.STATE_DISCONNECTING, mService.getConnectionState(device));
+ assertThat(mService.getConnectionState(device))
+ .isEqualTo(BluetoothProfile.STATE_DISCONNECTING);
LeAudioStackEvent create_event =
new LeAudioStackEvent(LeAudioStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED);
@@ -1833,8 +1834,8 @@ public class LeAudioBroadcastServiceTest {
device,
BluetoothProfile.STATE_DISCONNECTED,
BluetoothProfile.STATE_DISCONNECTING);
- Assert.assertEquals(
- BluetoothProfile.STATE_DISCONNECTED, mService.getConnectionState(device));
+ assertThat(mService.getConnectionState(device))
+ .isEqualTo(BluetoothProfile.STATE_DISCONNECTED);
mService.deviceDisconnected(device, false);
TestUtils.waitForLooperToFinishScheduledTask(mService.getMainLooper());
}
@@ -1848,18 +1849,17 @@ public class LeAudioBroadcastServiceTest {
public void testSetDefaultBroadcastToUnicastFallbackGroup() {
int groupId = 1;
int groupId2 = 2;
- int broadcastId = 243;
- byte[] code = {0x00, 0x01, 0x00, 0x02};
List<BluetoothDevice> devices = new ArrayList<>();
when(mDatabaseManager.getMostRecentlyConnectedDevices()).thenReturn(devices);
- Assert.assertEquals(
- mService.getBroadcastToUnicastFallbackGroup(), LE_AUDIO_GROUP_ID_INVALID);
+ /* If no connected devices - no fallback device */
+ assertThat(mService.getBroadcastToUnicastFallbackGroup())
+ .isEqualTo(LE_AUDIO_GROUP_ID_INVALID);
initializeNative();
devices.add(mDevice);
- prepareHandoverStreamingBroadcast(groupId, broadcastId, code);
+ prepareConnectedUnicastDevice(groupId, mDevice);
mService.deviceConnected(mDevice);
devices.add(mDevice2);
prepareConnectedUnicastDevice(groupId2, mDevice2);
@@ -1871,12 +1871,10 @@ public class LeAudioBroadcastServiceTest {
mService.messageFromNative(stackEvent);
/* First connected group become fallback group */
- Assert.assertEquals(mService.mUnicastGroupIdDeactivatedForBroadcastTransition, groupId);
-
- reset(mAudioManager);
+ assertThat(mService.getBroadcastToUnicastFallbackGroup()).isEqualTo(groupId);
+ /* Force group as fallback 1 -> 2 */
mService.setBroadcastToUnicastFallbackGroup(groupId2);
-
assertThat(mService.getBroadcastToUnicastFallbackGroup()).isEqualTo(groupId2);
/* Disconnected last device from fallback should trigger set default group 2 -> 1 */
@@ -1891,6 +1889,63 @@ public class LeAudioBroadcastServiceTest {
.isEqualTo(LE_AUDIO_GROUP_ID_INVALID);
}
+ @Test
+ @EnableFlags({
+ Flags.FLAG_LEAUDIO_BROADCAST_API_MANAGE_PRIMARY_GROUP,
+ Flags.FLAG_LEAUDIO_BROADCAST_PRIMARY_GROUP_SELECTION
+ })
+ public void testUpdateFallbackDeviceWhileSettingActiveDevice() {
+ int groupId = 1;
+ int groupId2 = 2;
+ int broadcastId = 243;
+ byte[] code = {0x00, 0x01, 0x00, 0x02};
+ List<BluetoothDevice> devices = new ArrayList<>();
+
+ when(mDatabaseManager.getMostRecentlyConnectedDevices()).thenReturn(devices);
+
+ synchronized (mService.mLeAudioCallbacks) {
+ mService.mLeAudioCallbacks.register(mLeAudioCallbacks);
+ }
+
+ initializeNative();
+ devices.add(mDevice2);
+ prepareConnectedUnicastDevice(groupId2, mDevice2);
+ devices.add(mDevice);
+ prepareHandoverStreamingBroadcast(groupId, broadcastId, code);
+
+ /* Earliest connected group (2) become fallback device */
+ assertThat(mService.mUnicastGroupIdDeactivatedForBroadcastTransition).isEqualTo(groupId2);
+ try {
+ verify(mLeAudioCallbacks).onBroadcastToUnicastFallbackGroupChanged(groupId2);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+
+ Mockito.clearInvocations(mLeAudioCallbacks);
+ reset(mAudioManager);
+
+ /* Change active device while broadcasting - result in replacing fallback group 2->1 */
+ assertThat(mService.setActiveDevice(mDevice)).isTrue();
+ TestUtils.waitForLooperToFinishScheduledTask(mService.getMainLooper());
+ assertThat(mService.mUnicastGroupIdDeactivatedForBroadcastTransition).isEqualTo(groupId);
+ try {
+ verify(mLeAudioCallbacks).onBroadcastToUnicastFallbackGroupChanged(groupId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+
+ /* Verify that fallback device is not changed when there is no running broadcast */
+ Mockito.clearInvocations(mLeAudioCallbacks);
+ verifyBroadcastStopped(broadcastId);
+ assertThat(mService.setActiveDevice(mDevice2)).isTrue();
+ TestUtils.waitForLooperToFinishScheduledTask(mService.getMainLooper());
+ try {
+ verify(mLeAudioCallbacks, times(0)).onBroadcastToUnicastFallbackGroupChanged(anyInt());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
private BluetoothLeBroadcastSettings buildBroadcastSettingsFromMetadata(
BluetoothLeAudioContentMetadata contentMetadata,
@Nullable byte[] broadcastCode,
diff --git a/android/app/tests/unit/src/com/android/bluetooth/le_audio/LeAudioServiceTest.java b/android/app/tests/unit/src/com/android/bluetooth/le_audio/LeAudioServiceTest.java
index 3e523ceecc..312a031f82 100644
--- a/android/app/tests/unit/src/com/android/bluetooth/le_audio/LeAudioServiceTest.java
+++ b/android/app/tests/unit/src/com/android/bluetooth/le_audio/LeAudioServiceTest.java
@@ -46,6 +46,8 @@ import android.bluetooth.BluetoothManager;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothUuid;
import android.bluetooth.IBluetoothLeAudioCallback;
+import android.bluetooth.le.IScannerCallback;
+import android.bluetooth.le.ScanResult;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
@@ -56,6 +58,7 @@ import android.media.BluetoothProfileConnectionInfo;
import android.os.Handler;
import android.os.Looper;
import android.os.ParcelUuid;
+import android.os.RemoteException;
import android.platform.test.annotations.DisableFlags;
import android.platform.test.annotations.EnableFlags;
import android.platform.test.flag.junit.SetFlagsRule;
@@ -282,6 +285,9 @@ public class LeAudioServiceTest {
.when(mAdapterService)
.getRemoteUuids(any(BluetoothDevice.class));
+ doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class));
+ doReturn(true).when(mNativeInterface).disconnectLeAudio(any(BluetoothDevice.class));
+
verify(mNativeInterface, timeout(3000).times(1)).init(any());
}
@@ -532,8 +538,6 @@ public class LeAudioServiceTest {
.thenReturn(BluetoothProfile.CONNECTION_POLICY_FORBIDDEN);
when(mDatabaseManager.getProfileConnectionPolicy(mSingleDevice, BluetoothProfile.LE_AUDIO))
.thenReturn(BluetoothProfile.CONNECTION_POLICY_FORBIDDEN);
- doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class));
- doReturn(true).when(mNativeInterface).disconnectLeAudio(any(BluetoothDevice.class));
// Return No UUID
doReturn(new ParcelUuid[] {})
@@ -547,9 +551,6 @@ public class LeAudioServiceTest {
/** Test that an outgoing connection to device with PRIORITY_OFF is rejected */
@Test
public void testOutgoingConnectPriorityOff() {
- doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class));
- doReturn(true).when(mNativeInterface).disconnectLeAudio(any(BluetoothDevice.class));
-
// Set the device priority to PRIORITY_OFF so connect() should fail
when(mDatabaseManager.getProfileConnectionPolicy(mLeftDevice, BluetoothProfile.LE_AUDIO))
.thenReturn(BluetoothProfile.CONNECTION_POLICY_FORBIDDEN);
@@ -568,8 +569,6 @@ public class LeAudioServiceTest {
.thenReturn(BluetoothProfile.CONNECTION_POLICY_FORBIDDEN);
when(mDatabaseManager.getProfileConnectionPolicy(mSingleDevice, BluetoothProfile.LE_AUDIO))
.thenReturn(BluetoothProfile.CONNECTION_POLICY_FORBIDDEN);
- doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class));
- doReturn(true).when(mNativeInterface).disconnectLeAudio(any(BluetoothDevice.class));
// Send a connect request
assertWithMessage("Connect failed").that(mService.connect(mLeftDevice)).isTrue();
@@ -621,6 +620,13 @@ public class LeAudioServiceTest {
}
}
+ private void injectAndVerifyDeviceConnected(BluetoothDevice device) {
+ generateConnectionMessageFromNative(
+ device,
+ LeAudioStackEvent.CONNECTION_STATE_CONNECTED,
+ LeAudioStackEvent.CONNECTION_STATE_DISCONNECTED);
+ }
+
private void injectNoVerifyDeviceConnected(BluetoothDevice device) {
generateUnexpectedConnectionMessageFromNative(
device, LeAudioStackEvent.CONNECTION_STATE_CONNECTED);
@@ -648,8 +654,6 @@ public class LeAudioServiceTest {
.thenReturn(BluetoothProfile.CONNECTION_POLICY_ALLOWED);
when(mDatabaseManager.getProfileConnectionPolicy(mSingleDevice, BluetoothProfile.LE_AUDIO))
.thenReturn(BluetoothProfile.CONNECTION_POLICY_FORBIDDEN);
- doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class));
- doReturn(true).when(mNativeInterface).disconnectLeAudio(any(BluetoothDevice.class));
// Send a connect request
assertWithMessage("Connect failed").that(mService.connect(mLeftDevice)).isTrue();
@@ -778,8 +782,6 @@ public class LeAudioServiceTest {
.thenReturn(BluetoothProfile.CONNECTION_POLICY_FORBIDDEN);
when(mDatabaseManager.getProfileConnectionPolicy(mSingleDevice, BluetoothProfile.LE_AUDIO))
.thenReturn(BluetoothProfile.CONNECTION_POLICY_FORBIDDEN);
- doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class));
- doReturn(true).when(mNativeInterface).disconnectLeAudio(any(BluetoothDevice.class));
// Create device descriptor with connect request
assertWithMessage("Connect failed").that(mService.connect(mLeftDevice)).isTrue();
@@ -855,8 +857,6 @@ public class LeAudioServiceTest {
.thenReturn(BluetoothProfile.CONNECTION_POLICY_FORBIDDEN);
when(mDatabaseManager.getProfileConnectionPolicy(mSingleDevice, BluetoothProfile.LE_AUDIO))
.thenReturn(BluetoothProfile.CONNECTION_POLICY_FORBIDDEN);
- doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class));
- doReturn(true).when(mNativeInterface).disconnectLeAudio(any(BluetoothDevice.class));
// Create device descriptor with connect request
assertWithMessage("Connect failed").that(mService.connect(mLeftDevice)).isTrue();
@@ -932,8 +932,6 @@ public class LeAudioServiceTest {
.thenReturn(BluetoothProfile.CONNECTION_POLICY_FORBIDDEN);
when(mDatabaseManager.getProfileConnectionPolicy(mSingleDevice, BluetoothProfile.LE_AUDIO))
.thenReturn(BluetoothProfile.CONNECTION_POLICY_FORBIDDEN);
- doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class));
- doReturn(true).when(mNativeInterface).disconnectLeAudio(any(BluetoothDevice.class));
// Create device descriptor with connect request
assertWithMessage("Connect failed").that(mService.connect(mLeftDevice)).isTrue();
@@ -1073,8 +1071,6 @@ public class LeAudioServiceTest {
/** 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());
@@ -1187,6 +1183,13 @@ public class LeAudioServiceTest {
// Make device bonded
mBondedDevices.add(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);
+
// 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
@@ -1214,13 +1217,6 @@ public class LeAudioServiceTest {
assertThat(mService.getConnectionState(device)).isEqualTo(BluetoothProfile.STATE_CONNECTED);
- 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
@@ -1254,8 +1250,7 @@ public class LeAudioServiceTest {
// Not connected device
assertThat(mService.setActiveDevice(mSingleDevice)).isFalse();
- // Connected device
- doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class));
+ // Connect device
connectTestDevice(mSingleDevice, testGroupId);
// Add location support
@@ -1305,8 +1300,7 @@ public class LeAudioServiceTest {
// Not connected device
assertThat(mService.setActiveDevice(mSingleDevice)).isFalse();
- // Connected device
- doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class));
+ // Connect device
connectTestDevice(mSingleDevice, testGroupId);
// Add location support
@@ -1373,8 +1367,7 @@ public class LeAudioServiceTest {
// Not connected device
assertThat(mService.setActiveDevice(mSingleDevice)).isFalse();
- // Connected device
- doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class));
+ // Connect device
connectTestDevice(mLeftDevice, groupId);
connectTestDevice(mRightDevice, groupId);
@@ -1424,27 +1417,16 @@ public class LeAudioServiceTest {
int groupId = 1;
/* AUDIO_DIRECTION_OUTPUT_BIT = 0x01 */
int direction = 1;
- int snkAudioLocation = 3;
- int srcAudioLocation = 4;
int availableContexts = 0;
// Not connected device
assertThat(mService.setActiveDevice(mSingleDevice)).isFalse();
- // Connected device
- doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class));
+ // Connect device
connectTestDevice(mSingleDevice, testGroupId);
// Add location support
- LeAudioStackEvent audioConfChangedEvent =
- new LeAudioStackEvent(LeAudioStackEvent.EVENT_TYPE_AUDIO_CONF_CHANGED);
- audioConfChangedEvent.device = mSingleDevice;
- audioConfChangedEvent.valueInt1 = direction;
- audioConfChangedEvent.valueInt2 = groupId;
- audioConfChangedEvent.valueInt3 = snkAudioLocation;
- audioConfChangedEvent.valueInt4 = srcAudioLocation;
- audioConfChangedEvent.valueInt5 = availableContexts;
- mService.messageFromNative(audioConfChangedEvent);
+ injectAudioConfChanged(groupId, availableContexts, direction);
assertThat(mService.setActiveDevice(mSingleDevice)).isFalse();
verify(mNativeInterface, times(0)).groupSetActive(groupId);
@@ -1458,8 +1440,6 @@ public class LeAudioServiceTest {
/* AUDIO_DIRECTION_OUTPUT_BIT = 0x01 */
int direction = 1;
- int snkAudioLocation = 3;
- int srcAudioLocation = 4;
int availableContexts = 5 + BluetoothLeAudio.CONTEXT_TYPE_RINGTONE;
// Not connected device
@@ -1467,33 +1447,14 @@ public class LeAudioServiceTest {
// Define some return values needed in test
doReturn(-1).when(mVolumeControlService).getAudioDeviceGroupVolume(anyInt());
- doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class));
// Connect both
connectTestDevice(mSingleDevice, groupId_1);
connectTestDevice(mSingleDevice_2, groupId_2);
// Add location support
- LeAudioStackEvent audioConfChangedEvent =
- new LeAudioStackEvent(LeAudioStackEvent.EVENT_TYPE_AUDIO_CONF_CHANGED);
- audioConfChangedEvent.device = mSingleDevice;
- audioConfChangedEvent.valueInt1 = direction;
- audioConfChangedEvent.valueInt2 = groupId_1;
- audioConfChangedEvent.valueInt3 = snkAudioLocation;
- audioConfChangedEvent.valueInt4 = srcAudioLocation;
- audioConfChangedEvent.valueInt5 = availableContexts;
- mService.messageFromNative(audioConfChangedEvent);
-
- // Add location support
- audioConfChangedEvent =
- new LeAudioStackEvent(LeAudioStackEvent.EVENT_TYPE_AUDIO_CONF_CHANGED);
- audioConfChangedEvent.device = mSingleDevice_2;
- audioConfChangedEvent.valueInt1 = direction;
- audioConfChangedEvent.valueInt2 = groupId_2;
- audioConfChangedEvent.valueInt3 = snkAudioLocation;
- audioConfChangedEvent.valueInt4 = srcAudioLocation;
- audioConfChangedEvent.valueInt5 = availableContexts;
- mService.messageFromNative(audioConfChangedEvent);
+ injectAudioConfChanged(groupId_1, availableContexts, direction);
+ injectAudioConfChanged(groupId_2, availableContexts, direction);
assertThat(mService.setActiveDevice(mSingleDevice)).isTrue();
verify(mNativeInterface).groupSetActive(groupId_1);
@@ -1556,8 +1517,6 @@ public class LeAudioServiceTest {
/* AUDIO_DIRECTION_OUTPUT_BIT = 0x01 */
int direction = 1;
- int snkAudioLocation = 3;
- int srcAudioLocation = 4;
int availableContexts = 5 + BluetoothLeAudio.CONTEXT_TYPE_RINGTONE;
// Not connected device
@@ -1571,15 +1530,7 @@ public class LeAudioServiceTest {
connectTestDevice(mSingleDevice, groupId_1);
// Add location support
- LeAudioStackEvent audioConfChangedEvent =
- new LeAudioStackEvent(LeAudioStackEvent.EVENT_TYPE_AUDIO_CONF_CHANGED);
- audioConfChangedEvent.device = mSingleDevice;
- audioConfChangedEvent.valueInt1 = direction;
- audioConfChangedEvent.valueInt2 = groupId_1;
- audioConfChangedEvent.valueInt3 = snkAudioLocation;
- audioConfChangedEvent.valueInt4 = srcAudioLocation;
- audioConfChangedEvent.valueInt5 = availableContexts;
- mService.messageFromNative(audioConfChangedEvent);
+ injectAudioConfChanged(groupId_1, availableContexts, direction);
assertThat(mService.setActiveDevice(mSingleDevice)).isTrue();
verify(mNativeInterface).groupSetActive(groupId_1);
@@ -1617,27 +1568,16 @@ public class LeAudioServiceTest {
int groupId = 1;
/* AUDIO_DIRECTION_OUTPUT_BIT = 0x01 */
int direction = 1;
- int snkAudioLocation = 3;
- int srcAudioLocation = 4;
int availableContexts = 5;
// Not connected device
assertThat(mService.setActiveDevice(mSingleDevice)).isFalse();
- // Connected device
- doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class));
+ // Connect device
connectTestDevice(mSingleDevice, testGroupId);
// Add location support
- LeAudioStackEvent audioConfChangedEvent =
- new LeAudioStackEvent(LeAudioStackEvent.EVENT_TYPE_AUDIO_CONF_CHANGED);
- audioConfChangedEvent.device = mSingleDevice;
- audioConfChangedEvent.valueInt1 = direction;
- audioConfChangedEvent.valueInt2 = groupId;
- audioConfChangedEvent.valueInt3 = snkAudioLocation;
- audioConfChangedEvent.valueInt4 = srcAudioLocation;
- audioConfChangedEvent.valueInt5 = availableContexts;
- mService.messageFromNative(audioConfChangedEvent);
+ injectAudioConfChanged(groupId, availableContexts, direction);
assertThat(mService.setActiveDevice(mSingleDevice)).isTrue();
verify(mNativeInterface).groupSetActive(groupId);
@@ -1691,8 +1631,9 @@ public class LeAudioServiceTest {
@Test
@DisableFlags(Flags.FLAG_LEAUDIO_BROADCAST_PRIMARY_GROUP_SELECTION)
public void testUpdateUnicastFallbackActiveDeviceGroupDuringBroadcast() {
+ List<BluetoothDevice> devices = new ArrayList<>();
int groupId = 1;
- int preGroupId = 2;
+ int groupId_2 = 2;
/* AUDIO_DIRECTION_OUTPUT_BIT = 0x01 */
int direction = 1;
int snkAudioLocation = 3;
@@ -1701,14 +1642,23 @@ public class LeAudioServiceTest {
int broadcastId = 243;
byte[] code = {0x00, 0x01, 0x00, 0x02};
+ when(mDatabaseManager.getMostRecentlyConnectedDevices()).thenReturn(devices);
+
// Not connected device
assertThat(mService.setActiveDevice(mSingleDevice)).isFalse();
- // Connected device
+ // Connect devices
doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class));
- connectTestDevice(mSingleDevice, testGroupId);
+ devices.add(mSingleDevice);
+ connectTestDevice(mSingleDevice, groupId);
+ devices.add(mSingleDevice_2);
+ connectTestDevice(mSingleDevice_2, groupId_2);
- mService.mUnicastGroupIdDeactivatedForBroadcastTransition = preGroupId;
+ // Default fallback group is LE_AUDIO_GROUP_ID_INVALID
+ assertThat(mService.mUnicastGroupIdDeactivatedForBroadcastTransition)
+ .isEqualTo(LE_AUDIO_GROUP_ID_INVALID);
+
+ mService.mUnicastGroupIdDeactivatedForBroadcastTransition = groupId_2;
// mock create broadcast and currentlyActiveGroupId remains LE_AUDIO_GROUP_ID_INVALID
BluetoothLeAudioContentMetadata.Builder meta_builder =
new BluetoothLeAudioContentMetadata.Builder();
@@ -1720,24 +1670,31 @@ public class LeAudioServiceTest {
LeAudioStackEvent broadcastCreatedEvent =
new LeAudioStackEvent(LeAudioStackEvent.EVENT_TYPE_BROADCAST_CREATED);
- broadcastCreatedEvent.device = mSingleDevice;
broadcastCreatedEvent.valueInt1 = broadcastId;
broadcastCreatedEvent.valueBool1 = true;
mService.messageFromNative(broadcastCreatedEvent);
+ LeAudioStackEvent broadcastStateStreamingEvent =
+ new LeAudioStackEvent(LeAudioStackEvent.EVENT_TYPE_BROADCAST_STATE);
+ broadcastStateStreamingEvent.valueInt1 = broadcastId;
+ broadcastStateStreamingEvent.valueInt2 = LeAudioStackEvent.BROADCAST_STATE_STREAMING;
+ mService.messageFromNative(broadcastStateStreamingEvent);
+
+ injectAudioConfChanged(groupId, availableContexts, direction);
+
LeAudioStackEvent audioConfChangedEvent =
new LeAudioStackEvent(LeAudioStackEvent.EVENT_TYPE_AUDIO_CONF_CHANGED);
- audioConfChangedEvent.device = mSingleDevice;
+ audioConfChangedEvent.device = mSingleDevice_2;
audioConfChangedEvent.valueInt1 = direction;
- audioConfChangedEvent.valueInt2 = groupId;
+ audioConfChangedEvent.valueInt2 = groupId_2;
audioConfChangedEvent.valueInt3 = snkAudioLocation;
audioConfChangedEvent.valueInt4 = srcAudioLocation;
audioConfChangedEvent.valueInt5 = availableContexts;
mService.messageFromNative(audioConfChangedEvent);
// Verify only update the fallback group and not proceed to change active
- assertThat(mService.setActiveDevice(mSingleDevice)).isTrue();
- assertThat(mService.mUnicastGroupIdDeactivatedForBroadcastTransition).isEqualTo(groupId);
+ assertThat(mService.setActiveDevice(mSingleDevice_2)).isTrue();
+ assertThat(mService.mUnicastGroupIdDeactivatedForBroadcastTransition).isEqualTo(groupId_2);
// Verify only update the fallback group to INVALID and not proceed to change active
assertThat(mService.setActiveDevice(null)).isTrue();
@@ -1753,14 +1710,11 @@ public class LeAudioServiceTest {
int groupId = 1;
/* AUDIO_DIRECTION_OUTPUT_BIT = 0x01 */
int direction = 1;
- int snkAudioLocation = 3;
- int srcAudioLocation = 4;
int availableContexts = 5;
int nodeStatus = LeAudioStackEvent.GROUP_NODE_ADDED;
int groupStatus = LeAudioStackEvent.GROUP_STATUS_ACTIVE;
// Single active device
- doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class));
connectTestDevice(mSingleDevice, testGroupId);
// Add device to group
@@ -1774,15 +1728,7 @@ public class LeAudioServiceTest {
assertThat(mService.setActiveDevice(mSingleDevice)).isFalse();
// Add location support
- LeAudioStackEvent audioConfChangedEvent =
- new LeAudioStackEvent(LeAudioStackEvent.EVENT_TYPE_AUDIO_CONF_CHANGED);
- audioConfChangedEvent.device = mSingleDevice;
- audioConfChangedEvent.valueInt1 = direction;
- audioConfChangedEvent.valueInt2 = groupId;
- audioConfChangedEvent.valueInt3 = snkAudioLocation;
- audioConfChangedEvent.valueInt4 = srcAudioLocation;
- audioConfChangedEvent.valueInt5 = availableContexts;
- mService.messageFromNative(audioConfChangedEvent);
+ injectAudioConfChanged(groupId, availableContexts, direction);
assertThat(mService.setActiveDevice(mSingleDevice)).isTrue();
@@ -1850,7 +1796,6 @@ public class LeAudioServiceTest {
doReturn(testVolume).when(mVolumeControlService).getAudioDeviceGroupVolume(testGroupId);
- doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class));
connectTestDevice(mSingleDevice, testGroupId);
injectAudioConfChanged(
testGroupId,
@@ -1921,7 +1866,6 @@ public class LeAudioServiceTest {
/** Test native interface audio configuration changed message handling */
@Test
public void testMessageFromNativeAudioConfChangedActiveGroup() {
- doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class));
connectTestDevice(mSingleDevice, testGroupId);
injectAudioConfChanged(
testGroupId,
@@ -1943,7 +1887,6 @@ public class LeAudioServiceTest {
/** Test native interface audio configuration changed message handling */
@Test
public void testMessageFromNativeAudioConfChangedInactiveGroup() {
- doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class));
connectTestDevice(mSingleDevice, testGroupId);
Integer contexts =
@@ -1956,7 +1899,6 @@ public class LeAudioServiceTest {
/** Test native interface audio configuration changed message handling */
@Test
public void testMessageFromNativeAudioConfChangedNoGroupChanged() {
- doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class));
connectTestDevice(mSingleDevice, testGroupId);
injectAudioConfChanged(testGroupId, 0, 3);
@@ -1969,7 +1911,6 @@ public class LeAudioServiceTest {
*/
@Test
public void testHealthBaseDeviceAction() {
- doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class));
connectTestDevice(mSingleDevice, testGroupId);
LeAudioStackEvent healthBaseDevAction =
@@ -1982,7 +1923,6 @@ public class LeAudioServiceTest {
@Test
public void testHealthBasedGroupAction() {
- doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class));
connectTestDevice(mSingleDevice, testGroupId);
LeAudioStackEvent healthBasedGroupAction =
@@ -2041,7 +1981,6 @@ public class LeAudioServiceTest {
/** Test native interface group status message handling */
@Test
public void testMessageFromNativeGroupStatusChanged() {
- doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class));
connectTestDevice(mSingleDevice, testGroupId);
injectAudioConfChanged(
@@ -2102,7 +2041,6 @@ public class LeAudioServiceTest {
/** Test native interface group stream status message handling */
@Test
public void testMessageFromNativeGroupStreamStatusChanged() {
- doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class));
connectTestDevice(mSingleDevice, testGroupId);
injectAudioConfChanged(
@@ -2164,7 +2102,6 @@ public class LeAudioServiceTest {
injectLocalCodecConfigCapaChanged(INPUT_CAPABILITIES_CONFIG, OUTPUT_CAPABILITIES_CONFIG);
- doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class));
connectTestDevice(mSingleDevice, testGroupId);
testCodecStatus =
@@ -2251,7 +2188,6 @@ public class LeAudioServiceTest {
injectLocalCodecConfigCapaChanged(INPUT_CAPABILITIES_CONFIG, OUTPUT_CAPABILITIES_CONFIG);
- doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class));
connectTestDevice(mSingleDevice, testGroupId);
testCodecStatus =
@@ -2347,7 +2283,6 @@ public class LeAudioServiceTest {
injectLocalCodecConfigCapaChanged(INPUT_CAPABILITIES_CONFIG, OUTPUT_CAPABILITIES_CONFIG);
- doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class));
connectTestDevice(mSingleDevice, testGroupId);
testCodecStatus =
@@ -2418,15 +2353,12 @@ public class LeAudioServiceTest {
int groupId = 1;
/* AUDIO_DIRECTION_OUTPUT_BIT = 0x01 */
int direction = 1;
- int snkAudioLocation = 3;
- int srcAudioLocation = 4;
int availableContexts = 5 + BluetoothLeAudio.CONTEXT_TYPE_RINGTONE;
- ;
+
int groupStatus = LeAudioStackEvent.GROUP_STATUS_ACTIVE;
BluetoothDevice leadDevice;
BluetoothDevice memberDevice = mLeftDevice;
- doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class));
connectTestDevice(mLeftDevice, groupId);
connectTestDevice(mRightDevice, groupId);
@@ -2437,15 +2369,7 @@ public class LeAudioServiceTest {
assertThat(mService.setActiveDevice(leadDevice)).isFalse();
- // Add location support
- LeAudioStackEvent audioConfChangedEvent =
- new LeAudioStackEvent(LeAudioStackEvent.EVENT_TYPE_AUDIO_CONF_CHANGED);
- audioConfChangedEvent.valueInt1 = direction;
- audioConfChangedEvent.valueInt2 = groupId;
- audioConfChangedEvent.valueInt3 = snkAudioLocation;
- audioConfChangedEvent.valueInt4 = srcAudioLocation;
- audioConfChangedEvent.valueInt5 = availableContexts;
- mService.messageFromNative(audioConfChangedEvent);
+ injectAudioConfChanged(groupId, availableContexts, direction);
assertThat(mService.setActiveDevice(leadDevice)).isTrue();
@@ -2494,15 +2418,12 @@ public class LeAudioServiceTest {
int groupId = 1;
/* AUDIO_DIRECTION_OUTPUT_BIT = 0x01 */
int direction = 1;
- int snkAudioLocation = 3;
- int srcAudioLocation = 4;
int availableContexts = 5 + BluetoothLeAudio.CONTEXT_TYPE_RINGTONE;
- ;
+
int groupStatus = LeAudioStackEvent.GROUP_STATUS_ACTIVE;
BluetoothDevice leadDevice;
BluetoothDevice memberDevice = mLeftDevice;
- doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class));
connectTestDevice(mLeftDevice, groupId);
connectTestDevice(mRightDevice, groupId);
@@ -2514,14 +2435,7 @@ public class LeAudioServiceTest {
assertThat(mService.setActiveDevice(leadDevice)).isFalse();
// Add location support
- LeAudioStackEvent audioConfChangedEvent =
- new LeAudioStackEvent(LeAudioStackEvent.EVENT_TYPE_AUDIO_CONF_CHANGED);
- audioConfChangedEvent.valueInt1 = direction;
- audioConfChangedEvent.valueInt2 = groupId;
- audioConfChangedEvent.valueInt3 = snkAudioLocation;
- audioConfChangedEvent.valueInt4 = srcAudioLocation;
- audioConfChangedEvent.valueInt5 = availableContexts;
- mService.messageFromNative(audioConfChangedEvent);
+ injectAudioConfChanged(groupId, availableContexts, direction);
assertThat(mService.setActiveDevice(leadDevice)).isTrue();
@@ -2573,7 +2487,6 @@ public class LeAudioServiceTest {
int direction = 1;
int availableContexts = 4;
- doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class));
connectTestDevice(mLeftDevice, groupId);
connectTestDevice(mRightDevice, groupId);
@@ -2632,7 +2545,6 @@ public class LeAudioServiceTest {
int direction = 1;
int availableContexts = 4;
- doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class));
connectTestDevice(mLeftDevice, groupId);
connectTestDevice(mRightDevice, groupId);
assertThat(mService.setActiveDevice(mLeftDevice)).isFalse();
@@ -2693,7 +2605,6 @@ public class LeAudioServiceTest {
@Test
public void testGetAudioLocation() {
- doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class));
connectTestDevice(mSingleDevice, testGroupId);
assertThat(mService.getAudioLocation(null))
@@ -2711,7 +2622,6 @@ public class LeAudioServiceTest {
@Test
public void testGetConnectedPeerDevices() {
- doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class));
connectTestDevice(mLeftDevice, testGroupId);
connectTestDevice(mRightDevice, testGroupId);
@@ -2772,8 +2682,6 @@ public class LeAudioServiceTest {
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);
@@ -2787,7 +2695,7 @@ public class LeAudioServiceTest {
int groupId = 1;
mService.handleBluetoothEnabled();
- doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class));
+
connectTestDevice(mLeftDevice, groupId);
connectTestDevice(mRightDevice, groupId);
verify(mMcpService).setDeviceAuthorized(mLeftDevice, true);
@@ -2799,7 +2707,6 @@ public class LeAudioServiceTest {
public void testAuthorizeMcpServiceOnBluetoothEnableAndNodeRemoval() {
int groupId = 1;
- doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class));
connectTestDevice(mLeftDevice, groupId);
connectTestDevice(mRightDevice, groupId);
@@ -2832,8 +2739,6 @@ public class LeAudioServiceTest {
int groupId = 1;
mService.handleBluetoothEnabled();
- 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());
@@ -2875,7 +2780,6 @@ public class LeAudioServiceTest {
int firstGroupId = 1;
int secondGroupId = 2;
- doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class));
connectTestDevice(mLeftDevice, firstGroupId);
connectTestDevice(mRightDevice, firstGroupId);
connectTestDevice(mSingleDevice, secondGroupId);
@@ -2923,14 +2827,11 @@ public class LeAudioServiceTest {
int groupId = 1;
/* AUDIO_DIRECTION_OUTPUT_BIT = 0x01 | AUDIO_DIRECTION_INPUT_BIT = 0x02; */
int direction = 3;
- int snkAudioLocation = 3;
- int srcAudioLocation = 4;
int availableContexts = 5;
int nodeStatus = LeAudioStackEvent.GROUP_NODE_ADDED;
int groupStatus = LeAudioStackEvent.GROUP_STATUS_ACTIVE;
// Single active device
- doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class));
connectTestDevice(mSingleDevice, testGroupId);
// Add device to group
@@ -2944,15 +2845,7 @@ public class LeAudioServiceTest {
assertThat(mService.setActiveDevice(mSingleDevice)).isFalse();
// Add location support
- LeAudioStackEvent audioConfChangedEvent =
- new LeAudioStackEvent(LeAudioStackEvent.EVENT_TYPE_AUDIO_CONF_CHANGED);
- audioConfChangedEvent.device = mSingleDevice;
- audioConfChangedEvent.valueInt1 = direction;
- audioConfChangedEvent.valueInt2 = groupId;
- audioConfChangedEvent.valueInt3 = snkAudioLocation;
- audioConfChangedEvent.valueInt4 = srcAudioLocation;
- audioConfChangedEvent.valueInt5 = availableContexts;
- mService.messageFromNative(audioConfChangedEvent);
+ injectAudioConfChanged(groupId, availableContexts, direction);
assertThat(mService.setActiveDevice(mSingleDevice)).isTrue();
@@ -2973,11 +2866,8 @@ public class LeAudioServiceTest {
int groupId = 1;
/* AUDIO_DIRECTION_OUTPUT_BIT = 0x01 */
int direction = 1;
- int snkAudioLocation = 3;
- int srcAudioLocation = 4;
int availableContexts = 5 + BluetoothLeAudio.CONTEXT_TYPE_RINGTONE;
- doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class));
connectTestDevice(mLeftDevice, groupId);
connectTestDevice(mRightDevice, groupId);
@@ -2989,14 +2879,7 @@ public class LeAudioServiceTest {
assertThat(groupDevicesById.contains(mRightDevice)).isTrue();
// Add location support
- LeAudioStackEvent audioConfChangedEvent =
- new LeAudioStackEvent(LeAudioStackEvent.EVENT_TYPE_AUDIO_CONF_CHANGED);
- audioConfChangedEvent.valueInt1 = direction;
- audioConfChangedEvent.valueInt2 = groupId;
- audioConfChangedEvent.valueInt3 = snkAudioLocation;
- audioConfChangedEvent.valueInt4 = srcAudioLocation;
- audioConfChangedEvent.valueInt5 = availableContexts;
- mService.messageFromNative(audioConfChangedEvent);
+ injectAudioConfChanged(groupId, availableContexts, direction);
assertThat(mService.setActiveDevice(mLeftDevice)).isTrue();
verify(mNativeInterface).groupSetActive(groupId);
@@ -3013,13 +2896,12 @@ public class LeAudioServiceTest {
reset(mNativeInterface);
/* Don't expect any change. */
- mService.messageFromNative(audioConfChangedEvent);
+ injectAudioConfChanged(groupId, availableContexts, direction);
verify(mNativeInterface, times(0)).groupSetActive(groupId);
reset(mNativeInterface);
/* Expect device to be incactive */
- audioConfChangedEvent.valueInt5 = 0;
- mService.messageFromNative(audioConfChangedEvent);
+ injectAudioConfChanged(groupId, 0, direction);
verify(mNativeInterface).groupSetActive(-1);
injectGroupStatusChange(groupId, LeAudioStackEvent.GROUP_STATUS_INACTIVE);
@@ -3034,8 +2916,7 @@ public class LeAudioServiceTest {
reset(mAudioManager);
/* Expect device to be incactive */
- audioConfChangedEvent.valueInt5 = 1;
- mService.messageFromNative(audioConfChangedEvent);
+ injectAudioConfChanged(groupId, 1, direction);
verify(mNativeInterface).groupSetActive(groupId);
reset(mNativeInterface);
@@ -3049,16 +2930,189 @@ public class LeAudioServiceTest {
any(BluetoothProfileConnectionInfo.class));
}
+ @Test
+ public void testAutoActiveMode_verifyDefaultState() {
+ int groupId = 1;
+
+ /* Test scenario:
+ * 1. Connected two devices
+ * 2. Verify that Auto Active Mode is true be default.
+ */
+
+ connectTestDevice(mLeftDevice, groupId);
+ connectTestDevice(mRightDevice, groupId);
+
+ assertThat(mService.isAutoActiveModeEnabled(groupId)).isTrue();
+ }
+
+ @Test
+ public void testAutoActiveMode_whenDeviceIsConnected_failToDisableIt() {
+ int groupId = 1;
+ /* AUDIO_DIRECTION_OUTPUT_BIT = 0x01 */
+ int direction = 1;
+ int availableContexts = 5 + BluetoothLeAudio.CONTEXT_TYPE_RINGTONE;
+
+ /* Test scenario:
+ * 1. Connected two devices
+ * 2. Disconnect one device
+ * 3. Verify that Auto Active Mode cannot be set.
+ */
+
+ connectTestDevice(mLeftDevice, groupId);
+ connectTestDevice(mRightDevice, groupId);
+
+ assertThat(mService.setAutoActiveModeState(groupId, false)).isFalse();
+
+ // Checks group device lists for groupId 1
+ List<BluetoothDevice> groupDevicesById = mService.getGroupDevices(groupId);
+
+ assertThat(groupDevicesById).containsExactly(mLeftDevice, mRightDevice);
+
+ injectAudioConfChanged(groupId, availableContexts, direction);
+ assertThat(mService.isGroupAvailableForStream(groupId)).isTrue();
+
+ injectAndVerifyDeviceDisconnected(mLeftDevice);
+
+ assertThat(mService.setAutoActiveModeState(groupId, false)).isFalse();
+ }
+
+ @Test
+ public void testAutoActiveMode_disabledWithSuccess() {
+ int groupId = 1;
+ /* AUDIO_DIRECTION_OUTPUT_BIT = 0x01 */
+ int direction = 1;
+ int availableContexts = 5 + BluetoothLeAudio.CONTEXT_TYPE_RINGTONE;
+
+ /* Test scenario:
+ * 1. Connected two devices
+ * 2. Disconnect both devices
+ * 3. Verify that Auto Active Mode can be set.
+ */
+
+ connectTestDevice(mLeftDevice, groupId);
+ connectTestDevice(mRightDevice, groupId);
+
+ // Checks group device lists for groupId 1
+ List<BluetoothDevice> groupDevicesById = mService.getGroupDevices(groupId);
+
+ assertThat(groupDevicesById).containsExactly(mLeftDevice, mRightDevice);
+
+ injectAudioConfChanged(groupId, availableContexts, direction);
+
+ assertThat(mService.isGroupAvailableForStream(groupId)).isTrue();
+
+ injectAndVerifyDeviceDisconnected(mLeftDevice);
+ injectAndVerifyDeviceDisconnected(mRightDevice);
+
+ assertThat(mService.setAutoActiveModeState(groupId, false)).isTrue();
+ assertThat(mService.isAutoActiveModeEnabled(groupId)).isFalse();
+ }
+
+ @Test
+ public void testAutoActiveMode_whenUserSetsDeviceAsActive_resetToDefault() {
+ int groupId = 1;
+ /* AUDIO_DIRECTION_OUTPUT_BIT = 0x01 */
+ int direction = 1;
+ int availableContexts = 5 + BluetoothLeAudio.CONTEXT_TYPE_RINGTONE;
+
+ /* Test scenario:
+ * 1. Connected two devices
+ * 2. Disconnect both devices
+ * 3. Disable Auto Active Mode
+ * 4. Connect at least one device
+ * 5. Set group as Active
+ * 6. Verify Auto Active Mode is back to default
+ */
+
+ mService.handleBluetoothEnabled();
+
+ connectTestDevice(mLeftDevice, groupId);
+ connectTestDevice(mRightDevice, groupId);
+
+ // Checks group device lists for groupId 1
+ List<BluetoothDevice> groupDevicesById = mService.getGroupDevices(groupId);
+
+ assertThat(groupDevicesById).containsExactly(mLeftDevice, mRightDevice);
+
+ injectAudioConfChanged(groupId, availableContexts, direction);
+
+ assertThat(mService.isGroupAvailableForStream(groupId)).isTrue();
+
+ injectAndVerifyDeviceDisconnected(mLeftDevice);
+ injectAndVerifyDeviceDisconnected(mRightDevice);
+
+ assertThat(mService.setAutoActiveModeState(groupId, false)).isTrue();
+ assertThat(mService.isAutoActiveModeEnabled(groupId)).isFalse();
+
+ injectAndVerifyDeviceConnected(mLeftDevice);
+ injectAudioConfChanged(groupId, availableContexts, direction);
+
+ assertThat(mService.setActiveDevice(mLeftDevice)).isTrue();
+ assertThat(mService.isAutoActiveModeEnabled(groupId)).isTrue();
+ }
+
+ @Test
+ public void testAutoActiveMode_whenRemoteUsesTargetedAnnouncements_resetToDefault()
+ throws RemoteException {
+ int groupId = 1;
+ /* AUDIO_DIRECTION_OUTPUT_BIT = 0x01 */
+ int direction = 1;
+ int availableContexts = 5 + BluetoothLeAudio.CONTEXT_TYPE_RINGTONE;
+
+ /* Test scenario:
+ * 1. Connected two devices
+ * 2. Disconnect both devices
+ * 3. Disable Auto Active Mode
+ * 4. Connect at least one device
+ * 5. Detect TA on remote device
+ * 6. Verify Auto Active Mode is back to default
+ */
+
+ mService.handleBluetoothEnabled();
+ ArgumentCaptor<IScannerCallback> scanCallbacks =
+ ArgumentCaptor.forClass(IScannerCallback.class);
+
+ connectTestDevice(mLeftDevice, groupId);
+ connectTestDevice(mRightDevice, groupId);
+
+ // Checks group device lists for groupId 1
+ List<BluetoothDevice> groupDevicesById = mService.getGroupDevices(groupId);
+
+ assertThat(groupDevicesById).containsExactly(mLeftDevice, mRightDevice);
+ injectAudioConfChanged(groupId, availableContexts, direction);
+
+ assertThat(mService.isGroupAvailableForStream(groupId)).isTrue();
+
+ injectAndVerifyDeviceDisconnected(mLeftDevice);
+ injectAndVerifyDeviceDisconnected(mRightDevice);
+
+ assertThat(mService.setAutoActiveModeState(groupId, false)).isTrue();
+ assertThat(mService.isAutoActiveModeEnabled(groupId)).isFalse();
+
+ injectAndVerifyDeviceConnected(mLeftDevice);
+ injectAudioConfChanged(groupId, availableContexts, direction);
+
+ verify(mScanController).registerScannerInternal(scanCallbacks.capture(), any(), any());
+
+ ScanResult scanResult = new ScanResult(mRightDevice, null, 0, 0);
+
+ scanCallbacks.getValue().onScanResult(scanResult);
+
+ assertThat(mService.isAutoActiveModeEnabled(groupId)).isTrue();
+ }
+
/**
* Test the group is activated once the available contexts are back.
*
+ * <pre>
* Scenario:
- * 1. Have a group of 2 devices that initially does not expose any available contexts.
- * The group shall be inactive at this point.
- * 2. Once the available contexts are updated with non-zero value,
- * the group shall become active.
- * 3. The available contexts are changed to zero. Group becomes inactive.
- * 4. The available contexts are back again. Group becomes active.
+ * 1. Have a group of 2 devices that initially does not expose any available contexts. The
+ * group shall be inactive at this point.
+ * 2. Once the available contexts are updated with non-zero value, the group shall become
+ * active.
+ * 3. The available contexts are changed to zero. Group becomes inactive.
+ * 4. The available contexts are back again. Group becomes active.
+ * </pre>
*/
@Test
@EnableFlags(Flags.FLAG_LEAUDIO_UNICAST_NO_AVAILABLE_CONTEXTS)
@@ -3066,11 +3120,8 @@ public class LeAudioServiceTest {
int groupId = 1;
/* AUDIO_DIRECTION_OUTPUT_BIT = 0x01 */
int direction = 1;
- int snkAudioLocation = 3;
- int srcAudioLocation = 4;
int availableContexts = 5 + BluetoothLeAudio.CONTEXT_TYPE_RINGTONE;
- doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class));
connectTestDevice(mLeftDevice, groupId);
connectTestDevice(mRightDevice, groupId);
@@ -3082,21 +3133,13 @@ public class LeAudioServiceTest {
assertThat(groupDevicesById.contains(mRightDevice)).isTrue();
// Add location support
- LeAudioStackEvent audioConfChangedEvent =
- new LeAudioStackEvent(LeAudioStackEvent.EVENT_TYPE_AUDIO_CONF_CHANGED);
- audioConfChangedEvent.valueInt1 = direction;
- audioConfChangedEvent.valueInt2 = groupId;
- audioConfChangedEvent.valueInt3 = snkAudioLocation;
- audioConfChangedEvent.valueInt4 = srcAudioLocation;
- audioConfChangedEvent.valueInt5 = 0;
- mService.messageFromNative(audioConfChangedEvent);
+ injectAudioConfChanged(groupId, 0, direction);
assertThat(mService.setActiveDevice(mLeftDevice)).isFalse();
verify(mNativeInterface, times(0)).groupSetActive(groupId);
// Expect device to be active
- audioConfChangedEvent.valueInt5 = availableContexts;
- mService.messageFromNative(audioConfChangedEvent);
+ injectAudioConfChanged(groupId, availableContexts, direction);
verify(mNativeInterface).groupSetActive(groupId);
@@ -3112,8 +3155,7 @@ public class LeAudioServiceTest {
reset(mNativeInterface);
// Expect device to be inactive
- audioConfChangedEvent.valueInt5 = 0;
- mService.messageFromNative(audioConfChangedEvent);
+ injectAudioConfChanged(groupId, 0, direction);
verify(mNativeInterface).groupSetActive(-1);
injectGroupStatusChange(groupId, LeAudioStackEvent.GROUP_STATUS_INACTIVE);
@@ -3128,8 +3170,7 @@ public class LeAudioServiceTest {
reset(mAudioManager);
// Expect device to be active
- audioConfChangedEvent.valueInt5 = availableContexts;
- mService.messageFromNative(audioConfChangedEvent);
+ injectAudioConfChanged(groupId, availableContexts, direction);
verify(mNativeInterface).groupSetActive(groupId);
reset(mNativeInterface);
@@ -3146,14 +3187,16 @@ public class LeAudioServiceTest {
/**
* Test the group is activated once the available contexts are back.
*
+ * <pre>
* Scenario:
- * 1. Have a group of 2 devices. The available contexts are non-zero.
- * The group shall be active at this point.
- * 2. Once the available contexts are updated with zero value,
- * the group shall become inactive.
- * 3. All group devices are disconnected.
- * 4. Group devices are reconnected. The available contexts are still zero.
- * 4. The available contexts are updated with non-zero value. Group becomes active.
+ * 1. Have a group of 2 devices. The available contexts are non-zero. The group shall be
+ * active at this point.
+ * 2. Once the available contexts are updated with zero value, the group shall become
+ * inactive.
+ * 3. All group devices are disconnected.
+ * 4. Group devices are reconnected. The available contexts are still zero.
+ * 5. The available contexts are updated with non-zero value. Group becomes active.
+ * </pre>
*/
@Test
@EnableFlags(Flags.FLAG_LEAUDIO_UNICAST_NO_AVAILABLE_CONTEXTS)
@@ -3161,11 +3204,8 @@ public class LeAudioServiceTest {
int groupId = 1;
/* AUDIO_DIRECTION_OUTPUT_BIT = 0x01 */
int direction = 1;
- int snkAudioLocation = 3;
- int srcAudioLocation = 4;
int availableContexts = 5 + BluetoothLeAudio.CONTEXT_TYPE_RINGTONE;
- doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class));
connectTestDevice(mLeftDevice, groupId);
connectTestDevice(mRightDevice, groupId);
@@ -3177,14 +3217,7 @@ public class LeAudioServiceTest {
assertThat(groupDevicesById.contains(mRightDevice)).isTrue();
// Add location support
- LeAudioStackEvent audioConfChangedEvent =
- new LeAudioStackEvent(LeAudioStackEvent.EVENT_TYPE_AUDIO_CONF_CHANGED);
- audioConfChangedEvent.valueInt1 = direction;
- audioConfChangedEvent.valueInt2 = groupId;
- audioConfChangedEvent.valueInt3 = snkAudioLocation;
- audioConfChangedEvent.valueInt4 = srcAudioLocation;
- audioConfChangedEvent.valueInt5 = availableContexts;
- mService.messageFromNative(audioConfChangedEvent);
+ injectAudioConfChanged(groupId, availableContexts, direction);
assertThat(mService.setActiveDevice(mLeftDevice)).isTrue();
verify(mNativeInterface).groupSetActive(groupId);
@@ -3201,8 +3234,7 @@ public class LeAudioServiceTest {
reset(mNativeInterface);
// Expect device to be inactive
- audioConfChangedEvent.valueInt5 = 0;
- mService.messageFromNative(audioConfChangedEvent);
+ injectAudioConfChanged(groupId, 0, direction);
verify(mNativeInterface).groupSetActive(-1);
injectGroupStatusChange(groupId, LeAudioStackEvent.GROUP_STATUS_INACTIVE);
@@ -3227,8 +3259,7 @@ public class LeAudioServiceTest {
assertThat(mService.getConnectedDevices().contains(mRightDevice)).isFalse();
// Expect device to be inactive
- audioConfChangedEvent.valueInt5 = 0;
- mService.messageFromNative(audioConfChangedEvent);
+ injectAudioConfChanged(groupId, 0, direction);
generateConnectionMessageFromNative(
mLeftDevice, BluetoothProfile.STATE_CONNECTED, BluetoothProfile.STATE_DISCONNECTED);
@@ -3237,18 +3268,18 @@ public class LeAudioServiceTest {
assertThat(mService.getConnectedDevices().contains(mLeftDevice)).isTrue();
// Expect device to be inactive
- audioConfChangedEvent.valueInt5 = 0;
- mService.messageFromNative(audioConfChangedEvent);
+ injectAudioConfChanged(groupId, 0, direction);
generateConnectionMessageFromNative(
- mRightDevice, BluetoothProfile.STATE_CONNECTED, BluetoothProfile.STATE_DISCONNECTED);
+ mRightDevice,
+ BluetoothProfile.STATE_CONNECTED,
+ BluetoothProfile.STATE_DISCONNECTED);
assertThat(mService.getConnectionState(mRightDevice))
.isEqualTo(BluetoothProfile.STATE_CONNECTED);
assertThat(mService.getConnectedDevices().contains(mRightDevice)).isTrue();
// Expect device to be active
- audioConfChangedEvent.valueInt5 = availableContexts;
- mService.messageFromNative(audioConfChangedEvent);
+ injectAudioConfChanged(groupId, availableContexts, direction);
verify(mNativeInterface).groupSetActive(groupId);
@@ -3277,11 +3308,8 @@ public class LeAudioServiceTest {
int groupId = 1;
/* AUDIO_DIRECTION_OUTPUT_BIT = 0x01 */
int direction = 1;
- int snkAudioLocation = 3;
- int srcAudioLocation = 4;
int availableContexts = 5 + BluetoothLeAudio.CONTEXT_TYPE_RINGTONE;
- doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class));
connectTestDevice(mLeftDevice, groupId);
connectTestDevice(mRightDevice, groupId);
@@ -3293,14 +3321,7 @@ public class LeAudioServiceTest {
assertThat(groupDevicesById.contains(mRightDevice)).isTrue();
// Add location support
- LeAudioStackEvent audioConfChangedEvent =
- new LeAudioStackEvent(LeAudioStackEvent.EVENT_TYPE_AUDIO_CONF_CHANGED);
- audioConfChangedEvent.valueInt1 = direction;
- audioConfChangedEvent.valueInt2 = groupId;
- audioConfChangedEvent.valueInt3 = snkAudioLocation;
- audioConfChangedEvent.valueInt4 = srcAudioLocation;
- audioConfChangedEvent.valueInt5 = availableContexts;
- mService.messageFromNative(audioConfChangedEvent);
+ injectAudioConfChanged(groupId, availableContexts, direction);
assertThat(mService.setActiveDevice(mLeftDevice)).isTrue();
verify(mNativeInterface).groupSetActive(groupId);
@@ -3326,8 +3347,7 @@ public class LeAudioServiceTest {
reset(mAudioManager);
// Expect device to be inactive
- audioConfChangedEvent.valueInt5 = 0;
- mService.messageFromNative(audioConfChangedEvent);
+ injectAudioConfChanged(groupId, 0, direction);
generateConnectionMessageFromNative(
mLeftDevice, BluetoothProfile.STATE_CONNECTED, BluetoothProfile.STATE_DISCONNECTED);
@@ -3336,8 +3356,7 @@ public class LeAudioServiceTest {
assertThat(mService.getConnectedDevices().contains(mLeftDevice)).isTrue();
// Expect device to be inactive
- audioConfChangedEvent.valueInt5 = 0;
- mService.messageFromNative(audioConfChangedEvent);
+ injectAudioConfChanged(groupId, 0, direction);
generateConnectionMessageFromNative(
mRightDevice, BluetoothProfile.STATE_CONNECTED, BluetoothProfile.STATE_DISCONNECTED);
@@ -3346,8 +3365,7 @@ public class LeAudioServiceTest {
assertThat(mService.getConnectedDevices().contains(mRightDevice)).isTrue();
// Expect device to be active
- audioConfChangedEvent.valueInt5 = availableContexts;
- mService.messageFromNative(audioConfChangedEvent);
+ injectAudioConfChanged(groupId, availableContexts, direction);
verify(mNativeInterface).groupSetActive(groupId);
@@ -3368,27 +3386,16 @@ public class LeAudioServiceTest {
int groupId = 1;
/* AUDIO_DIRECTION_OUTPUT_BIT = 0x01 */
int direction = 1;
- int snkAudioLocation = 3;
- int srcAudioLocation = 4;
int availableContexts = 5 + BluetoothLeAudio.CONTEXT_TYPE_RINGTONE;
// Not connected device
assertThat(mService.setActiveDevice(mSingleDevice)).isFalse();
- // Connected device
- doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class));
+ // Connect device
connectTestDevice(mSingleDevice, testGroupId);
// Add location support
- LeAudioStackEvent audioConfChangedEvent =
- new LeAudioStackEvent(LeAudioStackEvent.EVENT_TYPE_AUDIO_CONF_CHANGED);
- audioConfChangedEvent.device = mSingleDevice;
- audioConfChangedEvent.valueInt1 = direction;
- audioConfChangedEvent.valueInt2 = groupId;
- audioConfChangedEvent.valueInt3 = snkAudioLocation;
- audioConfChangedEvent.valueInt4 = srcAudioLocation;
- audioConfChangedEvent.valueInt5 = availableContexts;
- mService.messageFromNative(audioConfChangedEvent);
+ injectAudioConfChanged(groupId, availableContexts, direction);
assertThat(mService.setActiveDevice(mSingleDevice)).isTrue();
verify(mNativeInterface).groupSetActive(groupId);
@@ -3436,8 +3443,6 @@ public class LeAudioServiceTest {
int secondGroupId = 2;
/* AUDIO_DIRECTION_OUTPUT_BIT = 0x01 */
int direction = 1;
- int snkAudioLocation = 3;
- int srcAudioLocation = 4;
int availableContexts = 5 + BluetoothLeAudio.CONTEXT_TYPE_RINGTONE;
List<BluetoothDevice> devices = new ArrayList<>();
@@ -3448,8 +3453,7 @@ public class LeAudioServiceTest {
assertThat(mService.getBroadcastToUnicastFallbackGroup())
.isEqualTo(BluetoothLeAudio.GROUP_ID_INVALID);
- // Connected device
- doReturn(true).when(mNativeInterface).connectLeAudio(any(BluetoothDevice.class));
+ // Connect device
devices.add(mSingleDevice);
connectTestDevice(mSingleDevice, testGroupId);
@@ -3457,15 +3461,7 @@ public class LeAudioServiceTest {
assertThat(mService.getBroadcastToUnicastFallbackGroup()).isEqualTo(firstGroupId);
// Add location support
- LeAudioStackEvent audioConfChangedEvent =
- new LeAudioStackEvent(LeAudioStackEvent.EVENT_TYPE_AUDIO_CONF_CHANGED);
- audioConfChangedEvent.device = mSingleDevice;
- audioConfChangedEvent.valueInt1 = direction;
- audioConfChangedEvent.valueInt2 = firstGroupId;
- audioConfChangedEvent.valueInt3 = snkAudioLocation;
- audioConfChangedEvent.valueInt4 = srcAudioLocation;
- audioConfChangedEvent.valueInt5 = availableContexts;
- mService.messageFromNative(audioConfChangedEvent);
+ injectAudioConfChanged(firstGroupId, availableContexts, direction);
assertThat(mService.setActiveDevice(mSingleDevice)).isTrue();
verify(mNativeInterface).groupSetActive(firstGroupId);
diff --git a/android/app/tests/unit/src/com/android/bluetooth/le_scan/BatchScanThrottlerTest.java b/android/app/tests/unit/src/com/android/bluetooth/le_scan/BatchScanThrottlerTest.java
new file mode 100644
index 0000000000..67f32c4f0c
--- /dev/null
+++ b/android/app/tests/unit/src/com/android/bluetooth/le_scan/BatchScanThrottlerTest.java
@@ -0,0 +1,202 @@
+/*
+ * Copyright (C) 2025 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.le_scan;
+
+import static android.bluetooth.le.ScanSettings.SCAN_MODE_BALANCED;
+
+import static com.android.bluetooth.le_scan.ScanController.DEFAULT_REPORT_DELAY_FLOOR;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.bluetooth.le.ScanFilter;
+import android.bluetooth.le.ScanSettings;
+import android.platform.test.flag.junit.SetFlagsRule;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.bluetooth.TestUtils.FakeTimeProvider;
+
+import com.google.testing.junit.testparameterinjector.TestParameter;
+import com.google.testing.junit.testparameterinjector.TestParameterInjector;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import java.time.Duration;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.LongStream;
+
+/** Test cases for {@link BatchScanThrottler}. */
+@SmallTest
+@RunWith(TestParameterInjector.class)
+public class BatchScanThrottlerTest {
+ @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+ @Rule public MockitoRule mockitoRule = MockitoJUnit.rule();
+
+ private FakeTimeProvider mTimeProvider;
+
+ @Before
+ public void setUp() {
+ mTimeProvider = new FakeTimeProvider();
+ }
+
+ private void advanceTime(long amountToAdvanceMillis) {
+ mTimeProvider.advanceTime(Duration.ofMillis(amountToAdvanceMillis));
+ }
+
+ @Test
+ public void basicThrottling(
+ @TestParameter boolean isFiltered, @TestParameter boolean isScreenOn) {
+ BatchScanThrottler throttler = new BatchScanThrottler(mTimeProvider, isScreenOn);
+ if (!isScreenOn) {
+ advanceTime(BatchScanThrottler.SCREEN_OFF_DELAY_MS);
+ }
+ Set<ScanClient> clients =
+ Collections.singleton(
+ createBatchScanClient(DEFAULT_REPORT_DELAY_FLOOR, isFiltered));
+ long[] backoffIntervals =
+ getBackoffIntervals(
+ isScreenOn
+ ? DEFAULT_REPORT_DELAY_FLOOR
+ : BatchScanThrottler.SCREEN_OFF_MINIMUM_DELAY_FLOOR_MS);
+ for (long x : backoffIntervals) {
+ long expected = adjustExpectedInterval(x, isFiltered, isScreenOn);
+ assertThat(throttler.getBatchTriggerIntervalMillis(clients)).isEqualTo(expected);
+ }
+ long expected =
+ adjustExpectedInterval(
+ backoffIntervals[backoffIntervals.length - 1], isFiltered, isScreenOn);
+ // Ensure that subsequent calls continue to return the final throttled interval
+ assertThat(throttler.getBatchTriggerIntervalMillis(clients)).isEqualTo(expected);
+ assertThat(throttler.getBatchTriggerIntervalMillis(clients)).isEqualTo(expected);
+ }
+
+ @Test
+ public void screenOffDelayAndReset(@TestParameter boolean screenOnAtStart) {
+ BatchScanThrottler throttler = new BatchScanThrottler(mTimeProvider, screenOnAtStart);
+ if (screenOnAtStart) {
+ throttler.onScreenOn(false);
+ }
+ Set<ScanClient> clients =
+ Collections.singleton(createBatchScanClient(DEFAULT_REPORT_DELAY_FLOOR, true));
+ long[] backoffIntervals = getBackoffIntervals(DEFAULT_REPORT_DELAY_FLOOR);
+ advanceTime(BatchScanThrottler.SCREEN_OFF_DELAY_MS - 1);
+ for (long x : backoffIntervals) {
+ assertThat(throttler.getBatchTriggerIntervalMillis(clients)).isEqualTo(x);
+ }
+
+ backoffIntervals =
+ getBackoffIntervals(BatchScanThrottler.SCREEN_OFF_MINIMUM_DELAY_FLOOR_MS);
+ advanceTime(1);
+ for (long x : backoffIntervals) {
+ assertThat(throttler.getBatchTriggerIntervalMillis(clients)).isEqualTo(x);
+ }
+ assertThat(throttler.getBatchTriggerIntervalMillis(clients))
+ .isEqualTo(backoffIntervals[backoffIntervals.length - 1]);
+ }
+
+ @Test
+ public void testScreenOnReset() {
+ BatchScanThrottler throttler = new BatchScanThrottler(mTimeProvider, false);
+ advanceTime(BatchScanThrottler.SCREEN_OFF_DELAY_MS);
+ Set<ScanClient> clients =
+ Collections.singleton(createBatchScanClient(DEFAULT_REPORT_DELAY_FLOOR, true));
+ long[] backoffIntervals =
+ getBackoffIntervals(BatchScanThrottler.SCREEN_OFF_MINIMUM_DELAY_FLOOR_MS);
+ for (long x : backoffIntervals) {
+ assertThat(throttler.getBatchTriggerIntervalMillis(clients)).isEqualTo(x);
+ }
+
+ throttler.onScreenOn(true);
+ backoffIntervals = getBackoffIntervals(DEFAULT_REPORT_DELAY_FLOOR);
+ for (long x : backoffIntervals) {
+ assertThat(throttler.getBatchTriggerIntervalMillis(clients)).isEqualTo(x);
+ }
+ assertThat(throttler.getBatchTriggerIntervalMillis(clients))
+ .isEqualTo(backoffIntervals[backoffIntervals.length - 1]);
+ }
+
+ @Test
+ public void resetBackoff_restartsToFirstStage(@TestParameter boolean isScreenOn) {
+ BatchScanThrottler throttler = new BatchScanThrottler(mTimeProvider, isScreenOn);
+ if (!isScreenOn) {
+ // Advance the time before we start the test to when the screen-off intervals should be
+ // used
+ advanceTime(BatchScanThrottler.SCREEN_OFF_DELAY_MS);
+ }
+ Set<ScanClient> clients =
+ Collections.singleton(createBatchScanClient(DEFAULT_REPORT_DELAY_FLOOR, true));
+ long[] backoffIntervals =
+ getBackoffIntervals(
+ isScreenOn
+ ? DEFAULT_REPORT_DELAY_FLOOR
+ : BatchScanThrottler.SCREEN_OFF_MINIMUM_DELAY_FLOOR_MS);
+ for (long x : backoffIntervals) {
+ assertThat(throttler.getBatchTriggerIntervalMillis(clients)).isEqualTo(x);
+ }
+ assertThat(throttler.getBatchTriggerIntervalMillis(clients))
+ .isEqualTo(backoffIntervals[backoffIntervals.length - 1]);
+
+ throttler.resetBackoff();
+ for (long x : backoffIntervals) {
+ assertThat(throttler.getBatchTriggerIntervalMillis(clients)).isEqualTo(x);
+ }
+ assertThat(throttler.getBatchTriggerIntervalMillis(clients))
+ .isEqualTo(backoffIntervals[backoffIntervals.length - 1]);
+ }
+
+ private long adjustExpectedInterval(long interval, boolean isFiltered, boolean isScreenOn) {
+ if (isFiltered) {
+ return interval;
+ }
+ long threshold =
+ isScreenOn
+ ? BatchScanThrottler.UNFILTERED_DELAY_FLOOR_MS
+ : BatchScanThrottler.UNFILTERED_SCREEN_OFF_DELAY_FLOOR_MS;
+ return Math.max(interval, threshold);
+ }
+
+ private long[] getBackoffIntervals(long baseInterval) {
+ return LongStream.range(0, BatchScanThrottler.BACKOFF_MULTIPLIERS.length)
+ .map(x -> BatchScanThrottler.BACKOFF_MULTIPLIERS[(int) x] * baseInterval)
+ .toArray();
+ }
+
+ private ScanClient createBatchScanClient(long reportDelayMillis, boolean isFiltered) {
+ ScanSettings scanSettings =
+ new ScanSettings.Builder()
+ .setScanMode(SCAN_MODE_BALANCED)
+ .setReportDelay(reportDelayMillis)
+ .build();
+
+ return new ScanClient(1, scanSettings, createScanFilterList(isFiltered), 1);
+ }
+
+ private List<ScanFilter> createScanFilterList(boolean isFiltered) {
+ List<ScanFilter> scanFilterList = null;
+ if (isFiltered) {
+ scanFilterList = List.of(new ScanFilter.Builder().setDeviceName("TestName").build());
+ }
+ return scanFilterList;
+ }
+}
diff --git a/android/app/tests/unit/src/com/android/bluetooth/le_scan/ScanControllerTest.java b/android/app/tests/unit/src/com/android/bluetooth/le_scan/ScanControllerTest.java
index 2b19c1aa0b..ea051bc528 100644
--- a/android/app/tests/unit/src/com/android/bluetooth/le_scan/ScanControllerTest.java
+++ b/android/app/tests/unit/src/com/android/bluetooth/le_scan/ScanControllerTest.java
@@ -26,6 +26,7 @@ import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import android.bluetooth.BluetoothAdapter;
@@ -47,7 +48,6 @@ import android.platform.test.flag.junit.SetFlagsRule;
import androidx.test.InstrumentationRegistry;
import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
import com.android.bluetooth.TestUtils;
import com.android.bluetooth.btservice.AdapterService;
@@ -55,6 +55,9 @@ import com.android.bluetooth.btservice.CompanionManager;
import com.android.bluetooth.gatt.GattNativeInterface;
import com.android.bluetooth.gatt.GattObjectsFactory;
+import com.google.testing.junit.testparameterinjector.TestParameter;
+import com.google.testing.junit.testparameterinjector.TestParameterInjector;
+
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
@@ -73,7 +76,7 @@ import java.util.Set;
/** Test cases for {@link ScanController}. */
@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(TestParameterInjector.class)
public class ScanControllerTest {
@Rule public MockitoRule mockitoRule = MockitoJUnit.rule();
@Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
@@ -187,7 +190,8 @@ public class ScanControllerTest {
}
@Test
- public void onBatchScanReportsInternal_deliverBatchScan() throws RemoteException {
+ public void onBatchScanReportsInternal_deliverBatchScan_full(
+ @TestParameter boolean expectResults) throws RemoteException {
int status = 1;
int scannerId = 2;
int reportType = ScanManager.SCAN_RESULT_TYPE_FULL;
@@ -200,28 +204,59 @@ public class ScanControllerTest {
Set<ScanClient> scanClientSet = new HashSet<>();
ScanClient scanClient = new ScanClient(scannerId);
scanClient.associatedDevices = new ArrayList<>();
- scanClient.associatedDevices.add("02:00:00:00:00:00");
scanClient.scannerId = scannerId;
+ if (expectResults) {
+ scanClient.hasScanWithoutLocationPermission = true;
+ }
scanClientSet.add(scanClient);
doReturn(scanClientSet).when(mScanManager).getFullBatchScanQueue();
doReturn(mApp).when(mScannerMap).getById(scanClient.scannerId);
+ IScannerCallback callback = mock(IScannerCallback.class);
+ mApp.mCallback = callback;
mScanController.onBatchScanReportsInternal(
status, scannerId, reportType, numRecords, recordData);
verify(mScanManager).callbackDone(scannerId, status);
+ if (expectResults) {
+ verify(callback).onBatchScanResults(any());
+ } else {
+ verify(callback, never()).onBatchScanResults(any());
+ }
+ }
- reportType = ScanManager.SCAN_RESULT_TYPE_TRUNCATED;
- recordData =
+ @Test
+ public void onBatchScanReportsInternal_deliverBatchScan_truncated(
+ @TestParameter boolean expectResults) throws RemoteException {
+ int status = 1;
+ int scannerId = 2;
+ int reportType = ScanManager.SCAN_RESULT_TYPE_TRUNCATED;
+ int numRecords = 1;
+ byte[] recordData =
new byte[] {
0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x06, 0x04, 0x02, 0x02, 0x00, 0x00, 0x02
};
+
+ Set<ScanClient> scanClientSet = new HashSet<>();
+ ScanClient scanClient = new ScanClient(scannerId);
+ scanClient.associatedDevices = new ArrayList<>();
+ if (expectResults) {
+ scanClient.associatedDevices.add("02:00:00:00:00:00");
+ }
+ scanClient.scannerId = scannerId;
+ scanClientSet.add(scanClient);
doReturn(scanClientSet).when(mScanManager).getBatchScanQueue();
+ doReturn(mApp).when(mScannerMap).getById(scanClient.scannerId);
IScannerCallback callback = mock(IScannerCallback.class);
mApp.mCallback = callback;
mScanController.onBatchScanReportsInternal(
status, scannerId, reportType, numRecords, recordData);
- verify(callback).onBatchScanResults(any());
+ verify(mScanManager).callbackDone(scannerId, status);
+ if (expectResults) {
+ verify(callback).onBatchScanResults(any());
+ } else {
+ verify(callback, never()).onBatchScanResults(any());
+ }
}
@Test
diff --git a/android/app/tests/unit/src/com/android/bluetooth/le_scan/ScanManagerTest.java b/android/app/tests/unit/src/com/android/bluetooth/le_scan/ScanManagerTest.java
index 25a27ce876..041a982a9b 100644
--- a/android/app/tests/unit/src/com/android/bluetooth/le_scan/ScanManagerTest.java
+++ b/android/app/tests/unit/src/com/android/bluetooth/le_scan/ScanManagerTest.java
@@ -1045,7 +1045,7 @@ public class ScanManagerTest {
assertThat(mScanManager.getRegularScanQueue()).doesNotContain(client);
assertThat(mScanManager.getSuspendedScanQueue()).doesNotContain(client);
assertThat(mScanManager.getBatchScanQueue()).contains(client);
- assertThat(mScanManager.getBatchScanParams().scanMode).isEqualTo(expectedScanMode);
+ assertThat(mScanManager.getBatchScanParams().mScanMode).isEqualTo(expectedScanMode);
}
}
@@ -1075,13 +1075,13 @@ public class ScanManagerTest {
sendMessageWaitForProcessed(createStartStopScanMessage(true, client));
assertThat(mScanManager.getRegularScanQueue()).doesNotContain(client);
assertThat(mScanManager.getSuspendedScanQueue()).doesNotContain(client);
- assertThat(mScanManager.getBatchScanParams().scanMode).isEqualTo(expectedScanMode);
+ assertThat(mScanManager.getBatchScanParams().mScanMode).isEqualTo(expectedScanMode);
// Turn on screen
sendMessageWaitForProcessed(createScreenOnOffMessage(true));
assertThat(mScanManager.getRegularScanQueue()).doesNotContain(client);
assertThat(mScanManager.getSuspendedScanQueue()).doesNotContain(client);
assertThat(mScanManager.getBatchScanQueue()).contains(client);
- assertThat(mScanManager.getBatchScanParams().scanMode).isEqualTo(expectedScanMode);
+ assertThat(mScanManager.getBatchScanParams().mScanMode).isEqualTo(expectedScanMode);
}
}
@@ -1152,7 +1152,7 @@ public class ScanManagerTest {
assertThat(mScanManager.getRegularScanQueue()).doesNotContain(client);
assertThat(mScanManager.getSuspendedScanQueue()).doesNotContain(client);
assertThat(mScanManager.getBatchScanQueue()).contains(client);
- assertThat(mScanManager.getBatchScanParams().scanMode)
+ assertThat(mScanManager.getBatchScanParams().mScanMode)
.isEqualTo(expectedScanMode);
// Turn on screen
sendMessageWaitForProcessed(createScreenOnOffMessage(true));
@@ -1166,7 +1166,7 @@ public class ScanManagerTest {
assertThat(mScanManager.getRegularScanQueue()).doesNotContain(client);
assertThat(mScanManager.getSuspendedScanQueue()).doesNotContain(client);
assertThat(mScanManager.getBatchScanQueue()).contains(client);
- assertThat(mScanManager.getBatchScanParams().scanMode)
+ assertThat(mScanManager.getBatchScanParams().mScanMode)
.isEqualTo(expectedScanMode);
});
}
diff --git a/android/app/tests/unit/src/com/android/bluetooth/mcp/McpServiceTest.java b/android/app/tests/unit/src/com/android/bluetooth/mcp/McpServiceTest.java
index 7f519f2f01..35c7e125a5 100644
--- a/android/app/tests/unit/src/com/android/bluetooth/mcp/McpServiceTest.java
+++ b/android/app/tests/unit/src/com/android/bluetooth/mcp/McpServiceTest.java
@@ -34,7 +34,6 @@ import com.android.bluetooth.TestUtils;
import com.android.bluetooth.btservice.AdapterService;
import org.junit.After;
-import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
@@ -88,7 +87,7 @@ public class McpServiceTest {
public void testGetService() {
McpService mMcpServiceDuplicate = McpService.getMcpService();
assertThat(mMcpServiceDuplicate).isNotNull();
- Assert.assertSame(mMcpServiceDuplicate, mMcpService);
+ assertThat(mMcpServiceDuplicate).isSameInstanceAs(mMcpService);
}
@Test
diff --git a/android/app/tests/unit/src/com/android/bluetooth/mcp/MediaControlProfileTest.java b/android/app/tests/unit/src/com/android/bluetooth/mcp/MediaControlProfileTest.java
index 8c134f1980..6dede08e29 100644
--- a/android/app/tests/unit/src/com/android/bluetooth/mcp/MediaControlProfileTest.java
+++ b/android/app/tests/unit/src/com/android/bluetooth/mcp/MediaControlProfileTest.java
@@ -41,7 +41,6 @@ import com.android.bluetooth.audio_util.Metadata;
import com.android.bluetooth.btservice.AdapterService;
import org.junit.After;
-import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
@@ -212,9 +211,8 @@ public class MediaControlProfileTest {
mMockMediaData.state = bob.build();
doReturn(mMockMediaData.state).when(mMockMediaPlayerWrapper).getPlaybackState();
- Assert.assertNotEquals(
- mMcpServiceCallbacks.onGetCurrentTrackPosition(),
- MediaControlGattServiceInterface.TRACK_POSITION_UNAVAILABLE);
+ assertThat(mMcpServiceCallbacks.onGetCurrentTrackPosition())
+ .isNotEqualTo(MediaControlGattServiceInterface.TRACK_POSITION_UNAVAILABLE);
}
@Test
diff --git a/android/app/tests/unit/src/com/android/bluetooth/tbs/TbsGattTest.java b/android/app/tests/unit/src/com/android/bluetooth/tbs/TbsGattTest.java
index 4aca68e62e..a959a4a764 100644
--- a/android/app/tests/unit/src/com/android/bluetooth/tbs/TbsGattTest.java
+++ b/android/app/tests/unit/src/com/android/bluetooth/tbs/TbsGattTest.java
@@ -17,7 +17,6 @@
package com.android.bluetooth.tbs;
-import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
@@ -40,7 +39,6 @@ import com.android.bluetooth.btservice.AdapterService;
import com.google.common.primitives.Bytes;
-import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
@@ -62,9 +60,18 @@ import java.util.UUID;
@MediumTest
@RunWith(AndroidJUnit4.class)
public class TbsGattTest {
- private BluetoothAdapter mAdapter;
- private BluetoothDevice mFirstDevice;
- private BluetoothDevice mSecondDevice;
+ @Rule public MockitoRule mockitoRule = MockitoJUnit.rule();
+ @Rule public final ServiceTestRule mServiceRule = new ServiceTestRule();
+
+ @Mock private AdapterService mAdapterService;
+ @Mock private BluetoothGattServerProxy mGattServer;
+ @Mock private TbsGatt.Callback mCallback;
+ @Mock private TbsService mService;
+ @Captor private ArgumentCaptor<BluetoothGattService> mGattServiceCaptor;
+
+ private final BluetoothAdapter mAdapter = BluetoothAdapter.getDefaultAdapter();
+ private final BluetoothDevice mFirstDevice = TestUtils.getTestDevice(mAdapter, 0);
+ private final BluetoothDevice mSecondDevice = TestUtils.getTestDevice(mAdapter, 1);
private Integer mCurrentCcid;
private String mCurrentUci;
@@ -74,50 +81,19 @@ public class TbsGattTest {
private TbsGatt mTbsGatt;
- @Rule public MockitoRule mockitoRule = MockitoJUnit.rule();
-
- @Mock private AdapterService mAdapterService;
- @Mock private BluetoothGattServerProxy mMockGattServer;
- @Mock private TbsGatt.Callback mMockTbsGattCallback;
- @Mock private TbsService mMockTbsService;
-
- @Rule public final ServiceTestRule mServiceRule = new ServiceTestRule();
-
- @Captor private ArgumentCaptor<BluetoothGattService> mGattServiceCaptor;
-
@Before
- public void setUp() throws Exception {
+ public void setUp() {
if (Looper.myLooper() == null) {
Looper.prepare();
}
- getInstrumentation().getUiAutomation().adoptShellPermissionIdentity();
-
- TestUtils.setAdapterService(mAdapterService);
- mAdapter = BluetoothAdapter.getDefaultAdapter();
-
- doReturn(true).when(mMockGattServer).addService(any(BluetoothGattService.class));
- doReturn(true).when(mMockGattServer).open(any(BluetoothGattServerCallback.class));
+ doReturn(true).when(mGattServer).addService(any(BluetoothGattService.class));
+ doReturn(true).when(mGattServer).open(any(BluetoothGattServerCallback.class));
doReturn(BluetoothDevice.ACCESS_ALLOWED)
- .when(mMockTbsService)
+ .when(mService)
.getDeviceAuthorization(any(BluetoothDevice.class));
- mTbsGatt = new TbsGatt(mMockTbsService);
- mTbsGatt.setBluetoothGattServerForTesting(mMockGattServer);
-
- mFirstDevice = TestUtils.getTestDevice(mAdapter, 0);
- mSecondDevice = TestUtils.getTestDevice(mAdapter, 1);
-
- when(mMockTbsService.getDeviceAuthorization(any(BluetoothDevice.class)))
- .thenReturn(BluetoothDevice.ACCESS_ALLOWED);
- }
-
- @After
- public void tearDown() throws Exception {
- mFirstDevice = null;
- mSecondDevice = null;
- mTbsGatt = null;
- TestUtils.clearAdapterService(mAdapterService);
+ mTbsGatt = new TbsGatt(mAdapterService, mService, mGattServer);
}
private void prepareDefaultService() {
@@ -136,12 +112,12 @@ public class TbsGattTest {
true,
mCurrentProviderName,
mCurrentTechnology,
- mMockTbsGattCallback))
+ mCallback))
.isTrue();
verify(mAdapterService).registerBluetoothStateCallback(any(), any());
- verify(mMockGattServer).addService(mGattServiceCaptor.capture());
- doReturn(mGattServiceCaptor.getValue()).when(mMockGattServer).getService(any(UUID.class));
+ verify(mGattServer).addService(mGattServiceCaptor.capture());
+ doReturn(mGattServiceCaptor.getValue()).when(mGattServer).getService(any(UUID.class));
}
private BluetoothGattCharacteristic getCharacteristic(UUID uuid) {
@@ -168,9 +144,9 @@ public class TbsGattTest {
enable
? BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
: BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE);
- verify(mMockGattServer)
+ verify(mGattServer)
.sendResponse(eq(device), eq(1), eq(BluetoothGatt.GATT_SUCCESS), eq(0), any());
- reset(mMockGattServer);
+ reset(mGattServer);
}
private void verifySetValue(
@@ -288,26 +264,26 @@ public class TbsGattTest {
if (shouldNotify) {
if (notifyWithValue) {
- verify(mMockGattServer)
+ verify(mGattServer)
.notifyCharacteristicChanged(
eq(device), eq(characteristic), eq(false), any());
} else {
- verify(mMockGattServer)
+ verify(mGattServer)
.notifyCharacteristicChanged(eq(device), eq(characteristic), eq(false));
}
} else {
if (notifyWithValue) {
- verify(mMockGattServer, times(0))
+ verify(mGattServer, never())
.notifyCharacteristicChanged(
eq(device), eq(characteristic), anyBoolean(), any());
} else {
- verify(mMockGattServer, times(0))
+ verify(mGattServer, never())
.notifyCharacteristicChanged(eq(device), eq(characteristic), anyBoolean());
}
}
if (clearGattMock) {
- reset(mMockGattServer);
+ reset(mGattServer);
}
}
@@ -575,9 +551,9 @@ public class TbsGattTest {
.asList()
.containsExactly(requestedOpcode, callIndex, result)
.inOrder();
- verify(mMockGattServer, after(2000))
+ verify(mGattServer, after(2000))
.notifyCharacteristicChanged(eq(mFirstDevice), eq(characteristic), eq(false));
- reset(mMockGattServer);
+ reset(mGattServer);
callIndex = 0x02;
@@ -588,7 +564,7 @@ public class TbsGattTest {
.asList()
.containsExactly(requestedOpcode, callIndex, result)
.inOrder();
- verify(mMockGattServer, after(2000).times(0))
+ verify(mGattServer, after(2000).never())
.notifyCharacteristicChanged(any(), any(), anyBoolean());
}
@@ -695,7 +671,7 @@ public class TbsGattTest {
mTbsGatt.mGattServerCallback.onCharacteristicWriteRequest(
mFirstDevice, 1, characteristic, false, true, 0, value);
- verify(mMockGattServer)
+ verify(mGattServer)
.sendResponse(
eq(mFirstDevice),
eq(1),
@@ -704,7 +680,7 @@ public class TbsGattTest {
aryEq(new byte[] {0x00, 0x0A}));
// Verify the higher layer callback call
- verify(mMockTbsGattCallback)
+ verify(mCallback)
.onCallControlPointRequest(eq(mFirstDevice), eq(0x00), aryEq(new byte[] {0x0A}));
}
@@ -719,7 +695,7 @@ public class TbsGattTest {
mTbsGatt.mGattServerCallback.onCharacteristicWriteRequest(
mFirstDevice, 1, characteristic, false, true, 0, value);
- verify(mMockGattServer)
+ verify(mGattServer)
.sendResponse(
eq(mFirstDevice),
eq(1),
@@ -746,15 +722,15 @@ public class TbsGattTest {
mTbsGatt.setInbandRingtoneFlag(mFirstDevice);
mTbsGatt.setInbandRingtoneFlag(mFirstDevice);
- verify(mMockGattServer)
+ verify(mGattServer)
.notifyCharacteristicChanged(
eq(mFirstDevice), eq(characteristic), eq(false), eq(valueBytes));
- reset(mMockGattServer);
+ reset(mGattServer);
mTbsGatt.setInbandRingtoneFlag(mSecondDevice);
mTbsGatt.setInbandRingtoneFlag(mSecondDevice);
- verify(mMockGattServer)
+ verify(mGattServer)
.notifyCharacteristicChanged(
eq(mSecondDevice), eq(characteristic), eq(false), eq(valueBytes));
}
@@ -773,18 +749,18 @@ public class TbsGattTest {
valueBytes[1] = (byte) ((statusFlagValue >> 8) & 0xFF);
mTbsGatt.setInbandRingtoneFlag(mFirstDevice);
- verify(mMockGattServer)
+ verify(mGattServer)
.notifyCharacteristicChanged(
eq(mFirstDevice), eq(characteristic), eq(false), eq(valueBytes));
- reset(mMockGattServer);
+ reset(mGattServer);
mTbsGatt.setInbandRingtoneFlag(mSecondDevice);
- verify(mMockGattServer)
+ verify(mGattServer)
.notifyCharacteristicChanged(
eq(mSecondDevice), eq(characteristic), eq(false), eq(valueBytes));
- reset(mMockGattServer);
+ reset(mGattServer);
// clear flag
statusFlagValue = 0;
@@ -793,14 +769,14 @@ public class TbsGattTest {
mTbsGatt.clearInbandRingtoneFlag(mFirstDevice);
mTbsGatt.clearInbandRingtoneFlag(mFirstDevice);
- verify(mMockGattServer)
+ verify(mGattServer)
.notifyCharacteristicChanged(
eq(mFirstDevice), eq(characteristic), eq(false), eq(valueBytes));
- reset(mMockGattServer);
+ reset(mGattServer);
mTbsGatt.clearInbandRingtoneFlag(mSecondDevice);
mTbsGatt.clearInbandRingtoneFlag(mSecondDevice);
- verify(mMockGattServer)
+ verify(mGattServer)
.notifyCharacteristicChanged(
eq(mSecondDevice), eq(characteristic), eq(false), eq(valueBytes));
}
@@ -822,10 +798,10 @@ public class TbsGattTest {
mTbsGatt.setSilentModeFlag();
mTbsGatt.setSilentModeFlag();
mTbsGatt.setSilentModeFlag();
- verify(mMockGattServer, times(2))
+ verify(mGattServer, times(2))
.notifyCharacteristicChanged(any(), eq(characteristic), eq(false), eq(valueBytes));
- reset(mMockGattServer);
+ reset(mGattServer);
statusFlagValue =
TbsGatt.STATUS_FLAG_INBAND_RINGTONE_ENABLED
@@ -835,17 +811,17 @@ public class TbsGattTest {
mTbsGatt.setInbandRingtoneFlag(mFirstDevice);
- verify(mMockGattServer)
+ verify(mGattServer)
.notifyCharacteristicChanged(
eq(mFirstDevice), eq(characteristic), eq(false), eq(valueBytes));
- reset(mMockGattServer);
+ reset(mGattServer);
mTbsGatt.setInbandRingtoneFlag(mSecondDevice);
- verify(mMockGattServer)
+ verify(mGattServer)
.notifyCharacteristicChanged(
eq(mSecondDevice), eq(characteristic), eq(false), eq(valueBytes));
- reset(mMockGattServer);
+ reset(mGattServer);
statusFlagValue = TbsGatt.STATUS_FLAG_INBAND_RINGTONE_ENABLED;
valueBytes[0] = (byte) (statusFlagValue & 0xFF);
@@ -855,7 +831,7 @@ public class TbsGattTest {
mTbsGatt.clearSilentModeFlag();
mTbsGatt.clearSilentModeFlag();
mTbsGatt.clearSilentModeFlag();
- verify(mMockGattServer, times(2))
+ verify(mGattServer, times(2))
.notifyCharacteristicChanged(any(), eq(characteristic), eq(false), eq(valueBytes));
}
@@ -867,7 +843,7 @@ public class TbsGattTest {
mTbsGatt.mGattServerCallback.onCharacteristicReadRequest(
mFirstDevice, 1, 0, characteristic);
// Verify the higher layer callback call
- verify(mMockTbsGattCallback).isInbandRingtoneEnabled(eq(mFirstDevice));
+ verify(mCallback).isInbandRingtoneEnabled(eq(mFirstDevice));
}
@Test
@@ -881,31 +857,31 @@ public class TbsGattTest {
// Check with no configuration
mTbsGatt.mGattServerCallback.onDescriptorReadRequest(mFirstDevice, 1, 0, descriptor);
- verify(mMockGattServer)
+ verify(mGattServer)
.sendResponse(
eq(mFirstDevice),
eq(1),
eq(BluetoothGatt.GATT_SUCCESS),
eq(0),
eq(BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE));
- reset(mMockGattServer);
+ reset(mGattServer);
// Check with notifications enabled
configureNotifications(mFirstDevice, characteristic, true);
mTbsGatt.mGattServerCallback.onDescriptorReadRequest(mFirstDevice, 1, 0, descriptor);
- verify(mMockGattServer)
+ verify(mGattServer)
.sendResponse(
eq(mFirstDevice),
eq(1),
eq(BluetoothGatt.GATT_SUCCESS),
eq(0),
eq(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE));
- reset(mMockGattServer);
+ reset(mGattServer);
// Check with notifications disabled
configureNotifications(mFirstDevice, characteristic, false);
mTbsGatt.mGattServerCallback.onDescriptorReadRequest(mFirstDevice, 1, 0, descriptor);
- verify(mMockGattServer)
+ verify(mGattServer)
.sendResponse(
eq(mFirstDevice),
eq(1),
@@ -925,7 +901,7 @@ public class TbsGattTest {
// Check with no configuration
mTbsGatt.mGattServerCallback.onDescriptorReadRequest(mFirstDevice, 1, 0, descriptor);
- verify(mMockGattServer)
+ verify(mGattServer)
.sendResponse(
eq(mFirstDevice),
eq(1),
@@ -934,79 +910,79 @@ public class TbsGattTest {
eq(BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE));
mTbsGatt.mGattServerCallback.onDescriptorReadRequest(mSecondDevice, 1, 0, descriptor);
- verify(mMockGattServer)
+ verify(mGattServer)
.sendResponse(
eq(mSecondDevice),
eq(1),
eq(BluetoothGatt.GATT_SUCCESS),
eq(0),
eq(BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE));
- reset(mMockGattServer);
+ reset(mGattServer);
// Check with notifications enabled for first device
configureNotifications(mFirstDevice, characteristic, true);
verifySetValue(characteristic, 4, true, mFirstDevice, true);
mTbsGatt.mGattServerCallback.onDescriptorReadRequest(mFirstDevice, 1, 0, descriptor);
- verify(mMockGattServer)
+ verify(mGattServer)
.sendResponse(
eq(mFirstDevice),
eq(1),
eq(BluetoothGatt.GATT_SUCCESS),
eq(0),
eq(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE));
- reset(mMockGattServer);
+ reset(mGattServer);
// Check if second device is still not subscribed for notifications and will not get it
verifySetValue(characteristic, 5, false, mSecondDevice, false);
- verify(mMockGattServer)
+ verify(mGattServer)
.notifyCharacteristicChanged(eq(mFirstDevice), eq(characteristic), eq(false));
mTbsGatt.mGattServerCallback.onDescriptorReadRequest(mSecondDevice, 1, 0, descriptor);
- verify(mMockGattServer)
+ verify(mGattServer)
.sendResponse(
eq(mSecondDevice),
eq(1),
eq(BluetoothGatt.GATT_SUCCESS),
eq(0),
eq(BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE));
- reset(mMockGattServer);
+ reset(mGattServer);
// Check with notifications enabled for first and second device
configureNotifications(mSecondDevice, characteristic, true);
verifySetValue(characteristic, 6, true, mSecondDevice, false);
- verify(mMockGattServer)
+ verify(mGattServer)
.notifyCharacteristicChanged(eq(mFirstDevice), eq(characteristic), eq(false));
mTbsGatt.mGattServerCallback.onDescriptorReadRequest(mSecondDevice, 1, 0, descriptor);
- verify(mMockGattServer)
+ verify(mGattServer)
.sendResponse(
eq(mSecondDevice),
eq(1),
eq(BluetoothGatt.GATT_SUCCESS),
eq(0),
eq(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE));
- reset(mMockGattServer);
+ reset(mGattServer);
// Disable notification for first device, check if second will get notification
configureNotifications(mFirstDevice, characteristic, false);
verifySetValue(characteristic, 7, false, mFirstDevice, false);
- verify(mMockGattServer)
+ verify(mGattServer)
.notifyCharacteristicChanged(eq(mSecondDevice), eq(characteristic), eq(false));
mTbsGatt.mGattServerCallback.onDescriptorReadRequest(mFirstDevice, 1, 0, descriptor);
- verify(mMockGattServer)
+ verify(mGattServer)
.sendResponse(
eq(mFirstDevice),
eq(1),
eq(BluetoothGatt.GATT_SUCCESS),
eq(0),
eq(BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE));
- reset(mMockGattServer);
+ reset(mGattServer);
// Check with notifications disabled of both device
configureNotifications(mSecondDevice, characteristic, false);
verifySetValue(characteristic, 4, false, mFirstDevice, false);
- verify(mMockGattServer, times(0))
+ verify(mGattServer, never())
.notifyCharacteristicChanged(eq(mSecondDevice), eq(characteristic), eq(false));
mTbsGatt.mGattServerCallback.onDescriptorReadRequest(mSecondDevice, 1, 0, descriptor);
- verify(mMockGattServer)
+ verify(mGattServer)
.sendResponse(
eq(mSecondDevice),
eq(1),
@@ -1023,13 +999,13 @@ public class TbsGattTest {
getCharacteristic(TbsGatt.UUID_BEARER_TECHNOLOGY);
doReturn(BluetoothDevice.ACCESS_REJECTED)
- .when(mMockTbsService)
+ .when(mService)
.getDeviceAuthorization(any(BluetoothDevice.class));
mTbsGatt.mGattServerCallback.onCharacteristicReadRequest(
mFirstDevice, 1, 0, characteristic);
- verify(mMockGattServer)
+ verify(mGattServer)
.sendResponse(
eq(mFirstDevice),
eq(1),
@@ -1047,12 +1023,12 @@ public class TbsGattTest {
configureNotifications(mFirstDevice, characteristic, true);
configureNotifications(mSecondDevice, characteristic, true);
- doReturn(mGattServiceCaptor.getValue()).when(mMockGattServer).getService(any(UUID.class));
+ doReturn(mGattServiceCaptor.getValue()).when(mGattServer).getService(any(UUID.class));
assertThat(mGattServiceCaptor.getValue()).isNotNull();
// Leave it as unauthorized yet
doReturn(BluetoothDevice.ACCESS_REJECTED)
- .when(mMockTbsService)
+ .when(mService)
.getDeviceAuthorization(any(BluetoothDevice.class));
int statusFlagValue = TbsGatt.STATUS_FLAG_SILENT_MODE_ENABLED;
@@ -1068,18 +1044,18 @@ public class TbsGattTest {
valueBytes[0] = (byte) (statusFlagValue & 0xFF);
valueBytes[1] = (byte) ((statusFlagValue >> 8) & 0xFF);
mTbsGatt.setSilentModeFlag();
- verify(mMockGattServer, times(0))
+ verify(mGattServer, never())
.notifyCharacteristicChanged(any(), eq(characteristic), eq(false), eq(valueBytes));
// Expect a single notification for the just authorized device
doReturn(BluetoothDevice.ACCESS_ALLOWED)
- .when(mMockTbsService)
+ .when(mService)
.getDeviceAuthorization(any(BluetoothDevice.class));
assertThat(mGattServiceCaptor.getValue()).isNotNull();
mTbsGatt.onDeviceAuthorizationSet(mFirstDevice);
- verify(mMockGattServer, times(0))
+ verify(mGattServer, never())
.notifyCharacteristicChanged(any(), eq(characteristic2), eq(false));
- verify(mMockGattServer)
+ verify(mGattServer)
.notifyCharacteristicChanged(any(), eq(characteristic), eq(false), eq(valueBytes));
}
@@ -1091,13 +1067,13 @@ public class TbsGattTest {
getCharacteristic(TbsGatt.UUID_BEARER_TECHNOLOGY);
doReturn(BluetoothDevice.ACCESS_UNKNOWN)
- .when(mMockTbsService)
+ .when(mService)
.getDeviceAuthorization(any(BluetoothDevice.class));
mTbsGatt.mGattServerCallback.onCharacteristicReadRequest(
mFirstDevice, 1, 0, characteristic);
- verify(mMockTbsService, times(0)).onDeviceUnauthorized(eq(mFirstDevice));
+ verify(mService, never()).onDeviceUnauthorized(eq(mFirstDevice));
}
@Test
@@ -1108,7 +1084,7 @@ public class TbsGattTest {
getCharacteristic(TbsGatt.UUID_CALL_CONTROL_POINT);
doReturn(BluetoothDevice.ACCESS_REJECTED)
- .when(mMockTbsService)
+ .when(mService)
.getDeviceAuthorization(any(BluetoothDevice.class));
byte[] value =
@@ -1119,7 +1095,7 @@ public class TbsGattTest {
mTbsGatt.mGattServerCallback.onCharacteristicWriteRequest(
mFirstDevice, 1, characteristic, false, true, 0, value);
- verify(mMockGattServer)
+ verify(mGattServer)
.sendResponse(
eq(mFirstDevice),
eq(1),
@@ -1136,7 +1112,7 @@ public class TbsGattTest {
getCharacteristic(TbsGatt.UUID_CALL_CONTROL_POINT);
doReturn(BluetoothDevice.ACCESS_UNKNOWN)
- .when(mMockTbsService)
+ .when(mService)
.getDeviceAuthorization(any(BluetoothDevice.class));
byte[] value =
@@ -1147,7 +1123,7 @@ public class TbsGattTest {
mTbsGatt.mGattServerCallback.onCharacteristicWriteRequest(
mFirstDevice, 1, characteristic, false, true, 0, value);
- verify(mMockTbsService).onDeviceUnauthorized(eq(mFirstDevice));
+ verify(mService).onDeviceUnauthorized(eq(mFirstDevice));
}
@Test
@@ -1160,12 +1136,12 @@ public class TbsGattTest {
assertThat(descriptor).isNotNull();
doReturn(BluetoothDevice.ACCESS_REJECTED)
- .when(mMockTbsService)
+ .when(mService)
.getDeviceAuthorization(any(BluetoothDevice.class));
mTbsGatt.mGattServerCallback.onDescriptorReadRequest(mFirstDevice, 1, 0, descriptor);
- verify(mMockGattServer)
+ verify(mGattServer)
.sendResponse(
eq(mFirstDevice),
eq(1),
@@ -1184,12 +1160,12 @@ public class TbsGattTest {
assertThat(descriptor).isNotNull();
doReturn(BluetoothDevice.ACCESS_UNKNOWN)
- .when(mMockTbsService)
+ .when(mService)
.getDeviceAuthorization(any(BluetoothDevice.class));
mTbsGatt.mGattServerCallback.onDescriptorReadRequest(mFirstDevice, 1, 0, descriptor);
- verify(mMockTbsService, times(0)).onDeviceUnauthorized(eq(mFirstDevice));
+ verify(mService, never()).onDeviceUnauthorized(eq(mFirstDevice));
}
@Test
@@ -1202,7 +1178,7 @@ public class TbsGattTest {
assertThat(descriptor).isNotNull();
doReturn(BluetoothDevice.ACCESS_REJECTED)
- .when(mMockTbsService)
+ .when(mService)
.getDeviceAuthorization(any(BluetoothDevice.class));
byte[] value =
@@ -1213,7 +1189,7 @@ public class TbsGattTest {
mTbsGatt.mGattServerCallback.onDescriptorWriteRequest(
mFirstDevice, 1, descriptor, false, true, 0, value);
- verify(mMockGattServer)
+ verify(mGattServer)
.sendResponse(
eq(mFirstDevice),
eq(1),
@@ -1232,7 +1208,7 @@ public class TbsGattTest {
assertThat(descriptor).isNotNull();
doReturn(BluetoothDevice.ACCESS_UNKNOWN)
- .when(mMockTbsService)
+ .when(mService)
.getDeviceAuthorization(any(BluetoothDevice.class));
byte[] value =
@@ -1243,6 +1219,6 @@ public class TbsGattTest {
mTbsGatt.mGattServerCallback.onDescriptorWriteRequest(
mFirstDevice, 1, descriptor, false, true, 0, value);
- verify(mMockTbsService, times(0)).onDeviceUnauthorized(eq(mFirstDevice));
+ verify(mService, never()).onDeviceUnauthorized(eq(mFirstDevice));
}
}
diff --git a/android/app/tests/unit/src/com/android/bluetooth/tbs/TbsGenericTest.java b/android/app/tests/unit/src/com/android/bluetooth/tbs/TbsGenericTest.java
index d10db3e3e5..345dd8a5ee 100644
--- a/android/app/tests/unit/src/com/android/bluetooth/tbs/TbsGenericTest.java
+++ b/android/app/tests/unit/src/com/android/bluetooth/tbs/TbsGenericTest.java
@@ -33,7 +33,6 @@ import androidx.test.runner.AndroidJUnit4;
import com.android.bluetooth.TestUtils;
import com.android.bluetooth.le_audio.LeAudioService;
-import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
@@ -46,6 +45,7 @@ import org.mockito.junit.MockitoRule;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.UUID;
@@ -53,33 +53,27 @@ import java.util.UUID;
@MediumTest
@RunWith(AndroidJUnit4.class)
public class TbsGenericTest {
- private BluetoothAdapter mAdapter;
- private BluetoothDevice mCurrentDevice;
-
- private TbsGeneric mTbsGeneric;
-
@Rule public MockitoRule mockitoRule = MockitoJUnit.rule();
+ @Mock private TbsGatt mTbsGatt;
+ @Mock private IBluetoothLeCallControlCallback mIBluetoothLeCallControlCallback;
+ @Captor private ArgumentCaptor<Integer> mGtbsCcidCaptor;
+ @Captor private ArgumentCaptor<String> mGtbsUciCaptor;
- private @Mock TbsGatt mTbsGatt;
- private @Mock IBluetoothLeCallControlCallback mIBluetoothLeCallControlCallback;
- private @Captor ArgumentCaptor<Integer> mGtbsCcidCaptor;
- private @Captor ArgumentCaptor<String> mGtbsUciCaptor;
- private @Captor ArgumentCaptor<List> mDefaultGtbsUriSchemesCaptor =
- ArgumentCaptor.forClass(List.class);
- private @Captor ArgumentCaptor<String> mDefaultGtbsProviderNameCaptor;
- private @Captor ArgumentCaptor<Integer> mDefaultGtbsTechnologyCaptor;
+ @Captor
+ private ArgumentCaptor<List> mDefaultGtbsUriSchemesCaptor = ArgumentCaptor.forClass(List.class);
- private @Captor ArgumentCaptor<TbsGatt.Callback> mTbsGattCallback;
- private static Context mContext;
-
- @Before
- public void setUp() throws Exception {
+ @Captor private ArgumentCaptor<String> mDefaultGtbsProviderNameCaptor;
+ @Captor private ArgumentCaptor<Integer> mDefaultGtbsTechnologyCaptor;
+ @Captor private ArgumentCaptor<TbsGatt.Callback> mTbsGattCallback;
- mAdapter = BluetoothAdapter.getDefaultAdapter();
- mContext = getInstrumentation().getTargetContext();
+ private final Context mContext = getInstrumentation().getTargetContext();
+ private final BluetoothAdapter mAdapter = BluetoothAdapter.getDefaultAdapter();
+ private final BluetoothDevice mDevice = TestUtils.getTestDevice(mAdapter, 32);
- getInstrumentation().getUiAutomation().adoptShellPermissionIdentity();
+ private TbsGeneric mTbsGeneric;
+ @Before
+ public void setUp() {
// Default TbsGatt mock behavior
doReturn(true)
.when(mTbsGatt)
@@ -106,15 +100,8 @@ public class TbsGenericTest {
doReturn(true).when(mTbsGatt).clearIncomingCall();
doReturn(true).when(mTbsGatt).setCallFriendlyName(anyInt(), anyString());
doReturn(true).when(mTbsGatt).clearFriendlyName();
- doReturn(mContext).when(mTbsGatt).getContext();
-
- mTbsGeneric = new TbsGeneric();
- mTbsGeneric.init(mTbsGatt);
- }
- @After
- public void tearDown() throws Exception {
- mTbsGeneric = null;
+ mTbsGeneric = new TbsGeneric(mContext, mTbsGatt);
}
private Integer prepareTestBearer() {
@@ -150,14 +137,13 @@ public class TbsGenericTest {
@Test
public void testSetClearInbandRingtone() {
- mCurrentDevice = TestUtils.getTestDevice(mAdapter, 0);
prepareTestBearer();
- mTbsGeneric.setInbandRingtoneSupport(mCurrentDevice);
- verify(mTbsGatt).setInbandRingtoneFlag(mCurrentDevice);
+ mTbsGeneric.setInbandRingtoneSupport(mDevice);
+ verify(mTbsGatt).setInbandRingtoneFlag(mDevice);
- mTbsGeneric.clearInbandRingtoneSupport(mCurrentDevice);
- verify(mTbsGatt).clearInbandRingtoneFlag(mCurrentDevice);
+ mTbsGeneric.clearInbandRingtoneSupport(mDevice);
+ verify(mTbsGatt).clearInbandRingtoneFlag(mDevice);
}
@Test
@@ -362,7 +348,6 @@ public class TbsGenericTest {
@Test
public void testCallAccept() {
- mCurrentDevice = TestUtils.getTestDevice(mAdapter, 0);
Integer ccid = prepareTestBearer();
reset(mTbsGatt);
@@ -392,8 +377,7 @@ public class TbsGenericTest {
args[0] = (byte) (callIndex & 0xFF);
mTbsGattCallback
.getValue()
- .onCallControlPointRequest(
- mCurrentDevice, TbsGatt.CALL_CONTROL_POINT_OPCODE_ACCEPT, args);
+ .onCallControlPointRequest(mDevice, TbsGatt.CALL_CONTROL_POINT_OPCODE_ACCEPT, args);
ArgumentCaptor<Integer> requestIdCaptor = ArgumentCaptor.forClass(Integer.class);
ArgumentCaptor<ParcelUuid> callUuidCaptor = ArgumentCaptor.forClass(ParcelUuid.class);
@@ -405,7 +389,7 @@ public class TbsGenericTest {
}
assertThat(callUuidCaptor.getValue().getUuid()).isEqualTo(callUuid);
// Active device should be changed
- verify(leAudioService).setActiveDevice(mCurrentDevice);
+ verify(leAudioService).setActiveDevice(mDevice);
// Respond with requestComplete...
mTbsGeneric.requestResult(
@@ -415,7 +399,7 @@ public class TbsGenericTest {
// ..and verify if GTBS control point is updated to notifier the peer about the result
verify(mTbsGatt)
.setCallControlPointResult(
- eq(mCurrentDevice),
+ eq(mDevice),
eq(TbsGatt.CALL_CONTROL_POINT_OPCODE_ACCEPT),
eq(callIndex),
eq(BluetoothLeCallControl.RESULT_SUCCESS));
@@ -423,7 +407,6 @@ public class TbsGenericTest {
@Test
public void testCallTerminate() {
- mCurrentDevice = TestUtils.getTestDevice(mAdapter, 0);
Integer ccid = prepareTestBearer();
reset(mTbsGatt);
@@ -451,7 +434,7 @@ public class TbsGenericTest {
mTbsGattCallback
.getValue()
.onCallControlPointRequest(
- mCurrentDevice, TbsGatt.CALL_CONTROL_POINT_OPCODE_TERMINATE, args);
+ mDevice, TbsGatt.CALL_CONTROL_POINT_OPCODE_TERMINATE, args);
ArgumentCaptor<Integer> requestIdCaptor = ArgumentCaptor.forClass(Integer.class);
ArgumentCaptor<ParcelUuid> callUuidCaptor = ArgumentCaptor.forClass(ParcelUuid.class);
@@ -471,7 +454,7 @@ public class TbsGenericTest {
// ..and verify if GTBS control point is updated to notifier the peer about the result
verify(mTbsGatt)
.setCallControlPointResult(
- eq(mCurrentDevice),
+ eq(mDevice),
eq(TbsGatt.CALL_CONTROL_POINT_OPCODE_TERMINATE),
eq(callIndex),
eq(BluetoothLeCallControl.RESULT_SUCCESS));
@@ -479,7 +462,6 @@ public class TbsGenericTest {
@Test
public void testCallHold() {
- mCurrentDevice = TestUtils.getTestDevice(mAdapter, 0);
Integer ccid = prepareTestBearer();
reset(mTbsGatt);
@@ -507,7 +489,7 @@ public class TbsGenericTest {
mTbsGattCallback
.getValue()
.onCallControlPointRequest(
- mCurrentDevice, TbsGatt.CALL_CONTROL_POINT_OPCODE_LOCAL_HOLD, args);
+ mDevice, TbsGatt.CALL_CONTROL_POINT_OPCODE_LOCAL_HOLD, args);
ArgumentCaptor<Integer> requestIdCaptor = ArgumentCaptor.forClass(Integer.class);
ArgumentCaptor<ParcelUuid> callUuidCaptor = ArgumentCaptor.forClass(ParcelUuid.class);
@@ -527,7 +509,7 @@ public class TbsGenericTest {
// ..and verify if GTBS control point is updated to notifier the peer about the result
verify(mTbsGatt)
.setCallControlPointResult(
- eq(mCurrentDevice),
+ eq(mDevice),
eq(TbsGatt.CALL_CONTROL_POINT_OPCODE_LOCAL_HOLD),
eq(callIndex),
eq(BluetoothLeCallControl.RESULT_SUCCESS));
@@ -535,7 +517,6 @@ public class TbsGenericTest {
@Test
public void testCallRetrieve() {
- mCurrentDevice = TestUtils.getTestDevice(mAdapter, 0);
Integer ccid = prepareTestBearer();
reset(mTbsGatt);
@@ -563,7 +544,7 @@ public class TbsGenericTest {
mTbsGattCallback
.getValue()
.onCallControlPointRequest(
- mCurrentDevice, TbsGatt.CALL_CONTROL_POINT_OPCODE_LOCAL_RETRIEVE, args);
+ mDevice, TbsGatt.CALL_CONTROL_POINT_OPCODE_LOCAL_RETRIEVE, args);
ArgumentCaptor<Integer> requestIdCaptor = ArgumentCaptor.forClass(Integer.class);
ArgumentCaptor<ParcelUuid> callUuidCaptor = ArgumentCaptor.forClass(ParcelUuid.class);
@@ -583,7 +564,7 @@ public class TbsGenericTest {
// ..and verify if GTBS control point is updated to notifier the peer about the result
verify(mTbsGatt)
.setCallControlPointResult(
- eq(mCurrentDevice),
+ eq(mDevice),
eq(TbsGatt.CALL_CONTROL_POINT_OPCODE_LOCAL_RETRIEVE),
eq(callIndex),
eq(BluetoothLeCallControl.RESULT_SUCCESS));
@@ -591,7 +572,6 @@ public class TbsGenericTest {
@Test
public void testCallOriginate() {
- mCurrentDevice = TestUtils.getTestDevice(mAdapter, 0);
Integer ccid = prepareTestBearer();
reset(mTbsGatt);
@@ -603,9 +583,7 @@ public class TbsGenericTest {
mTbsGattCallback
.getValue()
.onCallControlPointRequest(
- mCurrentDevice,
- TbsGatt.CALL_CONTROL_POINT_OPCODE_ORIGINATE,
- uri.getBytes());
+ mDevice, TbsGatt.CALL_CONTROL_POINT_OPCODE_ORIGINATE, uri.getBytes());
ArgumentCaptor<Integer> requestIdCaptor = ArgumentCaptor.forClass(Integer.class);
ArgumentCaptor<ParcelUuid> callUuidCaptor = ArgumentCaptor.forClass(ParcelUuid.class);
@@ -617,7 +595,7 @@ public class TbsGenericTest {
}
// Active device should be changed
- verify(leAudioService).setActiveDevice(mCurrentDevice);
+ verify(leAudioService).setActiveDevice(mDevice);
// Respond with requestComplete...
mTbsGeneric.requestResult(
@@ -634,7 +612,7 @@ public class TbsGenericTest {
// ..and verify if GTBS control point is updated to notifier the peer about the result
verify(mTbsGatt)
.setCallControlPointResult(
- eq(mCurrentDevice),
+ eq(mDevice),
eq(TbsGatt.CALL_CONTROL_POINT_OPCODE_ORIGINATE),
anyInt(),
eq(BluetoothLeCallControl.RESULT_SUCCESS));
@@ -642,7 +620,6 @@ public class TbsGenericTest {
@Test
public void testCallJoin() {
- mCurrentDevice = TestUtils.getTestDevice(mAdapter, 0);
Integer ccid = prepareTestBearer();
reset(mTbsGatt);
@@ -678,8 +655,7 @@ public class TbsGenericTest {
}
mTbsGattCallback
.getValue()
- .onCallControlPointRequest(
- mCurrentDevice, TbsGatt.CALL_CONTROL_POINT_OPCODE_JOIN, args);
+ .onCallControlPointRequest(mDevice, TbsGatt.CALL_CONTROL_POINT_OPCODE_JOIN, args);
ArgumentCaptor<Integer> requestIdCaptor = ArgumentCaptor.forClass(Integer.class);
ArgumentCaptor<List<ParcelUuid>> callUuidCaptor = ArgumentCaptor.forClass(List.class);
@@ -703,9 +679,85 @@ public class TbsGenericTest {
// ..and verify if GTBS control point is updated to notifier the peer about the result
verify(mTbsGatt)
.setCallControlPointResult(
- eq(mCurrentDevice),
+ eq(mDevice),
eq(TbsGatt.CALL_CONTROL_POINT_OPCODE_JOIN),
anyInt(),
eq(BluetoothLeCallControl.RESULT_SUCCESS));
}
+
+ @Test
+ public void testCallOperationsBlockedForBroadcastReceiver() {
+ Integer ccid = prepareTestBearer();
+ reset(mTbsGatt);
+
+ LeAudioService leAudioService = mock(LeAudioService.class);
+ mTbsGeneric.setLeAudioServiceForTesting(leAudioService);
+
+ // Prepare the incoming call
+ UUID callUuid = UUID.randomUUID();
+ List<BluetoothLeCall> tbsCalls = new ArrayList<>();
+ tbsCalls.add(
+ new BluetoothLeCall(
+ callUuid,
+ "tel:987654321",
+ "aFriendlyCaller",
+ BluetoothLeCall.STATE_INCOMING,
+ 0));
+ mTbsGeneric.currentCallsList(ccid, tbsCalls);
+
+ ArgumentCaptor<Map> currentCallsCaptor = ArgumentCaptor.forClass(Map.class);
+ verify(mTbsGatt).setCallState(currentCallsCaptor.capture());
+ Map<Integer, TbsCall> capturedCurrentCalls = currentCallsCaptor.getValue();
+ assertThat(capturedCurrentCalls.size()).isEqualTo(1);
+ Integer callIndex = capturedCurrentCalls.entrySet().iterator().next().getKey();
+ reset(mTbsGatt);
+
+ doReturn(new HashSet<>(Arrays.asList(mDevice)))
+ .when(leAudioService)
+ .getLocalBroadcastReceivers();
+
+ doReturn(false).when(leAudioService).isPrimaryDevice(mDevice);
+
+ // Verify call accept
+ byte args[] = new byte[1];
+ args[0] = (byte) (callIndex & 0xFF);
+ mTbsGattCallback
+ .getValue()
+ .onCallControlPointRequest(
+ mDevice, TbsGatt.CALL_CONTROL_POINT_OPCODE_ACCEPT, args);
+
+ // Active device should not be changed
+ verify(leAudioService, never()).setActiveDevice(mDevice);
+ // Verify if GTBS control point is updated to notify the peer about the result
+ verify(mTbsGatt)
+ .setCallControlPointResult(
+ eq(mDevice),
+ eq(TbsGatt.CALL_CONTROL_POINT_OPCODE_ACCEPT),
+ eq(0),
+ eq(TbsGatt.CALL_CONTROL_POINT_RESULT_OPERATION_NOT_POSSIBLE));
+
+ // Verify call terminate
+ tbsCalls.clear();
+ tbsCalls.add(
+ new BluetoothLeCall(
+ callUuid,
+ "tel:987654321",
+ "aFriendlyCaller",
+ BluetoothLeCall.STATE_ACTIVE,
+ 0));
+ mTbsGeneric.currentCallsList(ccid, tbsCalls);
+
+ mTbsGattCallback
+ .getValue()
+ .onCallControlPointRequest(
+ mDevice, TbsGatt.CALL_CONTROL_POINT_OPCODE_TERMINATE, args);
+
+ // Verify if GTBS control point is updated to notify the peer about the result
+ verify(mTbsGatt)
+ .setCallControlPointResult(
+ eq(mDevice),
+ eq(TbsGatt.CALL_CONTROL_POINT_OPCODE_TERMINATE),
+ eq(0),
+ eq(TbsGatt.CALL_CONTROL_POINT_RESULT_OPERATION_NOT_POSSIBLE));
+ }
}
diff --git a/android/app/tests/unit/src/com/android/bluetooth/vc/VolumeControlServiceTest.java b/android/app/tests/unit/src/com/android/bluetooth/vc/VolumeControlServiceTest.java
index 69d4407b23..94e1db83b1 100644
--- a/android/app/tests/unit/src/com/android/bluetooth/vc/VolumeControlServiceTest.java
+++ b/android/app/tests/unit/src/com/android/bluetooth/vc/VolumeControlServiceTest.java
@@ -77,7 +77,6 @@ import com.android.bluetooth.le_audio.LeAudioService;
import org.hamcrest.Matcher;
import org.hamcrest.core.AllOf;
import org.junit.After;
-import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
@@ -1125,13 +1124,13 @@ public class VolumeControlServiceTest {
mBinder.setDeviceVolume(mDevice, deviceOneVolume, false, mAttributionSource);
inOrderNative.verify(mNativeInterface).setVolume(mDevice, deviceOneVolume);
assertThat(mService.getDeviceVolume(mDevice)).isEqualTo(deviceOneVolume);
- Assert.assertNotEquals(deviceOneVolume, mService.getDeviceVolume(mDeviceTwo));
+ assertThat(mService.getDeviceVolume(mDeviceTwo)).isNotEqualTo(deviceOneVolume);
inOrderNative.verify(mNativeInterface, never()).setGroupVolume(anyInt(), anyInt());
mBinder.setDeviceVolume(mDeviceTwo, deviceTwoVolume, false, mAttributionSource);
inOrderNative.verify(mNativeInterface).setVolume(mDeviceTwo, deviceTwoVolume);
assertThat(mService.getDeviceVolume(mDeviceTwo)).isEqualTo(deviceTwoVolume);
- Assert.assertNotEquals(deviceTwoVolume, mService.getDeviceVolume(mDevice));
+ assertThat(mService.getDeviceVolume(mDevice)).isNotEqualTo(deviceTwoVolume);
inOrderNative.verify(mNativeInterface, never()).setGroupVolume(anyInt(), anyInt());
}
diff --git a/android/pandora/mmi2grpc/mmi2grpc/_rootcanal.py b/android/pandora/mmi2grpc/mmi2grpc/_rootcanal.py
index 42ef33f68b..5f68da54f7 100644
--- a/android/pandora/mmi2grpc/mmi2grpc/_rootcanal.py
+++ b/android/pandora/mmi2grpc/mmi2grpc/_rootcanal.py
@@ -92,6 +92,7 @@ class Dongle(enum.Enum):
DEFAULT = "default"
LAIRD_BL654 = "laird_bl654"
CSR_RCK_PTS_DONGLE = "csr_rck_pts_dongle"
+ INTEL_BE200 = "intel_be200"
class RootCanal:
diff --git a/android/pandora/mmi2grpc/mmi2grpc/avrcp.py b/android/pandora/mmi2grpc/mmi2grpc/avrcp.py
index 9fbc55d56c..c64eb2113f 100644
--- a/android/pandora/mmi2grpc/mmi2grpc/avrcp.py
+++ b/android/pandora/mmi2grpc/mmi2grpc/avrcp.py
@@ -117,6 +117,7 @@ class AVRCPProxy(ProfileProxy):
Action: Make sure the IUT is in a connectable state.
"""
+ self.mediaplayer.ResetQueue()
return "OK"
@assert_description
@@ -1080,3 +1081,73 @@ class AVRCPProxy(ProfileProxy):
"""
return "OK"
+
+ @assert_description
+ def TSC_OBEX_MMI_iut_accept_slc_connect_l2cap(self, **kwargs):
+ """
+ Please accept the l2cap channel connection for an OBEX connection.
+ """
+
+ return "OK"
+
+ @assert_description
+ def TSC_OBEX_MMI_iut_accept_connect(self, **kwargs):
+ """
+ Please accept the OBEX CONNECT REQ.
+ """
+
+ return "OK"
+
+
+ @assert_description
+ def TSC_AVRCP_mmi_user_queue_cover_art_element(self, **kwargs):
+ """
+ Take action to play a media element with cover art. Press 'Ok' when
+ ready.
+ """
+ self.mediaplayer.Play()
+
+ return "OK"
+
+ @assert_description
+ def TSC_AVRCP_mmi_iut_reject_invalid_get_img(self, **kwargs):
+ """
+ Take action to reject the invalid 'get-img' request sent by the tester.
+ """
+
+ return "OK"
+
+ @assert_description
+ def TSC_BIP_MMI_iut_accept_get_img_properties(self, **kwargs):
+ """
+ Take action to accept the GetImgProperties operation from the tester.
+ """
+
+ return "OK"
+
+ @assert_description
+ def TSC_BIP_MMI_iut_accept_get_img(self, **kwargs):
+ """
+ Take action to accept the GetImg operation from the tester.
+ """
+
+ return "OK"
+
+ @assert_description
+ def TSC_OBEX_MMI_tester_verify_sent_file_or_folder(self, **kwargs):
+ """
+ Was the currently displayed file or folder sent by the IUT?
+ """
+
+ return "OK"
+
+ @assert_description
+ def TSC_AVRCP_mmi_user_queue_no_cover_art_element(self, **kwargs):
+ """
+ Take action to play a media element that does not have any cover art
+ with it. Press 'Ok' when ready.
+ """
+ self.mediaplayer.UpdateQueue()
+ self.mediaplayer.PlayUpdated()
+
+ return "OK"
diff --git a/android/pandora/server/configs/pts_bot_tests_config.json b/android/pandora/server/configs/pts_bot_tests_config.json
index 758d21f07c..9a80a7471e 100644
--- a/android/pandora/server/configs/pts_bot_tests_config.json
+++ b/android/pandora/server/configs/pts_bot_tests_config.json
@@ -129,6 +129,11 @@
"AVDTP/SRC/INT/SIG/SMG/BV-31-C",
"AVDTP/SRC/INT/SIG/SYN/BV-05-C",
"AVDTP/SRC/INT/TRA/BTR/BV-01-C",
+ "AVRCP/TG/CA/BI-03-C",
+ "AVRCP/TG/CA/BI-05-C",
+ "AVRCP/TG/CA/BI-07-C",
+ "AVRCP/TG/CA/BI-10-C",
+ "AVRCP/TG/CA/BV-16-C",
"AVRCP/CT/CEC/BV-02-I",
"AVRCP/CT/CRC/BV-02-I",
"AVRCP/TG/CEC/BV-01-I",
@@ -769,6 +774,21 @@
"AVDTP/SRC/INT/SIG/SMG/BV-23-C",
"AVDTP/SRC/INT/SIG/SMG/BV-33-C",
"AVDTP/SRC/INT/SIG/SMG/ESR05/BV-13-C",
+ "AVRCP/TG/CA/BI-01-C",
+ "AVRCP/TG/CA/BI-04-C",
+ "AVRCP/TG/CA/BI-06-C",
+ "AVRCP/TG/CA/BI-08-C",
+ "AVRCP/TG/CA/BI-09-C",
+ "AVRCP/TG/CA/BV-01-I",
+ "AVRCP/TG/CA/BV-02-C",
+ "AVRCP/TG/CA/BV-02-I",
+ "AVRCP/TG/CA/BV-03-I",
+ "AVRCP/TG/CA/BV-04-C",
+ "AVRCP/TG/CA/BV-06-C",
+ "AVRCP/TG/CA/BV-08-C",
+ "AVRCP/TG/CA/BV-10-C",
+ "AVRCP/TG/CA/BV-12-C",
+ "AVRCP/TG/CA/BV-14-C",
"AVRCP/CT/CEC/BV-01-I",
"AVRCP/CT/CRC/BV-01-I",
"AVRCP/CT/PTH/BV-01-C",
@@ -1566,6 +1586,7 @@
"TSPC_AVRCP_7_64": true,
"TSPC_AVRCP_7_65": true,
"TSPC_AVRCP_7_66": true,
+ "TSPC_AVRCP_7_67": true,
"TSPC_AVRCP_7b_4": true,
"TSPC_AVRCP_8_19": true,
"TSPC_AVRCP_8_20": true,
@@ -3210,12 +3231,25 @@
"SM": {},
"SPP": {},
"SUM ICS": {},
- "VCP": {
- }
+ "VCP": {}
},
"flags": [
{
"flags": [
+ "set_addressed_player",
+ "browsing_refactor",
+ "avrcp_16_default"
+ ],
+ "tests": [
+ "AVRCP/TG/CA/BI-03-C",
+ "AVRCP/TG/CA/BI-05-C",
+ "AVRCP/TG/CA/BI-07-C",
+ "AVRCP/TG/CA/BI-10-C",
+ "AVRCP/TG/CA/BV-16-C"
+ ]
+ },
+ {
+ "flags": [
"leaudio_allow_leaudio_only_devices",
"enable_hap_by_default"
],
@@ -3252,6 +3286,69 @@
"VCP/VC/SPE/BI-19-C",
"VCP/VC/SPE/BI-20-C"
]
+ },
+ {
+ "flags": [
+ "avdt_accept_open_timeout_ms"
+ ],
+ "tests": [
+ "A2DP/SNK/SYN/BV-01-C"
+ ]
+ }
+ ],
+ "system_properties": [
+ {
+ "system_properties": {
+ "bluetooth.profile.a2dp.sink.enabled": "true",
+ "bluetooth.profile.a2dp.source.enabled": "false"
+ },
+ "tests": [
+ "A2DP/SNK",
+ "AVCTP/CT",
+ "AVDTP/SNK",
+ "AVRCP/CT/CEC",
+ "AVRCP/CT/CRC",
+ "AVRCP/CT/PTH",
+ "AVRCP/CT/PTT"
+ ]
+ },
+ {
+ "system_properties": {
+ "bluetooth.profile.a2dp.sink.enabled": "false",
+ "bluetooth.profile.a2dp.source.enabled": "true"
+ },
+ "tests": [
+ "A2DP/SRC",
+ "AVCTP/TG",
+ "AVDTP/SRC",
+ "AVRCP/TG"
+ ]
+ },
+ {
+ "system_properties": {
+ "bluetooth.profile.hfp.hf.enabled": "true",
+ "bluetooth.profile.hfp.ag.enabled": "false"
+ },
+ "tests": [
+ "HFP/HF"
+ ]
+ },
+ {
+ "system_properties": {
+ "bluetooth.profile.hfp.hf.enabled": "false",
+ "bluetooth.profile.hfp.ag.enabled": "true"
+ },
+ "tests": [
+ "HFP/AG"
+ ]
+ },
+ {
+ "system_properties": {
+ "bluetooth.a2dp.avdt_accept_open_timeout_ms": "15000"
+ },
+ "tests": [
+ "A2DP/SNK/SYN/BV-01-C"
+ ]
}
]
}
diff --git a/android/pandora/server/configs/pts_bot_tests_config_auto.json b/android/pandora/server/configs/pts_bot_tests_config_auto.json
index abc188046c..425b79c8e8 100644
--- a/android/pandora/server/configs/pts_bot_tests_config_auto.json
+++ b/android/pandora/server/configs/pts_bot_tests_config_auto.json
@@ -375,5 +375,67 @@
"SPP": {},
"VCP": {}
},
- "flags": {}
+ "flags": {
+ "flags": [
+ "avdt_accept_open_timeout_ms"
+ ],
+ "tests": [
+ "A2DP/SNK/SYN/BV-01-C"
+ ]
+ },
+ "system_properties": [
+ {
+ "system_properties": {
+ "bluetooth.profile.a2dp.sink.enabled": "true",
+ "bluetooth.profile.a2dp.source.enabled": "false"
+ },
+ "tests": [
+ "A2DP/SNK",
+ "AVCTP/CT",
+ "AVDTP/SNK",
+ "AVRCP/CT/CEC",
+ "AVRCP/CT/CRC",
+ "AVRCP/CT/PTH",
+ "AVRCP/CT/PTT"
+ ]
+ },
+ {
+ "system_properties": {
+ "bluetooth.profile.a2dp.sink.enabled": "false",
+ "bluetooth.profile.a2dp.source.enabled": "true"
+ },
+ "tests": [
+ "A2DP/SRC",
+ "AVCTP/TG",
+ "AVDTP/SRC",
+ "AVRCP/TG"
+ ]
+ },
+ {
+ "system_properties": {
+ "bluetooth.profile.hfp.hf.enabled": "true",
+ "bluetooth.profile.hfp.ag.enabled": "false"
+ },
+ "tests": [
+ "HFP/HF"
+ ]
+ },
+ {
+ "system_properties": {
+ "bluetooth.profile.hfp.hf.enabled": "false",
+ "bluetooth.profile.hfp.ag.enabled": "true"
+ },
+ "tests": [
+ "HFP/AG"
+ ]
+ },
+ {
+ "system_properties": {
+ "bluetooth.a2dp.avdt_accept_open_timeout_ms": "15000"
+ },
+ "tests": [
+ "A2DP/SNK/SYN/BV-01-C"
+ ]
+ }
+ ]
}
diff --git a/android/pandora/server/src/A2dp.kt b/android/pandora/server/src/A2dp.kt
index 6708dd1c46..0b5a0d6640 100644
--- a/android/pandora/server/src/A2dp.kt
+++ b/android/pandora/server/src/A2dp.kt
@@ -41,7 +41,6 @@ import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
-import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.filter
@@ -110,10 +109,6 @@ class A2dp(val context: Context) : A2DPImplBase(), Closeable {
}
}
- // TODO: b/234891800, AVDTP start request sometimes never sent if playback starts too
- // early.
- delay(2000L)
-
val source =
Source.newBuilder().setCookie(ByteString.copyFrom(device.getAddress(), "UTF-8"))
OpenSourceResponse.newBuilder().setSource(source).build()
@@ -147,10 +142,6 @@ class A2dp(val context: Context) : A2DPImplBase(), Closeable {
}
}
- // TODO: b/234891800, AVDTP start request sometimes never sent if playback starts too
- // early.
- delay(2000L)
-
val source =
Source.newBuilder().setCookie(ByteString.copyFrom(device.getAddress(), "UTF-8"))
WaitSourceResponse.newBuilder().setSource(source).build()
diff --git a/android/pandora/server/src/MediaPlayer.kt b/android/pandora/server/src/MediaPlayer.kt
index 81d6a8a78a..a2c981f881 100644
--- a/android/pandora/server/src/MediaPlayer.kt
+++ b/android/pandora/server/src/MediaPlayer.kt
@@ -55,6 +55,13 @@ class MediaPlayer(val context: Context) : MediaPlayerImplBase(), Closeable {
}
}
+ override fun playUpdated(request: Empty, responseObserver: StreamObserver<Empty>) {
+ grpcUnary<Empty>(scope, responseObserver) {
+ MediaPlayerBrowserService.instance.playUpdated()
+ Empty.getDefaultInstance()
+ }
+ }
+
override fun stop(request: Empty, responseObserver: StreamObserver<Empty>) {
grpcUnary<Empty>(scope, responseObserver) {
MediaPlayerBrowserService.instance.stop()
@@ -111,6 +118,13 @@ class MediaPlayer(val context: Context) : MediaPlayerImplBase(), Closeable {
}
}
+ override fun resetQueue(request: Empty, responseObserver: StreamObserver<Empty>) {
+ grpcUnary<Empty>(scope, responseObserver) {
+ MediaPlayerBrowserService.instance.resetQueue()
+ Empty.getDefaultInstance()
+ }
+ }
+
override fun getShuffleMode(
request: Empty,
responseObserver: StreamObserver<GetShuffleModeResponse>
diff --git a/android/pandora/server/src/MediaPlayerBrowserService.kt b/android/pandora/server/src/MediaPlayerBrowserService.kt
index b7a358f79c..eaab8a9bbe 100644
--- a/android/pandora/server/src/MediaPlayerBrowserService.kt
+++ b/android/pandora/server/src/MediaPlayerBrowserService.kt
@@ -17,6 +17,7 @@
package com.android.pandora
import android.content.Intent
+import android.graphics.Bitmap
import android.media.MediaPlayer
import android.os.Bundle
import android.support.v4.media.*
@@ -43,6 +44,7 @@ class MediaPlayerBrowserService : MediaBrowserServiceCompat() {
private var metadataItems = mutableMapOf<String, MediaMetadataCompat>()
private var queue = mutableListOf<MediaSessionCompat.QueueItem>()
private var currentTrack = -1
+ private val testIcon = Bitmap.createBitmap(16, 16, Bitmap.Config.ARGB_8888)
override fun onCreate() {
super.onCreate()
@@ -114,6 +116,12 @@ class MediaPlayerBrowserService : MediaBrowserServiceCompat() {
mediaSession.setMetadata(metadataItems.get("" + currentTrack))
}
+ fun playUpdated() {
+ currentTrack = NEW_QUEUE_ITEM_INDEX
+ setPlaybackState(PlaybackStateCompat.STATE_PLAYING)
+ mediaSession.setMetadata(metadataItems.get("" + currentTrack))
+ }
+
fun stop() {
setPlaybackState(PlaybackStateCompat.STATE_STOPPED)
mediaSession.setMetadata(null)
@@ -170,12 +178,23 @@ class MediaPlayerBrowserService : MediaBrowserServiceCompat() {
.putLong(MediaMetadataCompat.METADATA_KEY_NUM_TRACKS, NEW_QUEUE_ITEM_INDEX.toLong())
.build()
val mediaItem = MediaItem(metaData.description, MediaItem.FLAG_PLAYABLE)
+ metadataItems.put("" + NEW_QUEUE_ITEM_INDEX, metaData)
queue.add(
MediaSessionCompat.QueueItem(mediaItem.description, NEW_QUEUE_ITEM_INDEX.toLong())
)
mediaSession.setQueue(queue)
}
+ fun resetQueue() {
+ if (metadataItems.contains("" + NEW_QUEUE_ITEM_INDEX)) {
+ metadataItems.remove("" + NEW_QUEUE_ITEM_INDEX)
+ queue.removeLast()
+ mediaSession.setQueue(queue)
+ stop()
+ currentTrack = QUEUE_START_INDEX
+ }
+ }
+
fun getShuffleMode(): Int {
val controller = mediaSession.getController()
return controller.getShuffleMode()
@@ -261,6 +280,7 @@ class MediaPlayerBrowserService : MediaBrowserServiceCompat() {
)
.putLong(MediaMetadataCompat.METADATA_KEY_TRACK_NUMBER, item.toLong())
.putLong(MediaMetadataCompat.METADATA_KEY_NUM_TRACKS, QUEUE_SIZE.toLong())
+ .putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, testIcon)
.build()
val mediaItem = MediaItem(metaData.description, MediaItem.FLAG_PLAYABLE)
mediaItems.add(mediaItem)
diff --git a/android/pandora/test/a2dp/signaling_channel.py b/android/pandora/test/a2dp/signaling_channel.py
new file mode 100644
index 0000000000..0fbda414c8
--- /dev/null
+++ b/android/pandora/test/a2dp/signaling_channel.py
@@ -0,0 +1,200 @@
+# Copyright 2024 Google LLC
+#
+# 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
+#
+# https://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.
+
+# -----------------------------------------------------------------------------
+# Imports
+# -----------------------------------------------------------------------------
+from __future__ import annotations
+
+from bumble.device import Connection
+try:
+ from packets import avdtp as avdt_packet_module
+ from packets.avdtp import *
+except ImportError:
+ from .packets import avdtp as avdt_packet_module
+ from .packets.avdtp import *
+from pyee import EventEmitter
+from typing import Union
+
+import asyncio
+import bumble.avdtp as avdtp
+import bumble.l2cap as l2cap
+import logging
+
+# -----------------------------------------------------------------------------
+# Logging
+# -----------------------------------------------------------------------------
+logger = logging.getLogger(__name__)
+
+avdt_packet_module.print = lambda *args, **kwargs: logger.debug(" ".join(map(str, args)))
+
+
+class Any:
+ """Helper class that will match all other values.
+ Use an element of this class in expected packets to match any value
+ returned by the AVDTP signaling."""
+
+ def __eq__(self, other) -> bool:
+ return True
+
+ def __format__(self, format_spec: str) -> str:
+ return "_"
+
+ def __len__(self) -> int:
+ return 1
+
+ def show(self, prefix: str = "") -> str:
+ return prefix + "_"
+
+
+class SignalingChannel(EventEmitter):
+ connection: Connection
+ signaling_channel: Optional[l2cap.ClassicChannel] = None
+ transport_channel: Optional[l2cap.ClassicChannel] = None
+ avdtp_server: Optional[l2cap.ClassicChannelServer] = None
+ role: Optional[str] = None
+
+ def __init__(self, connection: Connection):
+ super().__init__()
+ self.connection = connection
+ self.signaling_queue = asyncio.Queue()
+ self.transport_queue = asyncio.Queue()
+
+ @classmethod
+ async def initiate(cls, connection: Connection) -> SignalingChannel:
+ channel = cls(connection)
+ await channel._initiate_signaling_channel()
+ return channel
+
+ @classmethod
+ def accept(cls, connection: Connection) -> SignalingChannel:
+ channel = cls(connection)
+ channel._accept_signaling_channel()
+ return channel
+
+ async def disconnect(self):
+ if not self.signaling_channel:
+ raise ValueError("No connected signaling channel")
+ await self.signaling_channel.disconnect()
+ self.signaling_channel = None
+
+ async def initiate_transport_channel(self):
+ if self.transport_channel:
+ raise ValueError("RTP L2CAP channel already exists")
+ self.transport_channel = await self.connection.create_l2cap_channel(
+ l2cap.ClassicChannelSpec(psm=avdtp.AVDTP_PSM))
+
+ async def disconnect_transport_channel(self):
+ if not self.transport_channel:
+ raise ValueError("No connected RTP channel")
+ await self.transport_channel.disconnect()
+ self.transport_channel = None
+
+ async def expect_signal(self, expected_sig: Union[SignalingPacket, type], timeout: float = 3) -> SignalingPacket:
+ packet = await asyncio.wait_for(self.signaling_queue.get(), timeout=timeout)
+ sig = SignalingPacket.parse_all(packet)
+
+ if isinstance(expected_sig, type) and not isinstance(sig, expected_sig):
+ logger.error("Received unexpected signal")
+ logger.error(f"Expected signal: {expected_sig.__class__.__name__}")
+ logger.error("Received signal:")
+ sig.show()
+ raise ValueError(f"Received unexpected signal")
+
+ if isinstance(expected_sig, SignalingPacket) and sig != expected_sig:
+ logger.error("Received unexpected signal")
+ logger.error("Expected signal:")
+ expected_sig.show()
+ logger.error("Received signal:")
+ sig.show()
+ raise ValueError(f"Received unexpected signal")
+
+ logger.debug(f"<<< {self.connection.self_address} {self.role} received signal: <<<")
+ sig.show()
+ return sig
+
+ async def expect_media(self, timeout: float = 3.0) -> bytes:
+ packet = await asyncio.wait_for(self.transport_queue.get(), timeout=timeout)
+ logger.debug(f"<<< {self.connection.self_address} {self.role} received media <<<")
+ logger.debug(f"RTP Packet: {packet.hex()}")
+ return packet
+
+ def send_signal(self, packet: SignalingPacket):
+ logger.debug(f">>> {self.connection.self_address} {self.role} sending signal: >>>")
+ packet.show()
+ self.signaling_channel.send_pdu(packet.serialize())
+
+ def send_media(self, packet: bytes):
+ logger.debug(f">>> {self.connection.self_address} {self.role} sending media >>>")
+ self.transport_channel.send_pdu(packet)
+
+ async def _initiate_signaling_channel(self):
+ if self.signaling_channel:
+ raise ValueError("Signaling L2CAP channel already exists")
+ self.role = "initiator"
+ self.signaling_channel = await self.connection.create_l2cap_channel(spec=l2cap.ClassicChannelSpec(
+ psm=avdtp.AVDTP_PSM))
+ # Register to receive PDUs from the channel
+ self.signaling_channel.sink = self._on_pdu
+
+ def _accept_signaling_channel(self):
+ if self.avdtp_server:
+ raise ValueError("L2CAP server already exists")
+ self.role = "acceptor"
+ avdtp_server = self.connection.device.l2cap_channel_manager.servers.get(avdtp.AVDTP_PSM)
+ if not avdtp_server:
+ self.avdtp_server = self.connection.device.create_l2cap_server(spec=l2cap.ClassicChannelSpec(
+ psm=avdtp.AVDTP_PSM))
+ else:
+ self.avdtp_server = avdtp_server
+ self.avdtp_server.on('connection', self._on_l2cap_connection)
+
+ def _on_l2cap_connection(self, channel: l2cap.ClassicChannel):
+ logger.info(f"Incoming L2CAP channel: {channel}")
+
+ if not self.signaling_channel:
+
+ def _on_channel_open():
+ logger.info(f"Signaling opened on channel {self.signaling_channel}")
+ # Register to receive PDUs from the channel
+ self.signaling_channel.sink = self._on_pdu
+ self.emit('connection')
+
+ def _on_channel_close():
+ logger.info("Signaling channel closed")
+ self.signaling_channel = None
+
+ self.signaling_channel = channel
+ self.signaling_channel.on('open', _on_channel_open)
+ self.signaling_channel.on('close', _on_channel_close)
+ elif not self.transport_channel:
+
+ def _on_channel_open():
+ logger.info(f"RTP opened on channel {self.transport_channel}")
+ # Register to receive PDUs from the channel
+ self.transport_channel.sink = self._on_avdtp_packet
+
+ def _on_channel_close():
+ logger.info('RTP channel closed')
+ self.transport_channel = None
+
+ self.transport_channel = channel
+ self.transport_channel.on('open', _on_channel_open)
+ self.transport_channel.on('close', _on_channel_close)
+
+ def _on_pdu(self, pdu: bytes):
+ self.signaling_queue.put_nowait(pdu)
+
+ def _on_avdtp_packet(self, packet):
+ self.transport_queue.put_nowait(packet)
diff --git a/android/pandora/test/a2dp/signaling_channel_test.py b/android/pandora/test/a2dp/signaling_channel_test.py
new file mode 100644
index 0000000000..daf5c8f40b
--- /dev/null
+++ b/android/pandora/test/a2dp/signaling_channel_test.py
@@ -0,0 +1,378 @@
+# Copyright 2024 Google LLC
+#
+# 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
+#
+# https://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.
+"""
+Bumble tests for SignalingChannel implementation.
+
+Create venv and upgrade pip:
+
+ python -m venv .venv
+ source .venv/bin/activate
+ python -m pip install --upgrade pip
+
+Install the required dependencies using pip:
+
+ pip install pyee pytest bumble
+
+Run the tests:
+ python /path/signaling_channel_test.py
+"""
+
+# -----------------------------------------------------------------------------
+# Imports
+# -----------------------------------------------------------------------------
+import asyncio
+import logging
+import os
+import pytest
+import bumble.avdtp as avdtp
+from bumble.a2dp import (A2DP_SBC_CODEC_TYPE, SbcMediaCodecInformation)
+from bumble.controller import Controller
+from bumble.core import BT_BR_EDR_TRANSPORT
+from bumble.device import Device
+from bumble.host import Host
+from bumble.link import LocalLink
+from bumble.transport import AsyncPipeSink
+from packets.avdtp import *
+from signaling_channel import SignalingChannel, Any
+
+# -----------------------------------------------------------------------------
+# Logging
+# -----------------------------------------------------------------------------
+logger = logging.getLogger(__name__)
+
+
+# -----------------------------------------------------------------------------
+class TwoDevices:
+
+ def __init__(self):
+ self.connections = [None, None]
+
+ addresses = ['F0:F1:F2:F3:F4:F5', 'F5:F4:F3:F2:F1:F0']
+ self.link = LocalLink()
+ self.controllers = [
+ Controller('C1', link=self.link, public_address=addresses[0]),
+ Controller('C2', link=self.link, public_address=addresses[1]),
+ ]
+ self.devices = [
+ Device(
+ address=addresses[0],
+ host=Host(self.controllers[0], AsyncPipeSink(self.controllers[0])),
+ ),
+ Device(
+ address=addresses[1],
+ host=Host(self.controllers[1], AsyncPipeSink(self.controllers[1])),
+ ),
+ ]
+
+ self.paired = [None, None]
+
+ def on_connection(self, which, connection):
+ self.connections[which] = connection
+
+ def on_paired(self, which, keys):
+ self.paired[which] = keys
+
+
+# -----------------------------------------------------------------------------
+@pytest.mark.asyncio
+async def test_self_connection():
+ # Create two devices, each with a controller, attached to the same link
+ two_devices = TwoDevices()
+
+ # Attach listeners
+ two_devices.devices[0].on('connection', lambda connection: two_devices.on_connection(0, connection))
+ two_devices.devices[1].on('connection', lambda connection: two_devices.on_connection(1, connection))
+
+ # Enable Classic connections
+ two_devices.devices[0].classic_enabled = True
+ two_devices.devices[1].classic_enabled = True
+
+ # Start
+ await two_devices.devices[0].power_on()
+ await two_devices.devices[1].power_on()
+
+ # Connect the two devices
+ await asyncio.gather(
+ two_devices.devices[0].connect(two_devices.devices[1].public_address, transport=BT_BR_EDR_TRANSPORT),
+ two_devices.devices[1].accept(two_devices.devices[0].public_address),
+ )
+
+ # Check the post conditions
+ assert two_devices.connections[0] is not None
+ assert two_devices.connections[1] is not None
+
+
+# -----------------------------------------------------------------------------
+def sink_codec_capabilities():
+ return avdtp.MediaCodecCapabilities(
+ media_type=avdtp.AVDTP_AUDIO_MEDIA_TYPE,
+ media_codec_type=A2DP_SBC_CODEC_TYPE,
+ media_codec_information=SbcMediaCodecInformation.from_bytes(bytes([255, 255, 2, 53])),
+ )
+
+
+# -----------------------------------------------------------------------------
+@pytest.mark.asyncio
+async def test_signaling_channel_as_source():
+ any = Any()
+
+ two_devices = TwoDevices()
+ # Enable Classic connections
+ two_devices.devices[0].classic_enabled = True
+ two_devices.devices[1].classic_enabled = True
+ await two_devices.devices[0].power_on()
+ await two_devices.devices[1].power_on()
+
+ def on_rtp_packet(packet):
+ rtp_packets.append(packet)
+ if len(rtp_packets) == rtp_packets_expected:
+ rtp_packets_fully_received.set_result(None)
+
+ device_1_avdt_sink = None
+ avdtp_future = asyncio.get_running_loop().create_future()
+
+ def on_avdtp_connection(server):
+ logger.info("AVDTP Opened")
+ nonlocal device_1_avdt_sink
+ device_1_avdt_sink = server.add_sink(sink_codec_capabilities())
+ device_1_avdt_sink.on('rtp_packet', on_rtp_packet)
+ nonlocal avdtp_future
+ avdtp_future.set_result(None)
+
+ # Create a listener to wait for AVDTP connections
+ listener = avdtp.Listener.for_device(two_devices.devices[1])
+ listener.on('connection', on_avdtp_connection)
+
+ async def make_connection():
+ connections = await asyncio.gather(
+ two_devices.devices[0].connect(two_devices.devices[1].public_address, BT_BR_EDR_TRANSPORT),
+ two_devices.devices[1].accept(two_devices.devices[0].public_address),
+ )
+ return connections[0]
+
+ connection = await make_connection()
+
+ channel_int = await SignalingChannel.initiate(connection)
+
+ channel_int.send_signal(DiscoverCommand())
+
+ result = await channel_int.expect_signal(
+ DiscoverResponse(transaction_label=any, seid_information=[SeidInformation(acp_seid=1, tsep=Tsep.SINK)]))
+
+ acp_seid = result.seid_information[0].acp_seid
+
+ channel_int.send_signal(GetAllCapabilitiesCommand(acp_seid=acp_seid))
+
+ result = await channel_int.expect_signal(
+ GetAllCapabilitiesResponse(transaction_label=any,
+ service_capabilities=[
+ MediaTransportCapability(),
+ MediaCodecCapability(service_category=ServiceCategory.MEDIA_CODEC,
+ media_codec_specific_information_elements=[255, 255, 2, 53])
+ ]))
+
+ channel_int.send_signal(
+ SetConfigurationCommand(acp_seid=acp_seid, service_capabilities=[result.service_capabilities[0]]))
+
+ await channel_int.expect_signal(SetConfigurationResponse(transaction_label=any))
+
+ channel_int.send_signal(OpenCommand(acp_seid=acp_seid))
+
+ await channel_int.expect_signal(OpenResponse(transaction_label=any))
+
+ await asyncio.wait_for(avdtp_future, timeout=10.0)
+
+ assert device_1_avdt_sink.in_use == 1
+ assert device_1_avdt_sink.stream is not None
+ assert device_1_avdt_sink.stream.state == avdtp.AVDTP_OPEN_STATE
+
+ async def generate_packets(packet_count):
+ sequence_number = 0
+ timestamp = 0
+ for i in range(packet_count):
+ payload = bytes([sequence_number % 256])
+ packet = avdtp.MediaPacket(2, 0, 0, 0, sequence_number, timestamp, 0, [], 96, payload)
+ packet.timestamp_seconds = timestamp / 44100
+ timestamp += 10
+ sequence_number += 1
+ yield packet
+
+ # # Send packets using a pump object
+ rtp_packets_fully_received = asyncio.get_running_loop().create_future()
+ rtp_packets_expected = 3
+ rtp_packets = []
+ pump = avdtp.MediaPacketPump(generate_packets(3))
+
+ await channel_int.initiate_transport_channel()
+
+ channel_int.send_signal(StartCommand(acp_seid=acp_seid))
+
+ await channel_int.expect_signal(StartResponse(transaction_label=any))
+
+ assert device_1_avdt_sink.in_use == 1
+ assert device_1_avdt_sink.stream is not None
+ assert device_1_avdt_sink.stream.state == avdtp.AVDTP_STREAMING_STATE
+
+ await pump.start(channel_int.transport_channel)
+
+ await rtp_packets_fully_received
+
+ await pump.stop()
+
+ channel_int.send_signal(CloseCommand(acp_seid=acp_seid))
+
+ await channel_int.expect_signal(CloseResponse(transaction_label=any))
+
+ await channel_int.disconnect_transport_channel()
+
+ assert device_1_avdt_sink.in_use == 0
+ assert device_1_avdt_sink.stream.state == avdtp.AVDTP_IDLE_STATE
+
+
+# -----------------------------------------------------------------------------
+@pytest.mark.asyncio
+async def test_signaling_channel_as_sink():
+ any = Any()
+
+ two_devices = TwoDevices()
+ # Enable Classic connections
+ two_devices.devices[0].classic_enabled = True
+ two_devices.devices[1].classic_enabled = True
+ await two_devices.devices[0].power_on()
+ await two_devices.devices[1].power_on()
+
+ dev_0_dev_1_conn, dev_1_dev_0_conn = await asyncio.gather(
+ two_devices.devices[0].connect(two_devices.devices[1].public_address, BT_BR_EDR_TRANSPORT),
+ two_devices.devices[1].accept(two_devices.devices[0].public_address),
+ )
+
+ channel_acp = SignalingChannel.accept(dev_1_dev_0_conn)
+
+ avdtp_future = asyncio.get_running_loop().create_future()
+
+ def on_avdtp_connection():
+ logger.info(f" AVDTP Opened")
+ nonlocal avdtp_future
+ avdtp_future.set_result(None)
+
+ channel_acp.on('connection', on_avdtp_connection)
+
+ channel_int = await SignalingChannel.initiate(dev_0_dev_1_conn)
+
+ channel_int.send_signal(DiscoverCommand())
+
+ cmd = await channel_acp.expect_signal(DiscoverCommand(transaction_label=any))
+
+ seid_information = [SeidInformation(tsep=Tsep.SINK, media_type=avdtp.AVDTP_AUDIO_MEDIA_TYPE)]
+
+ channel_acp.send_signal(DiscoverResponse(transaction_label=cmd.transaction_label,
+ seid_information=seid_information))
+
+ result = await channel_int.expect_signal(
+ DiscoverResponse(
+ seid_information=[SeidInformation(acp_seid=0x0, tsep=Tsep.SINK, media_type=avdtp.AVDTP_AUDIO_MEDIA_TYPE)]))
+
+ int_to_acp_seid = result.seid_information[0].acp_seid
+
+ channel_int.send_signal(GetAllCapabilitiesCommand(acp_seid=int_to_acp_seid))
+
+ cmd = await channel_acp.expect_signal(GetAllCapabilitiesCommand(acp_seid=int_to_acp_seid, transaction_label=any))
+
+ acceptor_service_capabilities = [
+ MediaTransportCapability(),
+ MediaCodecCapability(service_category=ServiceCategory.MEDIA_CODEC,
+ media_codec_specific_information_elements=[255, 255, 2, 53])
+ ]
+
+ channel_acp.send_signal(
+ GetAllCapabilitiesResponse(transaction_label=cmd.transaction_label,
+ service_capabilities=acceptor_service_capabilities))
+
+ result = await channel_int.expect_signal(
+ GetAllCapabilitiesResponse(transaction_label=any,
+ service_capabilities=[
+ MediaTransportCapability(),
+ MediaCodecCapability(service_category=ServiceCategory.MEDIA_CODEC,
+ media_codec_specific_information_elements=[255, 255, 2, 53])
+ ]))
+
+ channel_int.send_signal(
+ SetConfigurationCommand(acp_seid=int_to_acp_seid, service_capabilities=[result.service_capabilities[0]]))
+
+ cmd = await channel_acp.expect_signal(
+ SetConfigurationCommand(transaction_label=any,
+ acp_seid=int_to_acp_seid,
+ service_capabilities=[result.service_capabilities[0]]))
+
+ channel_acp.send_signal(SetConfigurationResponse(transaction_label=cmd.transaction_label))
+
+ await channel_int.expect_signal(SetConfigurationResponse(transaction_label=any))
+
+ channel_int.send_signal(OpenCommand(acp_seid=int_to_acp_seid))
+
+ cmd = await channel_acp.expect_signal(OpenCommand(transaction_label=any, acp_seid=int_to_acp_seid))
+
+ channel_acp.send_signal(OpenResponse(transaction_label=cmd.transaction_label))
+
+ await channel_int.expect_signal(OpenResponse(transaction_label=any))
+
+ await asyncio.wait_for(avdtp_future, timeout=10.0)
+
+ rtp_packets_expected = 3
+ received_rtp_packets = []
+ source_packets = [
+ avdtp.MediaPacket(2, 0, 0, 0, i, i * 10, 0, [], 96, bytes([i])) for i in range(rtp_packets_expected)
+ ]
+
+ await channel_int.initiate_transport_channel()
+
+ channel_int.send_signal(StartCommand(acp_seid=int_to_acp_seid))
+
+ cmd = await channel_acp.expect_signal(StartCommand(transaction_label=any, acp_seid=int_to_acp_seid))
+
+ channel_acp.send_signal(StartResponse(transaction_label=cmd.transaction_label))
+
+ await channel_int.expect_signal(StartResponse(transaction_label=any))
+
+ channel_int.send_media(bytes(source_packets[0]))
+ channel_int.send_media(bytes(source_packets[1]))
+ channel_int.send_media(bytes(source_packets[2]))
+
+ for _ in range(rtp_packets_expected):
+ received_rtp_packets.append(await channel_acp.expect_media())
+ assert channel_acp.transport_queue.empty()
+
+ channel_int.send_signal(CloseCommand(acp_seid=int_to_acp_seid))
+
+ cmd = await channel_acp.expect_signal(CloseCommand(transaction_label=any, acp_seid=int_to_acp_seid))
+
+ channel_acp.send_signal(CloseResponse(transaction_label=cmd.transaction_label))
+
+ await channel_int.expect_signal(CloseResponse(transaction_label=any))
+
+ await channel_int.disconnect_transport_channel()
+ await channel_int.disconnect()
+
+
+# -----------------------------------------------------------------------------
+async def run_test_self():
+ await test_self_connection()
+ await test_signaling_channel_as_source()
+ await test_signaling_channel_as_sink()
+
+
+# -----------------------------------------------------------------------------
+if __name__ == '__main__':
+ logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'DEBUG').upper())
+ asyncio.run(run_test_self())
diff --git a/android/pandora/test/a2dp_test.py b/android/pandora/test/a2dp_test.py
index 8404c28200..a1efc015c3 100644
--- a/android/pandora/test/a2dp_test.py
+++ b/android/pandora/test/a2dp_test.py
@@ -20,7 +20,8 @@ import itertools
import logging
import numpy as np
-from a2dp.packets import avdtp
+from a2dp.packets.avdtp import *
+from a2dp.signaling_channel import Any, SignalingChannel
from avatar import BumblePandoraDevice, PandoraDevice, PandoraDevices, pandora_snippet, enableFlag
from bumble.a2dp import (
A2DP_MPEG_2_4_AAC_CODEC_TYPE,
@@ -40,8 +41,6 @@ from bumble.avctp import AVCTP_PSM
from bumble.avdtp import (
AVDTP_AUDIO_MEDIA_TYPE,
AVDTP_BAD_STATE_ERROR,
- AVDTP_CLOSING_STATE,
- AVDTP_IDLE_STATE,
AVDTP_OPEN_STATE,
AVDTP_PSM,
AVDTP_STREAMING_STATE,
@@ -292,6 +291,94 @@ class A2dpTest(base_test.BaseTestClass): # type: ignore[misc]
assert_equal(self.ref1.a2dp_sink.stream.state, AVDTP_OPEN_STATE)
@avatar.asynchronous
+ async def test_signaling_channel_and_streaming(self) -> None:
+ """Basic A2DP connection and streaming with SignalingChannel used by acceptor device test.
+
+ 1. Pair and Connect RD1
+ 2. Setup the acceptor expectations on signalling channel
+ 2. Start streaming
+ 4. Stop streaming
+ """
+ any = Any()
+
+ # Connect and pair RD1.
+ dut_ref1, ref1_dut = await asyncio.gather(
+ initiate_pairing(self.dut, self.ref1.address),
+ accept_pairing(self.ref1, self.dut.address),
+ )
+
+ connection = pandora_snippet.get_raw_connection(device=self.ref1, connection=ref1_dut)
+ channel = SignalingChannel.accept(connection)
+
+ async def acceptor_avdt_open(channel: SignalingChannel):
+
+ avdtp_future = asyncio.get_running_loop().create_future()
+
+ def on_avdtp_connection():
+ logger.info(f"AVDTP Opened")
+ nonlocal avdtp_future
+ avdtp_future.set_result(None)
+
+ channel.on('connection', on_avdtp_connection)
+
+ cmd = await channel.expect_signal(DiscoverCommand(transaction_label=any))
+
+ seid_information = [SeidInformation(acp_seid=0x01, tsep=Tsep.SINK, media_type=AVDTP_AUDIO_MEDIA_TYPE)]
+
+ channel.send_signal(
+ DiscoverResponse(transaction_label=cmd.transaction_label, seid_information=seid_information))
+
+ cmd = await channel.expect_signal(GetAllCapabilitiesCommand(acp_seid=any, transaction_label=any))
+
+ acceptor_service_capabilities = [
+ MediaTransportCapability(),
+ MediaCodecCapability(service_category=ServiceCategory.MEDIA_CODEC,
+ media_codec_specific_information_elements=[255, 255, 2, 53])
+ ]
+
+ channel.send_signal(
+ GetAllCapabilitiesResponse(transaction_label=cmd.transaction_label,
+ service_capabilities=acceptor_service_capabilities))
+
+ cmd = await channel.expect_signal(
+ SetConfigurationCommand(transaction_label=any,
+ acp_seid=any,
+ int_seid=any,
+ service_capabilities=[MediaTransportCapability(), any]))
+
+ channel.send_signal(SetConfigurationResponse(transaction_label=cmd.transaction_label))
+
+ cmd = await channel.expect_signal(OpenCommand(transaction_label=any, acp_seid=any))
+
+ channel.send_signal(OpenResponse(transaction_label=cmd.transaction_label))
+
+ await asyncio.wait_for(avdtp_future, timeout=10.0)
+
+ # Connect AVDTP to RD1.
+ _, dut_ref1_source = await asyncio.gather(acceptor_avdt_open(channel), open_source(self.dut, dut_ref1))
+
+ async def acceptor_avdt_start(channel: SignalingChannel):
+ cmd = await channel.expect_signal(StartCommand(transaction_label=any, acp_seid=any))
+
+ channel.send_signal(StartResponse(transaction_label=cmd.transaction_label))
+
+ # Start streaming to RD1.
+ await asyncio.gather(self.dut.a2dp.Start(source=dut_ref1_source), acceptor_avdt_start(channel))
+
+ audio = AudioSignal(self.dut.a2dp, dut_ref1_source, 0.8, 44100)
+
+ # Verify that at least one audio frame is received on the transport channel.
+ await asyncio.wait_for(channel.expect_media(), 5.0)
+
+ async def acceptor_avdt_suspend(channel: SignalingChannel):
+ cmd = await channel.expect_signal(SuspendCommand(transaction_label=any, acp_seid=any))
+
+ channel.send_signal(SuspendResponse(transaction_label=cmd.transaction_label))
+
+ # Stop streaming to RD1.
+ await asyncio.gather(self.dut.a2dp.Suspend(source=dut_ref1_source), acceptor_avdt_suspend(channel))
+
+ @avatar.asynchronous
async def test_avdtp_autoconnect_when_only_avctp_connected(self) -> None:
"""Test AVDTP automatically connects if peer device connects only AVCTP.
diff --git a/flags/Android.bp b/flags/Android.bp
index 8a1dc66552..80a57891a5 100644
--- a/flags/Android.bp
+++ b/flags/Android.bp
@@ -31,6 +31,7 @@ aconfig_declarations {
"hid.aconfig",
"l2cap.aconfig",
"le_advertising.aconfig",
+ "le_scanning.aconfig",
"leaudio.aconfig",
"mapclient.aconfig",
"mcp.aconfig",
diff --git a/flags/BUILD.gn b/flags/BUILD.gn
index 9f96d85933..da5aa8621b 100644
--- a/flags/BUILD.gn
+++ b/flags/BUILD.gn
@@ -24,6 +24,7 @@ aconfig("bluetooth_flags_c_lib") {
"hid.aconfig",
"l2cap.aconfig",
"le_advertising.aconfig",
+ "le_scanning.aconfig",
"leaudio.aconfig",
"mapclient.aconfig",
"mcp.aconfig",
diff --git a/flags/active_device_manager.aconfig b/flags/active_device_manager.aconfig
index 68ece5e73b..b2438be452 100644
--- a/flags/active_device_manager.aconfig
+++ b/flags/active_device_manager.aconfig
@@ -19,4 +19,14 @@ flag {
metadata {
purpose: PURPOSE_BUGFIX
}
+}
+
+flag {
+ name: "adm_remove_handling_wired"
+ namespace: "bluetooth"
+ description: "ActiveDeviceManager doesn't need to handle adding and removing wired devices."
+ bug: "390372480"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
} \ No newline at end of file
diff --git a/flags/bta_dm.aconfig b/flags/bta_dm.aconfig
index d91f3168b7..287a0f0187 100644
--- a/flags/bta_dm.aconfig
+++ b/flags/bta_dm.aconfig
@@ -9,13 +9,6 @@ flag {
}
flag {
- name: "bta_dm_discover_both"
- namespace: "bluetooth"
- description: "perform both LE and Classic service discovery simulteanously on capable devices"
- bug: "339217881"
-}
-
-flag {
name: "cancel_open_discovery_client"
namespace: "bluetooth"
description: "Cancel connection from discovery client correctly"
diff --git a/flags/framework.aconfig b/flags/framework.aconfig
index 757148b7df..6464f91927 100644
--- a/flags/framework.aconfig
+++ b/flags/framework.aconfig
@@ -86,3 +86,13 @@ flag {
description: "Make BluetoothDevice.ACTION_KEY_MISSING into public API"
bug: "379729762"
}
+
+flag {
+ name: "set_component_available_fix"
+ namespace: "bluetooth"
+ description: "Ensure the state in PackageManager has DISABLED to ENABLED to trigger PACKAGE_CHANGED"
+ bug: "391084450"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
diff --git a/flags/gap.aconfig b/flags/gap.aconfig
index 9b139eead2..5da5144aa1 100644
--- a/flags/gap.aconfig
+++ b/flags/gap.aconfig
@@ -271,3 +271,23 @@ flag {
purpose: PURPOSE_BUGFIX
}
}
+
+flag {
+ name: "fix_bluetooth_gatt_getting_duplicate_services"
+ namespace: "bluetooth"
+ description: "Fixes BluetoothGatt getting duplicate GATT services"
+ bug: "391773937"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
+ name: "batch_scan_optimization"
+ namespace: "bluetooth"
+ description: "Optimized batch scan for less wakeups"
+ bug: "392132489"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
diff --git a/flags/gatt.aconfig b/flags/gatt.aconfig
index 1b9bfb516e..e054fb3f7a 100644
--- a/flags/gatt.aconfig
+++ b/flags/gatt.aconfig
@@ -18,3 +18,14 @@ flag {
bug: "384794418"
is_exported: true
}
+
+flag {
+ name: "advertise_thread"
+ namespace: "bluetooth"
+ description: "Run all advertise functions on a single thread"
+ bug: "391508617"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
diff --git a/flags/hfp.aconfig b/flags/hfp.aconfig
index 7b81b483b4..026c3b22b3 100644
--- a/flags/hfp.aconfig
+++ b/flags/hfp.aconfig
@@ -100,16 +100,6 @@ flag {
}
flag {
- name: "hfp_allow_volume_change_without_sco"
- namespace: "bluetooth"
- description: "Allow Audio Fwk to change SCO volume when HFP profile is connected and SCO not connected"
- bug: "362313390"
- metadata {
- purpose: PURPOSE_BUGFIX
- }
-}
-
-flag {
name: "choose_wrong_hfp_codec_in_specific_config"
namespace: "bluetooth"
description: "Flag to fix codec selection in nego when the peer device only support NB and SWB."
diff --git a/flags/le_scanning.aconfig b/flags/le_scanning.aconfig
new file mode 100644
index 0000000000..0b4985e45e
--- /dev/null
+++ b/flags/le_scanning.aconfig
@@ -0,0 +1,12 @@
+package: "com.android.bluetooth.flags"
+container: "com.android.bt"
+
+flag {
+ name: "scan_results_in_main_thread"
+ namespace: "bluetooth"
+ description: "Use main thread for handling scan results"
+ bug: "392693506"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
diff --git a/flags/leaudio.aconfig b/flags/leaudio.aconfig
index e7b0b2abcf..03cc9a4c02 100644
--- a/flags/leaudio.aconfig
+++ b/flags/leaudio.aconfig
@@ -451,3 +451,13 @@ flag {
purpose: PURPOSE_BUGFIX
}
}
+
+flag {
+ name: "leaudio_disable_broadcast_for_hap_device"
+ namespace: "bluetooth"
+ description: "Disable broadcast feature for HAP device"
+ bug: "391702876"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
diff --git a/flags/security.aconfig b/flags/security.aconfig
index ddfd78611c..006c51f307 100644
--- a/flags/security.aconfig
+++ b/flags/security.aconfig
@@ -9,6 +9,16 @@ flag {
}
flag {
+ name: "key_missing_ble_peripheral"
+ namespace: "bluetooth"
+ description: "Key missing broadcast for LE devices in peripheral role"
+ bug: "392895615"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
name: "key_missing_as_ordered_broadcast"
namespace: "bluetooth"
description: "Key missing broadcast would be send as ordered broadcast"
diff --git a/framework/api/system-current.txt b/framework/api/system-current.txt
index 5a506f21fb..60657262e3 100644
--- a/framework/api/system-current.txt
+++ b/framework/api/system-current.txt
@@ -53,7 +53,7 @@ package android.bluetooth {
method @RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_CONNECT, android.Manifest.permission.BLUETOOTH_PRIVILEGED}) public void disableOptionalCodecs(@NonNull android.bluetooth.BluetoothDevice);
method @RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_CONNECT, android.Manifest.permission.BLUETOOTH_PRIVILEGED}) public void enableOptionalCodecs(@NonNull android.bluetooth.BluetoothDevice);
method @Nullable @RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_CONNECT, android.Manifest.permission.BLUETOOTH_PRIVILEGED}) public android.bluetooth.BufferConstraints getBufferConstraints();
- method @Nullable @RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_CONNECT, android.Manifest.permission.BLUETOOTH_PRIVILEGED}) public android.bluetooth.BluetoothCodecStatus getCodecStatus(@NonNull android.bluetooth.BluetoothDevice);
+ method @Nullable @RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_CONNECT, android.Manifest.permission.BLUETOOTH_PRIVILEGED}, conditional=true) public android.bluetooth.BluetoothCodecStatus getCodecStatus(@NonNull android.bluetooth.BluetoothDevice);
method @RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_CONNECT, android.Manifest.permission.BLUETOOTH_PRIVILEGED}) public int getConnectionPolicy(@NonNull android.bluetooth.BluetoothDevice);
method @RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_CONNECT, android.Manifest.permission.BLUETOOTH_PRIVILEGED}) public int getDynamicBufferSupport();
method @RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_CONNECT, android.Manifest.permission.BLUETOOTH_PRIVILEGED}) public int isOptionalCodecsEnabled(@NonNull android.bluetooth.BluetoothDevice);
diff --git a/framework/java/android/bluetooth/BluetoothA2dp.java b/framework/java/android/bluetooth/BluetoothA2dp.java
index 5bcd0789ab..54f2c702aa 100644
--- a/framework/java/android/bluetooth/BluetoothA2dp.java
+++ b/framework/java/android/bluetooth/BluetoothA2dp.java
@@ -724,20 +724,23 @@ public final class BluetoothA2dp implements BluetoothProfile {
/**
* Gets the current codec status (configuration and capability).
*
+ * <p>This method requires the calling app to have the {@link
+ * android.Manifest.permission#BLUETOOTH_CONNECT} permission. Additionally, an app must either
+ * have the {@link android.Manifest.permission#BLUETOOTH_PRIVILEGED} or be associated with the
+ * Companion Device manager (see {@link android.companion.CompanionDeviceManager#associate(
+ * AssociationRequest, android.companion.CompanionDeviceManager.Callback, Handler)})
+ *
* @param device the remote Bluetooth device.
* @return the current codec status
* @hide
*/
@SystemApi
- @Nullable
@RequiresLegacyBluetoothPermission
@RequiresBluetoothConnectPermission
@RequiresPermission(
- allOf = {
- BLUETOOTH_CONNECT,
- BLUETOOTH_PRIVILEGED,
- })
- public BluetoothCodecStatus getCodecStatus(@NonNull BluetoothDevice device) {
+ allOf = {BLUETOOTH_CONNECT, BLUETOOTH_PRIVILEGED},
+ conditional = true)
+ public @Nullable BluetoothCodecStatus getCodecStatus(@NonNull BluetoothDevice device) {
if (DBG) Log.d(TAG, "getCodecStatus(" + device + ")");
verifyDeviceNotNull(device, "getCodecStatus");
final IBluetoothA2dp service = getService();
diff --git a/framework/java/android/bluetooth/BluetoothGatt.java b/framework/java/android/bluetooth/BluetoothGatt.java
index e67d823c7e..a69c8ba792 100644
--- a/framework/java/android/bluetooth/BluetoothGatt.java
+++ b/framework/java/android/bluetooth/BluetoothGatt.java
@@ -37,6 +37,8 @@ import android.os.ParcelUuid;
import android.os.RemoteException;
import android.util.Log;
+import com.android.bluetooth.flags.Flags;
+
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
@@ -243,6 +245,9 @@ public final class BluetoothGatt implements BluetoothProfile {
+ " unregistering");
}
unregisterApp();
+ if (Flags.unregisterGattClientDisconnected()) {
+ mCallback = null;
+ }
return;
}
if (VDBG) {
@@ -274,7 +279,7 @@ public final class BluetoothGatt implements BluetoothProfile {
try {
// autoConnect is inverse of "isDirect"
mService.clientConnect(
- mClientIf,
+ clientIf,
mDevice.getAddress(),
mDevice.getAddressType(),
!mAutoConnect,
@@ -361,6 +366,8 @@ public final class BluetoothGatt implements BluetoothProfile {
* @hide
*/
@Override
+ @RequiresBluetoothConnectPermission
+ @RequiresPermission(BLUETOOTH_CONNECT)
public void onClientConnectionState(
int status, int clientIf, boolean connected, String address) {
if (DBG) {
@@ -381,6 +388,10 @@ public final class BluetoothGatt implements BluetoothProfile {
? BluetoothProfile.STATE_CONNECTED
: BluetoothProfile.STATE_DISCONNECTED;
+ if (Flags.unregisterGattClientDisconnected() && !connected && !mAutoConnect) {
+ unregisterApp();
+ }
+
runOrQueueCallback(
new Runnable() {
@Override
@@ -433,6 +444,10 @@ public final class BluetoothGatt implements BluetoothProfile {
s.setDevice(mDevice);
}
+ if (Flags.fixBluetoothGattGettingDuplicateServices()) {
+ mServices.clear();
+ }
+
mServices.addAll(services);
// Fix references to included services, as they doesn't point to right objects.
@@ -493,16 +508,18 @@ public final class BluetoothGatt implements BluetoothProfile {
mDeviceBusy = false;
}
+ int clientIf = mClientIf;
if ((status == GATT_INSUFFICIENT_AUTHENTICATION
|| status == GATT_INSUFFICIENT_ENCRYPTION)
- && (mAuthRetryState != AUTH_RETRY_STATE_MITM)) {
+ && (mAuthRetryState != AUTH_RETRY_STATE_MITM)
+ && (clientIf > 0)) {
try {
final int authReq =
(mAuthRetryState == AUTH_RETRY_STATE_IDLE)
? AUTHENTICATION_NO_MITM
: AUTHENTICATION_MITM;
mService.readCharacteristic(
- mClientIf, address, handle, authReq, mAttributionSource);
+ clientIf, address, handle, authReq, mAttributionSource);
mAuthRetryState++;
return;
} catch (RemoteException e) {
@@ -573,10 +590,13 @@ public final class BluetoothGatt implements BluetoothProfile {
? AUTHENTICATION_NO_MITM
: AUTHENTICATION_MITM;
int requestStatus = BluetoothStatusCodes.ERROR_UNKNOWN;
- for (int i = 0; i < WRITE_CHARACTERISTIC_MAX_RETRIES; i++) {
+ int clientIf = mClientIf;
+ for (int i = 0;
+ (i < WRITE_CHARACTERISTIC_MAX_RETRIES) && (clientIf > 0);
+ i++) {
requestStatus =
mService.writeCharacteristic(
- mClientIf,
+ clientIf,
address,
handle,
characteristic.getWriteType(),
@@ -679,16 +699,18 @@ public final class BluetoothGatt implements BluetoothProfile {
BluetoothGattDescriptor descriptor = getDescriptorById(mDevice, handle);
if (descriptor == null) return;
+ int clientIf = mClientIf;
if ((status == GATT_INSUFFICIENT_AUTHENTICATION
|| status == GATT_INSUFFICIENT_ENCRYPTION)
- && (mAuthRetryState != AUTH_RETRY_STATE_MITM)) {
+ && (mAuthRetryState != AUTH_RETRY_STATE_MITM)
+ && (clientIf > 0)) {
try {
final int authReq =
(mAuthRetryState == AUTH_RETRY_STATE_IDLE)
? AUTHENTICATION_NO_MITM
: AUTHENTICATION_MITM;
mService.readDescriptor(
- mClientIf, address, handle, authReq, mAttributionSource);
+ clientIf, address, handle, authReq, mAttributionSource);
mAuthRetryState++;
return;
} catch (RemoteException e) {
@@ -741,16 +763,18 @@ public final class BluetoothGatt implements BluetoothProfile {
BluetoothGattDescriptor descriptor = getDescriptorById(mDevice, handle);
if (descriptor == null) return;
+ int clientIf = mClientIf;
if ((status == GATT_INSUFFICIENT_AUTHENTICATION
|| status == GATT_INSUFFICIENT_ENCRYPTION)
- && (mAuthRetryState != AUTH_RETRY_STATE_MITM)) {
+ && (mAuthRetryState != AUTH_RETRY_STATE_MITM)
+ && (clientIf > 0)) {
try {
final int authReq =
(mAuthRetryState == AUTH_RETRY_STATE_IDLE)
? AUTHENTICATION_NO_MITM
: AUTHENTICATION_MITM;
mService.writeDescriptor(
- mClientIf, address, handle, authReq, value, mAttributionSource);
+ clientIf, address, handle, authReq, value, mAttributionSource);
mAuthRetryState++;
return;
} catch (RemoteException e) {
@@ -1033,6 +1057,10 @@ public final class BluetoothGatt implements BluetoothProfile {
if (DBG) Log.d(TAG, "close()");
unregisterApp();
+ if (Flags.unregisterGattClientDisconnected()) {
+ mCallback = null;
+ }
+
mConnState = CONN_STATE_CLOSED;
mAuthRetryState = AUTH_RETRY_STATE_IDLE;
}
@@ -1166,7 +1194,9 @@ public final class BluetoothGatt implements BluetoothProfile {
if (DBG) Log.d(TAG, "unregisterApp() - mClientIf=" + mClientIf);
try {
- mCallback = null;
+ if (!Flags.unregisterGattClientDisconnected()) {
+ mCallback = null;
+ }
mService.unregisterClient(mClientIf, mAttributionSource);
mClientIf = 0;
} catch (RemoteException e) {
@@ -1229,10 +1259,11 @@ public final class BluetoothGatt implements BluetoothProfile {
@RequiresPermission(BLUETOOTH_CONNECT)
public void disconnect() {
if (DBG) Log.d(TAG, "cancelOpen() - device: " + mDevice);
- if (mService == null || mClientIf == 0) return;
+ int clientIf = mClientIf;
+ if (mService == null || clientIf == 0) return;
try {
- mService.clientDisconnect(mClientIf, mDevice.getAddress(), mAttributionSource);
+ mService.clientDisconnect(clientIf, mDevice.getAddress(), mAttributionSource);
} catch (RemoteException e) {
Log.e(TAG, "", e);
}
@@ -1250,6 +1281,40 @@ public final class BluetoothGatt implements BluetoothProfile {
@RequiresBluetoothConnectPermission
@RequiresPermission(BLUETOOTH_CONNECT)
public boolean connect() {
+ int clientIf = mClientIf;
+ if (mService == null) return false;
+ if (clientIf == 0) {
+ if (!Flags.unregisterGattClientDisconnected()) {
+ return false;
+ }
+ synchronized (mStateLock) {
+ if (mConnState != CONN_STATE_IDLE) {
+ return false;
+ }
+ mConnState = CONN_STATE_CONNECTING;
+ }
+
+ UUID uuid = UUID.randomUUID();
+ if (DBG) Log.d(TAG, "reconnect from connect(), UUID=" + uuid);
+
+ try {
+ mService.registerClient(
+ new ParcelUuid(uuid),
+ mBluetoothGattCallback,
+ /* eatt_support= */ false,
+ mAttributionSource);
+ } catch (RemoteException e) {
+ Log.e(TAG, "", e);
+ synchronized (mStateLock) {
+ mConnState = CONN_STATE_IDLE;
+ }
+ Log.e(TAG, "Failed to register callback");
+ return false;
+ }
+
+ return true;
+ }
+
try {
if (DBG) {
Log.d(TAG, "connect(void) - device: " + mDevice + ", auto=" + mAutoConnect);
@@ -1257,7 +1322,7 @@ public final class BluetoothGatt implements BluetoothProfile {
// autoConnect is inverse of "isDirect"
mService.clientConnect(
- mClientIf,
+ clientIf,
mDevice.getAddress(),
mDevice.getAddressType(),
!mAutoConnect,
@@ -1293,9 +1358,12 @@ public final class BluetoothGatt implements BluetoothProfile {
@RequiresBluetoothConnectPermission
@RequiresPermission(BLUETOOTH_CONNECT)
public void setPreferredPhy(int txPhy, int rxPhy, int phyOptions) {
+ int clientIf = mClientIf;
+ if (mService == null || clientIf == 0) return;
+
try {
mService.clientSetPreferredPhy(
- mClientIf, mDevice.getAddress(), txPhy, rxPhy, phyOptions, mAttributionSource);
+ clientIf, mDevice.getAddress(), txPhy, rxPhy, phyOptions, mAttributionSource);
} catch (RemoteException e) {
Log.e(TAG, "", e);
}
@@ -1308,8 +1376,11 @@ public final class BluetoothGatt implements BluetoothProfile {
@RequiresBluetoothConnectPermission
@RequiresPermission(BLUETOOTH_CONNECT)
public void readPhy() {
+ int clientIf = mClientIf;
+ if (mService == null || clientIf == 0) return;
+
try {
- mService.clientReadPhy(mClientIf, mDevice.getAddress(), mAttributionSource);
+ mService.clientReadPhy(clientIf, mDevice.getAddress(), mAttributionSource);
} catch (RemoteException e) {
Log.e(TAG, "", e);
}
@@ -1340,12 +1411,16 @@ public final class BluetoothGatt implements BluetoothProfile {
@RequiresPermission(BLUETOOTH_CONNECT)
public boolean discoverServices() {
if (DBG) Log.d(TAG, "discoverServices() - device: " + mDevice);
- if (mService == null || mClientIf == 0) return false;
- mServices.clear();
+ int clientIf = mClientIf;
+ if (mService == null || clientIf == 0) return false;
+
+ if (!Flags.fixBluetoothGattGettingDuplicateServices()) {
+ mServices.clear();
+ }
try {
- mService.discoverServices(mClientIf, mDevice.getAddress(), mAttributionSource);
+ mService.discoverServices(clientIf, mDevice.getAddress(), mAttributionSource);
} catch (RemoteException e) {
Log.e(TAG, "", e);
return false;
@@ -1367,13 +1442,16 @@ public final class BluetoothGatt implements BluetoothProfile {
@RequiresPermission(BLUETOOTH_CONNECT)
public boolean discoverServiceByUuid(UUID uuid) {
if (DBG) Log.d(TAG, "discoverServiceByUuid() - device: " + mDevice);
- if (mService == null || mClientIf == 0) return false;
+ int clientIf = mClientIf;
+ if (mService == null || clientIf == 0) return false;
- mServices.clear();
+ if (!Flags.fixBluetoothGattGettingDuplicateServices()) {
+ mServices.clear();
+ }
try {
mService.discoverServiceByUuid(
- mClientIf, mDevice.getAddress(), new ParcelUuid(uuid), mAttributionSource);
+ clientIf, mDevice.getAddress(), new ParcelUuid(uuid), mAttributionSource);
} catch (RemoteException e) {
Log.e(TAG, "", e);
return false;
@@ -1447,7 +1525,8 @@ public final class BluetoothGatt implements BluetoothProfile {
}
if (VDBG) Log.d(TAG, "readCharacteristic() - uuid: " + characteristic.getUuid());
- if (mService == null || mClientIf == 0) return false;
+ int clientIf = mClientIf;
+ if (mService == null || clientIf == 0) return false;
BluetoothGattService service = characteristic.getService();
if (service == null) return false;
@@ -1462,7 +1541,7 @@ public final class BluetoothGatt implements BluetoothProfile {
try {
mService.readCharacteristic(
- mClientIf,
+ clientIf,
device.getAddress(),
characteristic.getInstanceId(),
AUTHENTICATION_NONE,
@@ -1494,7 +1573,8 @@ public final class BluetoothGatt implements BluetoothProfile {
@RequiresPermission(BLUETOOTH_CONNECT)
public boolean readUsingCharacteristicUuid(UUID uuid, int startHandle, int endHandle) {
if (VDBG) Log.d(TAG, "readUsingCharacteristicUuid() - uuid: " + uuid);
- if (mService == null || mClientIf == 0) return false;
+ int clientIf = mClientIf;
+ if (mService == null || clientIf == 0) return false;
synchronized (mDeviceBusyLock) {
if (mDeviceBusy) return false;
@@ -1503,7 +1583,7 @@ public final class BluetoothGatt implements BluetoothProfile {
try {
mService.readUsingCharacteristicUuid(
- mClientIf,
+ clientIf,
mDevice.getAddress(),
new ParcelUuid(uuid),
startHandle,
@@ -1601,7 +1681,8 @@ public final class BluetoothGatt implements BluetoothProfile {
== 0) {
return BluetoothStatusCodes.ERROR_GATT_WRITE_NOT_ALLOWED;
}
- if (mService == null || mClientIf == 0) {
+ int clientIf = mClientIf;
+ if (mService == null || clientIf == 0) {
return BluetoothStatusCodes.ERROR_PROFILE_SERVICE_NOT_BOUND;
}
@@ -1627,7 +1708,7 @@ public final class BluetoothGatt implements BluetoothProfile {
for (int i = 0; i < WRITE_CHARACTERISTIC_MAX_RETRIES; i++) {
requestStatus =
mService.writeCharacteristic(
- mClientIf,
+ clientIf,
device.getAddress(),
characteristic.getInstanceId(),
writeType,
@@ -1674,7 +1755,8 @@ public final class BluetoothGatt implements BluetoothProfile {
@RequiresPermission(BLUETOOTH_CONNECT)
public boolean readDescriptor(BluetoothGattDescriptor descriptor) {
if (VDBG) Log.d(TAG, "readDescriptor() - uuid: " + descriptor.getUuid());
- if (mService == null || mClientIf == 0) return false;
+ int clientIf = mClientIf;
+ if (mService == null || clientIf == 0) return false;
BluetoothGattCharacteristic characteristic = descriptor.getCharacteristic();
if (characteristic == null) return false;
@@ -1692,7 +1774,7 @@ public final class BluetoothGatt implements BluetoothProfile {
try {
mService.readDescriptor(
- mClientIf,
+ clientIf,
device.getAddress(),
descriptor.getInstanceId(),
AUTHENTICATION_NONE,
@@ -1755,7 +1837,8 @@ public final class BluetoothGatt implements BluetoothProfile {
throw new IllegalArgumentException("value must not be null");
}
if (VDBG) Log.d(TAG, "writeDescriptor() - uuid: " + descriptor.getUuid());
- if (mService == null || mClientIf == 0) {
+ int clientIf = mClientIf;
+ if (mService == null || clientIf == 0) {
return BluetoothStatusCodes.ERROR_PROFILE_SERVICE_NOT_BOUND;
}
@@ -1781,7 +1864,7 @@ public final class BluetoothGatt implements BluetoothProfile {
try {
return mService.writeDescriptor(
- mClientIf,
+ clientIf,
device.getAddress(),
descriptor.getInstanceId(),
AUTHENTICATION_NONE,
@@ -1818,10 +1901,11 @@ public final class BluetoothGatt implements BluetoothProfile {
@RequiresPermission(BLUETOOTH_CONNECT)
public boolean beginReliableWrite() {
if (VDBG) Log.d(TAG, "beginReliableWrite() - device: " + mDevice);
- if (mService == null || mClientIf == 0) return false;
+ int clientIf = mClientIf;
+ if (mService == null || clientIf == 0) return false;
try {
- mService.beginReliableWrite(mClientIf, mDevice.getAddress(), mAttributionSource);
+ mService.beginReliableWrite(clientIf, mDevice.getAddress(), mAttributionSource);
} catch (RemoteException e) {
Log.e(TAG, "", e);
return false;
@@ -1846,7 +1930,8 @@ public final class BluetoothGatt implements BluetoothProfile {
@RequiresPermission(BLUETOOTH_CONNECT)
public boolean executeReliableWrite() {
if (VDBG) Log.d(TAG, "executeReliableWrite() - device: " + mDevice);
- if (mService == null || mClientIf == 0) return false;
+ int clientIf = mClientIf;
+ if (mService == null || clientIf == 0) return false;
synchronized (mDeviceBusyLock) {
if (mDeviceBusy) return false;
@@ -1854,7 +1939,7 @@ public final class BluetoothGatt implements BluetoothProfile {
}
try {
- mService.endReliableWrite(mClientIf, mDevice.getAddress(), true, mAttributionSource);
+ mService.endReliableWrite(clientIf, mDevice.getAddress(), true, mAttributionSource);
} catch (RemoteException e) {
Log.e(TAG, "", e);
synchronized (mDeviceBusyLock) {
@@ -1877,10 +1962,11 @@ public final class BluetoothGatt implements BluetoothProfile {
@RequiresPermission(BLUETOOTH_CONNECT)
public void abortReliableWrite() {
if (VDBG) Log.d(TAG, "abortReliableWrite() - device: " + mDevice);
- if (mService == null || mClientIf == 0) return;
+ int clientIf = mClientIf;
+ if (mService == null || clientIf == 0) return;
try {
- mService.endReliableWrite(mClientIf, mDevice.getAddress(), false, mAttributionSource);
+ mService.endReliableWrite(clientIf, mDevice.getAddress(), false, mAttributionSource);
} catch (RemoteException e) {
Log.e(TAG, "", e);
}
@@ -1921,7 +2007,8 @@ public final class BluetoothGatt implements BluetoothProfile {
+ " enable: "
+ enable);
}
- if (mService == null || mClientIf == 0) return false;
+ int clientIf = mClientIf;
+ if (mService == null || clientIf == 0) return false;
BluetoothGattService service = characteristic.getService();
if (service == null) return false;
@@ -1931,7 +2018,7 @@ public final class BluetoothGatt implements BluetoothProfile {
try {
mService.registerForNotification(
- mClientIf,
+ clientIf,
device.getAddress(),
characteristic.getInstanceId(),
enable,
@@ -1954,10 +2041,11 @@ public final class BluetoothGatt implements BluetoothProfile {
@RequiresPermission(BLUETOOTH_CONNECT)
public boolean refresh() {
if (DBG) Log.d(TAG, "refresh() - device: " + mDevice);
- if (mService == null || mClientIf == 0) return false;
+ int clientIf = mClientIf;
+ if (mService == null || clientIf == 0) return false;
try {
- mService.refreshDevice(mClientIf, mDevice.getAddress(), mAttributionSource);
+ mService.refreshDevice(clientIf, mDevice.getAddress(), mAttributionSource);
} catch (RemoteException e) {
Log.e(TAG, "", e);
return false;
@@ -1979,10 +2067,11 @@ public final class BluetoothGatt implements BluetoothProfile {
@RequiresPermission(BLUETOOTH_CONNECT)
public boolean readRemoteRssi() {
if (DBG) Log.d(TAG, "readRssi() - device: " + mDevice);
- if (mService == null || mClientIf == 0) return false;
+ int clientIf = mClientIf;
+ if (mService == null || clientIf == 0) return false;
try {
- mService.readRemoteRssi(mClientIf, mDevice.getAddress(), mAttributionSource);
+ mService.readRemoteRssi(clientIf, mDevice.getAddress(), mAttributionSource);
} catch (RemoteException e) {
Log.e(TAG, "", e);
return false;
@@ -2014,10 +2103,11 @@ public final class BluetoothGatt implements BluetoothProfile {
if (DBG) {
Log.d(TAG, "configureMTU() - device: " + mDevice + " mtu: " + mtu);
}
- if (mService == null || mClientIf == 0) return false;
+ int clientIf = mClientIf;
+ if (mService == null || clientIf == 0) return false;
try {
- mService.configureMTU(mClientIf, mDevice.getAddress(), mtu, mAttributionSource);
+ mService.configureMTU(clientIf, mDevice.getAddress(), mtu, mAttributionSource);
} catch (RemoteException e) {
Log.e(TAG, "", e);
return false;
@@ -2047,11 +2137,12 @@ public final class BluetoothGatt implements BluetoothProfile {
}
if (DBG) Log.d(TAG, "requestConnectionPriority() - params: " + connectionPriority);
- if (mService == null || mClientIf == 0) return false;
+ int clientIf = mClientIf;
+ if (mService == null || clientIf == 0) return false;
try {
mService.connectionParameterUpdate(
- mClientIf, mDevice.getAddress(), connectionPriority, mAttributionSource);
+ clientIf, mDevice.getAddress(), connectionPriority, mAttributionSource);
} catch (RemoteException e) {
Log.e(TAG, "", e);
return false;
@@ -2098,11 +2189,12 @@ public final class BluetoothGatt implements BluetoothProfile {
+ ", max_ce="
+ maxConnectionEventLen);
}
- if (mService == null || mClientIf == 0) return false;
+ int clientIf = mClientIf;
+ if (mService == null || clientIf == 0) return false;
try {
mService.leConnectionUpdate(
- mClientIf,
+ clientIf,
mDevice.getAddress(),
minConnectionInterval,
maxConnectionInterval,
@@ -2148,12 +2240,13 @@ public final class BluetoothGatt implements BluetoothProfile {
if (DBG) {
Log.d(TAG, "requestsubrateMode(" + subrateMode + ")");
}
- if (mService == null || mClientIf == 0) {
+ int clientIf = mClientIf;
+ if (mService == null || clientIf == 0) {
return BluetoothStatusCodes.ERROR_BLUETOOTH_NOT_ENABLED;
}
try {
- return mService.subrateModeRequest(mClientIf, mDevice, subrateMode, mAttributionSource);
+ return mService.subrateModeRequest(clientIf, mDevice, subrateMode, mAttributionSource);
} catch (RemoteException e) {
logRemoteException(TAG, e);
return BluetoothStatusCodes.ERROR_BLUETOOTH_NOT_ENABLED;
diff --git a/framework/java/android/bluetooth/BluetoothSocket.java b/framework/java/android/bluetooth/BluetoothSocket.java
index 97bdf595a0..cde23baf4d 100644
--- a/framework/java/android/bluetooth/BluetoothSocket.java
+++ b/framework/java/android/bluetooth/BluetoothSocket.java
@@ -978,6 +978,8 @@ public final class BluetoothSocket implements Closeable {
if (mL2capBuffer.remaining() == 0) {
if (VDBG) Log.v(TAG, "l2cap buffer empty, refilling...");
if (fillL2capRxBuffer() == -1) {
+ Log.d(TAG, "socket EOF, returning -1");
+ mSocketState = SocketState.CLOSED;
return -1;
}
}
@@ -994,6 +996,7 @@ public final class BluetoothSocket implements Closeable {
ret = mSocketIS.read(b, offset, length);
}
if (ret < 0) {
+ mSocketState = SocketState.CLOSED;
throw new IOException("bt socket closed, read return: " + ret);
}
if (VDBG) Log.d(TAG, "read out: " + mSocketIS + " ret: " + ret);
diff --git a/framework/tests/bumble/src/android/bluetooth/DckL2capTest.kt b/framework/tests/bumble/src/android/bluetooth/DckL2capTest.kt
index e658e8c645..ea41c55ee0 100644
--- a/framework/tests/bumble/src/android/bluetooth/DckL2capTest.kt
+++ b/framework/tests/bumble/src/android/bluetooth/DckL2capTest.kt
@@ -226,6 +226,51 @@ public class DckL2capTest() : Closeable {
Log.d(TAG, "testReceive: done")
}
+ @Test
+ @VirtualOnly
+ fun testReadReturnOnRemoteSocketDisconnect() {
+ Log.d(TAG, "testReadReturnonSocketDisconnect: Connect L2CAP")
+ var bluetoothSocket: BluetoothSocket?
+ val l2capServer = bluetoothAdapter.listenUsingInsecureL2capChannel()
+ val socketFlow = flow { emit(l2capServer.accept()) }
+ val connectResponse = createAndConnectL2capChannelWithBumble(l2capServer.psm)
+ runBlocking {
+ bluetoothSocket = socketFlow.first()
+ assertThat(connectResponse.hasChannel()).isTrue()
+ }
+
+ val inputStream = bluetoothSocket!!.inputStream
+
+ // block on read() on server thread
+ val readThread = Thread {
+ Log.d(TAG, "testReadReturnOnRemoteSocketDisconnect: Receive data on Android")
+ val ret = inputStream.read()
+ Log.d(TAG, "testReadReturnOnRemoteSocketDisconnect: read returns : " + ret)
+ Log.d(
+ TAG,
+ "testReadReturnOnRemoteSocketDisconnect: isConnected() : " +
+ bluetoothSocket!!.isConnected(),
+ )
+ assertThat(ret).isEqualTo(-1)
+ assertThat(bluetoothSocket!!.isConnected()).isFalse()
+ }
+ readThread.start()
+ // check that socket is still connected
+ assertThat(bluetoothSocket!!.isConnected()).isTrue()
+
+ // read() would be blocking till underlying l2cap is disconnected
+ Thread.sleep(1000 * 10)
+ Log.d(TAG, "testReadReturnOnRemoteSocketDisconnect: disconnect after 10 secs")
+ val disconnectRequest =
+ DisconnectRequest.newBuilder().setChannel(connectResponse.channel).build()
+ val disconnectResponse = mBumble.l2capBlocking().disconnect(disconnectRequest)
+ assertThat(disconnectResponse.hasSuccess()).isTrue()
+ inputStream.close()
+ bluetoothSocket?.close()
+ l2capServer.close()
+ Log.d(TAG, "testReadReturnOnRemoteSocketDisconnect: done")
+ }
+
private fun createAndConnectL2capChannelWithBumble(psm: Int): ConnectResponse {
Log.d(TAG, "createAndConnectL2capChannelWithBumble")
val remoteDevice =
diff --git a/framework/tests/bumble/src/android/bluetooth/GattClientTest.java b/framework/tests/bumble/src/android/bluetooth/GattClientTest.java
index c9fa50bbd9..64ada411d6 100644
--- a/framework/tests/bumble/src/android/bluetooth/GattClientTest.java
+++ b/framework/tests/bumble/src/android/bluetooth/GattClientTest.java
@@ -720,6 +720,44 @@ public class GattClientTest {
}
}
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_UNREGISTER_GATT_CLIENT_DISCONNECTED)
+ public void connectAndDisconnectManyClientsWithoutClose() throws Exception {
+ advertiseWithBumble();
+
+ List<BluetoothGatt> gatts = new ArrayList<>();
+ try {
+ for (int i = 0; i < 100; i++) {
+ BluetoothGattCallback gattCallback = mock(BluetoothGattCallback.class);
+ InOrder inOrder = inOrder(gattCallback);
+
+ BluetoothGatt gatt = mRemoteLeDevice.connectGatt(mContext, false, gattCallback);
+ gatts.add(gatt);
+
+ inOrder.verify(gattCallback, timeout(1000))
+ .onConnectionStateChange(any(), anyInt(), eq(STATE_CONNECTED));
+
+ gatt.disconnect();
+ inOrder.verify(gattCallback, timeout(1000))
+ .onConnectionStateChange(
+ any(), anyInt(), eq(BluetoothProfile.STATE_DISCONNECTED));
+
+ gatt.connect();
+ inOrder.verify(gattCallback, timeout(1000))
+ .onConnectionStateChange(any(), anyInt(), eq(STATE_CONNECTED));
+
+ gatt.disconnect();
+ inOrder.verify(gattCallback, timeout(1000))
+ .onConnectionStateChange(
+ any(), anyInt(), eq(BluetoothProfile.STATE_DISCONNECTED));
+ }
+ } finally {
+ for (BluetoothGatt gatt : gatts) {
+ gatt.close();
+ }
+ }
+ }
+
private void createLeBondAndWaitBonding(BluetoothDevice device) {
advertiseWithBumble();
mHost.createBondAndVerify(device);
diff --git a/framework/tests/bumble/src/android/bluetooth/LeScanningTest.java b/framework/tests/bumble/src/android/bluetooth/LeScanningTest.java
index 506586bbe3..9ee8c279f1 100644
--- a/framework/tests/bumble/src/android/bluetooth/LeScanningTest.java
+++ b/framework/tests/bumble/src/android/bluetooth/LeScanningTest.java
@@ -68,7 +68,7 @@ import java.util.stream.Stream;
@RunWith(TestParameterInjector.class)
public class LeScanningTest {
private static final String TAG = "LeScanningTest";
- private static final int TIMEOUT_SCANNING_MS = 2000;
+ private static final int TIMEOUT_SCANNING_MS = 3000;
private static final String TEST_UUID_STRING = "00001805-0000-1000-8000-00805f9b34fb";
private static final String TEST_ADDRESS_RANDOM_STATIC = "F0:43:A8:23:10:11";
private static final String ACTION_DYNAMIC_RECEIVER_SCAN_RESULT =
@@ -376,6 +376,7 @@ public class LeScanningTest {
// Test against UUIDs that are close to TEST_UUID_STRING, one that has a few bits unset and one
// that has an extra bit set.
@Test
+ @VirtualOnly
public void startBleScan_withServiceData_uuidDoesntMatch(
@TestParameter({"00001800", "00001815"}) String uuid) {
advertiseWithBumbleWithServiceData();
diff --git a/framework/tests/bumble/src/android/bluetooth/pairing/PairingTest.java b/framework/tests/bumble/src/android/bluetooth/pairing/PairingTest.java
index 44c6564803..d289b7c43e 100644
--- a/framework/tests/bumble/src/android/bluetooth/pairing/PairingTest.java
+++ b/framework/tests/bumble/src/android/bluetooth/pairing/PairingTest.java
@@ -22,12 +22,8 @@ import static androidx.test.espresso.intent.matcher.IntentMatchers.hasExtra;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.any;
-import static org.mockito.Mockito.doAnswer;
-import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyNoMoreInteractions;
import android.bluetooth.BluetoothA2dp;
import android.bluetooth.BluetoothAdapter;
@@ -43,10 +39,8 @@ import android.bluetooth.StreamObserverSpliterator;
import android.bluetooth.Utils;
import android.bluetooth.test_utils.BlockingBluetoothAdapter;
import android.bluetooth.test_utils.EnableBluetoothRule;
-import android.content.BroadcastReceiver;
+import android.bluetooth.pairing.utils.IntentReceiver;
import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
import android.os.ParcelUuid;
import android.platform.test.annotations.RequiresFlagsEnabled;
import android.platform.test.flag.junit.CheckFlagsRule;
@@ -63,19 +57,15 @@ import com.google.testing.junit.testparameterinjector.TestParameterInjector;
import io.grpc.stub.StreamObserver;
-import org.hamcrest.Matcher;
import org.hamcrest.Matchers;
-import org.hamcrest.core.AllOf;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
-import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
-import org.mockito.hamcrest.MockitoHamcrest;
import pandora.GattProto;
import pandora.HostProto.AdvertiseRequest;
@@ -90,9 +80,6 @@ import pandora.SecurityProto.SecureRequest;
import pandora.SecurityProto.SecureResponse;
import java.time.Duration;
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.Map;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
@@ -127,12 +114,9 @@ public class PairingTest {
public final EnableBluetoothRule mEnableBluetoothRule =
new EnableBluetoothRule(false /* enableTestMode */, true /* toggleBluetooth */);
- private final Map<String, Integer> mActionRegistrationCounts = new HashMap<>();
private final StreamObserverSpliterator<PairingEvent> mPairingEventStreamObserver =
new StreamObserverSpliterator<>();
- @Mock private BroadcastReceiver mReceiver;
@Mock private BluetoothProfile.ServiceListener mProfileServiceListener;
- private InOrder mInOrder = null;
private BluetoothDevice mBumbleDevice;
private BluetoothDevice mRemoteLeDevice;
private BluetoothHidHost mHidService;
@@ -142,30 +126,6 @@ public class PairingTest {
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
- doAnswer(
- inv -> {
- Log.d(
- TAG,
- "onReceive(): intent=" + Arrays.toString(inv.getArguments()));
- Intent intent = inv.getArgument(1);
- String action = intent.getAction();
- if (BluetoothDevice.ACTION_UUID.equals(action)) {
- ParcelUuid[] uuids =
- intent.getParcelableArrayExtra(
- BluetoothDevice.EXTRA_UUID, ParcelUuid.class);
- Log.d(TAG, "onReceive(): UUID=" + Arrays.toString(uuids));
- } else if (BluetoothDevice.ACTION_BOND_STATE_CHANGED.equals(action)) {
- int bondState =
- intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, -1);
- Log.d(TAG, "onReceive(): bondState=" + bondState);
- }
- return null;
- })
- .when(mReceiver)
- .onReceive(any(), any());
-
- mInOrder = inOrder(mReceiver);
-
// Get profile proxies
mHidService = (BluetoothHidHost) getProfileProxy(BluetoothProfile.HID_HOST);
mHfpService = (BluetoothHeadset) getProfileProxy(BluetoothProfile.HEADSET);
@@ -175,28 +135,54 @@ public class PairingTest {
sAdapter.getRemoteLeDevice(
Utils.BUMBLE_RANDOM_ADDRESS, BluetoothDevice.ADDRESS_TYPE_RANDOM);
+ /*
+ * Note: Since there was no IntentReceiver registered, passing the instance as
+ * NULL in testStep_RemoveBond(). But, if there is an instance already present, that
+ * must be passed instead of NULL.
+ */
for (BluetoothDevice device : sAdapter.getBondedDevices()) {
- removeBond(device);
+ testStep_RemoveBond(null, device);
}
}
@After
public void tearDown() throws Exception {
Set<BluetoothDevice> bondedDevices = sAdapter.getBondedDevices();
+
+ /*
+ * Note: Since there was no IntentReceiver registered, passing the instance as
+ * NULL in testStep_RemoveBond(). But, if there is an instance already present, that
+ * must be passed instead of NULL.
+ */
if (bondedDevices.contains(mBumbleDevice)) {
- removeBond(mBumbleDevice);
+ testStep_RemoveBond(null, mBumbleDevice);
}
if (bondedDevices.contains(mRemoteLeDevice)) {
- removeBond(mRemoteLeDevice);
+ testStep_RemoveBond(null, mRemoteLeDevice);
}
mBumbleDevice = null;
mRemoteLeDevice = null;
- if (getTotalActionRegistrationCounts() > 0) {
- sTargetContext.unregisterReceiver(mReceiver);
- mActionRegistrationCounts.clear();
- }
}
+ /** All the test function goes here */
+
+ /**
+ * Process of writing a test function
+ *
+ * 1. Create an IntentReceiver object first with following way:
+ * IntentReceiver intentReceiver = new IntentReceiver.Builder(sTargetContext,
+ * BluetoothDevice.ACTION_1,
+ * BluetoothDevice.ACTION_2)
+ * .setIntentListener(--) // optional
+ * .setIntentTimeout(--) // optional
+ * .build();
+ * 2. Use the intentReceiver instance for all Intent related verification, and pass
+ * the same instance to all the helper/testStep functions which has similar Intent
+ * requirements.
+ * 3. Once all the verification is done, call `intentReceiver.close()` before returning
+ * from the function.
+ */
+
/**
* Test a simple BR/EDR just works pairing flow in the follow steps:
*
@@ -211,8 +197,10 @@ public class PairingTest {
*/
@Test
public void testBrEdrPairing_phoneInitiatedBrEdrInquiryOnlyJustWorks() {
- registerIntentActions(
- BluetoothDevice.ACTION_BOND_STATE_CHANGED, BluetoothDevice.ACTION_PAIRING_REQUEST);
+ IntentReceiver intentReceiver = new IntentReceiver.Builder(sTargetContext,
+ BluetoothDevice.ACTION_BOND_STATE_CHANGED,
+ BluetoothDevice.ACTION_PAIRING_REQUEST)
+ .build();
StreamObserver<PairingEventAnswer> pairingEventAnswerObserver =
mBumble.security()
@@ -220,12 +208,12 @@ public class PairingTest {
.onPairing(mPairingEventStreamObserver);
assertThat(mBumbleDevice.createBond()).isTrue();
- verifyIntentReceived(
+ intentReceiver.verifyReceivedOrdered(
hasAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED),
hasExtra(BluetoothDevice.EXTRA_DEVICE, mBumbleDevice),
hasExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.BOND_BONDING));
- verifyIntentReceived(
+ intentReceiver.verifyReceivedOrdered(
hasAction(BluetoothDevice.ACTION_PAIRING_REQUEST),
hasExtra(BluetoothDevice.EXTRA_DEVICE, mBumbleDevice),
hasExtra(
@@ -238,15 +226,12 @@ public class PairingTest {
pairingEventAnswerObserver.onNext(
PairingEventAnswer.newBuilder().setEvent(pairingEvent).setConfirm(true).build());
- verifyIntentReceived(
+ intentReceiver.verifyReceivedOrdered(
hasAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED),
hasExtra(BluetoothDevice.EXTRA_DEVICE, mBumbleDevice),
hasExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.BOND_BONDED));
- verifyNoMoreInteractions(mReceiver);
-
- unregisterIntentActions(
- BluetoothDevice.ACTION_BOND_STATE_CHANGED, BluetoothDevice.ACTION_PAIRING_REQUEST);
+ intentReceiver.close();
}
/**
@@ -266,8 +251,10 @@ public class PairingTest {
@Test
@RequiresFlagsEnabled({Flags.FLAG_IGNORE_UNRELATED_CANCEL_BOND})
public void testBrEdrPairing_cancelBond_forUnrelatedDevice() {
- registerIntentActions(
- BluetoothDevice.ACTION_BOND_STATE_CHANGED, BluetoothDevice.ACTION_PAIRING_REQUEST);
+ IntentReceiver intentReceiver = new IntentReceiver.Builder(sTargetContext,
+ BluetoothDevice.ACTION_BOND_STATE_CHANGED,
+ BluetoothDevice.ACTION_PAIRING_REQUEST)
+ .build();
StreamObserver<PairingEventAnswer> pairingEventAnswerObserver =
mBumble.security()
@@ -275,12 +262,12 @@ public class PairingTest {
.onPairing(mPairingEventStreamObserver);
assertThat(mBumbleDevice.createBond()).isTrue();
- verifyIntentReceived(
+ intentReceiver.verifyReceivedOrdered(
hasAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED),
hasExtra(BluetoothDevice.EXTRA_DEVICE, mBumbleDevice),
hasExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.BOND_BONDING));
- verifyIntentReceived(
+ intentReceiver.verifyReceivedOrdered(
hasAction(BluetoothDevice.ACTION_PAIRING_REQUEST),
hasExtra(BluetoothDevice.EXTRA_DEVICE, mBumbleDevice),
hasExtra(
@@ -296,15 +283,12 @@ public class PairingTest {
pairingEventAnswerObserver.onNext(
PairingEventAnswer.newBuilder().setEvent(pairingEvent).setConfirm(true).build());
- verifyIntentReceived(
+ intentReceiver.verifyReceivedOrdered(
hasAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED),
hasExtra(BluetoothDevice.EXTRA_DEVICE, mBumbleDevice),
hasExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.BOND_BONDED));
- verifyNoMoreInteractions(mReceiver);
-
- unregisterIntentActions(
- BluetoothDevice.ACTION_BOND_STATE_CHANGED, BluetoothDevice.ACTION_PAIRING_REQUEST);
+ intentReceiver.close();
}
/**
@@ -322,10 +306,11 @@ public class PairingTest {
*/
@Test
public void testBrEdrPairing_phoneInitiatedBrEdrInquiryOnlyJustWorksWhileSdpConnected() {
- registerIntentActions(
+ IntentReceiver intentReceiver = new IntentReceiver.Builder(sTargetContext,
BluetoothDevice.ACTION_ACL_CONNECTED,
BluetoothDevice.ACTION_BOND_STATE_CHANGED,
- BluetoothDevice.ACTION_PAIRING_REQUEST);
+ BluetoothDevice.ACTION_PAIRING_REQUEST)
+ .build();
StreamObserver<PairingEventAnswer> pairingEventAnswerObserver =
mBumble.security()
@@ -335,17 +320,17 @@ public class PairingTest {
// Start SDP. This will create an ACL connection before the bonding starts.
assertThat(mBumbleDevice.fetchUuidsWithSdp(BluetoothDevice.TRANSPORT_BREDR)).isTrue();
- verifyIntentReceived(
+ intentReceiver.verifyReceivedOrdered(
hasAction(BluetoothDevice.ACTION_ACL_CONNECTED),
hasExtra(BluetoothDevice.EXTRA_DEVICE, mBumbleDevice));
assertThat(mBumbleDevice.createBond()).isTrue();
- verifyIntentReceived(
+ intentReceiver.verifyReceivedOrdered(
hasAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED),
hasExtra(BluetoothDevice.EXTRA_DEVICE, mBumbleDevice),
hasExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.BOND_BONDING));
- verifyIntentReceived(
+ intentReceiver.verifyReceivedOrdered(
hasAction(BluetoothDevice.ACTION_PAIRING_REQUEST),
hasExtra(BluetoothDevice.EXTRA_DEVICE, mBumbleDevice),
hasExtra(
@@ -358,17 +343,12 @@ public class PairingTest {
pairingEventAnswerObserver.onNext(
PairingEventAnswer.newBuilder().setEvent(pairingEvent).setConfirm(true).build());
- verifyIntentReceived(
+ intentReceiver.verifyReceivedOrdered(
hasAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED),
hasExtra(BluetoothDevice.EXTRA_DEVICE, mBumbleDevice),
hasExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.BOND_BONDED));
- verifyNoMoreInteractions(mReceiver);
-
- unregisterIntentActions(
- BluetoothDevice.ACTION_ACL_CONNECTED,
- BluetoothDevice.ACTION_BOND_STATE_CHANGED,
- BluetoothDevice.ACTION_PAIRING_REQUEST);
+ intentReceiver.close();
}
/**
@@ -398,11 +378,13 @@ public class PairingTest {
@Test
@RequiresFlagsEnabled({Flags.FLAG_PREVENT_DUPLICATE_UUID_INTENT})
public void testCancelBondLe_WithGattServiceDiscovery() {
- registerIntentActions(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
+ IntentReceiver intentReceiver = new IntentReceiver.Builder(sTargetContext,
+ BluetoothDevice.ACTION_BOND_STATE_CHANGED)
+ .build();
// Outgoing GATT service discovery and incoming LE pairing in parallel
StreamObserverSpliterator<SecureResponse> responseObserver =
- helper_OutgoingGattServiceDiscoveryWithIncomingLePairing();
+ helper_OutgoingGattServiceDiscoveryWithIncomingLePairing(intentReceiver);
// Cancel pairing from Android
assertThat(mBumbleDevice.cancelBondProcess()).isTrue();
@@ -412,14 +394,12 @@ public class PairingTest {
// Pairing should be cancelled in a moment instead of timing out in 30
// seconds
- verifyIntentReceived(
+ intentReceiver.verifyReceivedOrdered(
hasAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED),
hasExtra(BluetoothDevice.EXTRA_DEVICE, mBumbleDevice),
hasExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.BOND_NONE));
- verifyNoMoreInteractions(mReceiver);
-
- unregisterIntentActions(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
+ intentReceiver.close();
}
/**
@@ -449,11 +429,13 @@ public class PairingTest {
@Test
@RequiresFlagsEnabled({Flags.FLAG_PREVENT_DUPLICATE_UUID_INTENT})
public void testBondLe_WithGattServiceDiscovery() {
- registerIntentActions(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
+ IntentReceiver intentReceiver = new IntentReceiver.Builder(sTargetContext,
+ BluetoothDevice.ACTION_BOND_STATE_CHANGED)
+ .build();
// Outgoing GATT service discovery and incoming LE pairing in parallel
StreamObserverSpliterator<SecureResponse> responseObserver =
- helper_OutgoingGattServiceDiscoveryWithIncomingLePairing();
+ helper_OutgoingGattServiceDiscoveryWithIncomingLePairing(intentReceiver);
// Approve pairing from Android
assertThat(mBumbleDevice.setPairingConfirmation(true)).isTrue();
@@ -462,14 +444,12 @@ public class PairingTest {
assertThat(secureResponse.hasSuccess()).isTrue();
// Ensure that pairing succeeds
- verifyIntentReceived(
+ intentReceiver.verifyReceivedOrdered(
hasAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED),
hasExtra(BluetoothDevice.EXTRA_DEVICE, mBumbleDevice),
hasExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.BOND_BONDED));
- verifyNoMoreInteractions(mReceiver);
-
- unregisterIntentActions(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
+ intentReceiver.close();
}
/**
@@ -495,9 +475,11 @@ public class PairingTest {
*/
@Test
public void testBondLe_Reconnect() {
- registerIntentActions(BluetoothDevice.ACTION_ACL_CONNECTED);
+ IntentReceiver intentReceiver = new IntentReceiver.Builder(sTargetContext,
+ BluetoothDevice.ACTION_ACL_CONNECTED)
+ .build();
- testStep_BondLe(mBumbleDevice, OwnAddressType.PUBLIC);
+ testStep_BondLe(intentReceiver, mBumbleDevice, OwnAddressType.PUBLIC);
assertThat(sAdapter.getBondedDevices()).contains(mBumbleDevice);
testStep_restartBt();
@@ -521,12 +503,12 @@ public class PairingTest {
.build());
assertThat(mBumbleDevice.connect()).isEqualTo(BluetoothStatusCodes.SUCCESS);
- verifyIntentReceived(
+ intentReceiver.verifyReceivedOrdered(
hasAction(BluetoothDevice.ACTION_ACL_CONNECTED),
hasExtra(BluetoothDevice.EXTRA_TRANSPORT, BluetoothDevice.TRANSPORT_LE),
hasExtra(BluetoothDevice.EXTRA_DEVICE, mBumbleDevice));
- verifyNoMoreInteractions(mReceiver);
- unregisterIntentActions(BluetoothDevice.ACTION_ACL_CONNECTED);
+
+ intentReceiver.close();
}
/**
@@ -559,98 +541,6 @@ public class PairingTest {
}
}
- private void doTestIdentityAddressWithType(
- BluetoothDevice device, OwnAddressType ownAddressType) {
- BluetoothAddress identityAddress = device.getIdentityAddressWithType();
- assertThat(identityAddress.getAddress()).isNull();
- assertThat(identityAddress.getAddressType())
- .isEqualTo(BluetoothDevice.ADDRESS_TYPE_UNKNOWN);
-
- testStep_BondLe(device, ownAddressType);
- assertThat(sAdapter.getBondedDevices()).contains(device);
-
- identityAddress = device.getIdentityAddressWithType();
- assertThat(identityAddress.getAddress()).isEqualTo(device.getAddress());
- assertThat(identityAddress.getAddressType())
- .isEqualTo(
- ownAddressType == OwnAddressType.RANDOM
- ? BluetoothDevice.ADDRESS_TYPE_RANDOM
- : BluetoothDevice.ADDRESS_TYPE_PUBLIC);
- }
-
- private void testStep_BondLe(BluetoothDevice device, OwnAddressType ownAddressType) {
- registerIntentActions(
- BluetoothDevice.ACTION_BOND_STATE_CHANGED,
- BluetoothDevice.ACTION_ACL_CONNECTED,
- BluetoothDevice.ACTION_PAIRING_REQUEST);
-
- mBumble.gattBlocking()
- .registerService(
- GattProto.RegisterServiceRequest.newBuilder()
- .setService(
- GattProto.GattServiceParams.newBuilder()
- .setUuid(BATTERY_UUID.toString())
- .build())
- .build());
- mBumble.gattBlocking()
- .registerService(
- GattProto.RegisterServiceRequest.newBuilder()
- .setService(
- GattProto.GattServiceParams.newBuilder()
- .setUuid(HOGP_UUID.toString())
- .build())
- .build());
-
- mBumble.hostBlocking()
- .advertise(
- AdvertiseRequest.newBuilder()
- .setLegacy(true)
- .setConnectable(true)
- .setOwnAddressType(ownAddressType)
- .build());
-
- StreamObserver<PairingEventAnswer> pairingEventAnswerObserver =
- mBumble.security()
- .withDeadlineAfter(BOND_INTENT_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS)
- .onPairing(mPairingEventStreamObserver);
-
- assertThat(device.createBond(BluetoothDevice.TRANSPORT_LE)).isTrue();
-
- verifyIntentReceivedUnordered(
- hasAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED),
- hasExtra(BluetoothDevice.EXTRA_DEVICE, device),
- hasExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.BOND_BONDING));
- verifyIntentReceived(
- hasAction(BluetoothDevice.ACTION_ACL_CONNECTED),
- hasExtra(BluetoothDevice.EXTRA_DEVICE, device),
- hasExtra(BluetoothDevice.EXTRA_TRANSPORT, BluetoothDevice.TRANSPORT_LE));
- verifyIntentReceivedUnordered(
- hasAction(BluetoothDevice.ACTION_PAIRING_REQUEST),
- hasExtra(BluetoothDevice.EXTRA_DEVICE, device),
- hasExtra(
- BluetoothDevice.EXTRA_PAIRING_VARIANT,
- BluetoothDevice.PAIRING_VARIANT_CONSENT));
-
- // Approve pairing from Android
- assertThat(device.setPairingConfirmation(true)).isTrue();
-
- PairingEvent pairingEvent = mPairingEventStreamObserver.iterator().next();
- assertThat(pairingEvent.hasJustWorks()).isTrue();
- pairingEventAnswerObserver.onNext(
- PairingEventAnswer.newBuilder().setEvent(pairingEvent).setConfirm(true).build());
-
- // Ensure that pairing succeeds
- verifyIntentReceived(
- hasAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED),
- hasExtra(BluetoothDevice.EXTRA_DEVICE, device),
- hasExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.BOND_BONDED));
-
- unregisterIntentActions(
- BluetoothDevice.ACTION_BOND_STATE_CHANGED,
- BluetoothDevice.ACTION_ACL_CONNECTED,
- BluetoothDevice.ACTION_PAIRING_REQUEST);
- }
-
/**
* Test if bonded BR/EDR device can reconnect after BT restart
*
@@ -674,9 +564,11 @@ public class PairingTest {
*/
@Test
public void testBondBredr_Reconnect() {
- registerIntentActions(BluetoothDevice.ACTION_ACL_CONNECTED);
+ IntentReceiver intentReceiver = new IntentReceiver.Builder(sTargetContext,
+ BluetoothDevice.ACTION_ACL_CONNECTED)
+ .build();
- testStep_BondBredr();
+ testStep_BondBredr(intentReceiver);
assertThat(sAdapter.getBondedDevices()).contains(mBumbleDevice);
testStep_restartBt();
@@ -689,12 +581,12 @@ public class PairingTest {
.build();
mBumble.hostBlocking().setConnectabilityMode(request);
assertThat(mBumbleDevice.connect()).isEqualTo(BluetoothStatusCodes.SUCCESS);
- verifyIntentReceived(
+ intentReceiver.verifyReceivedOrdered(
hasAction(BluetoothDevice.ACTION_ACL_CONNECTED),
hasExtra(BluetoothDevice.EXTRA_TRANSPORT, BluetoothDevice.TRANSPORT_BREDR),
hasExtra(BluetoothDevice.EXTRA_DEVICE, mBumbleDevice));
- verifyNoMoreInteractions(mReceiver);
- unregisterIntentActions(BluetoothDevice.ACTION_ACL_CONNECTED);
+
+ intentReceiver.close();
}
/**
@@ -720,27 +612,27 @@ public class PairingTest {
@Test
@RequiresFlagsEnabled({Flags.FLAG_WAIT_FOR_DISCONNECT_BEFORE_UNBOND})
public void testRemoveBondLe_WhenConnected() {
- registerIntentActions(
- BluetoothDevice.ACTION_ACL_DISCONNECTED, BluetoothDevice.ACTION_BOND_STATE_CHANGED);
+ IntentReceiver intentReceiver = new IntentReceiver.Builder(sTargetContext,
+ BluetoothDevice.ACTION_ACL_DISCONNECTED,
+ BluetoothDevice.ACTION_BOND_STATE_CHANGED)
+ .build();
- testStep_BondLe(mBumbleDevice, OwnAddressType.PUBLIC);
+ testStep_BondLe(intentReceiver, mBumbleDevice, OwnAddressType.PUBLIC);
assertThat(sAdapter.getBondedDevices()).contains(mBumbleDevice);
assertThat(mBumbleDevice.removeBond()).isTrue();
- verifyIntentReceived(
+ intentReceiver.verifyReceivedOrdered(
hasAction(BluetoothDevice.ACTION_ACL_DISCONNECTED),
hasExtra(BluetoothDevice.EXTRA_TRANSPORT, BluetoothDevice.TRANSPORT_LE),
hasExtra(BluetoothDevice.EXTRA_DEVICE, mBumbleDevice));
- verifyIntentReceived(
+ intentReceiver.verifyReceivedOrdered(
hasAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED),
hasExtra(BluetoothDevice.EXTRA_DEVICE, mBumbleDevice),
hasExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.BOND_NONE));
assertThat(sAdapter.getBondedDevices()).doesNotContain(mBumbleDevice);
- verifyNoMoreInteractions(mReceiver);
- unregisterIntentActions(
- BluetoothDevice.ACTION_ACL_DISCONNECTED, BluetoothDevice.ACTION_BOND_STATE_CHANGED);
+ intentReceiver.close();
}
/**
@@ -766,27 +658,27 @@ public class PairingTest {
@Test
@RequiresFlagsEnabled({Flags.FLAG_WAIT_FOR_DISCONNECT_BEFORE_UNBOND})
public void testRemoveBondBredr_WhenConnected() {
- registerIntentActions(
- BluetoothDevice.ACTION_ACL_DISCONNECTED, BluetoothDevice.ACTION_BOND_STATE_CHANGED);
+ IntentReceiver intentReceiver = new IntentReceiver.Builder(sTargetContext,
+ BluetoothDevice.ACTION_ACL_DISCONNECTED,
+ BluetoothDevice.ACTION_BOND_STATE_CHANGED)
+ .build();
- testStep_BondBredr();
+ testStep_BondBredr(intentReceiver);
assertThat(sAdapter.getBondedDevices()).contains(mBumbleDevice);
assertThat(mBumbleDevice.removeBond()).isTrue();
- verifyIntentReceived(
+ intentReceiver.verifyReceivedOrdered(
hasAction(BluetoothDevice.ACTION_ACL_DISCONNECTED),
hasExtra(BluetoothDevice.EXTRA_TRANSPORT, BluetoothDevice.TRANSPORT_BREDR),
hasExtra(BluetoothDevice.EXTRA_DEVICE, mBumbleDevice));
- verifyIntentReceived(
+ intentReceiver.verifyReceivedOrdered(
hasAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED),
hasExtra(BluetoothDevice.EXTRA_DEVICE, mBumbleDevice),
hasExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.BOND_NONE));
assertThat(sAdapter.getBondedDevices()).doesNotContain(mBumbleDevice);
- verifyNoMoreInteractions(mReceiver);
- unregisterIntentActions(
- BluetoothDevice.ACTION_ACL_DISCONNECTED, BluetoothDevice.ACTION_BOND_STATE_CHANGED);
+ intentReceiver.close();
}
/**
@@ -813,54 +705,51 @@ public class PairingTest {
*/
@Test
public void testRemoveBondLe_WhenDisconnected() {
- registerIntentActions(
+ IntentReceiver intentReceiver = new IntentReceiver.Builder(sTargetContext,
BluetoothDevice.ACTION_ACL_DISCONNECTED,
BluetoothDevice.ACTION_BOND_STATE_CHANGED,
- BluetoothHidHost.ACTION_CONNECTION_STATE_CHANGED);
+ BluetoothHidHost.ACTION_CONNECTION_STATE_CHANGED)
+ .build();
- testStep_BondLe(mBumbleDevice, OwnAddressType.PUBLIC);
+ testStep_BondLe(intentReceiver, mBumbleDevice, OwnAddressType.PUBLIC);
assertThat(sAdapter.getBondedDevices()).contains(mBumbleDevice);
// Wait for profiles to get connected
- verifyIntentReceived(
+ intentReceiver.verifyReceivedOrdered(
hasAction(BluetoothHidHost.ACTION_CONNECTION_STATE_CHANGED),
hasExtra(BluetoothDevice.EXTRA_DEVICE, mBumbleDevice),
hasExtra(BluetoothHidHost.EXTRA_STATE, BluetoothHidHost.STATE_CONNECTING));
- verifyIntentReceived(
+ intentReceiver.verifyReceivedOrdered(
hasAction(BluetoothHidHost.ACTION_CONNECTION_STATE_CHANGED),
hasExtra(BluetoothDevice.EXTRA_DEVICE, mBumbleDevice),
hasExtra(BluetoothHidHost.EXTRA_STATE, BluetoothHidHost.STATE_CONNECTED));
// Disconnect Bumble
assertThat(mBumbleDevice.disconnect()).isEqualTo(BluetoothStatusCodes.SUCCESS);
- verifyIntentReceived(
+ intentReceiver.verifyReceivedOrdered(
hasAction(BluetoothHidHost.ACTION_CONNECTION_STATE_CHANGED),
hasExtra(BluetoothDevice.EXTRA_DEVICE, mBumbleDevice),
hasExtra(BluetoothHidHost.EXTRA_STATE, BluetoothHidHost.STATE_DISCONNECTING));
- verifyIntentReceived(
+ intentReceiver.verifyReceivedOrdered(
hasAction(BluetoothHidHost.ACTION_CONNECTION_STATE_CHANGED),
hasExtra(BluetoothDevice.EXTRA_DEVICE, mBumbleDevice),
hasExtra(BluetoothHidHost.EXTRA_STATE, BluetoothHidHost.STATE_DISCONNECTED));
// Wait for ACL to get disconnected
- verifyIntentReceived(
+ intentReceiver.verifyReceivedOrdered(
hasAction(BluetoothDevice.ACTION_ACL_DISCONNECTED),
hasExtra(BluetoothDevice.EXTRA_TRANSPORT, BluetoothDevice.TRANSPORT_LE),
hasExtra(BluetoothDevice.EXTRA_DEVICE, mBumbleDevice));
// Remove bond
assertThat(mBumbleDevice.removeBond()).isTrue();
- verifyIntentReceived(
+ intentReceiver.verifyReceivedOrdered(
hasAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED),
hasExtra(BluetoothDevice.EXTRA_DEVICE, mBumbleDevice),
hasExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.BOND_NONE));
assertThat(sAdapter.getBondedDevices()).doesNotContain(mBumbleDevice);
- verifyNoMoreInteractions(mReceiver);
- unregisterIntentActions(
- BluetoothDevice.ACTION_ACL_DISCONNECTED,
- BluetoothDevice.ACTION_BOND_STATE_CHANGED,
- BluetoothHidHost.ACTION_CONNECTION_STATE_CHANGED);
+ intentReceiver.close();
}
/**
@@ -887,10 +776,11 @@ public class PairingTest {
*/
@Test
public void testRemoveBondBredr_WhenDisconnected() {
- registerIntentActions(
+ IntentReceiver intentReceiver = new IntentReceiver.Builder(sTargetContext,
BluetoothDevice.ACTION_ACL_DISCONNECTED,
BluetoothDevice.ACTION_BOND_STATE_CHANGED,
- BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED);
+ BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED)
+ .build();
// Disable all profiles other than A2DP as profile connections take too long
assertThat(
@@ -902,15 +792,15 @@ public class PairingTest {
mBumbleDevice, BluetoothProfile.CONNECTION_POLICY_FORBIDDEN))
.isTrue();
- testStep_BondBredr();
+ testStep_BondBredr(intentReceiver);
assertThat(sAdapter.getBondedDevices()).contains(mBumbleDevice);
// Wait for profiles to get connected
- verifyIntentReceived(
+ intentReceiver.verifyReceivedOrdered(
hasAction(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED),
hasExtra(BluetoothA2dp.EXTRA_STATE, BluetoothA2dp.STATE_CONNECTING),
hasExtra(BluetoothDevice.EXTRA_DEVICE, mBumbleDevice));
- verifyIntentReceived(
+ intentReceiver.verifyReceivedOrdered(
hasAction(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED),
hasExtra(BluetoothA2dp.EXTRA_STATE, BluetoothA2dp.STATE_CONNECTED),
hasExtra(BluetoothDevice.EXTRA_DEVICE, mBumbleDevice));
@@ -921,58 +811,78 @@ public class PairingTest {
future.completeOnTimeout(null, TEST_DELAY_MS, TimeUnit.MILLISECONDS).join();
// Disconnect all profiles
assertThat(mBumbleDevice.disconnect()).isEqualTo(BluetoothStatusCodes.SUCCESS);
- verifyIntentReceived(
+ intentReceiver.verifyReceivedOrdered(
hasAction(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED),
hasExtra(BluetoothA2dp.EXTRA_STATE, BluetoothA2dp.STATE_DISCONNECTING),
hasExtra(BluetoothDevice.EXTRA_DEVICE, mBumbleDevice));
- verifyIntentReceived(
+ intentReceiver.verifyReceivedOrdered(
hasAction(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED),
hasExtra(BluetoothA2dp.EXTRA_STATE, BluetoothA2dp.STATE_DISCONNECTED),
hasExtra(BluetoothDevice.EXTRA_DEVICE, mBumbleDevice));
// Wait for the ACL to get disconnected
- verifyIntentReceived(
+ intentReceiver.verifyReceivedOrdered(
hasAction(BluetoothDevice.ACTION_ACL_DISCONNECTED),
hasExtra(BluetoothDevice.EXTRA_TRANSPORT, BluetoothDevice.TRANSPORT_BREDR),
hasExtra(BluetoothDevice.EXTRA_DEVICE, mBumbleDevice));
// Remove bond
assertThat(mBumbleDevice.removeBond()).isTrue();
- verifyIntentReceived(
+ intentReceiver.verifyReceivedOrdered(
hasAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED),
hasExtra(BluetoothDevice.EXTRA_DEVICE, mBumbleDevice),
hasExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.BOND_NONE));
assertThat(sAdapter.getBondedDevices()).doesNotContain(mBumbleDevice);
- verifyNoMoreInteractions(mReceiver);
- unregisterIntentActions(
- BluetoothDevice.ACTION_ACL_DISCONNECTED,
- BluetoothDevice.ACTION_BOND_STATE_CHANGED,
- BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED);
+ intentReceiver.close();
}
- private void testStep_BondBredr() {
- registerIntentActions(
+ /** Helper/testStep functions goes here */
+
+ /**
+ * Process of writing a helper/test_step function.
+ *
+ * 1. All the helper functions should have IntentReceiver instance passed as an
+ * argument to them (if any intents needs to be registered).
+ * 2. The caller (if a test function) can initiate a fresh instance of IntentReceiver
+ * and use it for all subsequent helper/testStep functions.
+ * 3. The helper function should first register all required intent actions through the
+ * helper -> IntentReceiver.updateNewIntentActionsInParentReceiver()
+ * which either modifies the intentReceiver instance, or creates
+ * one (if the caller has passed a `null`).
+ * 4. At the end, all functions should call `intentReceiver.close()` which either
+ * unregisters the recent actions, or frees the original instance as per the call.
+ */
+
+ private void testStep_BondBredr(IntentReceiver parentIntentReceiver) {
+ IntentReceiver intentReceiver =
+ IntentReceiver.updateNewIntentActionsInParentReceiver(
+ parentIntentReceiver,
+ sTargetContext,
BluetoothDevice.ACTION_BOND_STATE_CHANGED,
BluetoothDevice.ACTION_ACL_CONNECTED,
BluetoothDevice.ACTION_PAIRING_REQUEST);
StreamObserver<PairingEventAnswer> pairingEventAnswerObserver =
mBumble.security()
- .withDeadlineAfter(BOND_INTENT_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS)
+ .withDeadlineAfter(BOND_INTENT_TIMEOUT.toMillis(),
+ TimeUnit.MILLISECONDS)
.onPairing(mPairingEventStreamObserver);
- assertThat(mBumbleDevice.createBond(BluetoothDevice.TRANSPORT_BREDR)).isTrue();
+ assertThat(mBumbleDevice.createBond(BluetoothDevice.TRANSPORT_BREDR)).
+ isTrue();
- verifyIntentReceivedUnordered(
+ intentReceiver.verifyReceived(
hasAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED),
hasExtra(BluetoothDevice.EXTRA_DEVICE, mBumbleDevice),
- hasExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.BOND_BONDING));
- verifyIntentReceived(
+ hasExtra(BluetoothDevice.EXTRA_BOND_STATE,
+ BluetoothDevice.BOND_BONDING));
+ intentReceiver.verifyReceivedOrdered(
hasAction(BluetoothDevice.ACTION_ACL_CONNECTED),
hasExtra(BluetoothDevice.EXTRA_DEVICE, mBumbleDevice),
- hasExtra(BluetoothDevice.EXTRA_TRANSPORT, BluetoothDevice.TRANSPORT_BREDR));
- verifyIntentReceivedUnordered(
+ hasExtra(BluetoothDevice.EXTRA_TRANSPORT,
+ BluetoothDevice.TRANSPORT_BREDR));
+ intentReceiver.verifyReceived(
hasAction(BluetoothDevice.ACTION_PAIRING_REQUEST),
hasExtra(BluetoothDevice.EXTRA_DEVICE, mBumbleDevice),
hasExtra(
@@ -985,18 +895,18 @@ public class PairingTest {
PairingEvent pairingEvent = mPairingEventStreamObserver.iterator().next();
assertThat(pairingEvent.hasJustWorks()).isTrue();
pairingEventAnswerObserver.onNext(
- PairingEventAnswer.newBuilder().setEvent(pairingEvent).setConfirm(true).build());
+ PairingEventAnswer.newBuilder().setEvent(pairingEvent)
+ .setConfirm(true).build());
// Ensure that pairing succeeds
- verifyIntentReceived(
+ intentReceiver.verifyReceivedOrdered(
hasAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED),
hasExtra(BluetoothDevice.EXTRA_DEVICE, mBumbleDevice),
- hasExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.BOND_BONDED));
+ hasExtra(BluetoothDevice.EXTRA_BOND_STATE,
+ BluetoothDevice.BOND_BONDED));
- unregisterIntentActions(
- BluetoothDevice.ACTION_BOND_STATE_CHANGED,
- BluetoothDevice.ACTION_ACL_CONNECTED,
- BluetoothDevice.ACTION_PAIRING_REQUEST);
+ /* Unregisters all intent actions registered in this function */
+ intentReceiver.close();
}
private void testStep_restartBt() {
@@ -1006,9 +916,13 @@ public class PairingTest {
/* Starts outgoing GATT service discovery and incoming LE pairing in parallel */
private StreamObserverSpliterator<SecureResponse>
- helper_OutgoingGattServiceDiscoveryWithIncomingLePairing() {
- // Setup intent filters
- registerIntentActions(
+ helper_OutgoingGattServiceDiscoveryWithIncomingLePairing(
+ IntentReceiver parentIntentReceiver) {
+ // Register new actions specific to this helper function
+ IntentReceiver intentReceiver =
+ IntentReceiver.updateNewIntentActionsInParentReceiver(
+ parentIntentReceiver,
+ sTargetContext,
BluetoothDevice.ACTION_BOND_STATE_CHANGED,
BluetoothDevice.ACTION_PAIRING_REQUEST,
BluetoothDevice.ACTION_UUID,
@@ -1027,7 +941,8 @@ public class PairingTest {
}
// Start GATT service discovery, this will establish LE ACL
- assertThat(mBumbleDevice.fetchUuidsWithSdp(BluetoothDevice.TRANSPORT_LE)).isTrue();
+ assertThat(mBumbleDevice.fetchUuidsWithSdp(BluetoothDevice.TRANSPORT_LE))
+ .isTrue();
// Make Bumble connectable
AdvertiseResponse advertiseResponse =
@@ -1041,12 +956,13 @@ public class PairingTest {
.next();
// Todo: Unexpected empty ACTION_UUID intent is generated
- verifyIntentReceivedUnordered(hasAction(BluetoothDevice.ACTION_UUID));
+ intentReceiver.verifyReceived(hasAction(BluetoothDevice.ACTION_UUID));
// Wait for connection on Android
- verifyIntentReceivedUnordered(
+ intentReceiver.verifyReceived(
hasAction(BluetoothDevice.ACTION_ACL_CONNECTED),
- hasExtra(BluetoothDevice.EXTRA_TRANSPORT, BluetoothDevice.TRANSPORT_LE));
+ hasExtra(BluetoothDevice.EXTRA_TRANSPORT,
+ BluetoothDevice.TRANSPORT_LE));
// Start pairing from Bumble
StreamObserverSpliterator<SecureResponse> responseObserver =
@@ -1061,11 +977,12 @@ public class PairingTest {
// Wait for incoming pairing notification on Android
// TODO: Order of these events is not deterministic
- verifyIntentReceivedUnordered(
+ intentReceiver.verifyReceived(
hasAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED),
hasExtra(BluetoothDevice.EXTRA_DEVICE, mBumbleDevice),
- hasExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.BOND_BONDING));
- verifyIntentReceivedUnordered(
+ hasExtra(BluetoothDevice.EXTRA_BOND_STATE,
+ BluetoothDevice.BOND_BONDING));
+ intentReceiver.verifyReceived(
hasAction(BluetoothDevice.ACTION_PAIRING_REQUEST),
hasExtra(BluetoothDevice.EXTRA_DEVICE, mBumbleDevice),
hasExtra(
@@ -1076,7 +993,7 @@ public class PairingTest {
assertThat(mBumbleDevice.setPairingConfirmation(true)).isTrue();
// Wait for pairing approval notification on Android
- verifyIntentReceivedUnordered(
+ intentReceiver.verifyReceived(
2,
hasAction(BluetoothDevice.ACTION_PAIRING_REQUEST),
hasExtra(BluetoothDevice.EXTRA_DEVICE, mBumbleDevice),
@@ -1086,134 +1003,142 @@ public class PairingTest {
// Wait for GATT service discovery to complete on Android
// so that ACTION_UUID is received here.
- verifyIntentReceivedUnordered(
+ intentReceiver.verifyReceived(
hasAction(BluetoothDevice.ACTION_UUID),
- hasExtra(BluetoothDevice.EXTRA_UUID, Matchers.hasItemInArray(BATTERY_UUID)));
-
- unregisterIntentActions(
- BluetoothDevice.ACTION_BOND_STATE_CHANGED,
- BluetoothDevice.ACTION_PAIRING_REQUEST,
- BluetoothDevice.ACTION_UUID,
- BluetoothDevice.ACTION_ACL_CONNECTED);
+ hasExtra(BluetoothDevice.EXTRA_UUID,
+ Matchers.hasItemInArray(BATTERY_UUID)));
+ intentReceiver.close();
return responseObserver;
}
- private void removeBond(BluetoothDevice device) {
- registerIntentActions(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
+ private void testStep_RemoveBond(IntentReceiver parentIntentReceiver,
+ BluetoothDevice device) {
+ IntentReceiver intentReceiver =
+ IntentReceiver.updateNewIntentActionsInParentReceiver(
+ parentIntentReceiver,
+ sTargetContext,
+ BluetoothDevice.ACTION_BOND_STATE_CHANGED);
assertThat(device.removeBond()).isTrue();
- verifyIntentReceived(
+ intentReceiver.verifyReceivedOrdered(
hasAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED),
hasExtra(BluetoothDevice.EXTRA_DEVICE, device),
- hasExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.BOND_NONE));
+ hasExtra(BluetoothDevice.EXTRA_BOND_STATE,
+ BluetoothDevice.BOND_NONE));
- unregisterIntentActions(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
+ intentReceiver.close();
}
- @SafeVarargs
- private void verifyIntentReceived(Matcher<Intent>... matchers) {
- mInOrder.verify(mReceiver, timeout(BOND_INTENT_TIMEOUT.toMillis()))
- .onReceive(any(Context.class), MockitoHamcrest.argThat(AllOf.allOf(matchers)));
+ private BluetoothProfile getProfileProxy(int profile) {
+ sAdapter.getProfileProxy(sTargetContext, mProfileServiceListener, profile);
+ ArgumentCaptor<BluetoothProfile> proxyCaptor =
+ ArgumentCaptor.forClass(BluetoothProfile.class);
+ verify(mProfileServiceListener, timeout(BOND_INTENT_TIMEOUT.toMillis()))
+ .onServiceConnected(eq(profile), proxyCaptor.capture());
+ return proxyCaptor.getValue();
}
- @SafeVarargs
- private void verifyIntentReceivedUnordered(int num, Matcher<Intent>... matchers) {
- verify(mReceiver, timeout(BOND_INTENT_TIMEOUT.toMillis()).times(num))
- .onReceive(any(Context.class), MockitoHamcrest.argThat(AllOf.allOf(matchers)));
- }
+ private void testStep_BondLe(IntentReceiver parentIntentReceiver,
+ BluetoothDevice device, OwnAddressType ownAddressType) {
+ IntentReceiver intentReceiver =
+ IntentReceiver.updateNewIntentActionsInParentReceiver(
+ parentIntentReceiver,
+ sTargetContext,
+ BluetoothDevice.ACTION_BOND_STATE_CHANGED,
+ BluetoothDevice.ACTION_ACL_CONNECTED,
+ BluetoothDevice.ACTION_PAIRING_REQUEST);
- @SafeVarargs
- private void verifyIntentReceivedUnordered(Matcher<Intent>... matchers) {
- verifyIntentReceivedUnordered(1, matchers);
- }
+ mBumble.gattBlocking()
+ .registerService(
+ GattProto.RegisterServiceRequest.newBuilder()
+ .setService(
+ GattProto.GattServiceParams.newBuilder()
+ .setUuid(BATTERY_UUID.toString())
+ .build())
+ .build());
+ mBumble.gattBlocking()
+ .registerService(
+ GattProto.RegisterServiceRequest.newBuilder()
+ .setService(
+ GattProto.GattServiceParams.newBuilder()
+ .setUuid(HOGP_UUID.toString())
+ .build())
+ .build());
- /**
- * Helper function to add reference count to registered intent actions
- *
- * @param actions new intent actions to add. If the array is empty, it is a no-op.
- */
- private void registerIntentActions(String... actions) {
- if (actions.length == 0) {
- return;
- }
- if (getTotalActionRegistrationCounts() > 0) {
- Log.d(TAG, "registerIntentActions(): unregister ALL intents");
- sTargetContext.unregisterReceiver(mReceiver);
- }
- for (String action : actions) {
- mActionRegistrationCounts.merge(action, 1, Integer::sum);
- }
- IntentFilter filter = new IntentFilter();
- mActionRegistrationCounts.entrySet().stream()
- .filter(entry -> entry.getValue() > 0)
- .forEach(
- entry -> {
- Log.d(
- TAG,
- "registerIntentActions(): Registering action = "
- + entry.getKey());
- filter.addAction(entry.getKey());
- });
- sTargetContext.registerReceiver(mReceiver, filter);
- }
+ mBumble.hostBlocking()
+ .advertise(
+ AdvertiseRequest.newBuilder()
+ .setLegacy(true)
+ .setConnectable(true)
+ .setOwnAddressType(ownAddressType)
+ .build());
- /**
- * Helper function to reduce reference count to registered intent actions If total reference
- * count is zero after removal, no broadcast receiver will be registered.
- *
- * @param actions intent actions to be removed. If some action is not registered, it is no-op
- * for that action. If the actions array is empty, it is also a no-op.
- */
- private void unregisterIntentActions(String... actions) {
- if (actions.length == 0) {
- return;
- }
- if (getTotalActionRegistrationCounts() <= 0) {
- return;
- }
- Log.d(TAG, "unregisterIntentActions(): unregister ALL intents");
- sTargetContext.unregisterReceiver(mReceiver);
- for (String action : actions) {
- if (!mActionRegistrationCounts.containsKey(action)) {
- continue;
- }
- mActionRegistrationCounts.put(action, mActionRegistrationCounts.get(action) - 1);
- if (mActionRegistrationCounts.get(action) <= 0) {
- mActionRegistrationCounts.remove(action);
- }
- }
- if (getTotalActionRegistrationCounts() > 0) {
- IntentFilter filter = new IntentFilter();
- mActionRegistrationCounts.entrySet().stream()
- .filter(entry -> entry.getValue() > 0)
- .forEach(
- entry -> {
- Log.d(
- TAG,
- "unregisterIntentActions(): Registering action = "
- + entry.getKey());
- filter.addAction(entry.getKey());
- });
- sTargetContext.registerReceiver(mReceiver, filter);
- }
- }
+ StreamObserver<PairingEventAnswer> pairingEventAnswerObserver =
+ mBumble.security()
+ .withDeadlineAfter(BOND_INTENT_TIMEOUT.toMillis(),
+ TimeUnit.MILLISECONDS)
+ .onPairing(mPairingEventStreamObserver);
- /**
- * Get sum of reference count from all registered actions
- *
- * @return sum of reference count from all registered actions
- */
- private int getTotalActionRegistrationCounts() {
- return mActionRegistrationCounts.values().stream().reduce(0, Integer::sum);
+ assertThat(device.createBond(BluetoothDevice.TRANSPORT_LE)).isTrue();
+
+ intentReceiver.verifyReceived(
+ hasAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED),
+ hasExtra(BluetoothDevice.EXTRA_DEVICE, device),
+ hasExtra(BluetoothDevice.EXTRA_BOND_STATE,
+ BluetoothDevice.BOND_BONDING));
+ intentReceiver.verifyReceivedOrdered(
+ hasAction(BluetoothDevice.ACTION_ACL_CONNECTED),
+ hasExtra(BluetoothDevice.EXTRA_DEVICE, device),
+ hasExtra(BluetoothDevice.EXTRA_TRANSPORT,
+ BluetoothDevice.TRANSPORT_LE));
+ intentReceiver.verifyReceived(
+ hasAction(BluetoothDevice.ACTION_PAIRING_REQUEST),
+ hasExtra(BluetoothDevice.EXTRA_DEVICE, device),
+ hasExtra(
+ BluetoothDevice.EXTRA_PAIRING_VARIANT,
+ BluetoothDevice.PAIRING_VARIANT_CONSENT));
+
+ // Approve pairing from Android
+ assertThat(device.setPairingConfirmation(true)).isTrue();
+
+ PairingEvent pairingEvent = mPairingEventStreamObserver.iterator().next();
+ assertThat(pairingEvent.hasJustWorks()).isTrue();
+ pairingEventAnswerObserver.onNext(
+ PairingEventAnswer.newBuilder().setEvent(pairingEvent)
+ .setConfirm(true).build());
+
+ // Ensure that pairing succeeds
+ intentReceiver.verifyReceivedOrdered(
+ hasAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED),
+ hasExtra(BluetoothDevice.EXTRA_DEVICE, device),
+ hasExtra(BluetoothDevice.EXTRA_BOND_STATE,
+ BluetoothDevice.BOND_BONDED));
+
+ intentReceiver.close();
}
- private BluetoothProfile getProfileProxy(int profile) {
- sAdapter.getProfileProxy(sTargetContext, mProfileServiceListener, profile);
- ArgumentCaptor<BluetoothProfile> proxyCaptor =
- ArgumentCaptor.forClass(BluetoothProfile.class);
- verify(mProfileServiceListener, timeout(BOND_INTENT_TIMEOUT.toMillis()))
- .onServiceConnected(eq(profile), proxyCaptor.capture());
- return proxyCaptor.getValue();
+ private void doTestIdentityAddressWithType(BluetoothDevice device,
+ OwnAddressType ownAddressType) {
+ BluetoothAddress identityAddress = device.getIdentityAddressWithType();
+ assertThat(identityAddress.getAddress()).isNull();
+ assertThat(identityAddress.getAddressType())
+ .isEqualTo(BluetoothDevice.ADDRESS_TYPE_UNKNOWN);
+
+ /*
+ * Note: Since there was no IntentReceiver registered, passing the
+ * instance as NULL. But, if there is an instance already present, that
+ * must be passed instead of NULL.
+ */
+ testStep_BondLe(null, device, ownAddressType);
+ assertThat(sAdapter.getBondedDevices()).contains(device);
+
+ identityAddress = device.getIdentityAddressWithType();
+ assertThat(identityAddress.getAddress()).isEqualTo(device.getAddress());
+ assertThat(identityAddress.getAddressType())
+ .isEqualTo(
+ ownAddressType == OwnAddressType.RANDOM
+ ? BluetoothDevice.ADDRESS_TYPE_RANDOM
+ : BluetoothDevice.ADDRESS_TYPE_PUBLIC);
}
}
diff --git a/framework/tests/bumble/src/android/bluetooth/pairing/utils/IntentReceiver.java b/framework/tests/bumble/src/android/bluetooth/pairing/utils/IntentReceiver.java
new file mode 100644
index 0000000000..ef8ab310dd
--- /dev/null
+++ b/framework/tests/bumble/src/android/bluetooth/pairing/utils/IntentReceiver.java
@@ -0,0 +1,400 @@
+/*
+ * Copyright (C) 2025 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 android.bluetooth.pairing.utils;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+
+import static java.util.Objects.requireNonNull;
+
+import android.annotation.NonNull;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.util.Log;
+
+import com.google.common.collect.Iterators;
+import org.hamcrest.Matcher;
+import org.hamcrest.core.AllOf;
+import org.mockito.InOrder;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+import org.mockito.hamcrest.MockitoHamcrest;
+
+import java.time.Duration;
+import java.util.Arrays;
+import java.util.ArrayDeque;
+import java.util.Deque;
+import java.util.Iterator;
+
+/**
+ * IntentReceiver helps in managing the Intents received through the Broadcast
+ * receiver, with specific intent actions registered.
+ * It uses Builder pattern for instance creation, and also allows setting up
+ * a custom listener's onReceive().
+ *
+ * Use the following way to create an instance of the IntentReceiver.
+ * IntentReceiver intentReceiver = new IntentReceiver.Builder(sTargetContext,
+ * BluetoothDevice.ACTION_1,
+ * BluetoothDevice.ACTION_2)
+ * .setIntentListener(--) // optional
+ * .setIntentTimeout(--) // optional
+ * .build();
+ *
+ * Ordered and unordered verification mechanisms are also provided through public methods.
+ */
+
+public class IntentReceiver {
+ private static final String TAG = IntentReceiver.class.getSimpleName();
+
+ /** Interface for listening & processing the received intents */
+ public interface IntentListener {
+ /**
+ * Callback for receiving intents
+ *
+ * @param intent Received intent
+ */
+ void onReceive(Intent intent);
+ }
+
+ @Mock private BroadcastReceiver mReceiver;
+
+ /** Intent timeout value, can be configured through constructor, or setter method */
+ private final Duration mIntentTimeout;
+
+ /** To verify the received intents in-order */
+ private final InOrder mInOrder;
+ private final Context mContext;
+ private final String[] mIntentStrings;
+ private final Deque<IntentFilter> mDqIntentFilter;
+ /*
+ * Note: Since we are using Builder pattern, also add new variables added
+ * to the Builder class
+ */
+
+ /** Listener for the received intents */
+ private final IntentListener mIntentListener;
+
+ /**
+ * Creates an Intent receiver from the builder instance
+ * Note: This is a private constructor, so always prepare IntentReceiver's
+ * instance through Builder().
+ *
+ * @param builder Pre-built builder instance
+ */
+ private IntentReceiver(Builder builder) {
+ this.mIntentTimeout = builder.mIntentTimeout;
+ this.mContext = builder.mContext;
+ this.mIntentStrings = builder.mIntentStrings;
+ this.mIntentListener = builder.mIntentListener;
+
+ /* Perform other calls required for instantiation */
+ MockitoAnnotations.initMocks(this);
+ mInOrder = inOrder(mReceiver);
+ mDqIntentFilter = new ArrayDeque<>();
+ mDqIntentFilter.addFirst(prepareIntentFilter(mIntentStrings));
+
+ setupListener();
+ registerReceiver();
+ }
+
+ /** Private constructor to avoid creation of IntentReceiver instance directly */
+ private IntentReceiver() {
+ mIntentTimeout = null;
+ mInOrder = null;
+ mContext = null;
+ mIntentStrings = null;
+ mDqIntentFilter = null;
+ mIntentListener = null;
+ }
+
+ /**
+ * Builder class which helps in avoiding overloading constructors (as the class grows)
+ * Usage:
+ * new IntentReceiver.Builder(ARGS)
+ * .setterMethods() **Optional calls, as these are default params
+ * .build();
+ */
+ public static class Builder {
+ /**
+ * Add all the instance variables from IntentReceiver,
+ * which needs to be initiated from the constructor,
+ * with either default, or user provided value.
+ */
+ private final Context mContext;
+ private final String[] mIntentStrings;
+
+ /** Non-final variables as there are setters available */
+ private Duration mIntentTimeout;
+ private IntentListener mIntentListener;
+
+ /**
+ * Private default constructor to avoid creation of Builder default
+ * instance directly as we need some instance variables to be initiated
+ * with user defined values.
+ */
+ private Builder() {
+ mContext = null;
+ mIntentStrings = null;
+ }
+
+ /**
+ * Creates a Builder instance with following required params
+ *
+ * @param context Context
+ * @param intentStrings Array of intents to filter and register
+ */
+ public Builder(@NonNull Context context, String... intentStrings) {
+ mContext = context;
+ mIntentStrings = requireNonNull(intentStrings,
+ "IntentReceiver.Builder(): Intent string cannot be null");
+
+ if (mIntentStrings.length == 0) {
+ throw new RuntimeException("IntentReceiver.Builder(): No intents to register");
+ }
+
+ /* Default values for remaining vars */
+ mIntentTimeout = Duration.ofSeconds(10);
+ mIntentListener = null;
+ }
+
+ public Builder setIntentListener(IntentListener intentListener) {
+ mIntentListener = intentListener;
+ return this;
+ }
+
+ public Builder setIntentTimeout(Duration intentTimeout) {
+ mIntentTimeout = intentTimeout;
+ return this;
+ }
+
+ /**
+ * Builds and returns the IntentReceiver object with all the passed,
+ * and default params supplied to Builder().
+ */
+ public IntentReceiver build() {
+ return new IntentReceiver(this);
+ }
+ }
+
+ /**
+ * Verifies if the intent is received in order
+ *
+ * @param matchers Matchers
+ */
+ public void verifyReceivedOrdered(Matcher<Intent>... matchers) {
+ mInOrder.verify(mReceiver, timeout(mIntentTimeout.toMillis()))
+ .onReceive(any(Context.class), MockitoHamcrest.argThat(AllOf.allOf(matchers)));
+ }
+
+ /**
+ * Verifies if requested number of intents are received (unordered)
+ *
+ * @param num Number of intents
+ * @param matchers Matchers
+ */
+ public void verifyReceived(int num, Matcher<Intent>... matchers) {
+ verify(mReceiver, timeout(mIntentTimeout.toMillis()).times(num))
+ .onReceive(any(Context.class), MockitoHamcrest.argThat(AllOf.allOf(matchers)));
+ }
+
+ /**
+ * Verifies if the intent is received (unordered)
+ *
+ * @param matchers Matchers
+ */
+ public void verifyReceived(Matcher<Intent>... matchers) {
+ verifyReceived(1, matchers);
+ }
+
+ /**
+ * This function will make sure that the instance is properly cleared
+ * based on the registered actions.
+ * Note: This function MUST be called before returning from the caller function,
+ * as this either unregisters the latest registered actions, or free resources.
+ */
+ public void close() {
+ Log.d(TAG, "close(): " + mDqIntentFilter.size());
+
+ /* More than 1 IntentFilters are present */
+ if(mDqIntentFilter.size() > 1) {
+ /*
+ * It represents there are IntentFilters present to be rolled back.
+ * So, unregister and roll back to previous IntentFilter.
+ */
+ unregisterRecentAllIntentActions();
+ }
+ else {
+ /*
+ * It represents that this close() is called in the scope of creation of
+ * the object, and hence there is only 1 IntentFilter which is present.
+ * So, we can safely close this instance.
+ */
+ verifyNoMoreInteractions();
+ unregisterReceiver();
+ }
+ }
+
+ /**
+ * Registers the new actions passed as argument.
+ * 1. Unregister the receiver, and in turn old IntentFilter.
+ * 2. Creates a new IntentFilter from the String[], and treat that as latest.
+ * 3. Registers the new IntentFilter with the receiver to the current context.
+ */
+ public void registerIntentActions(String... intentStrings) {
+ IntentFilter intentFilter = prepareIntentFilter(intentStrings);
+
+ unregisterReceiver();
+ /* Pushes the new intentFilter to top to make it the latest registered */
+ mDqIntentFilter.addFirst(intentFilter);
+ registerReceiver();
+ }
+
+ /**
+ * Helper function to register intent actions, and get the IntentReceiver
+ * instance.
+ *
+ * @param parentIntentReceiver IntentReceiver instance from the parent test caller
+ * This should be `null` if there is no parent IntentReceiver instance.
+ * @param targetContext Context instance
+ * @param intentStrings Intent actions string array
+ *
+ * This should be used to register new intent actions in a testStep
+ * function always.
+ */
+ public static IntentReceiver updateNewIntentActionsInParentReceiver(
+ IntentReceiver parentIntentReceiver, Context targetContext, String... intentStrings) {
+ /*
+ * If parentIntentReceiver is NULL, it indicates that the caller
+ * is a fresh test/testStep and a new IntentReceiver will be returned.
+ * else, update the intent actions and return the same instance.
+ */
+ // Create a new instance for the current test/testStep function.
+ if(parentIntentReceiver == null)
+ return new IntentReceiver.Builder(targetContext, intentStrings)
+ .build();
+
+ /* Update the intent actions in the parent IntentReceiver instance */
+ parentIntentReceiver.registerIntentActions(intentStrings);
+ return parentIntentReceiver;
+ }
+
+ /** Helper functions are added below, usually private */
+
+ /** Registers the listener for the received intents, and perform a custom logic as required */
+ private void setupListener() {
+ doAnswer(
+ inv -> {
+ Log.d(
+ TAG,
+ "onReceive(): intent=" +
+ Arrays.toString(inv.getArguments()));
+
+ if (mIntentListener == null) return null;
+
+ Intent intent = inv.getArgument(1);
+
+ /* Custom `onReceive` will be provided by the caller */
+ mIntentListener.onReceive(intent);
+ return null;
+ })
+ .when(mReceiver)
+ .onReceive(any(), any());
+ }
+
+ private IntentFilter prepareIntentFilter(String... intentStrings) {
+ IntentFilter intentFilter = new IntentFilter();
+ for (String intentString : intentStrings) {
+ intentFilter.addAction(intentString);
+ }
+
+ return intentFilter;
+ }
+
+ /**
+ * Registers the latest intent filter which is at the deque.peekFirst()
+ * Note: The mDqIntentFilter must not be empty here.
+ */
+ private void registerReceiver() {
+ Log.d(TAG, "registerReceiver(): Registering for intents: " +
+ getActionsFromIntentFilter(mDqIntentFilter.peekFirst()));
+
+ /* ArrayDeque should not be empty at all while registering a receiver */
+ assertThat(mDqIntentFilter.isEmpty()).isFalse();
+ mContext.registerReceiver(mReceiver,
+ (IntentFilter)mDqIntentFilter.peekFirst());
+ }
+
+ /**
+ * Unregisters the receiver from the list of active receivers.
+ * Also, we can now re-use the same receiver, or register a new
+ * receiver with the same or different intent filter, the old
+ * registration is no longer valid.
+ * Source: Intents and intent filters (Android Developers)
+ */
+ private void unregisterReceiver() {
+ Log.d(TAG, "unregisterReceiver()");
+ mContext.unregisterReceiver(mReceiver);
+ }
+
+ /** Verifies that no more intents are received */
+ private void verifyNoMoreInteractions() {
+ Log.d(TAG, "verifyNoMoreInteractions()");
+ Mockito.verifyNoMoreInteractions(mReceiver);
+ }
+
+ /**
+ * Registers the new actions passed as argument.
+ * 1. Unregister the receiver, and in turn new IntentFilter.
+ * 2. Pops the new IntentFilter to roll-back to the old one.
+ * 3. Registers the old IntentFilter with the receiver to the current context.
+ */
+ private void unregisterRecentAllIntentActions() {
+ assertThat(mDqIntentFilter.isEmpty()).isFalse();
+
+ unregisterReceiver();
+ /* Restores the previous intent filter, and discard the latest */
+ mDqIntentFilter.removeFirst();
+ registerReceiver();
+ }
+
+ /**
+ * Helper function to get the actions from the IntentFilter
+ *
+ * @param intentFilter IntentFilter instance
+ *
+ * This is a helper function to get the actions from the IntentFilter,
+ * and return as a String.
+ */
+ private String getActionsFromIntentFilter(
+ IntentFilter intentFilter) {
+ Iterator<String> iterator = intentFilter.actionsIterator();
+ StringBuilder allIntentActions = new StringBuilder();
+ while (iterator.hasNext()) {
+ allIntentActions.append(iterator.next() + ", ");
+ }
+
+ return allIntentActions.toString();
+ }
+} \ No newline at end of file
diff --git a/offload/hal/Android.bp b/offload/hal/Android.bp
new file mode 100644
index 0000000000..91324d0d94
--- /dev/null
+++ b/offload/hal/Android.bp
@@ -0,0 +1,48 @@
+// Copyright 2025, 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 {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+rust_library {
+ name: "libbluetooth_offload_hal",
+ vendor_available: true,
+ crate_name: "bluetooth_offload_hal",
+ crate_root: "lib.rs",
+ edition: "2021",
+ rustlibs: [
+ "android.hardware.bluetooth-V1-rust",
+ "libbinder_rs",
+ "libbluetooth_offload_hci",
+ "liblog_rust",
+ "liblogger",
+ ],
+ visibility: [
+ "//hardware/interfaces/bluetooth:__subpackages__",
+ "//packages/modules/Bluetooth/offload:__subpackages__",
+ ],
+}
+
+cc_library_headers {
+ name: "libbluetooth_offload_hal_headers",
+ vendor_available: true,
+ host_supported: true,
+ export_include_dirs: [
+ "include",
+ ],
+ visibility: [
+ "//hardware/interfaces/bluetooth:__subpackages__",
+ ],
+}
diff --git a/offload/hal/ffi.rs b/offload/hal/ffi.rs
new file mode 100644
index 0000000000..762e3c9d6b
--- /dev/null
+++ b/offload/hal/ffi.rs
@@ -0,0 +1,279 @@
+// Copyright 2024, 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.
+
+use core::{ffi::c_void, slice};
+use std::sync::{Mutex, RwLock};
+
+/// Callbacks from C to Rust
+/// `handle` is allocated as an `Option<T: Callbacks>`; It must be valid from the
+/// `CInterface.initialize()` call to the `CInterface.close()` call. This value
+/// is returned as the first parameter to all other functions.
+/// To prevent scheduling issues from the HAL Implementer, we enforce the validity
+/// until the end of `Ffi<T>` instance; aka until the end of process life.
+#[repr(C)]
+#[allow(dead_code)]
+pub struct CCallbacks {
+ handle: *const c_void,
+ initialization_complete: unsafe extern "C" fn(*mut c_void, CStatus),
+ event_received: unsafe extern "C" fn(*mut c_void, *const u8, usize),
+ acl_received: unsafe extern "C" fn(*mut c_void, *const u8, usize),
+ sco_received: unsafe extern "C" fn(*mut c_void, *const u8, usize),
+ iso_received: unsafe extern "C" fn(*mut c_void, *const u8, usize),
+}
+
+/// C Interface called from Rust
+/// `handle` is a pointer initialized by the C code and passed to all other functions.
+/// `callbacks` is only valid during the `initialize()` call.
+#[repr(C)]
+#[derive(Clone, Copy)]
+pub struct CInterface {
+ handle: *mut c_void,
+ initialize: unsafe extern "C" fn(handle: *mut c_void, callbacks: *const CCallbacks),
+ close: unsafe extern "C" fn(handle: *mut c_void),
+ send_command: unsafe extern "C" fn(handle: *mut c_void, data: *const u8, len: usize),
+ send_acl: unsafe extern "C" fn(handle: *mut c_void, data: *const u8, len: usize),
+ send_sco: unsafe extern "C" fn(handle: *mut c_void, data: *const u8, len: usize),
+ send_iso: unsafe extern "C" fn(handle: *mut c_void, data: *const u8, len: usize),
+}
+
+//SAFETY: CInterface is safe to send between threads because we require the C code
+// which initialises it to only use pointers to functions which are safe
+// to call from any thread.
+unsafe impl Send for CInterface {}
+
+#[repr(C)]
+#[allow(dead_code)]
+#[derive(Debug, PartialEq)]
+pub(crate) enum CStatus {
+ Success,
+ AlreadyInitialized,
+ UnableToOpenInterface,
+ HardwareInitializationError,
+ Unknown,
+}
+
+pub(crate) trait Callbacks: DataCallbacks {
+ fn initialization_complete(&self, status: CStatus);
+}
+
+pub(crate) trait DataCallbacks: Send + Sync {
+ fn event_received(&self, data: &[u8]);
+ fn acl_received(&self, data: &[u8]);
+ fn sco_received(&self, data: &[u8]);
+ fn iso_received(&self, data: &[u8]);
+}
+
+pub(crate) struct Ffi<T: Callbacks> {
+ intf: Mutex<CInterface>,
+ wrapper: RwLock<Option<T>>,
+}
+
+impl<T: Callbacks> Ffi<T> {
+ pub(crate) fn new(intf: CInterface) -> Self {
+ Self { intf: Mutex::new(intf), wrapper: RwLock::new(None) }
+ }
+
+ pub(crate) fn initialize(&self, client: T) {
+ let intf = self.intf.lock().unwrap();
+ self.set_client(client);
+
+ // SAFETY: The C Code has initialized the `CInterface` with a valid
+ // function pointer.
+ unsafe {
+ (intf.initialize)(intf.handle, &CCallbacks::new(&self.wrapper));
+ }
+ }
+
+ pub(crate) fn send_command(&self, data: &[u8]) {
+ let intf = self.intf.lock().unwrap();
+
+ // SAFETY: The C Code has initialized the `CInterface` with a valid
+ // function pointer and an initialized `handle`.
+ unsafe {
+ (intf.send_command)(intf.handle, data.as_ptr(), data.len());
+ }
+ }
+
+ pub(crate) fn send_acl(&self, data: &[u8]) {
+ let intf = self.intf.lock().unwrap();
+
+ // SAFETY: The C Code has initialized the `CInterface` with a valid
+ // function pointer and an initialized `handle`.
+ unsafe {
+ (intf.send_acl)(intf.handle, data.as_ptr(), data.len());
+ }
+ }
+
+ pub(crate) fn send_iso(&self, data: &[u8]) {
+ let intf = self.intf.lock().unwrap();
+
+ // SAFETY: The C Code has initialized the `CInterface` with a valid
+ // function pointer and an initialized `handle`.
+ unsafe {
+ (intf.send_iso)(intf.handle, data.as_ptr(), data.len());
+ }
+ }
+
+ pub(crate) fn send_sco(&self, data: &[u8]) {
+ let intf = self.intf.lock().unwrap();
+
+ // SAFETY: The C Code has initialized the `CInterface` with a valid
+ // function pointer and an initialized `handle`.
+ unsafe {
+ (intf.send_sco)(intf.handle, data.as_ptr(), data.len());
+ }
+ }
+
+ pub(crate) fn close(&self) {
+ let intf = self.intf.lock().unwrap();
+
+ // SAFETY: The C Code has initialized the `CInterface` with a valid
+ // function pointer and an initialized `handle`.
+ unsafe {
+ (intf.close)(intf.handle);
+ }
+ self.remove_client();
+ }
+
+ fn set_client(&self, client: T) {
+ *self.wrapper.write().unwrap() = Some(client);
+ }
+
+ fn remove_client(&self) {
+ *self.wrapper.write().unwrap() = None;
+ }
+}
+
+impl CCallbacks {
+ fn new<T: Callbacks>(wrapper: &RwLock<Option<T>>) -> Self {
+ Self {
+ handle: (wrapper as *const RwLock<Option<T>>).cast(),
+ initialization_complete: Self::initialization_complete::<T>,
+ event_received: Self::event_received::<T>,
+ acl_received: Self::acl_received::<T>,
+ sco_received: Self::sco_received::<T>,
+ iso_received: Self::iso_received::<T>,
+ }
+ }
+
+ /// #Safety
+ ///
+ /// `handle` must be a valid pointer previously passed to the corresponding `initialize()`,
+ /// and not yet destroyed (this is in fact an `RwLock<Option<T>>`).
+ unsafe fn unwrap_client<T: Callbacks, F: FnOnce(&T)>(handle: *mut c_void, f: F) {
+ let wrapper: *const RwLock<Option<T>> = handle.cast();
+
+ // SAFETY: The `handle` points the `RwLock<Option<T>>` wrapper object; it was allocated
+ // at the creation of the `Ffi` object and remain alive until its destruction.
+ if let Some(client) = unsafe { &*(*wrapper).read().unwrap() } {
+ f(client);
+ } else {
+ log::error!("FFI Callback called in bad state");
+ }
+ }
+
+ /// #Safety
+ ///
+ /// The C Interface requires that `handle` is a copy of the value given in `CCallbacks.handle`
+ unsafe extern "C" fn initialization_complete<T: Callbacks>(
+ handle: *mut c_void,
+ status: CStatus,
+ ) {
+ // SAFETY: The vendor HAL returns `handle` pointing `wrapper` object which has
+ // the same lifetime as the base `Ffi` instance.
+ unsafe {
+ Self::unwrap_client(handle, |client: &T| client.initialization_complete(status));
+ }
+ }
+
+ /// #Safety
+ ///
+ /// The C Interface requires that `handle` is a copy of the value given in `CCallbacks.handle`.
+ /// `data` must be a valid pointer to at least `len` bytes of memory, which remains valid and
+ /// is not mutated for the duration of this call.
+ unsafe extern "C" fn event_received<T: Callbacks>(
+ handle: *mut c_void,
+ data: *const u8,
+ len: usize,
+ ) {
+ // SAFETY: The C code returns `handle` pointing `wrapper` object which has
+ // the same lifetime as the base `Ffi` instance. `data` points to a buffer
+ // of `len` bytes valid until the function returns.
+ unsafe {
+ Self::unwrap_client(handle, |client: &T| {
+ client.event_received(slice::from_raw_parts(data, len))
+ });
+ }
+ }
+
+ /// #Safety
+ ///
+ /// The C Interface requires that `handle` is a copy of the value given in `CCallbacks.handle`.
+ /// `data` must be a valid pointer to at least `len` bytes of memory, which remains valid and
+ /// is not mutated for the duration of this call.
+ unsafe extern "C" fn acl_received<T: Callbacks>(
+ handle: *mut c_void,
+ data: *const u8,
+ len: usize,
+ ) {
+ // SAFETY: The C code returns `handle` pointing `wrapper` object which has
+ // the same lifetime as the base `Ffi` instance. `data` points to a buffer
+ // of `len` bytes valid until the function returns.
+ unsafe {
+ Self::unwrap_client(handle, |client: &T| {
+ client.acl_received(slice::from_raw_parts(data, len))
+ });
+ }
+ }
+
+ /// #Safety
+ ///
+ /// The C Interface requires that `handle` is a copy of the value given in `CCallbacks.handle`.
+ /// `data` must be a valid pointer to at least `len` bytes of memory, which remains valid and
+ /// is not mutated for the duration of this call.
+ unsafe extern "C" fn sco_received<T: Callbacks>(
+ handle: *mut c_void,
+ data: *const u8,
+ len: usize,
+ ) {
+ // SAFETY: The C code returns `handle` pointing `wrapper` object which has
+ // the same lifetime as the base `Ffi` instance. `data` points to a buffer
+ // of `len` bytes valid until the function returns.
+ unsafe {
+ Self::unwrap_client(handle, |client: &T| {
+ client.sco_received(slice::from_raw_parts(data, len))
+ });
+ }
+ }
+
+ /// #Safety
+ ///
+ /// The C Interface requires that `handle` is a copy of the value given in `CCallbacks.handle`.
+ /// `data` must be a valid pointer to at least `len` bytes of memory, which remains valid and
+ /// is not mutated for the duration of this call.
+ unsafe extern "C" fn iso_received<T: Callbacks>(
+ handle: *mut c_void,
+ data: *const u8,
+ len: usize,
+ ) {
+ // SAFETY: The C code returns `handle` pointing `wrapper` object which has
+ // the same lifetime as the base `Ffi` instance. `data` points to a buffer
+ // of `len` bytes valid until the function returns.
+ unsafe {
+ Self::unwrap_client(handle, |client: &T| {
+ client.iso_received(slice::from_raw_parts(data, len))
+ });
+ }
+ }
+}
diff --git a/offload/hal/include/hal/ffi.h b/offload/hal/include/hal/ffi.h
new file mode 100644
index 0000000000..e066c68a09
--- /dev/null
+++ b/offload/hal/include/hal/ffi.h
@@ -0,0 +1,62 @@
+/**
+ * Copyright 2024, 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.
+ */
+
+extern "C" {
+
+#include <stddef.h>
+#include <stdint.h>
+
+/**
+ * Callabcks from C to Rust
+ * The given `handle` must be passed as the first parameter of all functions.
+ * The functions can be called from `hal_interface.initialize()` call to
+ * `hal_interface.close()` call.
+ */
+
+enum HalStatus {
+ STATUS_SUCCESS,
+ STATUS_ALREADY_INITIALIZED,
+ STATUS_UNABLE_TO_OPEN_INTERFACE,
+ STATUS_HARDWARE_INITIALIZATION_ERROR,
+ STATUS_UNKNOWN,
+};
+
+struct hal_callbacks {
+ void *handle;
+ void (*initialization_complete)(const void *handle, enum HalStatus);
+ void (*event_received)(const void *handle, const uint8_t *data, size_t len);
+ void (*acl_received)(const void *handle, const uint8_t *data, size_t len);
+ void (*sco_received)(const void *handle, const uint8_t *data, size_t len);
+ void (*iso_received)(const void *handle, const uint8_t *data, size_t len);
+};
+
+/**
+ * Interface from Rust to C
+ * The `handle` value is passed as the first parameter of all functions.
+ * Theses functions can be called from different threads, but NOT concurrently.
+ * Locking over `handle` is not necessary.
+ */
+
+struct hal_interface {
+ void *handle;
+ void (*initialize)(void *handle, const struct hal_callbacks *);
+ void (*close)(void *handle);
+ void (*send_command)(void *handle, const uint8_t *data, size_t len);
+ void (*send_acl)(void *handle, const uint8_t *data, size_t len);
+ void (*send_sco)(void *handle, const uint8_t *data, size_t len);
+ void (*send_iso)(void *handle, const uint8_t *data, size_t len);
+};
+}
diff --git a/offload/hal/lib.rs b/offload/hal/lib.rs
new file mode 100644
index 0000000000..76730428fa
--- /dev/null
+++ b/offload/hal/lib.rs
@@ -0,0 +1,22 @@
+// Copyright 2024, 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.
+
+//! HCI HAL Binder implementation with proxy integration
+//! The Binder HAL interface is replicated as C Interface in `ffi` module
+
+mod ffi;
+mod service;
+
+pub use ffi::{CCallbacks, CInterface};
+pub use service::HciHalProxy;
diff --git a/offload/hal/service.rs b/offload/hal/service.rs
new file mode 100644
index 0000000000..779be6da8c
--- /dev/null
+++ b/offload/hal/service.rs
@@ -0,0 +1,245 @@
+// Copyright 2024, 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.
+
+use crate::ffi::{CInterface, CStatus, Callbacks, DataCallbacks, Ffi};
+use android_hardware_bluetooth::aidl::android::hardware::bluetooth::{
+ IBluetoothHci::IBluetoothHci, IBluetoothHciCallbacks::IBluetoothHciCallbacks, Status::Status,
+};
+use binder::{DeathRecipient, ExceptionCode, Interface, Result as BinderResult, Strong};
+use bluetooth_offload_hci::{Module, ModuleBuilder};
+use std::sync::{Arc, RwLock};
+
+/// Service Implementation of AIDL interface `hardware/interface/bluetoot/aidl`,
+/// including a proxy interface usable by third party modules.
+pub struct HciHalProxy {
+ modules: Vec<Box<dyn ModuleBuilder>>,
+ ffi: Arc<Ffi<FfiCallbacks>>,
+ state: Arc<RwLock<State>>,
+}
+
+struct FfiCallbacks {
+ callbacks: Strong<dyn IBluetoothHciCallbacks>,
+ proxy: Arc<dyn Module>,
+ state: Arc<RwLock<State>>,
+}
+
+struct SinkModule<T: Callbacks> {
+ ffi: Arc<Ffi<T>>,
+ callbacks: Strong<dyn IBluetoothHciCallbacks>,
+}
+
+enum State {
+ Closed,
+ Opening { ffi: Arc<Ffi<FfiCallbacks>>, proxy: Arc<dyn Module> },
+ Opened { proxy: Arc<dyn Module>, _death_recipient: DeathRecipient },
+}
+
+impl Interface for HciHalProxy {}
+
+impl HciHalProxy {
+ /// Create the HAL Proxy interface binded to the Bluetooth HCI HAL interface.
+ pub fn new(modules: Vec<Box<dyn ModuleBuilder>>, cintf: CInterface) -> Self {
+ Self {
+ modules,
+ ffi: Arc::new(Ffi::new(cintf)),
+ state: Arc::new(RwLock::new(State::Closed)),
+ }
+ }
+}
+
+impl IBluetoothHci for HciHalProxy {
+ fn initialize(&self, callbacks: &Strong<dyn IBluetoothHciCallbacks>) -> BinderResult<()> {
+ let (ffi, callbacks) = {
+ let mut state = self.state.write().unwrap();
+
+ if !matches!(*state, State::Closed) {
+ let _ = callbacks.initializationComplete(Status::ALREADY_INITIALIZED);
+ return Ok(());
+ }
+
+ let mut proxy: Arc<dyn Module> =
+ Arc::new(SinkModule::new(self.ffi.clone(), callbacks.clone()));
+ for m in self.modules.iter().rev() {
+ proxy = m.build(proxy);
+ }
+ let callbacks = FfiCallbacks::new(callbacks.clone(), proxy.clone(), self.state.clone());
+
+ *state = State::Opening { ffi: self.ffi.clone(), proxy: proxy.clone() };
+ (self.ffi.clone(), callbacks)
+ };
+
+ ffi.initialize(callbacks);
+ Ok(())
+ }
+
+ fn close(&self) -> BinderResult<()> {
+ *self.state.write().unwrap() = State::Closed;
+ self.ffi.close();
+ Ok(())
+ }
+
+ fn sendHciCommand(&self, data: &[u8]) -> BinderResult<()> {
+ let State::Opened { ref proxy, .. } = *self.state.read().unwrap() else {
+ return Err(ExceptionCode::ILLEGAL_STATE.into());
+ };
+
+ proxy.out_cmd(data);
+ Ok(())
+ }
+
+ fn sendAclData(&self, data: &[u8]) -> BinderResult<()> {
+ let State::Opened { ref proxy, .. } = *self.state.read().unwrap() else {
+ return Err(ExceptionCode::ILLEGAL_STATE.into());
+ };
+
+ proxy.out_acl(data);
+ Ok(())
+ }
+
+ fn sendScoData(&self, data: &[u8]) -> BinderResult<()> {
+ let State::Opened { ref proxy, .. } = *self.state.read().unwrap() else {
+ return Err(ExceptionCode::ILLEGAL_STATE.into());
+ };
+
+ proxy.out_sco(data);
+ Ok(())
+ }
+
+ fn sendIsoData(&self, data: &[u8]) -> BinderResult<()> {
+ let State::Opened { ref proxy, .. } = *self.state.read().unwrap() else {
+ return Err(ExceptionCode::ILLEGAL_STATE.into());
+ };
+
+ proxy.out_iso(data);
+ Ok(())
+ }
+}
+
+impl<T: Callbacks> SinkModule<T> {
+ pub(crate) fn new(ffi: Arc<Ffi<T>>, callbacks: Strong<dyn IBluetoothHciCallbacks>) -> Self {
+ Self { ffi, callbacks }
+ }
+}
+
+impl<T: Callbacks> Module for SinkModule<T> {
+ fn next(&self) -> &dyn Module {
+ unreachable!()
+ }
+
+ fn out_cmd(&self, data: &[u8]) {
+ self.ffi.send_command(data);
+ }
+ fn out_acl(&self, data: &[u8]) {
+ self.ffi.send_acl(data);
+ }
+ fn out_iso(&self, data: &[u8]) {
+ self.ffi.send_iso(data);
+ }
+ fn out_sco(&self, data: &[u8]) {
+ self.ffi.send_sco(data);
+ }
+
+ fn in_evt(&self, data: &[u8]) {
+ if let Err(e) = self.callbacks.hciEventReceived(data) {
+ log::error!("Cannot send event to client: {:?}", e);
+ }
+ }
+ fn in_acl(&self, data: &[u8]) {
+ if let Err(e) = self.callbacks.aclDataReceived(data) {
+ log::error!("Cannot send ACL to client: {:?}", e);
+ }
+ }
+ fn in_sco(&self, data: &[u8]) {
+ if let Err(e) = self.callbacks.scoDataReceived(data) {
+ log::error!("Cannot send SCO to client: {:?}", e);
+ }
+ }
+ fn in_iso(&self, data: &[u8]) {
+ if let Err(e) = self.callbacks.isoDataReceived(data) {
+ log::error!("Cannot send ISO to client: {:?}", e);
+ }
+ }
+}
+
+impl FfiCallbacks {
+ fn new(
+ callbacks: Strong<dyn IBluetoothHciCallbacks>,
+ proxy: Arc<dyn Module>,
+ state: Arc<RwLock<State>>,
+ ) -> Self {
+ Self { callbacks, proxy, state }
+ }
+}
+
+impl Callbacks for FfiCallbacks {
+ fn initialization_complete(&self, status: CStatus) {
+ let mut state = self.state.write().unwrap();
+ match status {
+ CStatus::Success => {
+ let State::Opening { ref ffi, ref proxy } = *state else {
+ panic!("Initialization completed called in bad state");
+ };
+
+ *state = State::Opened {
+ proxy: proxy.clone(),
+ _death_recipient: {
+ let (ffi, state) = (ffi.clone(), self.state.clone());
+ DeathRecipient::new(move || {
+ log::info!("Bluetooth stack has died");
+ *state.write().unwrap() = State::Closed;
+ ffi.close();
+ })
+ },
+ };
+ }
+
+ CStatus::AlreadyInitialized => panic!("Initialization completed called in bad state"),
+ _ => *state = State::Closed,
+ };
+
+ if let Err(e) = self.callbacks.initializationComplete(status.into()) {
+ log::error!("Cannot call-back client: {:?}", e);
+ }
+ }
+}
+
+impl DataCallbacks for FfiCallbacks {
+ fn event_received(&self, data: &[u8]) {
+ self.proxy.in_evt(data);
+ }
+
+ fn acl_received(&self, data: &[u8]) {
+ self.proxy.in_acl(data);
+ }
+
+ fn sco_received(&self, data: &[u8]) {
+ self.proxy.in_sco(data);
+ }
+
+ fn iso_received(&self, data: &[u8]) {
+ self.proxy.in_iso(data);
+ }
+}
+
+impl From<CStatus> for Status {
+ fn from(value: CStatus) -> Self {
+ match value {
+ CStatus::Success => Status::SUCCESS,
+ CStatus::AlreadyInitialized => Status::ALREADY_INITIALIZED,
+ CStatus::UnableToOpenInterface => Status::UNABLE_TO_OPEN_INTERFACE,
+ CStatus::HardwareInitializationError => Status::HARDWARE_INITIALIZATION_ERROR,
+ CStatus::Unknown => Status::UNKNOWN,
+ }
+ }
+}
diff --git a/offload/hci/Android.bp b/offload/hci/Android.bp
new file mode 100644
index 0000000000..323e9245b6
--- /dev/null
+++ b/offload/hci/Android.bp
@@ -0,0 +1,53 @@
+// Copyright 2025, 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 {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+rust_proc_macro {
+ name: "libbluetooth_offload_hci_derive",
+ crate_name: "bluetooth_offload_hci_derive",
+ crate_root: "derive/lib.rs",
+ edition: "2021",
+ rustlibs: [
+ "libproc_macro2",
+ "libquote",
+ "libsyn",
+ ],
+}
+
+rust_defaults {
+ name: "bluetooth_offload_hci_defaults",
+ crate_root: "lib.rs",
+ crate_name: "bluetooth_offload_hci",
+ edition: "2021",
+ proc_macros: [
+ "libbluetooth_offload_hci_derive",
+ ],
+ visibility: [
+ "//packages/modules/Bluetooth/offload:__subpackages__",
+ ],
+}
+
+rust_library {
+ name: "libbluetooth_offload_hci",
+ defaults: ["bluetooth_offload_hci_defaults"],
+ vendor_available: true,
+}
+
+rust_test_host {
+ name: "libbluetooth_offload_hci_test",
+ defaults: ["bluetooth_offload_hci_defaults"],
+}
diff --git a/offload/hci/command.rs b/offload/hci/command.rs
new file mode 100644
index 0000000000..a4bbe6f701
--- /dev/null
+++ b/offload/hci/command.rs
@@ -0,0 +1,544 @@
+// Copyright 2024, 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.
+
+use crate::derive::{Read, Write};
+use crate::reader::{Read, Reader};
+use crate::writer::{pack, Write, Writer};
+
+/// HCI Command, as defined in Part E - 5.4.1
+#[derive(Debug)]
+pub enum Command {
+ /// 7.3.2 Reset Command
+ Reset(Reset),
+ /// 7.8.97 LE Set CIG Parameters
+ LeSetCigParameters(LeSetCigParameters),
+ /// 7.8.99 LE Create CIS
+ LeCreateCis(LeCreateCis),
+ /// 7.8.100 LE Remove CIG
+ LeRemoveCig(LeRemoveCig),
+ /// 7.8.103 LE Create BIG
+ LeCreateBig(LeCreateBig),
+ /// 7.8.109 LE Setup ISO Data Path
+ LeSetupIsoDataPath(LeSetupIsoDataPath),
+ /// 7.8.110 LE Remove ISO Data Path
+ LeRemoveIsoDataPath(LeRemoveIsoDataPath),
+ /// Unknown command
+ Unknown(OpCode),
+}
+
+/// HCI Command Return Parameters
+#[derive(Debug, Read, Write)]
+pub enum ReturnParameters {
+ /// 7.3.2 Reset Command
+ Reset(ResetComplete),
+ /// 7.8.2 LE Read Buffer Size [V1]
+ LeReadBufferSizeV1(LeReadBufferSizeV1Complete),
+ /// 7.8.2 LE Read Buffer Size [V2]
+ LeReadBufferSizeV2(LeReadBufferSizeV2Complete),
+ /// 7.8.97 LE Set CIG Parameters
+ LeSetCigParameters(LeSetCigParametersComplete),
+ /// 7.8.100 LE Remove CIG
+ LeRemoveCig(LeRemoveCigComplete),
+ /// 7.8.109 LE Setup ISO Data Path
+ LeSetupIsoDataPath(LeIsoDataPathComplete),
+ /// 7.8.110 LE Remove ISO Data Path
+ LeRemoveIsoDataPath(LeIsoDataPathComplete),
+ /// Unknown command
+ Unknown(OpCode),
+}
+
+impl Command {
+ /// Read an HCI Command packet
+ pub fn from_bytes(data: &[u8]) -> Result<Self, Option<OpCode>> {
+ fn parse_packet(data: &[u8]) -> Option<(OpCode, Reader)> {
+ let mut r = Reader::new(data);
+ let opcode = r.read()?;
+ let len = r.read_u8()? as usize;
+ Some((opcode, Reader::new(r.get(len)?)))
+ }
+
+ let Some((opcode, mut r)) = parse_packet(data) else {
+ return Err(None);
+ };
+ Self::dispatch_read(opcode, &mut r).ok_or(Some(opcode))
+ }
+
+ fn dispatch_read(opcode: OpCode, r: &mut Reader) -> Option<Command> {
+ Some(match opcode {
+ Reset::OPCODE => Self::Reset(r.read()?),
+ LeSetCigParameters::OPCODE => Self::LeSetCigParameters(r.read()?),
+ LeCreateCis::OPCODE => Self::LeCreateCis(r.read()?),
+ LeRemoveCig::OPCODE => Self::LeRemoveCig(r.read()?),
+ LeCreateBig::OPCODE => Self::LeCreateBig(r.read()?),
+ LeSetupIsoDataPath::OPCODE => Self::LeSetupIsoDataPath(r.read()?),
+ LeRemoveIsoDataPath::OPCODE => Self::LeRemoveIsoDataPath(r.read()?),
+ opcode => Self::Unknown(opcode),
+ })
+ }
+
+ fn to_bytes<T: CommandOpCode + Write>(command: &T) -> Vec<u8> {
+ let mut w = Writer::new(Vec::with_capacity(3 + 255));
+ w.write(&T::OPCODE);
+ w.write_u8(0);
+ w.write(command);
+
+ let mut vec = w.into_vec();
+ vec[2] = (vec.len() - 3).try_into().unwrap();
+ vec
+ }
+}
+
+/// OpCode of HCI Command, as defined in Part E - 5.4.1
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub struct OpCode(u16);
+
+impl OpCode {
+ /// OpCode from OpCode Group Field (OGF) and OpCode Command Field (OCF).
+ pub const fn from(ogf: u16, ocf: u16) -> Self {
+ Self(pack!((ocf, 10), (ogf, 6)))
+ }
+}
+
+impl From<u16> for OpCode {
+ fn from(v: u16) -> Self {
+ OpCode(v)
+ }
+}
+
+impl Read for OpCode {
+ fn read(r: &mut Reader) -> Option<Self> {
+ Some(r.read_u16()?.into())
+ }
+}
+
+impl Write for OpCode {
+ fn write(&self, w: &mut Writer) {
+ w.write_u16(self.0)
+ }
+}
+
+/// Define command OpCode
+pub trait CommandOpCode {
+ /// OpCode of the command
+ const OPCODE: OpCode;
+}
+
+/// Build command from definition
+pub trait CommandToBytes: CommandOpCode + Write {
+ /// Output the HCI Command packet
+ fn to_bytes(&self) -> Vec<u8>
+ where
+ Self: Sized + CommandOpCode + Write;
+}
+
+pub use defs::*;
+
+#[allow(missing_docs)]
+#[rustfmt::skip]
+mod defs {
+
+use super::*;
+use crate::derive::CommandToBytes;
+use crate::status::*;
+
+#[cfg(test)]
+use crate::{Event, EventToBytes};
+
+
+// 7.3.2 Reset Command
+
+impl CommandOpCode for Reset {
+ const OPCODE: OpCode = OpCode::from(0x03, 0x003);
+}
+
+#[derive(Debug, Read, Write, CommandToBytes)]
+pub struct Reset {}
+
+#[derive(Debug, Read, Write)]
+pub struct ResetComplete {
+ pub status: Status,
+}
+
+#[test]
+fn test_reset() {
+ let dump = [0x03, 0x0c, 0x00];
+ let Ok(Command::Reset(c)) = Command::from_bytes(&dump) else { panic!() };
+ assert_eq!(c.to_bytes(), &dump[..]);
+}
+
+#[test]
+fn test_reset_complete() {
+ let dump = [0x0e, 0x04, 0x01, 0x03, 0x0c, 0x00];
+ let Ok(Event::CommandComplete(e)) = Event::from_bytes(&dump) else { panic!() };
+ let ReturnParameters::Reset(ref p) = e.return_parameters else { panic!() };
+ assert_eq!(p.status, Status::Success);
+ assert_eq!(e.to_bytes(), &dump[..]);
+}
+
+
+// 7.8.2 LE Read Buffer Size
+
+impl CommandOpCode for LeReadBufferSizeV1 {
+ const OPCODE: OpCode = OpCode::from(0x08, 0x002);
+}
+
+#[derive(Debug)]
+pub struct LeReadBufferSizeV1;
+
+#[derive(Debug, Read, Write)]
+pub struct LeReadBufferSizeV1Complete {
+ pub status: Status,
+ pub le_acl_data_packet_length: u16,
+ pub total_num_le_acl_data_packets: u8,
+}
+
+#[test]
+fn test_le_read_buffer_size_v1_complete() {
+ let dump = [0x0e, 0x07, 0x01, 0x02, 0x20, 0x00, 0xfb, 0x00, 0x0f];
+ let Ok(Event::CommandComplete(e)) = Event::from_bytes(&dump) else { panic!() };
+ let ReturnParameters::LeReadBufferSizeV1(ref p) = e.return_parameters else { panic!() };
+ assert_eq!(p.status, Status::Success);
+ assert_eq!(p.le_acl_data_packet_length, 251);
+ assert_eq!(p.total_num_le_acl_data_packets, 15);
+ assert_eq!(e.to_bytes(), &dump[..]);
+}
+
+impl CommandOpCode for LeReadBufferSizeV2 {
+ const OPCODE: OpCode = OpCode::from(0x08, 0x060);
+}
+
+#[derive(Debug)]
+pub struct LeReadBufferSizeV2;
+
+#[derive(Debug, Read, Write)]
+pub struct LeReadBufferSizeV2Complete {
+ pub status: Status,
+ pub le_acl_data_packet_length: u16,
+ pub total_num_le_acl_data_packets: u8,
+ pub iso_data_packet_length: u16,
+ pub total_num_iso_data_packets: u8,
+}
+
+#[test]
+fn test_le_read_buffer_size_v2_complete() {
+ let dump = [0x0e, 0x0a, 0x01, 0x60, 0x20, 0x00, 0xfb, 0x00, 0x0f, 0xfd, 0x03, 0x18];
+ let Ok(Event::CommandComplete(e)) = Event::from_bytes(&dump) else { panic!() };
+ let ReturnParameters::LeReadBufferSizeV2(ref p) = e.return_parameters else { panic!() };
+ assert_eq!(p.status, Status::Success);
+ assert_eq!(p.le_acl_data_packet_length, 251);
+ assert_eq!(p.total_num_le_acl_data_packets, 15);
+ assert_eq!(p.iso_data_packet_length, 1021);
+ assert_eq!(p.total_num_iso_data_packets, 24);
+ assert_eq!(e.to_bytes(), &dump[..]);
+}
+
+
+// 7.8.97 LE Set CIG Parameters
+
+impl CommandOpCode for LeSetCigParameters {
+ const OPCODE: OpCode = OpCode::from(0x08, 0x062);
+}
+
+#[derive(Debug, Read, Write, CommandToBytes)]
+pub struct LeSetCigParameters {
+ pub cig_id: u8,
+ #[N(3)] pub sdu_interval_c_to_p: u32,
+ #[N(3)] pub sdu_interval_p_to_c: u32,
+ pub worst_case_sca: u8,
+ pub packing: u8,
+ pub framing: u8,
+ pub max_transport_latency_c_to_p: u16,
+ pub max_transport_latency_p_to_c: u16,
+ pub cis: Vec<LeCisInCigParameters>,
+}
+
+#[derive(Debug, Read, Write)]
+pub struct LeCisInCigParameters {
+ pub cis_id: u8,
+ pub max_sdu_c_to_p: u16,
+ pub max_sdu_p_to_c: u16,
+ pub phy_c_to_p: u8,
+ pub phy_p_to_c: u8,
+ pub rtn_c_to_p: u8,
+ pub rtn_p_to_c: u8,
+}
+
+#[test]
+fn test_le_set_cig_parameters() {
+ let dump = [
+ 0x62, 0x20, 0x21, 0x01, 0x10, 0x27, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x64, 0x00, 0x05,
+ 0x00, 0x02, 0x00, 0x78, 0x00, 0x00, 0x00, 0x02, 0x03, 0x0d, 0x00, 0x01, 0x78, 0x00, 0x00, 0x00,
+ 0x02, 0x03, 0x0d, 0x00
+ ];
+ let Ok(Command::LeSetCigParameters(c)) = Command::from_bytes(&dump) else { panic!() };
+ assert_eq!(c.cig_id, 0x01);
+ assert_eq!(c.sdu_interval_c_to_p, 10_000);
+ assert_eq!(c.sdu_interval_p_to_c, 0);
+ assert_eq!(c.worst_case_sca, 1);
+ assert_eq!(c.packing, 0);
+ assert_eq!(c.framing, 0);
+ assert_eq!(c.max_transport_latency_c_to_p, 100);
+ assert_eq!(c.max_transport_latency_p_to_c, 5);
+ assert_eq!(c.cis.len(), 2);
+ assert_eq!(c.cis[0].cis_id, 0);
+ assert_eq!(c.cis[0].max_sdu_c_to_p, 120);
+ assert_eq!(c.cis[0].max_sdu_p_to_c, 0);
+ assert_eq!(c.cis[0].phy_c_to_p, 0x02);
+ assert_eq!(c.cis[0].phy_p_to_c, 0x03);
+ assert_eq!(c.cis[0].rtn_c_to_p, 13);
+ assert_eq!(c.cis[0].rtn_p_to_c, 0);
+ assert_eq!(c.cis[1].cis_id, 1);
+ assert_eq!(c.cis[1].max_sdu_c_to_p, 120);
+ assert_eq!(c.cis[1].max_sdu_p_to_c, 0);
+ assert_eq!(c.cis[1].phy_c_to_p, 0x02);
+ assert_eq!(c.cis[1].phy_p_to_c, 0x03);
+ assert_eq!(c.cis[1].rtn_c_to_p, 13);
+ assert_eq!(c.cis[1].rtn_p_to_c, 0);
+ assert_eq!(c.to_bytes(), &dump[..]);
+}
+
+#[derive(Debug, Read, Write)]
+pub struct LeSetCigParametersComplete {
+ pub status: Status,
+ pub cig_id: u8,
+ pub connection_handles: Vec<u16>,
+}
+
+#[test]
+fn test_le_set_cig_parameters_complete() {
+ let dump = [0x0e, 0x0a, 0x01, 0x62, 0x20, 0x00, 0x01, 0x02, 0x60, 0x00, 0x61, 0x00];
+ let Ok(Event::CommandComplete(e)) = Event::from_bytes(&dump) else { panic!() };
+ let ReturnParameters::LeSetCigParameters(ref p) = e.return_parameters else { panic!() };
+ assert_eq!(p.status, Status::Success);
+ assert_eq!(p.cig_id, 1);
+ assert_eq!(p.connection_handles.len(), 2);
+ assert_eq!(p.connection_handles[0], 0x60);
+ assert_eq!(p.connection_handles[1], 0x61);
+ assert_eq!(e.to_bytes(), &dump[..]);
+}
+
+
+// 7.8.99 LE Create CIS
+
+impl CommandOpCode for LeCreateCis {
+ const OPCODE: OpCode = OpCode::from(0x08, 0x064);
+}
+
+#[derive(Debug, Read, Write, CommandToBytes)]
+pub struct LeCreateCis {
+ pub connection_handles: Vec<CisAclConnectionHandle>,
+}
+
+#[derive(Debug, Read, Write)]
+pub struct CisAclConnectionHandle {
+ pub cis: u16,
+ pub acl: u16,
+}
+
+#[test]
+fn test_le_create_cis () {
+ let dump = [0x64, 0x20, 0x09, 0x02, 0x60, 0x00, 0x40, 0x00, 0x61, 0x00, 0x41, 0x00];
+ let Ok(Command::LeCreateCis(c)) = Command::from_bytes(&dump) else { panic!() };
+ assert_eq!(c.connection_handles.len(), 2);
+ assert_eq!(c.connection_handles[0].cis, 0x60);
+ assert_eq!(c.connection_handles[0].acl, 0x40);
+ assert_eq!(c.connection_handles[1].cis, 0x61);
+ assert_eq!(c.connection_handles[1].acl, 0x41);
+ assert_eq!(c.to_bytes(), &dump[..]);
+}
+
+
+// 7.8.100 LE Remove CIG
+
+impl CommandOpCode for LeRemoveCig {
+ const OPCODE: OpCode = OpCode::from(0x08, 0x065);
+}
+
+#[derive(Debug, Read, Write, CommandToBytes)]
+pub struct LeRemoveCig {
+ pub cig_id: u8,
+}
+
+#[derive(Debug, Read, Write)]
+pub struct LeRemoveCigComplete {
+ pub status: Status,
+ pub cig_id: u8,
+}
+
+#[test]
+fn test_le_remove_cig() {
+ let dump = [0x65, 0x20, 0x01, 0x01];
+ let Ok(Command::LeRemoveCig(c)) = Command::from_bytes(&dump) else { panic!() };
+ assert_eq!(c.cig_id, 0x01);
+ assert_eq!(c.to_bytes(), &dump[..]);
+}
+
+#[test]
+fn test_le_remove_cig_complete() {
+ let dump = [0x0e, 0x05, 0x01, 0x65, 0x20, 0x00, 0x01];
+ let Ok(Event::CommandComplete(e)) = Event::from_bytes(&dump) else { panic!() };
+ let ReturnParameters::LeRemoveCig(ref p) = e.return_parameters else { panic!() };
+ assert_eq!(p.status, Status::Success);
+ assert_eq!(p.cig_id, 0x01);
+ assert_eq!(e.to_bytes(), &dump[..]);
+}
+
+
+// 7.8.103 LE Create BIG
+
+impl CommandOpCode for LeCreateBig {
+ const OPCODE: OpCode = OpCode::from(0x08, 0x068);
+}
+
+#[derive(Debug, Read, Write, CommandToBytes)]
+pub struct LeCreateBig {
+ pub big_handle: u8,
+ pub advertising_handle: u8,
+ pub num_bis: u8,
+ #[N(3)] pub sdu_interval: u32,
+ pub max_sdu: u16,
+ pub max_transport_latency: u16,
+ pub rtn: u8,
+ pub phy: u8,
+ pub packing: u8,
+ pub framing: u8,
+ pub encryption: u8,
+ pub broadcast_code: [u8; 16],
+}
+
+#[test]
+fn test_le_create_big() {
+ let dump = [
+ 0x68, 0x20, 0x1f, 0x00, 0x00, 0x02, 0x10, 0x27, 0x00, 0x78, 0x00, 0x3c, 0x00, 0x04, 0x02, 0x00,
+ 0x00, 0x01, 0x31, 0x32, 0x33, 0x34, 0x31, 0x32, 0x33, 0x34, 0x31, 0x32, 0x33, 0x34, 0x31, 0x32,
+ 0x33, 0x34
+ ];
+ let Ok(Command::LeCreateBig(c)) = Command::from_bytes(&dump) else { panic!() };
+ assert_eq!(c.big_handle, 0x00);
+ assert_eq!(c.advertising_handle, 0x00);
+ assert_eq!(c.num_bis, 2);
+ assert_eq!(c.sdu_interval, 10_000);
+ assert_eq!(c.max_sdu, 120);
+ assert_eq!(c.max_transport_latency, 60);
+ assert_eq!(c.rtn, 4);
+ assert_eq!(c.phy, 0x02);
+ assert_eq!(c.packing, 0x00);
+ assert_eq!(c.framing, 0x00);
+ assert_eq!(c.encryption, 1);
+ assert_eq!(c.broadcast_code, [
+ 0x31, 0x32, 0x33, 0x34, 0x31, 0x32, 0x33, 0x34,
+ 0x31, 0x32, 0x33, 0x34, 0x31, 0x32, 0x33, 0x34
+ ]);
+ assert_eq!(c.to_bytes(), &dump[..]);
+}
+
+
+// 7.8.109 LE Setup ISO Data Path
+
+impl CommandOpCode for LeSetupIsoDataPath {
+ const OPCODE: OpCode = OpCode::from(0x08, 0x06e);
+}
+
+#[derive(Debug, Read, Write, CommandToBytes)]
+pub struct LeSetupIsoDataPath {
+ pub connection_handle: u16,
+ pub data_path_direction: LeDataPathDirection,
+ pub data_path_id: u8,
+ pub codec_id: LeCodecId,
+ #[N(3)] pub controller_delay: u32,
+ pub codec_configuration: Vec<u8>,
+}
+
+#[derive(Debug, PartialEq, Read, Write)]
+pub enum LeDataPathDirection {
+ Input = 0x00,
+ Output = 0x01,
+}
+
+#[derive(Debug, Read, Write)]
+pub struct LeCodecId {
+ pub coding_format: CodingFormat,
+ pub company_id: u16,
+ pub vendor_id: u16,
+}
+
+#[derive(Debug, PartialEq, Read, Write)]
+pub enum CodingFormat {
+ ULawLog = 0x00,
+ ALawLog = 0x01,
+ Cvsd = 0x02,
+ Transparent = 0x03,
+ LinearPcm = 0x04,
+ MSbc = 0x05,
+ Lc3 = 0x06,
+ G729A = 0x07,
+ VendorSpecific = 0xff,
+}
+
+#[derive(Debug, Read, Write)]
+pub struct LeIsoDataPathComplete {
+ pub status: Status,
+ pub connection_handle: u16,
+}
+
+#[test]
+fn test_le_setup_iso_data_path() {
+ let dump = [
+ 0x6e, 0x20, 0x0d, 0x60, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
+ ];
+ let Ok(Command::LeSetupIsoDataPath(c)) = Command::from_bytes(&dump) else { panic!() };
+ assert_eq!(c.connection_handle, 0x60);
+ assert_eq!(c.data_path_direction, LeDataPathDirection::Input);
+ assert_eq!(c.data_path_id, 0x00);
+ assert_eq!(c.codec_id.coding_format, CodingFormat::Transparent);
+ assert_eq!(c.codec_id.company_id, 0);
+ assert_eq!(c.codec_id.vendor_id, 0);
+ assert_eq!(c.controller_delay, 0);
+ assert_eq!(c.codec_configuration.len(), 0);
+ assert_eq!(c.to_bytes(), &dump[..]);
+}
+
+#[test]
+fn test_le_setup_iso_data_path_complete() {
+ let dump = [0x0e, 0x06, 0x01, 0x6e, 0x20, 0x00, 0x60, 0x00];
+ let Ok(Event::CommandComplete(e)) = Event::from_bytes(&dump) else { panic!() };
+ let ReturnParameters::LeSetupIsoDataPath(ref p) = e.return_parameters else { panic!() };
+ assert_eq!(p.status, Status::Success);
+ assert_eq!(p.connection_handle, 0x60);
+ assert_eq!(e.to_bytes(), &dump[..]);
+}
+
+
+// 7.8.110 LE Remove ISO Data Path
+
+impl CommandOpCode for LeRemoveIsoDataPath {
+ const OPCODE: OpCode = OpCode::from(0x08, 0x06f);
+}
+
+#[derive(Debug, Read, Write, CommandToBytes)]
+pub struct LeRemoveIsoDataPath {
+ pub connection_handle: u16,
+ pub data_path_direction: u8,
+}
+
+#[test]
+fn test_le_remove_iso_data_path() {
+ let dump = [0x6f, 0x20, 0x03, 0x60, 0x00, 0x01];
+ let Ok(Command::LeRemoveIsoDataPath(c)) = Command::from_bytes(&dump) else { panic!() };
+ assert_eq!(c.connection_handle, 0x60);
+ assert_eq!(c.data_path_direction, 0x01);
+ assert_eq!(c.to_bytes(), &dump[..]);
+}
+
+}
diff --git a/offload/hci/data.rs b/offload/hci/data.rs
new file mode 100644
index 0000000000..bb4a452a59
--- /dev/null
+++ b/offload/hci/data.rs
@@ -0,0 +1,204 @@
+// Copyright 2024, 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.
+
+use crate::reader::{unpack, Reader};
+use crate::writer::{pack, Write, Writer};
+
+/// 5.4.5 ISO Data Packets
+
+/// Exchange of Isochronous Data between the Host and Controller
+#[derive(Debug)]
+pub struct IsoData<'a> {
+ /// Identify the connection
+ pub connection_handle: u16,
+ /// Fragmentation of the packet
+ pub sdu_fragment: IsoSduFragment,
+ /// Payload
+ pub payload: &'a [u8],
+}
+
+/// Fragmentation indication of the SDU
+#[derive(Debug)]
+pub enum IsoSduFragment {
+ /// First SDU Fragment
+ First {
+ /// SDU Header
+ hdr: IsoSduHeader,
+ /// Last SDU fragment indication
+ is_last: bool,
+ },
+ /// Continuous fragment
+ Continue {
+ /// Last SDU fragment indication
+ is_last: bool,
+ },
+}
+
+/// SDU Header information, when ISO Data in a first SDU fragment
+#[derive(Debug, Default)]
+pub struct IsoSduHeader {
+ /// Optional timestamp in microseconds
+ pub timestamp: Option<u32>,
+ /// Sequence number of the SDU
+ pub sequence_number: u16,
+ /// Total length of the SDU (sum of all fragments)
+ pub sdu_length: u16,
+ /// Only valid from Controller, indicate valid SDU data when 0
+ pub status: u16,
+}
+
+impl<'a> IsoData<'a> {
+ /// Read an HCI ISO Data packet
+ pub fn from_bytes(data: &'a [u8]) -> Option<Self> {
+ Self::parse(&mut Reader::new(data))
+ }
+
+ /// Output the HCI ISO Data packet
+ pub fn to_bytes(&self) -> Vec<u8> {
+ let mut w = Writer::new(Vec::with_capacity(12 + self.payload.len()));
+ w.write(self);
+ w.into_vec()
+ }
+
+ /// New ISO Data packet, including a complete SDU
+ pub fn new(connection_handle: u16, sequence_number: u16, data: &'a [u8]) -> Self {
+ Self {
+ connection_handle,
+ sdu_fragment: IsoSduFragment::First {
+ hdr: IsoSduHeader {
+ sequence_number,
+ sdu_length: data.len().try_into().unwrap(),
+ ..Default::default()
+ },
+ is_last: true,
+ },
+ payload: data,
+ }
+ }
+
+ fn parse(r: &mut Reader<'a>) -> Option<Self> {
+ let (connection_handle, pb_flag, ts_present) = unpack!(r.read_u16()?, (12, 2, 1));
+ let data_len = unpack!(r.read_u16()?, 14) as usize;
+
+ let sdu_fragment = match pb_flag {
+ 0b00 => IsoSduFragment::First {
+ hdr: IsoSduHeader::parse(r, ts_present != 0)?,
+ is_last: false,
+ },
+ 0b10 => IsoSduFragment::First {
+ hdr: IsoSduHeader::parse(r, ts_present != 0)?,
+ is_last: true,
+ },
+ 0b01 => IsoSduFragment::Continue { is_last: false },
+ 0b11 => IsoSduFragment::Continue { is_last: true },
+ _ => unreachable!(),
+ };
+ let sdu_header_len = Self::sdu_header_len(&sdu_fragment);
+ if data_len < sdu_header_len {
+ return None;
+ }
+
+ Some(Self { connection_handle, sdu_fragment, payload: r.get(data_len - sdu_header_len)? })
+ }
+
+ fn sdu_header_len(sdu_fragment: &IsoSduFragment) -> usize {
+ match sdu_fragment {
+ IsoSduFragment::First { ref hdr, .. } => 4 * (1 + hdr.timestamp.is_some() as usize),
+ IsoSduFragment::Continue { .. } => 0,
+ }
+ }
+}
+
+impl Write for IsoData<'_> {
+ fn write(&self, w: &mut Writer) {
+ let (pb_flag, hdr) = match self.sdu_fragment {
+ IsoSduFragment::First { ref hdr, is_last: false } => (0b00, Some(hdr)),
+ IsoSduFragment::First { ref hdr, is_last: true } => (0b10, Some(hdr)),
+ IsoSduFragment::Continue { is_last: false } => (0b01, None),
+ IsoSduFragment::Continue { is_last: true } => (0b11, None),
+ };
+
+ let ts_present = hdr.is_some() && hdr.unwrap().timestamp.is_some();
+ w.write_u16(pack!((self.connection_handle, 12), (pb_flag, 2), ((ts_present as u16), 1)));
+
+ let packet_len = Self::sdu_header_len(&self.sdu_fragment) + self.payload.len();
+ w.write_u16(pack!(u16::try_from(packet_len).unwrap(), 14));
+
+ if let Some(hdr) = hdr {
+ w.write(hdr);
+ }
+ w.put(self.payload);
+ }
+}
+
+impl IsoSduHeader {
+ fn parse(r: &mut Reader, ts_present: bool) -> Option<Self> {
+ let timestamp = match ts_present {
+ true => Some(r.read_u32::<4>()?),
+ false => None,
+ };
+ let sequence_number = r.read_u16()?;
+ let (sdu_length, _, status) = unpack!(r.read_u16()?, (12, 2, 2));
+ Some(Self { timestamp, sequence_number, sdu_length, status })
+ }
+}
+
+impl Write for IsoSduHeader {
+ fn write(&self, w: &mut Writer) {
+ if let Some(timestamp) = self.timestamp {
+ w.write_u32::<4>(timestamp);
+ };
+ w.write_u16(self.sequence_number);
+ w.write_u16(pack!((self.sdu_length, 12), (0, 2), (self.status, 2)));
+ }
+}
+
+#[test]
+fn test_iso_data() {
+ let dump = [
+ 0x60, 0x60, 0x80, 0x00, 0x4d, 0xc8, 0xd0, 0x2f, 0x19, 0x03, 0x78, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0xe0, 0x93, 0xe5, 0x28, 0x34, 0x00, 0x00, 0x04,
+ ];
+ let Some(pkt) = IsoData::from_bytes(&dump) else { panic!() };
+ assert_eq!(pkt.connection_handle, 0x060);
+
+ let IsoSduFragment::First { ref hdr, is_last } = pkt.sdu_fragment else { panic!() };
+ assert_eq!(hdr.timestamp, Some(802_211_917));
+ assert_eq!(hdr.sequence_number, 793);
+ assert_eq!(hdr.sdu_length, 120);
+ assert!(is_last);
+
+ assert_eq!(
+ pkt.payload,
+ &[
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0xe0, 0x93, 0xe5, 0x28, 0x34, 0x00, 0x00, 0x04
+ ]
+ );
+ assert_eq!(pkt.to_bytes(), &dump[..]);
+}
diff --git a/offload/hci/derive/enum_data.rs b/offload/hci/derive/enum_data.rs
new file mode 100644
index 0000000000..c758fd756c
--- /dev/null
+++ b/offload/hci/derive/enum_data.rs
@@ -0,0 +1,95 @@
+// Copyright 2024, 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.
+
+//! Derive of `hci::reader::Read` and `hci::writer::Write` traits on an `enum`
+//!
+//! ```
+//! #[derive(Read, Write)]
+//! enum Example {
+//! FirstVariant = 0,
+//! SecondVariant = 1,
+//! }
+//! ```
+//!
+//! Produces:
+//!
+//! ```
+//! impl Read for Example {
+//! fn read(r: &mut Reader) -> Option<Self> {
+//! match r.read_u8()? {
+//! 0 => Some(Self::FirstVariant),
+//! 1 => Some(Self::SecondVariant),
+//! _ => None,
+//! }
+//! }
+//! }
+//!
+//! impl Write for Example {
+//! fn write(&self, w: &mut Writer) {
+//! w.write_u8(match self {
+//! Self::FirstVariant => 0,
+//! Self::SecondVariant => 1,
+//! })
+//! }
+//! }
+//! ```
+
+use proc_macro2::TokenStream;
+use quote::quote;
+use syn::{spanned::Spanned, Error};
+
+pub(crate) fn derive_read(name: &syn::Ident, data: &syn::DataEnum) -> Result<TokenStream, Error> {
+ let mut variants: Vec<TokenStream> = Vec::new();
+ for variant in &data.variants {
+ let ident = &variant.ident;
+ let Some((_, ref discriminant)) = variant.discriminant else {
+ return Err(Error::new(variant.span(), "Missing discriminant"));
+ };
+ variants.push(quote! { #discriminant => Some(Self::#ident) });
+ }
+ variants.push(quote! { _ => None });
+
+ Ok(quote! {
+ impl Read for #name {
+ fn read(r: &mut Reader) -> Option<Self> {
+ match r.read_u8()? {
+ #( #variants ),*
+ }
+ }
+ }
+ })
+}
+
+pub(crate) fn derive_write(name: &syn::Ident, data: &syn::DataEnum) -> Result<TokenStream, Error> {
+ let mut variants: Vec<TokenStream> = Vec::new();
+ for variant in &data.variants {
+ let ident = &variant.ident;
+ let Some((_, ref discriminant)) = variant.discriminant else {
+ return Err(Error::new(variant.span(), "Missing discriminant"));
+ };
+ variants.push(quote! { Self::#ident => #discriminant });
+ }
+
+ Ok(quote! {
+ impl Write for #name {
+ fn write(&self, w: &mut Writer) {
+ w.write_u8(
+ match self {
+ #( #variants ),*
+ }
+ )
+ }
+ }
+ })
+}
diff --git a/offload/hci/derive/lib.rs b/offload/hci/derive/lib.rs
new file mode 100644
index 0000000000..97b5d15789
--- /dev/null
+++ b/offload/hci/derive/lib.rs
@@ -0,0 +1,89 @@
+// Copyright 2024, 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.
+
+//! Derive of traits :
+//! - `hci::reader::Read`, `hci::writer::Write`
+//! - `hci::command::CommandToBytes`
+//! - `hci::event::EventToBytes`
+
+extern crate proc_macro;
+mod enum_data;
+mod return_parameters;
+mod struct_data;
+
+use proc_macro::TokenStream;
+use quote::quote;
+use syn::{DeriveInput, Error};
+
+/// Derive of `hci::reader::Read` trait
+#[proc_macro_derive(Read, attributes(N))]
+pub fn derive_read(input: TokenStream) -> TokenStream {
+ let input = syn::parse_macro_input!(input as DeriveInput);
+ let (ident, data) = (&input.ident, &input.data);
+ let expanded = match (ident.to_string().as_str(), data) {
+ ("ReturnParameters", syn::Data::Enum(ref data)) => {
+ return_parameters::derive_read(ident, data)
+ }
+ (_, syn::Data::Enum(ref data)) => enum_data::derive_read(ident, data),
+ (_, syn::Data::Struct(ref data)) => struct_data::derive_read(ident, data),
+ (_, _) => panic!("Unsupported kind of input"),
+ }
+ .unwrap_or_else(Error::into_compile_error);
+ TokenStream::from(expanded)
+}
+
+/// Derive of `hci::reader::Write` trait
+#[proc_macro_derive(Write)]
+pub fn derive_write(input: TokenStream) -> TokenStream {
+ let input = syn::parse_macro_input!(input as DeriveInput);
+ let (ident, data) = (&input.ident, &input.data);
+ let expanded = match (ident.to_string().as_str(), &data) {
+ ("ReturnParameters", syn::Data::Enum(ref data)) => {
+ return_parameters::derive_write(ident, data)
+ }
+ (_, syn::Data::Enum(ref data)) => enum_data::derive_write(ident, data),
+ (_, syn::Data::Struct(ref data)) => struct_data::derive_write(ident, data),
+ (_, _) => panic!("Unsupported kind of input"),
+ }
+ .unwrap_or_else(Error::into_compile_error);
+ TokenStream::from(expanded)
+}
+
+/// Derive of `hci::command::CommandToBytes`
+#[proc_macro_derive(CommandToBytes)]
+pub fn derive_command_to_bytes(input: TokenStream) -> TokenStream {
+ let input = syn::parse_macro_input!(input as DeriveInput);
+ let name = &input.ident;
+ TokenStream::from(quote! {
+ impl CommandToBytes for #name {
+ fn to_bytes(&self) -> Vec<u8> {
+ Command::to_bytes(self)
+ }
+ }
+ })
+}
+
+/// Derive of `hci::command::EventToBytes`
+#[proc_macro_derive(EventToBytes)]
+pub fn derive_event_to_bytes(input: TokenStream) -> TokenStream {
+ let input = syn::parse_macro_input!(input as DeriveInput);
+ let name = &input.ident;
+ TokenStream::from(quote! {
+ impl EventToBytes for #name {
+ fn to_bytes(&self) -> Vec<u8> {
+ Event::to_bytes(self)
+ }
+ }
+ })
+}
diff --git a/offload/hci/derive/return_parameters.rs b/offload/hci/derive/return_parameters.rs
new file mode 100644
index 0000000000..20baf439c5
--- /dev/null
+++ b/offload/hci/derive/return_parameters.rs
@@ -0,0 +1,116 @@
+// Copyright 2024, 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.
+
+//! Derive of `hci::reader::Read` and `hci::writer::Write` traits on
+//! `enum returnReturnParameters`.
+//!
+//! ```
+//! #[derive(Read, Write)]
+//! enum ReturnParameters {
+//! CommandOne(CommandOneComplete),
+//! CommandTwo(CommandTwoComplete),
+//! LastIsDefault(OpCode),
+//! }
+//! ```
+//!
+//! Produces:
+//!
+//! ```
+//! impl Read for ReturnParameters {
+//! fn read(r: &mut Reader) -> Option<Self> {
+//! Some(match r.read_u16()?.into() {
+//! CommandOne::OPCODE => Self::CommandOne(r.read()?),
+//! CommandTwo::OPCODE => Self::CommandTwo(r.read()?),
+//! opcode => Self::LastIsDefault(opcode),
+//! })
+//! }
+//! }
+//!
+//! impl Write for ReturnParameters {
+//! fn write(&self, w: &mut Writer) {
+//! match self {
+//! Self::CommandOne(p) => {
+//! w.write(&CommandOne::OPCODE);
+//! w.write(p);
+//! }
+//! Self::CommandTwo(p) => {
+//! w.write(&CommandOne::OPCODE);
+//! w.write(p);
+//! }
+//! Self::LastIsDefault(..) => panic!(),
+//! };
+//! }
+//! }
+//! ```
+
+use proc_macro2::TokenStream;
+use quote::quote;
+use syn::Error;
+
+pub(crate) fn derive_read(name: &syn::Ident, data: &syn::DataEnum) -> Result<TokenStream, Error> {
+ let mut variants = Vec::new();
+ for (i, variant) in data.variants.iter().enumerate() {
+ let ident = &variant.ident;
+
+ if i < data.variants.len() - 1 {
+ variants.push(quote! {
+ #ident::OPCODE => Self::#ident(r.read()?),
+ });
+ } else {
+ variants.push(quote! {
+ opcode => Self::#ident(opcode),
+ });
+ }
+ }
+
+ Ok(quote! {
+ impl Read for #name {
+ fn read(r: &mut Reader) -> Option<Self> {
+ Some(match r.read_u16()?.into() {
+ #( #variants )*
+ })
+ }
+ }
+ })
+}
+
+pub(crate) fn derive_write(name: &syn::Ident, data: &syn::DataEnum) -> Result<TokenStream, Error> {
+ let mut variants = Vec::new();
+ for (i, variant) in data.variants.iter().enumerate() {
+ let ident = &variant.ident;
+
+ if i < data.variants.len() - 1 {
+ variants.push(quote! {
+ Self::#ident(p) => {
+ w.write(&#ident::OPCODE);
+ w.write(p);
+ }
+ });
+ } else {
+ variants.push(quote! {
+ Self::#ident(..) => panic!(),
+ });
+ }
+ }
+
+ Ok(quote! {
+ impl Write for #name {
+ fn write(&self, w: &mut Writer) {
+ match self {
+ #( #variants )*
+ };
+ }
+ }
+ })
+}
diff --git a/offload/hci/derive/struct_data.rs b/offload/hci/derive/struct_data.rs
new file mode 100644
index 0000000000..757a71baec
--- /dev/null
+++ b/offload/hci/derive/struct_data.rs
@@ -0,0 +1,178 @@
+// Copyright 2024, 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.
+
+//! Derive of `hci::reader::Read` and `hci::writer::Write` traits on a `struct`
+//!
+//! ```
+//! #[derive(Read, Write)]
+//! struct Example {
+//! one_byte: u8,
+//! two_bytes: u16,
+//! #[N(3)] three_bytes: u32,
+//! #[N(4)] four_bytes: u32,
+//! bytes: [u8; 123],
+//! other: OtherType,
+//! }
+//! ```
+//!
+//! Produces:
+//!
+//! ```
+//! impl Read for Example {
+//! fn read(r: &mut Reader) -> Option<Self> {
+//! Some(Self {
+//! one_byte: r.read_u8()?,
+//! two_bytes: r.read_u16()?,
+//! three_bytes: r.read_u32::<3>()?,
+//! four_bytes: r.read_u32::<4>()?,
+//! bytes: r.read_bytes()?,
+//! other_type: r.read()?,
+//! })
+//! }
+//! }
+//!
+//! impl Write for Example {
+//! fn write(&self, w: &mut Writer) {
+//! w.write_u8(self.one_byte);
+//! w.write_u16(self.two_bytes);
+//! w.write_u32::<3>(self.three_bytes);
+//! w.write_u32::<4>(self.four_bytes);
+//! w.write_bytes(&self.bytes);
+//! w.write(&self.other_type);
+//! }
+//! }
+//! ```
+
+use proc_macro2::TokenStream;
+use quote::{quote, quote_spanned};
+use syn::{spanned::Spanned, Error};
+
+struct Attributes {
+ n: Option<usize>,
+}
+
+impl Attributes {
+ fn parse(syn_attrs: &[syn::Attribute]) -> Result<Attributes, Error> {
+ let mut n = None;
+ for attr in syn_attrs.iter() {
+ match attr {
+ attr if attr.path().is_ident("N") => {
+ let lit: syn::LitInt = attr.parse_args()?;
+ n = Some(lit.base10_parse()?);
+ }
+ attr => return Err(Error::new(attr.span(), "Unrecognized attribute")),
+ }
+ }
+ Ok(Attributes { n })
+ }
+}
+
+pub(crate) fn derive_read(name: &syn::Ident, data: &syn::DataStruct) -> Result<TokenStream, Error> {
+ let mut fields = Vec::new();
+ for field in &data.fields {
+ let ident = &field.ident.as_ref().unwrap();
+ let attrs = Attributes::parse(&field.attrs)?;
+ let fn_token = match &field.ty {
+ syn::Type::Path(v) if v.path.is_ident("u8") => {
+ if attrs.n.unwrap_or(1) != 1 {
+ return Err(Error::new(v.span(), "Expected N(1) for type `u8`"));
+ }
+ quote_spanned! { v.span() => read_u8()? }
+ }
+ syn::Type::Path(v) if v.path.is_ident("u16") => {
+ if attrs.n.unwrap_or(2) != 2 {
+ return Err(Error::new(v.span(), "Expected N(2) for type `u16`"));
+ }
+ quote_spanned! { v.span() => read_u16()? }
+ }
+ syn::Type::Path(v) if v.path.is_ident("u32") => {
+ let Some(n) = attrs.n else {
+ return Err(Error::new(v.span(), "`N()` attribute required"));
+ };
+ if n > 4 {
+ return Err(Error::new(v.span(), "Expected N(n <= 4)"));
+ }
+ quote_spanned! { v.span() => read_u32::<#n>()? }
+ }
+ syn::Type::Array(v) => match &*v.elem {
+ syn::Type::Path(v) if v.path.is_ident("u8") => {
+ quote_spanned! { v.span() => read_bytes()? }
+ }
+ _ => return Err(Error::new(v.elem.span(), "Only Byte array supported")),
+ },
+ ty => quote_spanned! { ty.span() => read()? },
+ };
+ fields.push(quote! { #ident: r.#fn_token });
+ }
+
+ Ok(quote! {
+ impl Read for #name {
+ fn read(r: &mut Reader) -> Option<Self> {
+ Some(Self {
+ #( #fields ),*
+ })
+ }
+ }
+ })
+}
+
+pub(crate) fn derive_write(
+ name: &syn::Ident,
+ data: &syn::DataStruct,
+) -> Result<TokenStream, Error> {
+ let mut fields = Vec::new();
+ for field in &data.fields {
+ let ident = &field.ident.as_ref().unwrap();
+ let attrs = Attributes::parse(&field.attrs)?;
+ let fn_token = match &field.ty {
+ syn::Type::Path(v) if v.path.is_ident("u8") => {
+ if attrs.n.unwrap_or(1) != 1 {
+ return Err(Error::new(v.span(), "Expected N(1) for type `u8`"));
+ }
+ quote_spanned! { v.span() => write_u8(self.#ident) }
+ }
+ syn::Type::Path(v) if v.path.is_ident("u16") => {
+ if attrs.n.unwrap_or(2) != 2 {
+ return Err(Error::new(v.span(), "Expected N(2) for type `u16`"));
+ }
+ quote_spanned! { v.span() => write_u16(self.#ident) }
+ }
+ syn::Type::Path(v) if v.path.is_ident("u32") => {
+ let Some(n) = attrs.n else {
+ return Err(Error::new(v.span(), "`N()` attribute required"));
+ };
+ if n > 4 {
+ return Err(Error::new(v.span(), "Expected N(n <= 4)"));
+ }
+ quote_spanned! { v.span() => write_u32::<#n>(self.#ident) }
+ }
+ syn::Type::Array(v) => match &*v.elem {
+ syn::Type::Path(v) if v.path.is_ident("u8") => {
+ quote_spanned! { v.span() => write_bytes(&self.#ident) }
+ }
+ _ => return Err(Error::new(v.elem.span(), "Only Byte array supported")),
+ },
+ ty => quote_spanned! { ty.span() => write(&self.#ident) },
+ };
+ fields.push(quote! { w.#fn_token; });
+ }
+
+ Ok(quote! {
+ impl Write for #name {
+ fn write(&self, w: &mut Writer) {
+ #( #fields )*
+ }
+ }
+ })
+}
diff --git a/offload/hci/event.rs b/offload/hci/event.rs
new file mode 100644
index 0000000000..222d0e3546
--- /dev/null
+++ b/offload/hci/event.rs
@@ -0,0 +1,343 @@
+// Copyright 2024, 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.
+
+use crate::reader::{Read, Reader};
+use crate::writer::{Write, Writer};
+
+/// HCI Event Packet, as defined in Part E - 5.4.4
+#[derive(Debug)]
+pub enum Event {
+ /// 7.7.5 Disconnection Complete
+ DisconnectionComplete(DisconnectionComplete),
+ /// 7.7.14 Command Complete
+ CommandComplete(CommandComplete),
+ /// 7.7.15 Command Status
+ CommandStatus(CommandStatus),
+ /// 7.7.19 Number Of Completed Packets
+ NumberOfCompletedPackets(NumberOfCompletedPackets),
+ /// 7.7.65.25 LE CIS Established
+ LeCisEstablished(LeCisEstablished),
+ /// 7.7.65.27 LE Create BIG Complete
+ LeCreateBigComplete(LeCreateBigComplete),
+ /// 7.7.65.28 LE Terminate BIG Complete
+ LeTerminateBigComplete(LeTerminateBigComplete),
+ /// Unknown Event
+ Unknown(Code),
+}
+
+impl Event {
+ /// Read an HCI Event packet
+ pub fn from_bytes(data: &[u8]) -> Result<Self, Option<Code>> {
+ fn parse_packet(data: &[u8]) -> Option<(Code, Reader)> {
+ let mut r = Reader::new(data);
+ let code = r.read_u8()?;
+ let len = r.read_u8()? as usize;
+
+ let mut r = Reader::new(r.get(len)?);
+ let code = match code {
+ Code::LE_META => Code(Code::LE_META, Some(r.read_u8()?)),
+ _ => Code(code, None),
+ };
+
+ Some((code, r))
+ }
+
+ let Some((code, mut r)) = parse_packet(data) else {
+ return Err(None);
+ };
+ Self::dispatch_read(code, &mut r).ok_or(Some(code))
+ }
+
+ fn dispatch_read(code: Code, r: &mut Reader) -> Option<Event> {
+ Some(match code {
+ CommandComplete::CODE => Self::CommandComplete(r.read()?),
+ CommandStatus::CODE => Self::CommandStatus(r.read()?),
+ DisconnectionComplete::CODE => Self::DisconnectionComplete(r.read()?),
+ NumberOfCompletedPackets::CODE => Self::NumberOfCompletedPackets(r.read()?),
+ LeCisEstablished::CODE => Self::LeCisEstablished(r.read()?),
+ LeCreateBigComplete::CODE => Self::LeCreateBigComplete(r.read()?),
+ LeTerminateBigComplete::CODE => Self::LeTerminateBigComplete(r.read()?),
+ code => Self::Unknown(code),
+ })
+ }
+
+ fn to_bytes<T: EventCode + Write>(event: &T) -> Vec<u8> {
+ let mut w = Writer::new(Vec::with_capacity(2 + 255));
+ w.write_u8(T::CODE.0);
+ w.write_u8(0);
+ if let Some(sub_code) = T::CODE.1 {
+ w.write_u8(sub_code)
+ }
+ w.write(event);
+
+ let mut vec = w.into_vec();
+ vec[1] = (vec.len() - 2).try_into().unwrap();
+ vec
+ }
+}
+
+/// Code of HCI Event, as defined in Part E - 5.4.4
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub struct Code(u8, Option<u8>);
+
+impl Code {
+ const LE_META: u8 = 0x3e;
+}
+
+/// Define event Code
+pub trait EventCode {
+ /// Code of the event
+ const CODE: Code;
+}
+
+/// Build event from definition
+pub trait EventToBytes: EventCode + Write {
+ /// Output the HCI Event packet
+ fn to_bytes(&self) -> Vec<u8>
+ where
+ Self: Sized + EventCode + Write;
+}
+
+pub use defs::*;
+
+#[allow(missing_docs)]
+#[rustfmt::skip]
+mod defs {
+
+use super::*;
+use crate::derive::{Read, Write, EventToBytes};
+use crate::command::{OpCode, ReturnParameters};
+use crate::status::Status;
+
+
+// 7.7.5 Disconnection Complete
+
+impl EventCode for DisconnectionComplete {
+ const CODE: Code = Code(0x05, None);
+}
+
+#[derive(Debug, Read, Write, EventToBytes)]
+pub struct DisconnectionComplete {
+ pub status: Status,
+ pub connection_handle: u16,
+ pub reason: u8,
+}
+
+#[test]
+fn test_disconnection_complete() {
+ let dump = [0x05, 0x04, 0x00, 0x60, 0x00, 0x16];
+ let Ok(Event::DisconnectionComplete(e)) = Event::from_bytes(&dump) else { panic!() };
+ assert_eq!(e.status, Status::Success);
+ assert_eq!(e.connection_handle, 0x60);
+ assert_eq!(e.reason, 0x16);
+ assert_eq!(e.to_bytes(), &dump[..]);
+}
+
+
+// 7.7.14 Command Complete
+
+impl EventCode for CommandComplete {
+ const CODE: Code = Code(0x0e, None);
+}
+
+#[derive(Debug, Read, Write, EventToBytes)]
+pub struct CommandComplete {
+ pub num_hci_command_packets: u8,
+ pub return_parameters: ReturnParameters,
+}
+
+#[test]
+fn test_command_complete() {
+ let dump = [0x0e, 0x04, 0x01, 0x03, 0x0c, 0x00];
+ let Ok(Event::CommandComplete(e)) = Event::from_bytes(&dump) else { panic!() };
+ assert_eq!(e.num_hci_command_packets, 1);
+ assert_eq!(e.to_bytes(), &dump[..]);
+}
+
+
+// 7.7.15 Command Status
+
+impl EventCode for CommandStatus {
+ const CODE: Code = Code(0x0f, None);
+}
+
+#[derive(Debug, Read, Write, EventToBytes)]
+pub struct CommandStatus {
+ pub status: Status,
+ pub num_hci_command_packets: u8,
+ pub opcode: OpCode,
+}
+
+#[test]
+fn test_command_status() {
+ let dump = [0x0f, 0x04, 0x00, 0x01, 0x01, 0x04];
+ let Ok(Event::CommandStatus(e)) = Event::from_bytes(&dump) else { panic!() };
+ assert_eq!(e.status, Status::Success);
+ assert_eq!(e.num_hci_command_packets, 1);
+ assert_eq!(e.opcode, OpCode::from(0x01, 0x001));
+ assert_eq!(e.to_bytes(), &dump[..]);
+}
+
+
+// 7.7.19 Number Of Completed Packets
+
+impl EventCode for NumberOfCompletedPackets {
+ const CODE: Code = Code(0x13, None);
+}
+
+#[derive(Debug, Read, Write, EventToBytes)]
+pub struct NumberOfCompletedPackets {
+ pub handles: Vec<NumberOfCompletedPacketsHandle>,
+}
+
+#[derive(Debug, Copy, Clone, Read, Write)]
+pub struct NumberOfCompletedPacketsHandle {
+ pub connection_handle: u16,
+ pub num_completed_packets: u16,
+}
+
+#[test]
+fn test_number_of_completed_packets() {
+ let dump = [0x13, 0x09, 0x02, 0x40, 0x00, 0x01, 0x00, 0x41, 0x00, 0x01, 0x00];
+ let Ok(Event::NumberOfCompletedPackets(e)) = Event::from_bytes(&dump) else { panic!() };
+ assert_eq!(e.handles.len(), 2);
+ assert_eq!(e.handles[0].connection_handle, 0x40);
+ assert_eq!(e.handles[0].num_completed_packets, 1);
+ assert_eq!(e.handles[1].connection_handle, 0x41);
+ assert_eq!(e.handles[1].num_completed_packets, 1);
+ assert_eq!(e.to_bytes(), &dump[..]);
+}
+
+
+// 7.7.65.25 LE CIS Established
+
+impl EventCode for LeCisEstablished {
+ const CODE: Code = Code(Code::LE_META, Some(0x19));
+}
+
+#[derive(Debug, Read, Write, EventToBytes)]
+pub struct LeCisEstablished {
+ pub status: Status,
+ pub connection_handle: u16,
+ #[N(3)] pub cig_sync_delay: u32,
+ #[N(3)] pub cis_sync_delay: u32,
+ #[N(3)] pub transport_latency_c_to_p: u32,
+ #[N(3)] pub transport_latency_p_to_c: u32,
+ pub phy_c_to_p: u8,
+ pub phy_p_to_c: u8,
+ pub nse: u8,
+ pub bn_c_to_p: u8,
+ pub bn_p_to_c: u8,
+ pub ft_c_to_p: u8,
+ pub ft_p_to_c: u8,
+ pub max_pdu_c_to_p: u16,
+ pub max_pdu_p_to_c: u16,
+ pub iso_interval: u16,
+}
+
+#[test]
+fn test_le_cis_established() {
+ let dump = [
+ 0x3e, 0x1d, 0x19, 0x00, 0x60, 0x00, 0x40, 0x2c, 0x00, 0x40, 0x2c, 0x00, 0xd0, 0x8b, 0x01, 0x60,
+ 0x7a, 0x00, 0x02, 0x02, 0x06, 0x02, 0x00, 0x05, 0x01, 0x78, 0x00, 0x00, 0x00, 0x10, 0x00 ];
+ let Ok(Event::LeCisEstablished(e)) = Event::from_bytes(&dump) else { panic!() };
+ assert_eq!(e.status, Status::Success);
+ assert_eq!(e.connection_handle, 0x60);
+ assert_eq!(e.cig_sync_delay, 11_328);
+ assert_eq!(e.cis_sync_delay, 11_328);
+ assert_eq!(e.transport_latency_c_to_p, 101_328);
+ assert_eq!(e.transport_latency_p_to_c, 31_328);
+ assert_eq!(e.phy_c_to_p, 0x02);
+ assert_eq!(e.phy_p_to_c, 0x02);
+ assert_eq!(e.nse, 6);
+ assert_eq!(e.bn_c_to_p, 2);
+ assert_eq!(e.bn_p_to_c, 0);
+ assert_eq!(e.ft_c_to_p, 5);
+ assert_eq!(e.ft_p_to_c, 1);
+ assert_eq!(e.max_pdu_c_to_p, 120);
+ assert_eq!(e.max_pdu_p_to_c, 0);
+ assert_eq!(e.iso_interval, 16);
+ assert_eq!(e.to_bytes(), &dump[..]);
+}
+
+
+// 7.7.65.27 LE Create BIG Complete
+
+impl EventCode for LeCreateBigComplete {
+ const CODE: Code = Code(Code::LE_META, Some(0x1b));
+}
+
+#[derive(Debug, Read, Write, EventToBytes)]
+pub struct LeCreateBigComplete {
+ pub status: Status,
+ pub big_handle: u8,
+ #[N(3)] pub big_sync_delay: u32,
+ #[N(3)] pub big_transport_latency: u32,
+ pub phy: u8,
+ pub nse: u8,
+ pub bn: u8,
+ pub pto: u8,
+ pub irc: u8,
+ pub max_pdu: u16,
+ pub iso_interval: u16,
+ pub bis_handles: Vec<u16>,
+}
+
+#[test]
+fn test_le_create_big_complete() {
+ let dump = [
+ 0x3e, 0x17, 0x1b, 0x00, 0x00, 0x46, 0x50, 0x00, 0x66, 0x9e, 0x00, 0x02, 0x0f, 0x03, 0x00, 0x05,
+ 0x78, 0x00, 0x18, 0x00, 0x02, 0x00, 0x04, 0x01, 0x04
+ ];
+ let Ok(Event::LeCreateBigComplete(e)) = Event::from_bytes(&dump) else { panic!() };
+ assert_eq!(e.status, Status::Success);
+ assert_eq!(e.big_handle, 0x00);
+ assert_eq!(e.big_sync_delay, 20_550);
+ assert_eq!(e.big_transport_latency, 40_550);
+ assert_eq!(e.phy, 0x02);
+ assert_eq!(e.nse, 15);
+ assert_eq!(e.bn, 3);
+ assert_eq!(e.pto, 0);
+ assert_eq!(e.irc, 5);
+ assert_eq!(e.max_pdu, 120);
+ assert_eq!(e.iso_interval, 24);
+ assert_eq!(e.bis_handles.len(), 2);
+ assert_eq!(e.bis_handles[0], 0x400);
+ assert_eq!(e.bis_handles[1], 0x401);
+ assert_eq!(e.to_bytes(), &dump[..]);
+}
+
+
+// 7.7.65.28 LE Terminate BIG Complete
+
+impl EventCode for LeTerminateBigComplete {
+ const CODE: Code = Code(Code::LE_META, Some(0x1c));
+}
+
+#[derive(Debug, Read, Write, EventToBytes)]
+pub struct LeTerminateBigComplete {
+ pub big_handle: u8,
+ pub reason: u8,
+}
+
+#[test]
+fn test_le_terminate_big_complete() {
+ let dump = [0x3e, 0x03, 0x1c, 0x00, 0x16];
+ let Ok(Event::LeTerminateBigComplete(e)) = Event::from_bytes(&dump) else { panic!() };
+ assert_eq!(e.big_handle, 0x00);
+ assert_eq!(e.reason, 0x16);
+ assert_eq!(e.to_bytes(), &dump[..]);
+}
+
+}
diff --git a/offload/hci/lib.rs b/offload/hci/lib.rs
new file mode 100644
index 0000000000..a84e538fec
--- /dev/null
+++ b/offload/hci/lib.rs
@@ -0,0 +1,78 @@
+// Copyright 2024, 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.
+
+//! HCI Proxy module implementation, along with
+//! reading / writing helpers of Bluetooth HCI Commands, Events and Data encapsulations.
+
+use std::sync::Arc;
+
+/// Interface for building a module
+pub trait ModuleBuilder: Send + Sync {
+ /// Build the module from the next module in the chain
+ fn build(&self, next_module: Arc<dyn Module>) -> Arc<dyn Module>;
+}
+
+/// Interface of a an HCI proxy module
+pub trait Module: Send + Sync {
+ /// Returns the next chained proxy module
+ fn next(&self) -> &dyn Module;
+
+ /// HCI Command from Host to Controller
+ fn out_cmd(&self, data: &[u8]) {
+ self.next().out_cmd(data);
+ }
+ /// ACL Data from Host to Controller
+ fn out_acl(&self, data: &[u8]) {
+ self.next().out_acl(data);
+ }
+ /// SCO Data from Host to Controller
+ fn out_sco(&self, data: &[u8]) {
+ self.next().out_sco(data);
+ }
+ /// ISO Data from Host to Controller
+ fn out_iso(&self, data: &[u8]) {
+ self.next().out_iso(data);
+ }
+
+ /// HCI Command from Controller to Host
+ fn in_evt(&self, data: &[u8]) {
+ self.next().in_evt(data);
+ }
+ /// ACL Data from Controller to Host
+ fn in_acl(&self, data: &[u8]) {
+ self.next().in_acl(data);
+ }
+ /// SCO Data from Controller to Host
+ fn in_sco(&self, data: &[u8]) {
+ self.next().in_sco(data);
+ }
+ /// ISO Data from Controller to Host
+ fn in_iso(&self, data: &[u8]) {
+ self.next().in_iso(data);
+ }
+}
+
+use bluetooth_offload_hci_derive as derive;
+
+mod command;
+mod data;
+mod event;
+mod reader;
+mod status;
+mod writer;
+
+pub use command::*;
+pub use data::*;
+pub use event::*;
+pub use status::*;
diff --git a/offload/hci/reader.rs b/offload/hci/reader.rs
new file mode 100644
index 0000000000..98aea61735
--- /dev/null
+++ b/offload/hci/reader.rs
@@ -0,0 +1,99 @@
+// Copyright 2024, 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.
+
+pub(crate) trait Read {
+ fn read(r: &mut Reader) -> Option<Self>
+ where
+ Self: Sized;
+}
+
+pub(crate) struct Reader<'a> {
+ data: &'a [u8],
+ pos: usize,
+}
+
+impl<'a> Reader<'a> {
+ pub(crate) fn new(data: &'a [u8]) -> Self {
+ Self { data, pos: 0 }
+ }
+
+ pub(crate) fn get(&mut self, n: usize) -> Option<&'a [u8]> {
+ if self.pos + n > self.data.len() {
+ return None;
+ }
+ let old_pos = self.pos;
+ self.pos += n;
+ Some(&self.data[old_pos..self.pos])
+ }
+
+ pub(crate) fn read<T: Read>(&mut self) -> Option<T> {
+ T::read(self)
+ }
+
+ pub(crate) fn read_u8(&mut self) -> Option<u8> {
+ Some(self.read_u32::<1>()? as u8)
+ }
+
+ pub(crate) fn read_u16(&mut self) -> Option<u16> {
+ Some(self.read_u32::<2>()? as u16)
+ }
+
+ pub(crate) fn read_u32<const N: usize>(&mut self) -> Option<u32> {
+ let data_it = self.get(N)?.iter().enumerate();
+ Some(data_it.fold(0u32, |v, (i, byte)| v | (*byte as u32) << (i * 8)))
+ }
+
+ pub(crate) fn read_bytes<const N: usize>(&mut self) -> Option<[u8; N]> {
+ Some(<[u8; N]>::try_from(self.get(N)?).unwrap())
+ }
+}
+
+impl Read for Vec<u8> {
+ fn read(r: &mut Reader) -> Option<Self> {
+ let len = r.read_u8()? as usize;
+ Some(Vec::from(r.get(len)?))
+ }
+}
+
+impl Read for Vec<u16> {
+ fn read(r: &mut Reader) -> Option<Self> {
+ let len = r.read_u8()? as usize;
+ let vec: Vec<_> = (0..len).map_while(|_| r.read_u16()).collect();
+ Some(vec).take_if(|v| v.len() == len)
+ }
+}
+
+impl<T: Read> Read for Vec<T> {
+ fn read(r: &mut Reader) -> Option<Self> {
+ let len = r.read_u8()? as usize;
+ let vec: Vec<_> = (0..len).map_while(|_| r.read()).collect();
+ Some(vec).take_if(|v| v.len() == len)
+ }
+}
+
+macro_rules! unpack {
+ ($v:expr, ($( $n:expr ),*)) => {
+ {
+ let mut _x = $v;
+ ($({
+ let y = _x & ((1 << $n) - 1);
+ _x >>= $n;
+ y
+ }),*)
+ }
+ };
+ ($v:expr, $n:expr) => { unpack!($v, ($n)) };
+}
+
+pub(crate) use unpack;
diff --git a/offload/hci/status.rs b/offload/hci/status.rs
new file mode 100644
index 0000000000..ee92d06bad
--- /dev/null
+++ b/offload/hci/status.rs
@@ -0,0 +1,95 @@
+// Copyright 2024, 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.
+
+use crate::derive::{Read, Write};
+use crate::reader::{Read, Reader};
+use crate::writer::{Write, Writer};
+
+/// Status / Error codes, as defined in Part F
+#[derive(Debug, PartialEq, Read, Write)]
+#[allow(missing_docs)]
+pub enum Status {
+ Success = 0x00,
+ UnknownHciCommand = 0x01,
+ UnknownConnectionIdentifier = 0x02,
+ HardwareFailure = 0x03,
+ PageTimeout = 0x04,
+ AuthenticationFailure = 0x05,
+ PinorKeyMissing = 0x06,
+ MemoryCapacityExceeded = 0x07,
+ ConnectionTimeout = 0x08,
+ ConnectionLimitExceeded = 0x09,
+ SynchronousConnectionLimitExceeded = 0x0A,
+ ConnectionAlreadyExists = 0x0B,
+ CommandDisallowed = 0x0C,
+ ConnectionRejectedLimitedResources = 0x0D,
+ ConnectionRejectedSecurityReasons = 0x0E,
+ ConnectionRejectedUnacceptableBdAddr = 0x0F,
+ ConnectionAcceptTimeoutExceeded = 0x10,
+ UnsupportedFeatureOrParameterValue = 0x11,
+ InvalidHciCommandParameters = 0x12,
+ RemoteUserTerminatedConnection = 0x13,
+ RemoteDeviceTerminatedConnectionLowResources = 0x14,
+ RemoteDeviceTerminatedConnectionPowerOff = 0x15,
+ ConnectionTerminatedByLocalHost = 0x16,
+ RepeatedAttempts = 0x17,
+ PairingNotAllowed = 0x18,
+ UnknownLmpPdu = 0x19,
+ UnsupportedRemoteFeature = 0x1A,
+ ScoOffsetRejected = 0x1B,
+ ScoIntervalRejected = 0x1C,
+ ScoAirModeRejected = 0x1D,
+ InvalidLmpParameters = 0x1E,
+ UnspecifiedError = 0x1F,
+ UnsupportedLmpParameterValue = 0x20,
+ RoleChangeNotAllowed = 0x21,
+ LmpResponseTimeout = 0x22,
+ LmpErrorTransactionCollision = 0x23,
+ LmpPduNotAllowed = 0x24,
+ EncryptionModeNotAcceptable = 0x25,
+ LinkKeyCannotBeChanged = 0x26,
+ RequestedQosNotSupported = 0x27,
+ InstantPassed = 0x28,
+ PairingWithUnitKeyNotSupported = 0x29,
+ DifferentTransactionCollision = 0x2A,
+ ReservedForUse2B = 0x2B,
+ QosUnacceptableParameter = 0x2C,
+ QosRejected = 0x2D,
+ ChannelClassificationNotSupported = 0x2E,
+ InsufficientSecurity = 0x2F,
+ ParameterOutOfMandatoryRange = 0x30,
+ ReservedForUse31 = 0x31,
+ RoleSwitchPending = 0x32,
+ ReservedForUse33 = 0x33,
+ ReservedSlotViolation = 0x34,
+ RoleSwitchFailed = 0x35,
+ ExtendedInquiryResponseTooLarge = 0x36,
+ SecureSimplePairingNotSupportedByHost = 0x37,
+ HostBusy = 0x38,
+ ConnectionRejectedNoSuitableChannelFound = 0x39,
+ ControllerBusy = 0x3A,
+ UnacceptableConnectionParameters = 0x3B,
+ AdvertisingTimeout = 0x3C,
+ ConnectionTerminatedMicFailure = 0x3D,
+ ConnectionFailedEstablished = 0x3E,
+ PreviouslyUsed3F = 0x3F,
+ CoarseClockAdjustmentRejected = 0x40,
+ Type0SubmapNotDefined = 0x41,
+ UnknownAdvertisingIdentifier = 0x42,
+ LimitReached = 0x43,
+ OperationCancelledByHost = 0x44,
+ PacketTooLong = 0x45,
+ TooLate = 0x46,
+ TooEarly = 0x47,
+}
diff --git a/offload/hci/writer.rs b/offload/hci/writer.rs
new file mode 100644
index 0000000000..ca0e8e274a
--- /dev/null
+++ b/offload/hci/writer.rs
@@ -0,0 +1,103 @@
+// Copyright 2024, 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.
+
+pub trait Write {
+ fn write(&self, w: &mut Writer)
+ where
+ Self: Sized;
+}
+
+pub struct Writer {
+ vec: Vec<u8>,
+}
+
+impl Writer {
+ pub(crate) fn new(vec: Vec<u8>) -> Self {
+ Self { vec }
+ }
+
+ pub(crate) fn into_vec(self) -> Vec<u8> {
+ self.vec
+ }
+
+ pub(crate) fn put(&mut self, slice: &[u8]) {
+ self.vec.extend_from_slice(slice);
+ }
+
+ pub(crate) fn write<T: Write>(&mut self, v: &T) {
+ v.write(self)
+ }
+
+ pub(crate) fn write_u8(&mut self, v: u8) {
+ self.write_u32::<1>(v.into());
+ }
+
+ pub(crate) fn write_u16(&mut self, v: u16) {
+ self.write_u32::<2>(v.into());
+ }
+
+ pub(crate) fn write_u32<const N: usize>(&mut self, mut v: u32) {
+ for _ in 0..N {
+ self.vec.push((v & 0xff) as u8);
+ v >>= 8;
+ }
+ }
+
+ pub(crate) fn write_bytes<const N: usize>(&mut self, bytes: &[u8; N]) {
+ self.put(bytes);
+ }
+}
+
+impl Write for Vec<u8> {
+ fn write(&self, w: &mut Writer) {
+ w.write_u8(self.len().try_into().unwrap());
+ w.put(self);
+ }
+}
+
+impl Write for Vec<u16> {
+ fn write(&self, w: &mut Writer) {
+ w.write_u8(self.len().try_into().unwrap());
+ for item in self {
+ w.write_u16(*item);
+ }
+ }
+}
+
+impl<T: Write> Write for Vec<T> {
+ fn write(&self, w: &mut Writer) {
+ w.write_u8(self.len().try_into().unwrap());
+ for item in self {
+ w.write(item);
+ }
+ }
+}
+
+macro_rules! pack {
+ ( $( ($x:expr, $n:expr) ),* ) => {
+ {
+ let mut y = 0;
+ let mut _shl = 0;
+ $(
+ assert!($x & !((1 << $n) - 1) == 0);
+ y |= ($x << _shl);
+ _shl += $n;
+ )*
+ y
+ }
+ };
+ ( $x:expr, $n:expr ) => { pack!(($x, $n)) };
+}
+
+pub(crate) use pack;
diff --git a/offload/leaudio/aidl/Android.bp b/offload/leaudio/aidl/Android.bp
new file mode 100644
index 0000000000..1e904d2a64
--- /dev/null
+++ b/offload/leaudio/aidl/Android.bp
@@ -0,0 +1,33 @@
+// Copyright 2025, 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 {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+aidl_interface {
+ name: "android.hardware.bluetooth.offload.leaudio",
+ vendor_available: true,
+ unstable: true,
+ srcs: ["android/hardware/bluetooth/offload/leaudio/*.aidl"],
+ backend: {
+ rust: {
+ enabled: true,
+ },
+ },
+ visibility: [
+ "//packages/modules/Bluetooth/offload/leaudio:__subpackages__",
+ "//system/tools/aidl/build",
+ ],
+}
diff --git a/offload/leaudio/aidl/android/hardware/bluetooth/offload/leaudio/IHciProxy.aidl b/offload/leaudio/aidl/android/hardware/bluetooth/offload/leaudio/IHciProxy.aidl
new file mode 100644
index 0000000000..a9a1289181
--- /dev/null
+++ b/offload/leaudio/aidl/android/hardware/bluetooth/offload/leaudio/IHciProxy.aidl
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2024 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 android.hardware.bluetooth.offload.leaudio;
+
+import android.hardware.bluetooth.offload.leaudio.IHciProxyCallbacks;
+
+/**
+ * Interface of the HCI-Proxy module
+ */
+interface IHciProxy {
+
+ /**
+ * Register callbacks for events in the other direction
+ */
+ void registerCallbacks(in IHciProxyCallbacks callbacks);
+
+ /**
+ * Send an ISO packet on a `handle`
+ */
+ oneway void sendPacket(in int handle, in int sequence_number, in byte[] data);
+}
diff --git a/offload/leaudio/aidl/android/hardware/bluetooth/offload/leaudio/IHciProxyCallbacks.aidl b/offload/leaudio/aidl/android/hardware/bluetooth/offload/leaudio/IHciProxyCallbacks.aidl
new file mode 100644
index 0000000000..619b22b9ea
--- /dev/null
+++ b/offload/leaudio/aidl/android/hardware/bluetooth/offload/leaudio/IHciProxyCallbacks.aidl
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2024 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 android.hardware.bluetooth.offload.leaudio;
+
+import android.hardware.bluetooth.offload.leaudio.StreamConfiguration;
+
+/**
+ * Interface from HCI-Proxy module to the audio module
+ */
+interface IHciProxyCallbacks {
+
+ /**
+ * Start indication of a stream
+ */
+ void startStream(in int handle, in StreamConfiguration configuration);
+
+ /**
+ * Stop indication of a stream
+ */
+ void stopStream(in int handle);
+}
diff --git a/offload/leaudio/aidl/android/hardware/bluetooth/offload/leaudio/StreamConfiguration.aidl b/offload/leaudio/aidl/android/hardware/bluetooth/offload/leaudio/StreamConfiguration.aidl
new file mode 100644
index 0000000000..944facb2ef
--- /dev/null
+++ b/offload/leaudio/aidl/android/hardware/bluetooth/offload/leaudio/StreamConfiguration.aidl
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2024 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 android.hardware.bluetooth.offload.leaudio;
+
+/**
+ * Configuration relative to a stream
+ */
+parcelable StreamConfiguration {
+
+ /**
+ * Constant ISO time transmission interval, in micro-seconds
+ */
+ int isoIntervalUs;
+
+ /**
+ * Constant SDU transmission interval, in micro-seconds
+ */
+ int sduIntervalUs;
+
+ /**
+ * Maximum size of a SDU
+ */
+ int maxSduSize;
+
+ /**
+ * Number of PDU's transmitted by ISO-Intervals
+ */
+ int burstNumber;
+
+ /**
+ * How many consecutive Isochronous Intervals can be used to transmit a PDU
+ */
+ int flushTimeout;
+}
diff --git a/offload/leaudio/hci/Android.bp b/offload/leaudio/hci/Android.bp
new file mode 100644
index 0000000000..e4f1b51881
--- /dev/null
+++ b/offload/leaudio/hci/Android.bp
@@ -0,0 +1,46 @@
+// Copyright 2025, 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 {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+rust_defaults {
+ name: "bluetooth_offload_leaudio_hci_defaults",
+ crate_root: "lib.rs",
+ crate_name: "bluetooth_offload_leaudio_hci",
+ edition: "2021",
+ rustlibs: [
+ "android.hardware.bluetooth.offload.leaudio-rust",
+ "libbinder_rs",
+ "libbluetooth_offload_hci",
+ "liblog_rust",
+ "liblogger",
+ ],
+}
+
+rust_library {
+ name: "libbluetooth_offload_leaudio_hci",
+ defaults: ["bluetooth_offload_leaudio_hci_defaults"],
+ vendor_available: true,
+ visibility: [
+ "//hardware/interfaces/bluetooth:__subpackages__",
+ "//packages/modules/Bluetooth/offload:__subpackages__",
+ ],
+}
+
+rust_test {
+ name: "libbluetooth_offload_leaudio_hci_test",
+ defaults: ["bluetooth_offload_leaudio_hci_defaults"],
+}
diff --git a/offload/leaudio/hci/arbiter.rs b/offload/leaudio/hci/arbiter.rs
new file mode 100644
index 0000000000..072295f568
--- /dev/null
+++ b/offload/leaudio/hci/arbiter.rs
@@ -0,0 +1,154 @@
+// Copyright 2024, 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.
+
+use bluetooth_offload_hci::{IsoData, Module};
+use std::collections::{HashMap, VecDeque};
+use std::sync::{Arc, Condvar, Mutex, MutexGuard};
+use std::thread::{self, JoinHandle};
+
+pub struct Arbiter {
+ state_cvar: Arc<(Mutex<State>, Condvar)>,
+ thread: Option<JoinHandle<()>>,
+ max_buf_len: usize,
+}
+
+#[derive(Default)]
+struct State {
+ /// Halt indication of the sender thread
+ halt: bool,
+
+ /// Software transmission queues for each `Origin`.
+ /// A queue is pair of connection handle, and packet raw ISO data.
+ queues: [VecDeque<(u16, Vec<u8>)>; 2],
+
+ /// Count of packets sent to the controller and not yet acknowledged,
+ /// by connection handle stored on `u16`.
+ in_transit: HashMap<u16, usize>,
+}
+
+enum Origin {
+ Audio,
+ Incoming,
+}
+
+impl Arbiter {
+ pub fn new(sink: Arc<dyn Module>, max_buf_len: usize, max_buf_count: usize) -> Self {
+ let state_cvar = Arc::new((Mutex::<State>::new(Default::default()), Condvar::new()));
+ let thread = {
+ let state_cvar = state_cvar.clone();
+ thread::spawn(move || Self::thread_loop(state_cvar.clone(), sink, max_buf_count))
+ };
+
+ Self { state_cvar, thread: Some(thread), max_buf_len }
+ }
+
+ pub fn add_connection(&self, handle: u16) {
+ let (state, _) = &*self.state_cvar;
+ if state.lock().unwrap().in_transit.insert(handle, 0).is_some() {
+ panic!("Connection with handle 0x{:03x} already exists", handle);
+ }
+ }
+
+ pub fn remove_connection(&self, handle: u16) {
+ let (state, cvar) = &*self.state_cvar;
+ let mut state = state.lock().unwrap();
+ for q in state.queues.iter_mut() {
+ while let Some(idx) = q.iter().position(|&(h, _)| h == handle) {
+ q.remove(idx);
+ }
+ }
+ if state.in_transit.remove(&handle).is_some() {
+ cvar.notify_one();
+ }
+ }
+
+ pub fn push_incoming(&self, iso_data: &IsoData) {
+ self.push(Origin::Incoming, iso_data);
+ }
+
+ pub fn push_audio(&self, iso_data: &IsoData) {
+ self.push(Origin::Audio, iso_data);
+ }
+
+ pub fn set_completed(&self, handle: u16, num: usize) {
+ let (state, cvar) = &*self.state_cvar;
+ if let Some(buf_usage) = state.lock().unwrap().in_transit.get_mut(&handle) {
+ *buf_usage -= num;
+ cvar.notify_one();
+ }
+ }
+
+ fn push(&self, origin: Origin, iso_data: &IsoData) {
+ let handle = iso_data.connection_handle;
+ let data = iso_data.to_bytes();
+ assert!(data.len() <= self.max_buf_len + 4);
+
+ let (state, cvar) = &*self.state_cvar;
+ let mut state = state.lock().unwrap();
+ if state.in_transit.contains_key(&handle) {
+ state.queues[origin as usize].push_back((handle, data));
+ cvar.notify_one();
+ }
+ }
+
+ fn thread_loop(
+ state_cvar: Arc<(Mutex<State>, Condvar)>,
+ sink: Arc<dyn Module>,
+ max_buf_count: usize,
+ ) {
+ let (state, cvar) = &*state_cvar;
+ 'main: loop {
+ let packet = {
+ let mut state = state.lock().unwrap();
+ let mut packet = None;
+ while !state.halt && {
+ packet = Self::pull(&mut state, max_buf_count);
+ packet.is_none()
+ } {
+ state = cvar.wait(state).unwrap();
+ }
+ if state.halt {
+ break 'main;
+ }
+ packet.unwrap()
+ };
+ sink.out_iso(&packet);
+ }
+ }
+
+ fn pull(state: &mut MutexGuard<'_, State>, max_buf_count: usize) -> Option<Vec<u8>> {
+ for idx in 0..state.queues.len() {
+ if state.queues[idx].is_empty() || max_buf_count <= state.in_transit.values().sum() {
+ continue;
+ }
+ let (handle, vec) = state.queues[idx].pop_front().unwrap();
+ *state.in_transit.get_mut(&handle).unwrap() += 1;
+ return Some(vec);
+ }
+ None
+ }
+}
+
+impl Drop for Arbiter {
+ fn drop(&mut self) {
+ let (state, cvar) = &*self.state_cvar;
+ {
+ let mut state = state.lock().unwrap();
+ state.halt = true;
+ cvar.notify_one();
+ }
+ let thread = self.thread.take().unwrap();
+ thread.join().expect("End of thread loop");
+ }
+}
diff --git a/offload/leaudio/hci/lib.rs b/offload/leaudio/hci/lib.rs
new file mode 100644
index 0000000000..e06cd79356
--- /dev/null
+++ b/offload/leaudio/hci/lib.rs
@@ -0,0 +1,42 @@
+// Copyright 2024, 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.
+
+//! LE Audio HCI-Proxy module
+//!
+//! The module looks at HCI Commands / Events to control the mux of ISO packet
+//! flows coming from the stack and the exposed "LE Audio HCI Proxy" AIDL service:
+//!
+//! HCI | ^ | HCI AIDL
+//! Command | | | ISO Packets Interface
+//! ___|_|________|__ ___________ ____________
+//! | : : proxy | | | arbiter | | service |
+//! | : : | | | | | |
+//! | : : `-|-------|---- ----|--------| |
+//! | : : | | \ / | | |
+//! | : : | Ctrl | : | | |
+//! | : : ---------|------>| : | Ctrl | |
+//! | : : ---------|------ | : | ------>| |
+//! |___:_:________ __| |_____:_____| |____________|
+//! | | |
+//! | | HCI | HCI
+//! v | Event V ISO Packets
+
+mod arbiter;
+mod proxy;
+mod service;
+
+#[cfg(test)]
+mod tests;
+
+pub use proxy::LeAudioModuleBuilder;
diff --git a/offload/leaudio/hci/proxy.rs b/offload/leaudio/hci/proxy.rs
new file mode 100644
index 0000000000..413a7f84b5
--- /dev/null
+++ b/offload/leaudio/hci/proxy.rs
@@ -0,0 +1,381 @@
+// Copyright 2024, 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.
+
+use bluetooth_offload_hci as hci;
+
+use crate::arbiter::Arbiter;
+use crate::service::{Service, StreamConfiguration};
+use hci::{Command, Event, EventToBytes, IsoData, ReturnParameters, Status};
+use hci::{Module, ModuleBuilder};
+use std::collections::HashMap;
+use std::sync::{Arc, Mutex};
+
+const DATA_PATH_ID_SOFTWARE: u8 = 0x19; // TODO
+
+/// LE Audio HCI-Proxy module builder
+pub struct LeAudioModuleBuilder {}
+
+pub(crate) struct LeAudioModule {
+ next_module: Arc<dyn Module>,
+ state: Mutex<State>,
+ service: Service,
+}
+
+#[derive(Default)]
+struct State {
+ big: HashMap<u8, BigParameters>,
+ cig: HashMap<u8, CigParameters>,
+ stream: HashMap<u16, Stream>,
+ arbiter: Option<Arc<Arbiter>>,
+}
+
+struct BigParameters {
+ bis_handles: Vec<u16>,
+ sdu_interval: u32,
+}
+
+struct CigParameters {
+ cis_handles: Vec<u16>,
+ sdu_interval_c_to_p: u32,
+ sdu_interval_p_to_c: u32,
+}
+
+#[derive(Debug, Clone)]
+struct Stream {
+ state: StreamState,
+ iso_type: IsoType,
+ iso_interval_us: u32,
+}
+
+#[derive(Debug, PartialEq, Clone)]
+enum StreamState {
+ Disabled,
+ Enabling,
+ Enabled,
+}
+
+#[derive(Debug, Clone)]
+enum IsoType {
+ Cis { c_to_p: IsoInDirection, _p_to_c: IsoInDirection },
+ Bis { c_to_p: IsoInDirection },
+}
+
+#[derive(Debug, Clone)]
+struct IsoInDirection {
+ sdu_interval_us: u32,
+ max_sdu_size: u16,
+ burst_number: u8,
+ flush_timeout: u8,
+}
+
+impl Stream {
+ fn new_cis(cig: &CigParameters, e: &hci::LeCisEstablished) -> Self {
+ let iso_interval_us = (e.iso_interval as u32) * 1250;
+
+ assert_eq!(iso_interval_us % cig.sdu_interval_c_to_p, 0, "Framing mode not supported");
+ assert_eq!(iso_interval_us % cig.sdu_interval_p_to_c, 0, "Framing mode not supported");
+
+ assert_eq!(
+ iso_interval_us / cig.sdu_interval_c_to_p,
+ e.bn_c_to_p.into(),
+ "SDU fragmentation not supported"
+ );
+ assert_eq!(
+ iso_interval_us / cig.sdu_interval_p_to_c,
+ e.bn_p_to_c.into(),
+ "SDU fragmentation not supported"
+ );
+
+ Self {
+ state: StreamState::Disabled,
+ iso_interval_us,
+ iso_type: IsoType::Cis {
+ c_to_p: IsoInDirection {
+ sdu_interval_us: cig.sdu_interval_c_to_p,
+ max_sdu_size: e.max_pdu_c_to_p,
+ burst_number: e.bn_c_to_p,
+ flush_timeout: e.ft_c_to_p,
+ },
+ _p_to_c: IsoInDirection {
+ sdu_interval_us: cig.sdu_interval_p_to_c,
+ max_sdu_size: e.max_pdu_p_to_c,
+ burst_number: e.bn_p_to_c,
+ flush_timeout: e.ft_p_to_c,
+ },
+ },
+ }
+ }
+
+ fn new_bis(big: &BigParameters, e: &hci::LeCreateBigComplete) -> Self {
+ let iso_interval_us = (e.iso_interval as u32) * 1250;
+
+ assert_eq!(iso_interval_us % big.sdu_interval, 0, "Framing mode not supported");
+ assert_eq!(
+ iso_interval_us / big.sdu_interval,
+ e.bn.into(),
+ "SDU fragmentation not supported"
+ );
+
+ Self {
+ state: StreamState::Disabled,
+ iso_interval_us,
+ iso_type: IsoType::Bis {
+ c_to_p: IsoInDirection {
+ sdu_interval_us: big.sdu_interval,
+ max_sdu_size: e.max_pdu,
+ burst_number: e.bn,
+ flush_timeout: e.irc,
+ },
+ },
+ }
+ }
+}
+
+impl ModuleBuilder for LeAudioModuleBuilder {
+ /// Build the HCI-Proxy module from the next module in the chain
+ fn build(&self, next_module: Arc<dyn Module>) -> Arc<dyn Module> {
+ Arc::new(LeAudioModule::new(next_module))
+ }
+}
+
+impl LeAudioModule {
+ pub(crate) fn new(next_module: Arc<dyn Module>) -> Self {
+ Self { next_module, state: Mutex::new(Default::default()), service: Service::new() }
+ }
+
+ #[cfg(test)]
+ pub(crate) fn arbiter(&self) -> Option<Arc<Arbiter>> {
+ let state = self.state.lock().unwrap();
+ state.arbiter.clone()
+ }
+}
+
+impl Module for LeAudioModule {
+ fn next(&self) -> &dyn Module {
+ &*self.next_module
+ }
+
+ fn out_cmd(&self, data: &[u8]) {
+ match Command::from_bytes(data) {
+ Ok(Command::LeSetCigParameters(ref c)) => {
+ let mut state = self.state.lock().unwrap();
+ state.cig.insert(
+ c.cig_id,
+ CigParameters {
+ cis_handles: vec![],
+ sdu_interval_c_to_p: c.sdu_interval_c_to_p,
+ sdu_interval_p_to_c: c.sdu_interval_p_to_c,
+ },
+ );
+ }
+
+ Ok(Command::LeCreateBig(ref c)) => {
+ let mut state = self.state.lock().unwrap();
+ state.big.insert(
+ c.big_handle,
+ BigParameters { bis_handles: vec![], sdu_interval: c.sdu_interval },
+ );
+ }
+
+ Ok(Command::LeSetupIsoDataPath(ref c)) if c.data_path_id == DATA_PATH_ID_SOFTWARE => {
+ assert_eq!(c.data_path_direction, hci::LeDataPathDirection::Input);
+ let mut state = self.state.lock().unwrap();
+ let stream = state.stream.get_mut(&c.connection_handle).unwrap();
+ stream.state = StreamState::Enabling;
+ }
+
+ _ => (),
+ }
+
+ self.next().out_cmd(data);
+ }
+
+ fn in_evt(&self, data: &[u8]) {
+ match Event::from_bytes(data) {
+ Ok(Event::CommandComplete(ref e)) => match e.return_parameters {
+ ReturnParameters::Reset(ref ret) if ret.status == Status::Success => {
+ let mut state = self.state.lock().unwrap();
+ *state = Default::default();
+ }
+
+ ReturnParameters::LeReadBufferSizeV2(ref ret) if ret.status == Status::Success => {
+ let mut state = self.state.lock().unwrap();
+ state.arbiter = Some(Arc::new(Arbiter::new(
+ self.next_module.clone(),
+ ret.iso_data_packet_length.into(),
+ ret.total_num_iso_data_packets.into(),
+ )));
+ self.service.reset(Arc::downgrade(state.arbiter.as_ref().unwrap()));
+ }
+
+ ReturnParameters::LeSetCigParameters(ref ret) if ret.status == Status::Success => {
+ let mut state = self.state.lock().unwrap();
+ let cig = state.cig.get_mut(&ret.cig_id).unwrap();
+ cig.cis_handles = ret.connection_handles.clone();
+ }
+
+ ReturnParameters::LeRemoveCig(ref ret) if ret.status == Status::Success => {
+ let mut state = self.state.lock().unwrap();
+ state.cig.remove(&ret.cig_id);
+ }
+
+ ReturnParameters::LeSetupIsoDataPath(ref ret) => 'event: {
+ let mut state = self.state.lock().unwrap();
+ let stream = state.stream.get_mut(&ret.connection_handle).unwrap();
+ stream.state =
+ if stream.state == StreamState::Enabling && ret.status == Status::Success {
+ StreamState::Enabled
+ } else {
+ StreamState::Disabled
+ };
+
+ if stream.state != StreamState::Enabled {
+ break 'event;
+ }
+
+ let c_to_p = match stream.iso_type {
+ IsoType::Cis { ref c_to_p, .. } => c_to_p,
+ IsoType::Bis { ref c_to_p } => c_to_p,
+ };
+
+ self.service.start_stream(
+ ret.connection_handle,
+ StreamConfiguration {
+ isoIntervalUs: stream.iso_interval_us as i32,
+ sduIntervalUs: c_to_p.sdu_interval_us as i32,
+ maxSduSize: c_to_p.max_sdu_size as i32,
+ burstNumber: c_to_p.burst_number as i32,
+ flushTimeout: c_to_p.flush_timeout as i32,
+ },
+ );
+ }
+
+ ReturnParameters::LeRemoveIsoDataPath(ref ret) if ret.status == Status::Success => {
+ let mut state = self.state.lock().unwrap();
+ let stream = state.stream.get_mut(&ret.connection_handle).unwrap();
+ if stream.state == StreamState::Enabled {
+ self.service.stop_stream(ret.connection_handle);
+ }
+ stream.state = StreamState::Disabled;
+ }
+
+ _ => (),
+ },
+
+ Ok(Event::LeCisEstablished(ref e)) if e.status == Status::Success => {
+ let mut state = self.state.lock().unwrap();
+ let mut cig_values = state.cig.values();
+ let Some(cig) =
+ cig_values.find(|&g| g.cis_handles.iter().any(|&h| h == e.connection_handle))
+ else {
+ panic!("CIG not set-up for CIS 0x{:03x}", e.connection_handle);
+ };
+
+ let cis = Stream::new_cis(cig, e);
+ if state.stream.insert(e.connection_handle, cis).is_some() {
+ log::error!("CIS already established");
+ } else {
+ let arbiter = state.arbiter.as_ref().unwrap();
+ arbiter.add_connection(e.connection_handle);
+ }
+ }
+
+ Ok(Event::DisconnectionComplete(ref e)) if e.status == Status::Success => {
+ let mut state = self.state.lock().unwrap();
+ if state.stream.remove(&e.connection_handle).is_some() {
+ let arbiter = state.arbiter.as_ref().unwrap();
+ arbiter.remove_connection(e.connection_handle);
+ }
+ }
+
+ Ok(Event::LeCreateBigComplete(ref e)) if e.status == Status::Success => {
+ let mut state_guard = self.state.lock().unwrap();
+ let state = &mut *state_guard;
+
+ let big = state.big.get_mut(&e.big_handle).unwrap();
+ big.bis_handles = e.bis_handles.clone();
+
+ let bis = Stream::new_bis(big, e);
+ for h in &big.bis_handles {
+ if state.stream.insert(*h, bis.clone()).is_some() {
+ log::error!("BIS already established");
+ } else {
+ let arbiter = state.arbiter.as_ref().unwrap();
+ arbiter.add_connection(*h);
+ }
+ }
+ }
+
+ Ok(Event::LeTerminateBigComplete(ref e)) => {
+ let mut state = self.state.lock().unwrap();
+ let big = state.big.remove(&e.big_handle).unwrap();
+ for h in big.bis_handles {
+ state.stream.remove(&h);
+
+ let arbiter = state.arbiter.as_ref().unwrap();
+ arbiter.remove_connection(h);
+ }
+ }
+
+ Ok(Event::NumberOfCompletedPackets(ref e)) => 'event: {
+ let state = self.state.lock().unwrap();
+ let Some(arbiter) = state.arbiter.as_ref() else {
+ break 'event;
+ };
+
+ let (stack_event, _) = {
+ let mut stack_event = hci::NumberOfCompletedPackets {
+ handles: Vec::with_capacity(e.handles.len()),
+ };
+ let mut audio_event = hci::NumberOfCompletedPackets {
+ handles: Vec::with_capacity(e.handles.len()),
+ };
+ for item in &e.handles {
+ let handle = item.connection_handle;
+ arbiter.set_completed(handle, item.num_completed_packets.into());
+
+ if match state.stream.get(&handle) {
+ Some(stream) => stream.state == StreamState::Enabled,
+ None => false,
+ } {
+ audio_event.handles.push(*item);
+ } else {
+ stack_event.handles.push(*item);
+ }
+ }
+ (stack_event, audio_event)
+ };
+
+ if !stack_event.handles.is_empty() {
+ self.next().in_evt(&stack_event.to_bytes());
+ }
+ return;
+ }
+
+ Ok(..) => (),
+
+ Err(code) => {
+ log::error!("Malformed event with code: {:?}", code);
+ }
+ }
+
+ self.next().in_evt(data);
+ }
+
+ fn out_iso(&self, data: &[u8]) {
+ let state = self.state.lock().unwrap();
+ let arbiter = state.arbiter.as_ref().unwrap();
+ arbiter.push_incoming(&IsoData::from_bytes(data).unwrap());
+ }
+}
diff --git a/offload/leaudio/hci/service.rs b/offload/leaudio/hci/service.rs
new file mode 100644
index 0000000000..85fac7168c
--- /dev/null
+++ b/offload/leaudio/hci/service.rs
@@ -0,0 +1,126 @@
+// Copyright 2024, 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.
+
+use android_hardware_bluetooth_offload_leaudio::{aidl, binder};
+
+use crate::arbiter::Arbiter;
+use aidl::android::hardware::bluetooth::offload::leaudio::{
+ IHciProxy::{BnHciProxy, BpHciProxy, IHciProxy},
+ IHciProxyCallbacks::IHciProxyCallbacks,
+};
+use binder::{
+ BinderFeatures, DeathRecipient, ExceptionCode, Interface, Result as BinderResult, Strong,
+};
+use bluetooth_offload_hci::IsoData;
+use std::collections::HashMap;
+use std::sync::{Arc, Mutex, Weak};
+
+pub(crate) use aidl::android::hardware::bluetooth::offload::leaudio::StreamConfiguration::StreamConfiguration;
+
+pub(crate) struct Service {
+ state: Arc<Mutex<State>>,
+}
+
+#[derive(Default)]
+struct State {
+ arbiter: Weak<Arbiter>,
+ callbacks: Option<Strong<dyn IHciProxyCallbacks>>,
+ streams: HashMap<u16, StreamConfiguration>,
+}
+
+impl Service {
+ pub(crate) fn new() -> Self {
+ let state = Arc::new(Mutex::new(State::default()));
+ HciProxy::register(state.clone());
+ Self { state }
+ }
+
+ pub(crate) fn reset(&self, arbiter: Weak<Arbiter>) {
+ let mut state = self.state.lock().unwrap();
+ *state = State { arbiter, ..Default::default() }
+ }
+
+ pub(crate) fn start_stream(&self, handle: u16, config: StreamConfiguration) {
+ let mut state = self.state.lock().unwrap();
+ if let Some(callbacks) = &state.callbacks {
+ let _ = callbacks.startStream(handle.into(), &config);
+ } else {
+ log::warn!("Stream started without registered client");
+ };
+ state.streams.insert(handle, config);
+ }
+
+ pub(crate) fn stop_stream(&self, handle: u16) {
+ let mut state = self.state.lock().unwrap();
+ state.streams.remove(&handle);
+ if let Some(callbacks) = &state.callbacks {
+ let _ = callbacks.stopStream(handle.into());
+ };
+ }
+}
+
+struct HciProxy {
+ state: Arc<Mutex<State>>,
+ _death_recipient: DeathRecipient,
+}
+
+impl Interface for HciProxy {}
+
+impl HciProxy {
+ fn register(state: Arc<Mutex<State>>) {
+ let death_recipient = {
+ let state = state.clone();
+ DeathRecipient::new(move || {
+ log::info!("Client has died");
+ state.lock().unwrap().callbacks = None;
+ })
+ };
+
+ binder::add_service(
+ &format!("{}/default", BpHciProxy::get_descriptor()),
+ BnHciProxy::new_binder(
+ Self { state, _death_recipient: death_recipient },
+ BinderFeatures::default(),
+ )
+ .as_binder(),
+ )
+ .expect("Failed to register service");
+ }
+}
+
+impl IHciProxy for HciProxy {
+ fn registerCallbacks(&self, callbacks: &Strong<dyn IHciProxyCallbacks>) -> BinderResult<()> {
+ let mut state = self.state.lock().unwrap();
+ state.callbacks = Some(callbacks.clone());
+ for (handle, config) in &state.streams {
+ let _ = callbacks.startStream((*handle).into(), config);
+ }
+
+ Ok(())
+ }
+
+ fn sendPacket(&self, handle: i32, seqnum: i32, data: &[u8]) -> BinderResult<()> {
+ let handle: u16 = handle.try_into().map_err(|_| ExceptionCode::ILLEGAL_ARGUMENT)?;
+ let seqnum: u16 = seqnum.try_into().map_err(|_| ExceptionCode::ILLEGAL_ARGUMENT)?;
+
+ let state = self.state.lock().unwrap();
+ if let Some(arbiter) = state.arbiter.upgrade() {
+ arbiter.push_audio(&IsoData::new(handle, seqnum, data));
+ } else {
+ log::warn!("Trashing packet received in bad state");
+ }
+
+ Ok(())
+ }
+}
diff --git a/offload/leaudio/hci/tests.rs b/offload/leaudio/hci/tests.rs
new file mode 100644
index 0000000000..dd55a93c2c
--- /dev/null
+++ b/offload/leaudio/hci/tests.rs
@@ -0,0 +1,914 @@
+// Copyright 2024, 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.
+
+use bluetooth_offload_hci as hci;
+
+use crate::proxy::LeAudioModule;
+use hci::{CommandToBytes, EventToBytes, IsoData, Module, ReturnParameters, Status};
+use std::sync::{mpsc, Arc, Mutex};
+use std::time::Duration;
+
+struct ModuleSinkState {
+ out_cmd: Vec<Vec<u8>>,
+ in_evt: Vec<Vec<u8>>,
+ out_iso: mpsc::Receiver<Vec<u8>>,
+}
+
+struct ModuleSink {
+ state: Mutex<ModuleSinkState>,
+ out_iso: mpsc::Sender<Vec<u8>>,
+}
+
+impl ModuleSink {
+ fn new() -> Self {
+ let (out_iso_tx, out_iso_rx) = mpsc::channel();
+ ModuleSink {
+ state: Mutex::new(ModuleSinkState {
+ out_cmd: Default::default(),
+ in_evt: Default::default(),
+ out_iso: out_iso_rx,
+ }),
+ out_iso: out_iso_tx,
+ }
+ }
+}
+
+impl Module for ModuleSink {
+ fn out_cmd(&self, data: &[u8]) {
+ self.state.lock().unwrap().out_cmd.push(data.to_vec());
+ }
+ fn in_evt(&self, data: &[u8]) {
+ self.state.lock().unwrap().in_evt.push(data.to_vec());
+ }
+ fn out_iso(&self, data: &[u8]) {
+ self.out_iso.send(data.to_vec()).expect("Sending ISO packet");
+ }
+
+ fn next(&self) -> &dyn Module {
+ panic!();
+ }
+}
+
+#[test]
+fn cig() {
+ let sink: Arc<ModuleSink> = Arc::new(ModuleSink::new());
+ let m = LeAudioModule::new(sink.clone());
+
+ m.in_evt(
+ &hci::CommandComplete {
+ num_hci_command_packets: 0,
+ return_parameters: ReturnParameters::Reset(hci::ResetComplete {
+ status: Status::Success,
+ }),
+ }
+ .to_bytes(),
+ );
+
+ m.in_evt(
+ &hci::CommandComplete {
+ num_hci_command_packets: 0,
+ return_parameters: ReturnParameters::LeReadBufferSizeV2(
+ hci::LeReadBufferSizeV2Complete {
+ status: Status::Success,
+ le_acl_data_packet_length: 0,
+ total_num_le_acl_data_packets: 0,
+ iso_data_packet_length: 16,
+ total_num_iso_data_packets: 2,
+ },
+ ),
+ }
+ .to_bytes(),
+ );
+
+ m.out_cmd(
+ &hci::LeSetCigParameters {
+ cig_id: 0x01,
+ sdu_interval_c_to_p: 10_000,
+ sdu_interval_p_to_c: 10_000,
+ worst_case_sca: 0,
+ packing: 0,
+ framing: 0,
+ max_transport_latency_c_to_p: 0,
+ max_transport_latency_p_to_c: 0,
+ cis: vec![],
+ }
+ .to_bytes(),
+ );
+
+ m.in_evt(
+ &hci::CommandComplete {
+ num_hci_command_packets: 0,
+ return_parameters: ReturnParameters::LeSetCigParameters(
+ hci::LeSetCigParametersComplete {
+ status: Status::Success,
+ cig_id: 0x01,
+ connection_handles: vec![0x123, 0x456],
+ },
+ ),
+ }
+ .to_bytes(),
+ );
+
+ m.in_evt(
+ &hci::LeCisEstablished {
+ status: Status::Success,
+ connection_handle: 0x456,
+ cig_sync_delay: 0,
+ cis_sync_delay: 0,
+ transport_latency_c_to_p: 0,
+ transport_latency_p_to_c: 0,
+ phy_c_to_p: 0x02,
+ phy_p_to_c: 0x02,
+ nse: 0,
+ bn_c_to_p: 2,
+ bn_p_to_c: 2,
+ ft_c_to_p: 1,
+ ft_p_to_c: 1,
+ max_pdu_c_to_p: 10,
+ max_pdu_p_to_c: 0,
+ iso_interval: 20_000 / 1250,
+ }
+ .to_bytes(),
+ );
+
+ m.out_cmd(
+ &hci::LeSetupIsoDataPath {
+ connection_handle: 0x456,
+ data_path_direction: hci::LeDataPathDirection::Input,
+ data_path_id: 0,
+ codec_id: hci::LeCodecId {
+ coding_format: hci::CodingFormat::Transparent,
+ company_id: 0,
+ vendor_id: 0,
+ },
+ controller_delay: 0,
+ codec_configuration: vec![],
+ }
+ .to_bytes(),
+ );
+
+ m.in_evt(
+ &hci::CommandComplete {
+ num_hci_command_packets: 0,
+ return_parameters: ReturnParameters::LeSetupIsoDataPath(hci::LeIsoDataPathComplete {
+ status: Status::Success,
+ connection_handle: 0x456,
+ }),
+ }
+ .to_bytes(),
+ );
+
+ m.out_iso(&IsoData::new(0x456, 0, &[0x00, 0x11]).to_bytes());
+ m.out_iso(&IsoData::new(0x456, 1, &[]).to_bytes());
+ {
+ let state = sink.state.lock().unwrap();
+ state.out_iso.recv_timeout(Duration::from_millis(100)).expect("Receiving ISO Packet");
+ state.out_iso.recv_timeout(Duration::from_millis(100)).expect("Receiving ISO Packet");
+ }
+
+ m.in_evt(
+ &hci::NumberOfCompletedPackets {
+ handles: vec![hci::NumberOfCompletedPacketsHandle {
+ connection_handle: 0x456,
+ num_completed_packets: 1,
+ }],
+ }
+ .to_bytes(),
+ );
+
+ m.out_iso(&IsoData::new(0x456, 2, &[0x22]).to_bytes());
+ {
+ let state = sink.state.lock().unwrap();
+ state.out_iso.recv_timeout(Duration::from_millis(100)).expect("Receiving ISO Packet");
+ }
+
+ m.in_evt(
+ &hci::LeCisEstablished {
+ status: Status::Success,
+ connection_handle: 0x123,
+ cig_sync_delay: 0,
+ cis_sync_delay: 0,
+ transport_latency_c_to_p: 0,
+ transport_latency_p_to_c: 0,
+ phy_c_to_p: 0x02,
+ phy_p_to_c: 0x02,
+ nse: 0,
+ bn_c_to_p: 2,
+ bn_p_to_c: 2,
+ ft_c_to_p: 1,
+ ft_p_to_c: 1,
+ max_pdu_c_to_p: 10,
+ max_pdu_p_to_c: 0,
+ iso_interval: 20_000 / 1250,
+ }
+ .to_bytes(),
+ );
+
+ m.in_evt(
+ &hci::NumberOfCompletedPackets {
+ handles: vec![hci::NumberOfCompletedPacketsHandle {
+ connection_handle: 0x456,
+ num_completed_packets: 1,
+ }],
+ }
+ .to_bytes(),
+ );
+
+ m.out_iso(&IsoData::new(0x123, 0, &[]).to_bytes());
+ {
+ let state = sink.state.lock().unwrap();
+ state.out_iso.recv_timeout(Duration::from_millis(100)).expect("Receiving ISO Packet");
+ }
+
+ m.in_evt(
+ &hci::DisconnectionComplete {
+ status: Status::Success,
+ connection_handle: 0x456,
+ reason: 0,
+ }
+ .to_bytes(),
+ );
+
+ m.out_iso(&IsoData::new(0x456, 3, &[0x33]).to_bytes());
+ m.out_iso(&IsoData::new(0x123, 1, &[0x11, 0x22]).to_bytes());
+ {
+ let state = sink.state.lock().unwrap();
+ state.out_iso.recv_timeout(Duration::from_millis(100)).expect("Receiving ISO Packet");
+ }
+
+ {
+ let state = sink.state.lock().unwrap();
+ assert_eq!(state.out_cmd.len(), 2);
+ assert_eq!(state.in_evt.len(), 9);
+ }
+}
+
+#[test]
+fn big() {
+ let sink: Arc<ModuleSink> = Arc::new(ModuleSink::new());
+ let m = LeAudioModule::new(sink.clone());
+
+ m.in_evt(
+ &hci::CommandComplete {
+ num_hci_command_packets: 0,
+ return_parameters: ReturnParameters::Reset(hci::ResetComplete {
+ status: Status::Success,
+ }),
+ }
+ .to_bytes(),
+ );
+
+ m.in_evt(
+ &hci::CommandComplete {
+ num_hci_command_packets: 0,
+ return_parameters: ReturnParameters::LeReadBufferSizeV2(
+ hci::LeReadBufferSizeV2Complete {
+ status: Status::Success,
+ le_acl_data_packet_length: 0,
+ total_num_le_acl_data_packets: 0,
+ iso_data_packet_length: 16,
+ total_num_iso_data_packets: 2,
+ },
+ ),
+ }
+ .to_bytes(),
+ );
+
+ m.out_cmd(
+ &hci::LeCreateBig {
+ big_handle: 0x10,
+ advertising_handle: 0,
+ num_bis: 2,
+ sdu_interval: 10_000,
+ max_sdu: 120,
+ max_transport_latency: 0,
+ rtn: 5,
+ phy: 0x02,
+ packing: 0,
+ framing: 0,
+ encryption: 0,
+ broadcast_code: [0u8; 16],
+ }
+ .to_bytes(),
+ );
+
+ m.in_evt(
+ &hci::LeCreateBigComplete {
+ status: Status::Success,
+ big_handle: 0x10,
+ big_sync_delay: 0,
+ big_transport_latency: 0,
+ phy: 0x02,
+ nse: 0,
+ bn: 2,
+ pto: 0,
+ irc: 0,
+ max_pdu: 10,
+ iso_interval: 20_000 / 1250,
+ bis_handles: vec![0x123, 0x456],
+ }
+ .to_bytes(),
+ );
+
+ m.out_cmd(
+ &hci::LeSetupIsoDataPath {
+ connection_handle: 0x123,
+ data_path_direction: hci::LeDataPathDirection::Input,
+ data_path_id: 0,
+ codec_id: hci::LeCodecId {
+ coding_format: hci::CodingFormat::Transparent,
+ company_id: 0,
+ vendor_id: 0,
+ },
+ controller_delay: 0,
+ codec_configuration: vec![],
+ }
+ .to_bytes(),
+ );
+
+ m.in_evt(
+ &hci::CommandComplete {
+ num_hci_command_packets: 0,
+ return_parameters: ReturnParameters::LeSetupIsoDataPath(hci::LeIsoDataPathComplete {
+ status: Status::Success,
+ connection_handle: 0x123,
+ }),
+ }
+ .to_bytes(),
+ );
+
+ m.out_cmd(
+ &hci::LeSetupIsoDataPath {
+ connection_handle: 0x456,
+ data_path_direction: hci::LeDataPathDirection::Input,
+ data_path_id: 0,
+ codec_id: hci::LeCodecId {
+ coding_format: hci::CodingFormat::Transparent,
+ company_id: 0,
+ vendor_id: 0,
+ },
+ controller_delay: 0,
+ codec_configuration: vec![],
+ }
+ .to_bytes(),
+ );
+
+ m.in_evt(
+ &hci::CommandComplete {
+ num_hci_command_packets: 0,
+ return_parameters: ReturnParameters::LeSetupIsoDataPath(hci::LeIsoDataPathComplete {
+ status: Status::Success,
+ connection_handle: 0x456,
+ }),
+ }
+ .to_bytes(),
+ );
+
+ m.out_iso(&IsoData::new(0x456, 0, &[0x00, 0x11]).to_bytes());
+ m.out_iso(&IsoData::new(0x456, 1, &[]).to_bytes());
+ {
+ let state = sink.state.lock().unwrap();
+ state.out_iso.recv_timeout(Duration::from_millis(100)).expect("Receiving ISO Packet");
+ state.out_iso.recv_timeout(Duration::from_millis(100)).expect("Receiving ISO Packet");
+ }
+
+ m.in_evt(
+ &hci::NumberOfCompletedPackets {
+ handles: vec![hci::NumberOfCompletedPacketsHandle {
+ connection_handle: 0x456,
+ num_completed_packets: 2,
+ }],
+ }
+ .to_bytes(),
+ );
+
+ m.out_iso(&IsoData::new(0x123, 0, &[0x22, 0x33]).to_bytes());
+ m.out_iso(&IsoData::new(0x456, 2, &[]).to_bytes());
+ {
+ let state = sink.state.lock().unwrap();
+ state.out_iso.recv_timeout(Duration::from_millis(100)).expect("Receiving ISO Packet");
+ state.out_iso.recv_timeout(Duration::from_millis(100)).expect("Receiving ISO Packet");
+ }
+
+ m.in_evt(
+ &hci::NumberOfCompletedPackets {
+ handles: vec![
+ hci::NumberOfCompletedPacketsHandle {
+ connection_handle: 0x456,
+ num_completed_packets: 1,
+ },
+ hci::NumberOfCompletedPacketsHandle {
+ connection_handle: 0x123,
+ num_completed_packets: 1,
+ },
+ ],
+ }
+ .to_bytes(),
+ );
+
+ m.out_iso(&IsoData::new(0x123, 1, &[0x44]).to_bytes());
+ m.out_iso(&IsoData::new(0x123, 2, &[0x55, 0x66]).to_bytes());
+ {
+ let state = sink.state.lock().unwrap();
+ state.out_iso.recv_timeout(Duration::from_millis(100)).expect("Receiving ISO Packet");
+ state.out_iso.recv_timeout(Duration::from_millis(100)).expect("Receiving ISO Packet");
+ }
+
+ m.in_evt(
+ &hci::NumberOfCompletedPackets {
+ handles: vec![hci::NumberOfCompletedPacketsHandle {
+ connection_handle: 0x123,
+ num_completed_packets: 2,
+ }],
+ }
+ .to_bytes(),
+ );
+
+ m.out_iso(&IsoData::new(0x123, 3, &[]).to_bytes());
+ m.out_iso(&IsoData::new(0x123, 4, &[]).to_bytes());
+ {
+ let state = sink.state.lock().unwrap();
+ state.out_iso.recv_timeout(Duration::from_millis(100)).expect("Receiving ISO Packet");
+ state.out_iso.recv_timeout(Duration::from_millis(100)).expect("Receiving ISO Packet");
+ }
+
+ {
+ let state = sink.state.lock().unwrap();
+ assert_eq!(state.out_cmd.len(), 3);
+ assert_eq!(state.in_evt.len(), 8);
+ }
+}
+
+#[test]
+fn merge() {
+ let sink: Arc<ModuleSink> = Arc::new(ModuleSink::new());
+ let m = LeAudioModule::new(sink.clone());
+
+ m.in_evt(
+ &hci::CommandComplete {
+ num_hci_command_packets: 0,
+ return_parameters: ReturnParameters::Reset(hci::ResetComplete {
+ status: Status::Success,
+ }),
+ }
+ .to_bytes(),
+ );
+
+ m.in_evt(
+ &hci::CommandComplete {
+ num_hci_command_packets: 0,
+ return_parameters: ReturnParameters::LeReadBufferSizeV2(
+ hci::LeReadBufferSizeV2Complete {
+ status: Status::Success,
+ le_acl_data_packet_length: 0,
+ total_num_le_acl_data_packets: 0,
+ iso_data_packet_length: 16,
+ total_num_iso_data_packets: 2,
+ },
+ ),
+ }
+ .to_bytes(),
+ );
+
+ m.out_cmd(
+ &hci::LeSetCigParameters {
+ cig_id: 0x01,
+ sdu_interval_c_to_p: 10_000,
+ sdu_interval_p_to_c: 10_000,
+ worst_case_sca: 0,
+ packing: 0,
+ framing: 0,
+ max_transport_latency_c_to_p: 0,
+ max_transport_latency_p_to_c: 0,
+ cis: vec![],
+ }
+ .to_bytes(),
+ );
+
+ m.in_evt(
+ &hci::CommandComplete {
+ num_hci_command_packets: 0,
+ return_parameters: ReturnParameters::LeSetCigParameters(
+ hci::LeSetCigParametersComplete {
+ status: Status::Success,
+ cig_id: 0x01,
+ connection_handles: vec![0x123, 0x456],
+ },
+ ),
+ }
+ .to_bytes(),
+ );
+
+ // Establish CIS 0x123, using Software Offload path
+ // Establish CIS 0x456, using HCI (stack) path
+
+ m.in_evt(
+ &hci::LeCisEstablished {
+ status: Status::Success,
+ connection_handle: 0x123,
+ cig_sync_delay: 0,
+ cis_sync_delay: 0,
+ transport_latency_c_to_p: 0,
+ transport_latency_p_to_c: 0,
+ phy_c_to_p: 0x02,
+ phy_p_to_c: 0x02,
+ nse: 0,
+ bn_c_to_p: 2,
+ bn_p_to_c: 2,
+ ft_c_to_p: 1,
+ ft_p_to_c: 1,
+ max_pdu_c_to_p: 10,
+ max_pdu_p_to_c: 0,
+ iso_interval: 20_000 / 1250,
+ }
+ .to_bytes(),
+ );
+
+ m.in_evt(
+ &hci::LeCisEstablished {
+ status: Status::Success,
+ connection_handle: 0x456,
+ cig_sync_delay: 0,
+ cis_sync_delay: 0,
+ transport_latency_c_to_p: 0,
+ transport_latency_p_to_c: 0,
+ phy_c_to_p: 0x02,
+ phy_p_to_c: 0x02,
+ nse: 0,
+ bn_c_to_p: 2,
+ bn_p_to_c: 2,
+ ft_c_to_p: 1,
+ ft_p_to_c: 1,
+ max_pdu_c_to_p: 10,
+ max_pdu_p_to_c: 0,
+ iso_interval: 20_000 / 1250,
+ }
+ .to_bytes(),
+ );
+
+ m.out_cmd(
+ &hci::LeSetupIsoDataPath {
+ connection_handle: 0x123,
+ data_path_direction: hci::LeDataPathDirection::Input,
+ data_path_id: 0x19,
+ codec_id: hci::LeCodecId {
+ coding_format: hci::CodingFormat::Transparent,
+ company_id: 0,
+ vendor_id: 0,
+ },
+ controller_delay: 0,
+ codec_configuration: vec![],
+ }
+ .to_bytes(),
+ );
+
+ m.in_evt(
+ &hci::CommandComplete {
+ num_hci_command_packets: 0,
+ return_parameters: ReturnParameters::LeSetupIsoDataPath(hci::LeIsoDataPathComplete {
+ status: Status::Success,
+ connection_handle: 0x123,
+ }),
+ }
+ .to_bytes(),
+ );
+
+ m.out_cmd(
+ &hci::LeSetupIsoDataPath {
+ connection_handle: 0x456,
+ data_path_direction: hci::LeDataPathDirection::Input,
+ data_path_id: 0,
+ codec_id: hci::LeCodecId {
+ coding_format: hci::CodingFormat::Transparent,
+ company_id: 0,
+ vendor_id: 0,
+ },
+ controller_delay: 0,
+ codec_configuration: vec![],
+ }
+ .to_bytes(),
+ );
+
+ m.in_evt(
+ &hci::CommandComplete {
+ num_hci_command_packets: 0,
+ return_parameters: ReturnParameters::LeSetupIsoDataPath(hci::LeIsoDataPathComplete {
+ status: Status::Success,
+ connection_handle: 0x456,
+ }),
+ }
+ .to_bytes(),
+ );
+
+ {
+ let mut state = sink.state.lock().unwrap();
+ assert_eq!(state.out_cmd.len(), 3);
+ assert_eq!(state.in_evt.len(), 7);
+ state.out_cmd.clear();
+ state.in_evt.clear();
+ }
+
+ // Send 2 Packets on 0x123
+ // -> The packets are sent to the controller, and fulfill the FIFO
+
+ m.arbiter().unwrap().push_audio(&IsoData::new(0x123, 1, &[0x44]));
+ m.arbiter().unwrap().push_audio(&IsoData::new(0x123, 2, &[0x55, 0x66]));
+ {
+ let state = sink.state.lock().unwrap();
+ state.out_iso.recv_timeout(Duration::from_millis(100)).expect("Receiving ISO Packet");
+ state.out_iso.recv_timeout(Duration::from_millis(100)).expect("Receiving ISO Packet");
+ }
+
+ // Send 2 packets on 0x456
+ // -> The packets are buffered, the controller FIFO is full
+
+ m.out_iso(&IsoData::new(0x456, 1, &[0x11]).to_bytes());
+ m.out_iso(&IsoData::new(0x456, 2, &[0x22, 0x33]).to_bytes());
+ {
+ let state = sink.state.lock().unwrap();
+ assert_eq!(
+ state.out_iso.recv_timeout(Duration::from_millis(100)),
+ Err(mpsc::RecvTimeoutError::Timeout),
+ );
+ }
+
+ // Acknowledge packet 1 on 0x123:
+ // -> The acknowledgment is filtered
+ // -> Packet 1 on 0x456 is tranmitted
+
+ m.in_evt(
+ &hci::NumberOfCompletedPackets {
+ handles: vec![hci::NumberOfCompletedPacketsHandle {
+ connection_handle: 0x123,
+ num_completed_packets: 1,
+ }],
+ }
+ .to_bytes(),
+ );
+
+ {
+ let mut state = sink.state.lock().unwrap();
+ assert_eq!(state.in_evt.pop(), None);
+ assert_eq!(
+ state.out_iso.recv_timeout(Duration::from_millis(100)),
+ Ok(IsoData::new(0x456, 1, &[0x11]).to_bytes())
+ );
+ assert_eq!(
+ state.out_iso.recv_timeout(Duration::from_millis(100)),
+ Err(mpsc::RecvTimeoutError::Timeout),
+ );
+ }
+
+ // Link 0x123 disconnect (implicitly ack packet 2 on 0x123)
+ // -> Packet 2 on 0x456 is tranmitted
+
+ m.in_evt(
+ &hci::DisconnectionComplete {
+ status: Status::Success,
+ connection_handle: 0x123,
+ reason: 0,
+ }
+ .to_bytes(),
+ );
+
+ {
+ let state = sink.state.lock().unwrap();
+ assert_eq!(
+ state.out_iso.recv_timeout(Duration::from_millis(100)),
+ Ok(IsoData::new(0x456, 2, &[0x22, 0x33]).to_bytes())
+ );
+ }
+
+ // Send packets and ack packets 1 and 2 on 0x456.
+ // -> Connection 0x123 is disconnected, ignored
+ // -> Packets 3, and 4 on 0x456 are sent
+ // -> Packet 5 on 0x456 is buffered
+
+ m.out_iso(&IsoData::new(0x456, 3, &[0x33]).to_bytes());
+ m.arbiter().unwrap().push_audio(&IsoData::new(0x123, 3, &[0x33]));
+ m.arbiter().unwrap().push_audio(&IsoData::new(0x123, 4, &[0x44, 0x55]));
+ m.out_iso(&IsoData::new(0x456, 4, &[0x44, 0x55]).to_bytes());
+ m.out_iso(&IsoData::new(0x456, 5, &[0x55, 0x66]).to_bytes());
+ {
+ let state = sink.state.lock().unwrap();
+ assert_eq!(
+ state.out_iso.recv_timeout(Duration::from_millis(100)),
+ Err(mpsc::RecvTimeoutError::Timeout),
+ );
+ }
+
+ m.in_evt(
+ &hci::NumberOfCompletedPackets {
+ handles: vec![hci::NumberOfCompletedPacketsHandle {
+ connection_handle: 0x456,
+ num_completed_packets: 2,
+ }],
+ }
+ .to_bytes(),
+ );
+
+ {
+ let mut state = sink.state.lock().unwrap();
+ assert_eq!(
+ state.in_evt.pop(),
+ Some(
+ hci::NumberOfCompletedPackets {
+ handles: vec![hci::NumberOfCompletedPacketsHandle {
+ connection_handle: 0x456,
+ num_completed_packets: 2,
+ }],
+ }
+ .to_bytes(),
+ )
+ );
+ assert_eq!(state.in_evt.len(), 1);
+ assert_eq!(
+ state.out_iso.recv_timeout(Duration::from_millis(100)),
+ Ok(IsoData::new(0x456, 3, &[0x33]).to_bytes())
+ );
+ assert_eq!(
+ state.out_iso.recv_timeout(Duration::from_millis(100)),
+ Ok(IsoData::new(0x456, 4, &[0x44, 0x55]).to_bytes())
+ );
+ state.in_evt.clear();
+ }
+
+ // Re-establish CIS 0x123, using Software Offload path
+
+ m.in_evt(
+ &hci::LeCisEstablished {
+ status: Status::Success,
+ connection_handle: 0x123,
+ cig_sync_delay: 0,
+ cis_sync_delay: 0,
+ transport_latency_c_to_p: 0,
+ transport_latency_p_to_c: 0,
+ phy_c_to_p: 0x02,
+ phy_p_to_c: 0x02,
+ nse: 0,
+ bn_c_to_p: 2,
+ bn_p_to_c: 2,
+ ft_c_to_p: 1,
+ ft_p_to_c: 1,
+ max_pdu_c_to_p: 10,
+ max_pdu_p_to_c: 0,
+ iso_interval: 20_000 / 1250,
+ }
+ .to_bytes(),
+ );
+
+ m.out_cmd(
+ &hci::LeSetupIsoDataPath {
+ connection_handle: 0x123,
+ data_path_direction: hci::LeDataPathDirection::Input,
+ data_path_id: 0x19,
+ codec_id: hci::LeCodecId {
+ coding_format: hci::CodingFormat::Transparent,
+ company_id: 0,
+ vendor_id: 0,
+ },
+ controller_delay: 0,
+ codec_configuration: vec![],
+ }
+ .to_bytes(),
+ );
+
+ m.in_evt(
+ &hci::CommandComplete {
+ num_hci_command_packets: 0,
+ return_parameters: ReturnParameters::LeSetupIsoDataPath(hci::LeIsoDataPathComplete {
+ status: Status::Success,
+ connection_handle: 0x123,
+ }),
+ }
+ .to_bytes(),
+ );
+
+ // Acknowledge packets 3 and 4 on 0x456
+ // -> Packet 5 on 0x456 is sent
+
+ m.in_evt(
+ &hci::NumberOfCompletedPackets {
+ handles: vec![hci::NumberOfCompletedPacketsHandle {
+ connection_handle: 0x456,
+ num_completed_packets: 2,
+ }],
+ }
+ .to_bytes(),
+ );
+
+ {
+ let mut state = sink.state.lock().unwrap();
+ assert_eq!(
+ state.in_evt.pop(),
+ Some(
+ hci::NumberOfCompletedPackets {
+ handles: vec![hci::NumberOfCompletedPacketsHandle {
+ connection_handle: 0x456,
+ num_completed_packets: 2,
+ }],
+ }
+ .to_bytes(),
+ )
+ );
+ assert_eq!(
+ state.out_iso.recv_timeout(Duration::from_millis(100)),
+ Ok(IsoData::new(0x456, 5, &[0x55, 0x66]).to_bytes())
+ );
+ assert_eq!(
+ state.out_iso.recv_timeout(Duration::from_millis(100)),
+ Err(mpsc::RecvTimeoutError::Timeout),
+ );
+ state.out_cmd.clear();
+ state.in_evt.clear();
+ }
+
+ // Acknowledge packet 5 on 0x456
+ // -> Controller FIFO is now empty
+
+ m.in_evt(
+ &hci::NumberOfCompletedPackets {
+ handles: vec![hci::NumberOfCompletedPacketsHandle {
+ connection_handle: 0x456,
+ num_completed_packets: 1,
+ }],
+ }
+ .to_bytes(),
+ );
+
+ {
+ let mut state = sink.state.lock().unwrap();
+ assert_eq!(
+ state.in_evt.pop(),
+ Some(
+ hci::NumberOfCompletedPackets {
+ handles: vec![hci::NumberOfCompletedPacketsHandle {
+ connection_handle: 0x456,
+ num_completed_packets: 1,
+ }],
+ }
+ .to_bytes(),
+ )
+ );
+ }
+
+ // Send 1 packet on each CIS, and acknowledge them
+ // -> The CIS 0x123 is removed from "NumberOfCompletedPackets" event
+
+ m.out_iso(&IsoData::new(0x123, 0, &[]).to_bytes());
+ m.arbiter().unwrap().push_audio(&IsoData::new(0x456, 6, &[0x66, 0x77]));
+
+ {
+ let state = sink.state.lock().unwrap();
+ for _ in 0..2 {
+ let pkt = state.out_iso.recv_timeout(Duration::from_millis(100));
+ assert!(
+ pkt == Ok(IsoData::new(0x123, 0, &[]).to_bytes())
+ || pkt == Ok(IsoData::new(0x456, 6, &[0x66, 0x77]).to_bytes())
+ );
+ }
+ }
+
+ m.in_evt(
+ &hci::NumberOfCompletedPackets {
+ handles: vec![
+ hci::NumberOfCompletedPacketsHandle {
+ connection_handle: 0x456,
+ num_completed_packets: 1,
+ },
+ hci::NumberOfCompletedPacketsHandle {
+ connection_handle: 0x123,
+ num_completed_packets: 1,
+ },
+ ],
+ }
+ .to_bytes(),
+ );
+
+ {
+ let mut state = sink.state.lock().unwrap();
+ assert_eq!(
+ state.in_evt.pop(),
+ Some(
+ hci::NumberOfCompletedPackets {
+ handles: vec![hci::NumberOfCompletedPacketsHandle {
+ connection_handle: 0x456,
+ num_completed_packets: 1,
+ }],
+ }
+ .to_bytes(),
+ )
+ );
+ }
+}
diff --git a/pandora/interfaces/pandora_experimental/mediaplayer.proto b/pandora/interfaces/pandora_experimental/mediaplayer.proto
index 98ac7b674d..69ead553d8 100644
--- a/pandora/interfaces/pandora_experimental/mediaplayer.proto
+++ b/pandora/interfaces/pandora_experimental/mediaplayer.proto
@@ -9,6 +9,7 @@ import "google/protobuf/empty.proto";
service MediaPlayer {
rpc Play(google.protobuf.Empty) returns (google.protobuf.Empty);
+ rpc PlayUpdated(google.protobuf.Empty) returns (google.protobuf.Empty);
rpc Stop(google.protobuf.Empty) returns (google.protobuf.Empty);
rpc Pause(google.protobuf.Empty) returns (google.protobuf.Empty);
rpc Rewind(google.protobuf.Empty) returns (google.protobuf.Empty);
@@ -17,6 +18,7 @@ service MediaPlayer {
rpc Backward(google.protobuf.Empty) returns (google.protobuf.Empty);
rpc SetLargeMetadata(google.protobuf.Empty) returns (google.protobuf.Empty);
rpc UpdateQueue(google.protobuf.Empty) returns (google.protobuf.Empty);
+ rpc ResetQueue(google.protobuf.Empty) returns (google.protobuf.Empty);
rpc GetShuffleMode(google.protobuf.Empty) returns (GetShuffleModeResponse);
rpc SetShuffleMode(SetShuffleModeRequest) returns (google.protobuf.Empty);
rpc StartTestPlayback(google.protobuf.Empty) returns (google.protobuf.Empty);
diff --git a/sysprop/a2dp.sysprop b/sysprop/a2dp.sysprop
index 80b359c390..ae02fbbb99 100644
--- a/sysprop/a2dp.sysprop
+++ b/sysprop/a2dp.sysprop
@@ -9,3 +9,10 @@ prop {
prop_name: "bluetooth.a2dp.src_sink_coexist.enabled"
}
+prop {
+ api_name: "avdt_accept_open_timeout_ms"
+ type: Integer
+ scope: Internal
+ access: Readonly
+ prop_name: "bluetooth.a2dp.avdt_accept_open_timeout_ms"
+} \ No newline at end of file
diff --git a/system/bta/Android.bp b/system/bta/Android.bp
index 63bd5f4e53..57fd56b039 100644
--- a/system/bta/Android.bp
+++ b/system/bta/Android.bp
@@ -1063,6 +1063,95 @@ cc_test {
}
cc_test {
+ name: "bluetooth_ras_test",
+ test_suites: ["general-tests"],
+ defaults: [
+ "fluoride_bta_defaults",
+ "mts_defaults",
+ ],
+ host_supported: true,
+ isolated: false,
+ include_dirs: [
+ "packages/modules/Bluetooth/system",
+ "packages/modules/Bluetooth/system/bta/include",
+ "packages/modules/Bluetooth/system/bta/test/common",
+ "packages/modules/Bluetooth/system/stack/include",
+ ],
+ srcs: [
+ ":TestCommonMockFunctions",
+ ":TestMockBtaGatt",
+ ":TestMockMainShim",
+ ":TestMockMainShimEntry",
+ ":TestMockStackBtm",
+ ":TestMockStackBtmInterface",
+ ":TestMockStackBtmIso",
+ ":TestMockStackGatt",
+ ":TestMockStackL2cap",
+ ":TestStubOsi",
+ "gatt/database.cc",
+ "gatt/database_builder.cc",
+ "ras/ras_utils.cc",
+ "ras/ras_utils_test.cc",
+ "test/common/bta_gatt_queue_mock.cc",
+ "test/common/btif_storage_mock.cc",
+ "test/common/mock_device_groups.cc",
+ ],
+ shared_libs: [
+ "libaconfig_storage_read_api_cc",
+ "libbase",
+ "libcrypto",
+ "libcutils",
+ "libhidlbase",
+ "liblog",
+ ],
+ static_libs: [
+ "bluetooth_flags_c_lib_for_test",
+ "libbluetooth-types",
+ "libbluetooth_crypto_toolbox",
+ "libbluetooth_gd",
+ "libbluetooth_log",
+ "libbt-audio-hal-interface",
+ "libbt-common",
+ "libbt-platform-protos-lite",
+ "libchrome",
+ "libcom.android.sysprop.bluetooth.wrapped",
+ "libevent",
+ "libflagtest",
+ "libflatbuffers-cpp",
+ "libgmock",
+ "libgtest",
+ "liblc3",
+ "libosi",
+ "server_configurable_flags",
+ ],
+ target: {
+ android: {
+ shared_libs: [
+ "libbinder_ndk",
+ ],
+ static_libs: [
+ "libPlatformProperties",
+ ],
+ },
+ host: {
+ static_libs: [
+ "libbinder_ndk",
+ ],
+ },
+ },
+ sanitize: {
+ cfi: true,
+ scs: true,
+ address: true,
+ all_undefined: true,
+ integer_overflow: true,
+ diag: {
+ undefined: true,
+ },
+ },
+}
+
+cc_test {
name: "bluetooth_test_broadcaster_state_machine",
test_suites: ["general-tests"],
defaults: [
diff --git a/system/bta/av/bta_av_aact.cc b/system/bta/av/bta_av_aact.cc
index 8cb73a4402..769cefc4d9 100644
--- a/system/bta/av/bta_av_aact.cc
+++ b/system/bta/av/bta_av_aact.cc
@@ -26,6 +26,7 @@
#define LOG_TAG "bluetooth-a2dp"
+#include <android_bluetooth_sysprop.h>
#include <bluetooth/log.h>
#include <com_android_bluetooth_flags.h>
@@ -877,17 +878,6 @@ void bta_av_cleanup(tBTA_AV_SCB* p_scb, tBTA_AV_DATA* /* p_data */) {
alarm_cancel(p_scb->accept_open_timer);
}
- /* TODO(eisenbach): RE-IMPLEMENT USING VSC OR HAL EXTENSION
- vendor_get_interface()->send_command(
- (vendor_opcode_t)BT_VND_OP_A2DP_OFFLOAD_STOP, (void*)&p_scb->l2c_cid);
- if (p_scb->offload_start_pending) {
- tBTA_AV_STATUS status = BTA_AV_FAIL_STREAM;
- tBTA_AV bta_av_data;
- bta_av_data.status = status;
- (*bta_av_cb.p_cback)(BTA_AV_OFFLOAD_START_RSP_EVT, &bta_av_data);
- }
- */
-
if (p_scb->deregistering) {
/* remove stream */
for (int i = 0; i < BTAV_A2DP_CODEC_INDEX_MAX; i++) {
@@ -1134,7 +1124,11 @@ void bta_av_setconfig_rsp(tBTA_AV_SCB* p_scb, tBTA_AV_DATA* p_data) {
if (!p_scb->accept_open_timer) {
p_scb->accept_open_timer = alarm_new("accept_open_timer");
}
- alarm_set_on_mloop(p_scb->accept_open_timer, BTA_AV_ACCEPT_OPEN_TIMEOUT_MS,
+ const uint64_t accept_open_timeout =
+ android::sysprop::bluetooth::A2dp::avdt_accept_open_timeout_ms().value_or(
+ BTA_AV_ACCEPT_OPEN_TIMEOUT_MS);
+ log::debug("accept_open_timeout = {} ms", accept_open_timeout);
+ alarm_set_on_mloop(p_scb->accept_open_timer, accept_open_timeout,
bta_av_accept_open_timer_cback, UINT_TO_PTR(p_scb->hdi));
}
}
@@ -3180,67 +3174,32 @@ void bta_av_vendor_offload_stop() {
*
******************************************************************************/
void bta_av_offload_req(tBTA_AV_SCB* p_scb, tBTA_AV_DATA* /*p_data*/) {
- tBTA_AV_STATUS status = BTA_AV_FAIL_RESOURCES;
-
+ tBTA_AV bta_av_data = {};
tBT_A2DP_OFFLOAD offload_start;
log::verbose("stream {}, audio channels open {}", p_scb->started ? "STARTED" : "STOPPED",
bta_av_cb.audio_open_cnt);
- A2dpCodecConfig* codec_config = bta_av_get_a2dp_current_codec();
- log::assert_that(codec_config != nullptr, "assert failed: codec_config != nullptr");
+ if (!p_scb->started) {
+ log::warn("stream not started, start offload failed.");
+ bta_av_data.status = BTA_AV_FAIL_STREAM;
+ (*bta_av_cb.p_cback)(BTA_AV_OFFLOAD_START_RSP_EVT, &bta_av_data);
+ return;
+ }
- /* Check if stream has already been started. */
- /* Support offload if only one audio source stream is open. */
- if (p_scb->started != true) {
- status = BTA_AV_FAIL_STREAM;
- } else if (bta_av_cb.offload_start_pending_hndl || bta_av_cb.offload_started_hndl) {
+ if (bta_av_cb.offload_start_pending_hndl || bta_av_cb.offload_started_hndl) {
log::warn("offload already started, ignore request");
return;
- } else if (::bluetooth::audio::a2dp::provider::supports_codec(codec_config->codecIndex())) {
+ }
+
+ A2dpCodecConfig* codec_config = bta_av_get_a2dp_current_codec();
+ log::assert_that(codec_config != nullptr, "assert failed: codec_config != nullptr");
+
+ if (::bluetooth::audio::a2dp::provider::supports_codec(codec_config->codecIndex())) {
bta_av_vendor_offload_start_v2(p_scb, static_cast<A2dpCodecConfigExt*>(codec_config));
} else {
bta_av_offload_codec_builder(p_scb, &offload_start);
bta_av_vendor_offload_start(p_scb, &offload_start);
- return;
- }
- if (status != BTA_AV_SUCCESS) {
- tBTA_AV bta_av_data;
- bta_av_data.status = status;
- (*bta_av_cb.p_cback)(BTA_AV_OFFLOAD_START_RSP_EVT, &bta_av_data);
- }
- /* TODO(eisenbach): RE-IMPLEMENT USING VSC OR HAL EXTENSION
- else if (bta_av_cb.audio_open_cnt == 1 &&
- p_scb->seps[p_scb->sep_idx].tsep == AVDT_TSEP_SRC &&
- p_scb->chnl == BTA_AV_CHNL_AUDIO) {
- bt_vendor_op_a2dp_offload_t a2dp_offload_start;
-
- if (L2CA_GetConnectionConfig(
- p_scb->l2c_cid, &a2dp_offload_start.acl_data_size,
- &a2dp_offload_start.remote_cid, &a2dp_offload_start.lm_handle)) {
- log::verbose("l2cmtu {} lcid 0x{:02X} rcid 0x{:02X} lm_handle 0x{:02X}",
- a2dp_offload_start.acl_data_size, p_scb->l2c_cid,
- a2dp_offload_start.remote_cid, a2dp_offload_start.lm_handle);
-
- a2dp_offload_start.bta_av_handle = p_scb->hndl;
- a2dp_offload_start.xmit_quota = BTA_AV_A2DP_OFFLOAD_XMIT_QUOTA;
- a2dp_offload_start.stream_mtu = p_scb->stream_mtu;
- a2dp_offload_start.local_cid = p_scb->l2c_cid;
- a2dp_offload_start.is_flushable = true;
- a2dp_offload_start.stream_source =
- ((uint32_t)(p_scb->cfg.codec_info[1] | p_scb->cfg.codec_info[2]));
-
- memcpy(a2dp_offload_start.codec_info, p_scb->cfg.codec_info,
- sizeof(a2dp_offload_start.codec_info));
-
- if (!vendor_get_interface()->send_command(
- (vendor_opcode_t)BT_VND_OP_A2DP_OFFLOAD_START,
- &a2dp_offload_start)) {
- status = BTA_AV_SUCCESS;
- p_scb->offload_start_pending = true;
- }
- }
}
- */
}
/*******************************************************************************
@@ -3402,4 +3361,4 @@ static void bta_av_accept_open_timer_cback(void* data) {
tBTA_AV_API_OPEN* p_buf = (tBTA_AV_API_OPEN*)osi_malloc(sizeof(tBTA_AV_API_OPEN));
memcpy(p_buf, &(p_scb->open_api), sizeof(tBTA_AV_API_OPEN));
bta_sys_sendmsg(p_buf);
-} \ No newline at end of file
+}
diff --git a/system/bta/dm/bta_dm_disc.cc b/system/bta/dm/bta_dm_disc.cc
index e9c6ba5e77..c3977454f5 100644
--- a/system/bta/dm/bta_dm_disc.cc
+++ b/system/bta/dm/bta_dm_disc.cc
@@ -511,7 +511,7 @@ static void bta_dm_gatt_disc_complete(tCONN_ID conn_id, tGATT_STATUS status) {
log::verbose("conn_id = {}, status = {}, sdp_pending = {}, le_pending = {}", conn_id, status,
sdp_pending, le_pending);
- if (com::android::bluetooth::flags::bta_dm_discover_both() && sdp_pending && !le_pending) {
+ if (sdp_pending && !le_pending) {
/* LE Service discovery finished, and services were reported, but SDP is not
* finished yet. gatt_close_timer closed the connection, and we received
* this callback because of disconnection */
@@ -784,8 +784,7 @@ static void bta_dm_disc_sm_execute(tBTA_DM_DISC_EVT event, std::unique_ptr<tBTA_
"bad message type: {}", msg->index());
auto req = std::get<tBTA_DM_API_DISCOVER>(*msg);
- if (com::android::bluetooth::flags::bta_dm_discover_both() &&
- is_same_device(req.bd_addr, bta_dm_discovery_cb.peer_bdaddr)) {
+ if (is_same_device(req.bd_addr, bta_dm_discovery_cb.peer_bdaddr)) {
bta_dm_discover_services(std::get<tBTA_DM_API_DISCOVER>(*msg));
} else {
bta_dm_queue_disc(std::get<tBTA_DM_API_DISCOVER>(*msg));
diff --git a/system/bta/hh/bta_hh_headtracker.cc b/system/bta/hh/bta_hh_headtracker.cc
index c8b3e20f05..0282cb59d0 100644
--- a/system/bta/hh/bta_hh_headtracker.cc
+++ b/system/bta/hh/bta_hh_headtracker.cc
@@ -140,7 +140,10 @@ void bta_hh_headtracker_parse_service(tBTA_HH_DEV_CB* p_dev_cb, const gatt::Serv
bool bta_hh_headtracker_supported(tBTA_HH_DEV_CB* p_dev_cb) {
if (p_dev_cb->hid_srvc.headtracker_support == BTA_HH_UNKNOWN) {
bluetooth::Uuid remote_uuids[BT_MAX_NUM_UUIDS] = {};
- bt_property_t remote_properties = {BT_PROPERTY_UUIDS, sizeof(remote_uuids), &remote_uuids};
+ bt_property_t remote_properties = {com::android::bluetooth::flags::separate_service_storage()
+ ? BT_PROPERTY_UUIDS_LE
+ : BT_PROPERTY_UUIDS,
+ sizeof(remote_uuids), &remote_uuids};
const RawAddress& bd_addr = p_dev_cb->link_spec.addrt.bda;
p_dev_cb->hid_srvc.headtracker_support = BTA_HH_UNAVAILABLE;
diff --git a/system/bta/include/bta_ras_api.h b/system/bta/include/bta_ras_api.h
index ee6eccec4f..48113e5edf 100644
--- a/system/bta/include/bta_ras_api.h
+++ b/system/bta/include/bta_ras_api.h
@@ -25,6 +25,12 @@
namespace bluetooth {
namespace ras {
+enum class RasDisconnectReason {
+ GATT_DISCONNECT,
+ SERVER_NOT_AVAILABLE,
+ FATAL_ERROR,
+};
+
struct VendorSpecificCharacteristic {
bluetooth::Uuid characteristicUuid_;
std::vector<uint8_t> value_;
@@ -64,7 +70,8 @@ public:
const std::vector<VendorSpecificCharacteristic>& vendor_specific_characteristics,
uint16_t conn_interval) = 0;
virtual void OnConnIntervalUpdated(const RawAddress& address, uint16_t conn_interval) = 0;
- virtual void OnDisconnected(const RawAddress& address) = 0;
+ virtual void OnDisconnected(const RawAddress& address,
+ const RasDisconnectReason& ras_disconnect_reason) = 0;
virtual void OnWriteVendorSpecificReplyComplete(const RawAddress& address, bool success) = 0;
virtual void OnRemoteData(const RawAddress& address, const std::vector<uint8_t>& data) = 0;
virtual void OnRemoteDataTimeout(const RawAddress& address) = 0;
diff --git a/system/bta/jv/bta_jv_act.cc b/system/bta/jv/bta_jv_act.cc
index 7c3cb13d47..ffe1756943 100644
--- a/system/bta/jv/bta_jv_act.cc
+++ b/system/bta/jv/bta_jv_act.cc
@@ -233,7 +233,7 @@ tBTA_JV_RFC_CB* bta_jv_alloc_rfc_cb(uint16_t port_handle, tBTA_JV_PCB** pp_pcb)
p_cb->rfc_hdl[j] = 0;
}
p_cb->rfc_hdl[0] = port_handle;
- log::verbose("port_handle={}, handle=0x{:x}", port_handle, p_cb->handle);
+ log::verbose("port_handle={}, jv_handle=0x{:x}", port_handle, p_cb->handle);
p_pcb = &bta_jv_cb.port_cb[port_handle - 1];
p_pcb->handle = p_cb->handle;
@@ -307,7 +307,7 @@ static tBTA_JV_STATUS bta_jv_free_rfc_cb(tBTA_JV_RFC_CB* p_cb, tBTA_JV_PCB* p_pc
log::error("p_cb or p_pcb cannot be null");
return tBTA_JV_STATUS::FAILURE;
}
- log::verbose("max_sess={}, curr_sess={}, p_pcb={}, user={}, state={}, jv handle=0x{:x}",
+ log::verbose("max_sess={}, curr_sess={}, p_pcb={}, user={}, state={}, jv_handle=0x{:x}",
p_cb->max_sess, p_cb->curr_sess, std::format_ptr(p_pcb), p_pcb->rfcomm_slot_id,
p_pcb->state, p_pcb->handle);
@@ -341,7 +341,7 @@ static tBTA_JV_STATUS bta_jv_free_rfc_cb(tBTA_JV_RFC_CB* p_cb, tBTA_JV_PCB* p_pc
break;
default:
log::warn(
- "failed, ignore port state= {}, scn={}, p_pcb= {}, jv handle=0x{:x}, "
+ "failed, ignore port state= {}, scn={}, p_pcb= {}, jv_handle=0x{:x}, "
"port_handle={}, user_data={}",
p_pcb->state, p_cb->scn, std::format_ptr(p_pcb), p_pcb->handle, p_pcb->port_handle,
p_pcb->rfcomm_slot_id);
@@ -359,7 +359,7 @@ static tBTA_JV_STATUS bta_jv_free_rfc_cb(tBTA_JV_RFC_CB* p_cb, tBTA_JV_PCB* p_pc
if (port_status != PORT_SUCCESS) {
status = tBTA_JV_STATUS::FAILURE;
log::warn(
- "Remove jv handle=0x{:x}, state={}, port_status={}, port_handle={}, close_pending={}",
+ "Remove jv_handle=0x{:x}, state={}, port_status={}, port_handle={}, close_pending={}",
p_pcb->handle, p_pcb->state, port_status, p_pcb->port_handle, close_pending);
}
}
@@ -470,8 +470,8 @@ static tBTA_JV_STATUS bta_jv_free_set_pm_profile_cb(uint32_t jv_handle) {
}
}
- log::verbose("jv_handle=0x{:x}, idx={}app_id={}, bd_counter={}, appid_counter={}", jv_handle,
- i, bta_jv_cb.pm_cb[i].app_id, bd_counter, appid_counter);
+ log::verbose("jv_handle=0x{:x}, idx={}, app_id={}, bd_counter={}, appid_counter={}",
+ jv_handle, i, bta_jv_cb.pm_cb[i].app_id, bd_counter, appid_counter);
if (bd_counter > 1) {
bta_jv_pm_conn_idle(&bta_jv_cb.pm_cb[i]);
}
@@ -559,7 +559,7 @@ static tBTA_JV_PM_CB* bta_jv_alloc_set_pm_profile_cb(uint32_t jv_handle, tBTA_JV
}
}
}
- log::verbose("handle=0x{:x}, app_id={}, idx={}, BTA_JV_PM_MAX_NUM={}, pp_cb={}", jv_handle,
+ log::verbose("jv_handle=0x{:x}, app_id={}, idx={}, BTA_JV_PM_MAX_NUM={}, pp_cb={}", jv_handle,
app_id, i, BTA_JV_PM_MAX_NUM, std::format_ptr(pp_cb));
break;
}
@@ -573,7 +573,7 @@ static tBTA_JV_PM_CB* bta_jv_alloc_set_pm_profile_cb(uint32_t jv_handle, tBTA_JV
bta_jv_cb.pm_cb[i].state = BTA_JV_PM_IDLE_ST;
return &bta_jv_cb.pm_cb[i];
}
- log::warn("handle=0x{:x}, app_id={}, return NULL", jv_handle, app_id);
+ log::warn("jv_handle=0x{:x}, app_id={}, return NULL", jv_handle, app_id);
return NULL;
}
@@ -954,7 +954,7 @@ void bta_jv_delete_record(uint32_t handle) {
if (handle) {
/* this is a record created by btif layer*/
if (!get_legacy_stack_sdp_api()->handle.SDP_DeleteRecord(handle)) {
- log::warn("Unable to delete SDP record handle:{}", handle);
+ log::warn("Unable to delete SDP record handle:{}", handle);
}
}
}
@@ -997,7 +997,7 @@ static void bta_jv_l2cap_client_cback(uint16_t gap_handle, uint16_t event, tGAP_
if (GAP_GetLeChannelInfo(gap_handle, &remote_mtu, &local_mps, &remote_mps, &local_credit,
&remote_credit, &local_cid, &remote_cid,
&acl_handle) != PORT_SUCCESS) {
- log::warn("Unable to get GAP channel info handle:{}", gap_handle);
+ log::warn("Unable to get GAP channel info gap_handle:{}", gap_handle);
}
evt_data.l2c_open.tx_mtu = remote_mtu;
evt_data.l2c_open.local_coc_mps = local_mps;
@@ -1426,10 +1426,10 @@ static void bta_jv_port_mgmt_cl_cback(const tPORT_RESULT code, uint16_t port_han
return;
}
- log::verbose("code={}, port_handle={}, handle={}", code, port_handle, p_cb->handle);
+ log::verbose("code={}, port_handle={}, rfc_handle={}", code, port_handle, p_cb->handle);
if (PORT_CheckConnection(port_handle, &rem_bda, &lcid) != PORT_SUCCESS) {
- log::warn("Unable to check RFCOMM connection peer:{} handle:{}", rem_bda, port_handle);
+ log::warn("Unable to check RFCOMM connection peer:{} port_handle:{}", rem_bda, port_handle);
}
if (code == PORT_SUCCESS) {
@@ -1448,7 +1448,7 @@ static void bta_jv_port_mgmt_cl_cback(const tPORT_RESULT code, uint16_t port_han
&evt_data.rfc_open.dlci, &evt_data.rfc_open.max_frame_size,
&evt_data.rfc_open.acl_handle,
&evt_data.rfc_open.mux_initiator) != PORT_SUCCESS) {
- log::warn("Unable to get RFCOMM channel info peer:{} handle:{}", rem_bda, port_handle);
+ log::warn("Unable to get RFCOMM channel info peer:{} port_handle:{}", rem_bda, port_handle);
}
}
p_pcb->state = BTA_JV_ST_CL_OPEN;
@@ -1490,7 +1490,7 @@ static void bta_jv_port_event_cl_cback(uint32_t code, uint16_t port_handle) {
return;
}
- log::verbose("code=0x{:x}, port_handle={}, handle={}", code, port_handle, p_cb->handle);
+ log::verbose("code=0x{:x}, port_handle={}, rfc_handle={}", code, port_handle, p_cb->handle);
if (code & PORT_EV_RXCHAR) {
evt_data.data_ind.handle = p_cb->handle;
p_cb->p_cback(BTA_JV_RFCOMM_DATA_IND_EVT, &evt_data, p_pcb->rfcomm_slot_id);
@@ -1550,23 +1550,23 @@ void bta_jv_rfcomm_connect(tBTA_SEC sec_mask, uint8_t remote_scn, const RawAddre
bta_jv.rfc_cl_init.use_co = true;
if (PORT_SetAppUid(handle, app_uid) != PORT_SUCCESS) {
- log::warn("Unable to set app_uid for port handle:{}", handle);
+ log::warn("Unable to set app_uid for port_handle:{}", handle);
}
if (PORT_SetEventMaskAndCallback(handle, event_mask, bta_jv_port_event_cl_cback) !=
PORT_SUCCESS) {
- log::warn("Unable to set RFCOMM client event mask and callback handle:{}", handle);
+ log::warn("Unable to set RFCOMM client event mask and callback port_handle:{}", handle);
}
if (PORT_SetDataCOCallback(handle, bta_jv_port_data_co_cback) != PORT_SUCCESS) {
- log::warn("Unable to set RFCOMM client data callback handle:{}", handle);
+ log::warn("Unable to set RFCOMM client data callback port_handle:{}", handle);
}
if (PORT_GetSettings(handle, &port_settings) != PORT_SUCCESS) {
- log::warn("Unable to get RFCOMM client state handle:{}", handle);
+ log::warn("Unable to get RFCOMM client state port_handle:{}", handle);
}
port_settings.fc_type = (PORT_FC_CTS_ON_INPUT | PORT_FC_CTS_ON_OUTPUT);
if (PORT_SetSettings(handle, &port_settings) != PORT_SUCCESS) {
- log::warn("Unable to set RFCOMM client state handle:{}", handle);
+ log::warn("Unable to set RFCOMM client state port_handle:{}", handle);
}
bta_jv.rfc_cl_init.handle = p_cb->handle;
@@ -1580,7 +1580,7 @@ void bta_jv_rfcomm_connect(tBTA_SEC sec_mask, uint8_t remote_scn, const RawAddre
if (bta_jv.rfc_cl_init.status == tBTA_JV_STATUS::FAILURE) {
if (handle) {
if (RFCOMM_RemoveConnection(handle) != PORT_SUCCESS) {
- log::warn("Unable to remove RFCOMM connection handle:{}", handle);
+ log::warn("Unable to remove RFCOMM connection port_handle:{}", handle);
}
}
}
@@ -1597,7 +1597,7 @@ static int find_rfc_pcb(uint32_t rfcomm_slot_id, tBTA_JV_RFC_CB** cb, tBTA_JV_PC
*pcb = &bta_jv_cb.port_cb[i];
*cb = &bta_jv_cb.rfc_cb[rfc_handle - 1];
log::verbose(
- "FOUND rfc_cb_handle=0x{:x}, port.jv_handle=0x{:x}, state={}, rfc_cb->handle=0x{:x}",
+ "FOUND rfc_handle=0x{:x}, port.jv_handle=0x{:x}, state={}, rfc_cb->handle=0x{:x}",
rfc_handle, (*pcb)->handle, (*pcb)->state, (*cb)->handle);
return 1;
}
@@ -1609,11 +1609,11 @@ static int find_rfc_pcb(uint32_t rfcomm_slot_id, tBTA_JV_RFC_CB** cb, tBTA_JV_PC
/* Close an RFCOMM connection */
void bta_jv_rfcomm_close(uint32_t handle, uint32_t rfcomm_slot_id) {
if (!handle) {
- log::error("rfc handle is null");
+ log::error("rfc_handle is null");
return;
}
- log::verbose("rfc handle={}", handle);
+ log::verbose("rfc_handle={}", handle);
tBTA_JV_RFC_CB* p_cb = NULL;
tBTA_JV_PCB* p_pcb = NULL;
@@ -1647,7 +1647,7 @@ static void bta_jv_port_mgmt_sr_cback(const tPORT_RESULT code, uint16_t port_han
return;
}
uint32_t rfcomm_slot_id = p_pcb->rfcomm_slot_id;
- log::verbose("code={}, port_handle=0x{:x}, handle=0x{:x}, p_pcb{}, user={}", code, port_handle,
+ log::verbose("code={}, port_handle=0x{:x}, jv_handle=0x{:x}, p_pcb{}, user={}", code, port_handle,
p_cb->handle, std::format_ptr(p_pcb), p_pcb->rfcomm_slot_id);
int status = PORT_CheckConnection(port_handle, &rem_bda, &lcid);
@@ -1668,7 +1668,7 @@ static void bta_jv_port_mgmt_sr_cback(const tPORT_RESULT code, uint16_t port_han
&evt_data.rfc_srv_open.dlci, &evt_data.rfc_srv_open.max_frame_size,
&evt_data.rfc_srv_open.acl_handle,
&evt_data.rfc_srv_open.mux_initiator) != PORT_SUCCESS) {
- log::warn("Unable to get RFCOMM channel info peer:{} handle:{}", rem_bda, port_handle);
+ log::warn("Unable to get RFCOMM channel info peer:{} port_handle:{}", rem_bda, port_handle);
}
}
tBTA_JV_PCB* p_pcb_new_listen = bta_jv_add_rfc_port(p_cb, p_pcb);
@@ -1729,7 +1729,7 @@ static void bta_jv_port_event_sr_cback(uint32_t code, uint16_t port_handle) {
return;
}
- log::verbose("code=0x{:x}, port_handle={}, handle={}", code, port_handle, p_cb->handle);
+ log::verbose("code=0x{:x}, port_handle={}, rfc_handle={}", code, port_handle, p_cb->handle);
uint32_t user_data = p_pcb->rfcomm_slot_id;
if (code & PORT_EV_RXCHAR) {
@@ -1779,7 +1779,8 @@ static tBTA_JV_PCB* bta_jv_add_rfc_port(tBTA_JV_RFC_CB* p_cb, tBTA_JV_PCB* p_pcb
} else {
log::error(
- "open pcb not matching listen one, count={}, listen pcb handle={}, open pcb={}",
+ "open pcb not matching listen one, count={}, listen port_handle={}, open "
+ "pcb={}",
listen, p_pcb->port_handle, p_pcb_open->handle);
return NULL;
}
@@ -1809,24 +1810,25 @@ static tBTA_JV_PCB* bta_jv_add_rfc_port(tBTA_JV_RFC_CB* p_cb, tBTA_JV_PCB* p_pcb
p_pcb->rfcomm_slot_id = p_pcb_open->rfcomm_slot_id;
if (PORT_ClearKeepHandleFlag(p_pcb->port_handle) != PORT_SUCCESS) {
- log::warn("Unable to clear RFCOMM server keep handle flag handle:{}", p_pcb->port_handle);
+ log::warn("Unable to clear RFCOMM server keep handle flag port_handle:{}",
+ p_pcb->port_handle);
}
if (PORT_SetEventMaskAndCallback(p_pcb->port_handle, event_mask,
bta_jv_port_event_sr_cback) != PORT_SUCCESS) {
- log::warn("Unable to set RFCOMM server event mask and callback handle:{}",
+ log::warn("Unable to set RFCOMM server event mask and callback port_handle:{}",
p_pcb->port_handle);
}
if (PORT_SetDataCOCallback(p_pcb->port_handle, bta_jv_port_data_co_cback) != PORT_SUCCESS) {
- log::warn("Unable to set RFCOMM server data callback handle:{}", p_pcb->port_handle);
+ log::warn("Unable to set RFCOMM server data callback port_handle:{}", p_pcb->port_handle);
}
if (PORT_GetSettings(p_pcb->port_handle, &port_settings) != PORT_SUCCESS) {
- log::warn("Unable to get RFCOMM server state handle:{}", p_pcb->port_handle);
+ log::warn("Unable to get RFCOMM server state port_handle:{}", p_pcb->port_handle);
}
port_settings.fc_type = (PORT_FC_CTS_ON_INPUT | PORT_FC_CTS_ON_OUTPUT);
if (PORT_SetSettings(p_pcb->port_handle, &port_settings) != PORT_SUCCESS) {
- log::warn("Unable to set RFCOMM server state handle:{}", p_pcb->port_handle);
+ log::warn("Unable to set RFCOMM server state port_handle:{}", p_pcb->port_handle);
}
p_pcb->handle = BTA_JV_RFC_H_S_TO_HDL(p_cb->handle, si);
log::verbose("p_pcb->handle=0x{:x}, curr_sess={}", p_pcb->handle, p_cb->curr_sess);
@@ -1881,23 +1883,23 @@ void bta_jv_rfcomm_start_server(tBTA_SEC sec_mask, uint8_t local_scn, uint8_t ma
evt_data.use_co = true;
if (PORT_SetAppUid(handle, app_uid) != PORT_SUCCESS) {
- log::warn("Unable to set app_uid for port handle:{}", handle);
+ log::warn("Unable to set app_uid for port_handle:{}", handle);
}
if (PORT_ClearKeepHandleFlag(handle) != PORT_SUCCESS) {
- log::warn("Unable to clear RFCOMM server keep handle flag handle:{}", handle);
+ log::warn("Unable to clear RFCOMM server keep handle flag port_handle:{}", handle);
}
if (PORT_SetEventMaskAndCallback(handle, event_mask, bta_jv_port_event_sr_cback) !=
PORT_SUCCESS) {
- log::warn("Unable to set RFCOMM server event mask and callback handle:{}", handle);
+ log::warn("Unable to set RFCOMM server event mask and callback port_handle:{}", handle);
}
if (PORT_GetSettings(handle, &port_settings) != PORT_SUCCESS) {
- log::warn("Unable to get RFCOMM server state handle:{}", handle);
+ log::warn("Unable to get RFCOMM server state port_handle:{}", handle);
}
port_settings.fc_type = (PORT_FC_CTS_ON_INPUT | PORT_FC_CTS_ON_OUTPUT);
if (PORT_SetSettings(handle, &port_settings) != PORT_SUCCESS) {
- log::warn("Unable to set RFCOMM port state handle:{}", handle);
+ log::warn("Unable to set RFCOMM port state port_handle:{}", handle);
}
} while (0);
@@ -1906,12 +1908,12 @@ void bta_jv_rfcomm_start_server(tBTA_SEC sec_mask, uint8_t local_scn, uint8_t ma
p_cback(BTA_JV_RFCOMM_START_EVT, &bta_jv, rfcomm_slot_id);
if (bta_jv.rfc_start.status == tBTA_JV_STATUS::SUCCESS) {
if (PORT_SetDataCOCallback(handle, bta_jv_port_data_co_cback) != PORT_SUCCESS) {
- log::error("Unable to set RFCOMM server data callback handle:{}", handle);
+ log::error("Unable to set RFCOMM server data callback port_handle:{}", handle);
}
} else {
if (handle) {
if (RFCOMM_RemoveConnection(handle) != PORT_SUCCESS) {
- log::warn("Unable to remote RFCOMM server connection handle:{}", handle);
+ log::warn("Unable to remote RFCOMM server connection port_handle:{}", handle);
}
}
}
@@ -1920,7 +1922,7 @@ void bta_jv_rfcomm_start_server(tBTA_SEC sec_mask, uint8_t local_scn, uint8_t ma
/* stops an RFCOMM server */
void bta_jv_rfcomm_stop_server(uint32_t handle, uint32_t rfcomm_slot_id) {
if (!handle) {
- log::error("jv handle is null");
+ log::error("jv_handle is null");
return;
}
@@ -1971,23 +1973,24 @@ void bta_jv_rfcomm_write(uint32_t handle, uint32_t req_id, tBTA_JV_RFC_CB* p_cb,
/* Set or free power mode profile for a JV application */
void bta_jv_set_pm_profile(uint32_t handle, tBTA_JV_PM_ID app_id, tBTA_JV_CONN_STATE init_st) {
- log::verbose("handle=0x{:x}, app_id={}, init_st={}", handle, app_id,
+ log::verbose("jv_handle=0x{:x}, app_id={}, init_st={}", handle, app_id,
bta_jv_conn_state_text(init_st));
/* clear PM control block */
if (app_id == BTA_JV_PM_ID_CLEAR) {
tBTA_JV_STATUS status = bta_jv_free_set_pm_profile_cb(handle);
if (status != tBTA_JV_STATUS::SUCCESS) {
- log::warn("Unable to free a power mode profile handle:0x:{:x} app_id:{} state:{} status:{}",
- handle, app_id, init_st, bta_jv_status_text(status));
+ log::warn(
+ "Unable to free a power mode profile jv_handle:0x:{:x} app_id:{} state:{} status:{}",
+ handle, app_id, init_st, bta_jv_status_text(status));
}
} else { /* set PM control block */
tBTA_JV_PM_CB* p_cb = bta_jv_alloc_set_pm_profile_cb(handle, app_id);
if (p_cb) {
bta_jv_pm_state_change(p_cb, init_st);
} else {
- log::warn("Unable to allocate a power mode profile handle:0x:{:x} app_id:{} state:{}", handle,
- app_id, init_st);
+ log::warn("Unable to allocate a power mode profile jv_handle:0x:{:x} app_id:{} state:{}",
+ handle, app_id, init_st);
}
}
}
@@ -2038,7 +2041,7 @@ static void bta_jv_pm_conn_idle(tBTA_JV_PM_CB* p_cb) {
*
******************************************************************************/
static void bta_jv_pm_state_change(tBTA_JV_PM_CB* p_cb, const tBTA_JV_CONN_STATE state) {
- log::verbose("p_cb={}, handle=0x{:x}, busy/idle_state={}, app_id={}, conn_state={}",
+ log::verbose("p_cb={}, jv_handle=0x{:x}, busy/idle_state={}, app_id={}, conn_state={}",
std::format_ptr(p_cb), p_cb->handle, p_cb->state, p_cb->app_id,
bta_jv_conn_state_text(state));
diff --git a/system/bta/le_audio/client.cc b/system/bta/le_audio/client.cc
index c5901065d5..6dbb4ace85 100644
--- a/system/bta/le_audio/client.cc
+++ b/system/bta/le_audio/client.cc
@@ -6160,6 +6160,20 @@ public:
}
break;
}
+ case GroupStreamStatus::RELEASING_AUTONOMOUS:
+ /* Remote device releases all the ASEs autonomusly. This should not happen and not sure what
+ * is the remote device intention. If remote wants stop the stream then MCS shall be used to
+ * stop the stream in a proper way. For a phone call, GTBS shall be used. For now we assume
+ * this device has does not want to be used for streaming and mark it as Inactive.
+ */
+ log::warn("Group {} is doing autonomous release, make it inactive", group_id);
+ if (group) {
+ group->PrintDebugState();
+ groupSetAndNotifyInactive();
+ }
+ audio_sender_state_ = AudioState::IDLE;
+ audio_receiver_state_ = AudioState::IDLE;
+ break;
case GroupStreamStatus::RELEASING:
case GroupStreamStatus::SUSPENDING:
if (active_group_id_ != bluetooth::groups::kGroupUnknown &&
@@ -6172,9 +6186,12 @@ public:
* it means that it is some internal state machine error. This is very unlikely and
* for now just Inactivate the group.
*/
- log::error("Internal state machine error");
+ log::error("Internal state machine error for group {}", group_id);
group->PrintDebugState();
groupSetAndNotifyInactive();
+ audio_sender_state_ = AudioState::IDLE;
+ audio_receiver_state_ = AudioState::IDLE;
+ return;
}
if (is_active_group_operation) {
diff --git a/system/bta/le_audio/le_audio_client_test.cc b/system/bta/le_audio/le_audio_client_test.cc
index 3e4c50aff3..693494f73c 100644
--- a/system/bta/le_audio/le_audio_client_test.cc
+++ b/system/bta/le_audio/le_audio_client_test.cc
@@ -6817,11 +6817,18 @@ TEST_F(UnicastTest, SpeakerStreamingAutonomousRelease) {
Mock::VerifyAndClearExpectations(&mock_audio_hal_client_callbacks_);
Mock::VerifyAndClearExpectations(mock_le_audio_source_hal_client_);
+ Mock::VerifyAndClearExpectations(mock_le_audio_sink_hal_client_);
SyncOnMainLoop();
// Verify Data transfer on one audio source cis
TestAudioDataTransfer(group_id, 1 /* cis_count_out */, 0 /* cis_count_in */, 1920);
+ EXPECT_CALL(mock_audio_hal_client_callbacks_, OnGroupStatus(group_id, GroupStatus::INACTIVE))
+ .Times(1);
+ EXPECT_CALL(mock_audio_hal_client_callbacks_,
+ OnGroupStreamStatus(group_id, GroupStreamStatus::IDLE))
+ .Times(1);
+
// Inject the IDLE state as if an autonomous release happened
ASSERT_NE(0lu, streaming_groups.count(group_id));
auto group = streaming_groups.at(group_id);
@@ -6835,9 +6842,14 @@ TEST_F(UnicastTest, SpeakerStreamingAutonomousRelease) {
InjectCisDisconnected(group_id, ase.cis_conn_hdl);
}
}
-
// Verify no Data transfer after the autonomous release
TestAudioDataTransfer(group_id, 0 /* cis_count_out */, 0 /* cis_count_in */, 1920);
+
+ // Inject Releasing
+ state_machine_callbacks_->StatusReportCb(group->group_id_,
+ GroupStreamStatus::RELEASING_AUTONOMOUS);
+ SyncOnMainLoop();
+ Mock::VerifyAndClearExpectations(&mock_audio_hal_client_callbacks_);
}
TEST_F(UnicastTest, TwoEarbudsStreaming) {
@@ -12121,6 +12133,16 @@ TEST_F(UnicastTest, GroupStreamStatus) {
EXPECT_CALL(mock_audio_hal_client_callbacks_,
OnGroupStreamStatus(group_id, GroupStreamStatus::IDLE))
.Times(1);
+ state_machine_callbacks_->StatusReportCb(group_id, GroupStreamStatus::RELEASING_AUTONOMOUS);
+
+ EXPECT_CALL(mock_audio_hal_client_callbacks_,
+ OnGroupStreamStatus(group_id, GroupStreamStatus::STREAMING))
+ .Times(1);
+ state_machine_callbacks_->StatusReportCb(group_id, GroupStreamStatus::STREAMING);
+
+ EXPECT_CALL(mock_audio_hal_client_callbacks_,
+ OnGroupStreamStatus(group_id, GroupStreamStatus::IDLE))
+ .Times(1);
state_machine_callbacks_->StatusReportCb(group_id, GroupStreamStatus::SUSPENDING);
EXPECT_CALL(mock_audio_hal_client_callbacks_,
diff --git a/system/bta/le_audio/state_machine.cc b/system/bta/le_audio/state_machine.cc
index dd1c535be1..cc0a79690c 100644
--- a/system/bta/le_audio/state_machine.cc
+++ b/system/bta/le_audio/state_machine.cc
@@ -1084,7 +1084,7 @@ public:
/* Note, that this type is actually LONG WRITE.
* Meaning all the Prepare Writes plus Execute is handled in the stack
*/
- write_type = GATT_WRITE_PREPARE;
+ write_type = GATT_WRITE;
}
BtaGattQueue::WriteCharacteristic(leAudioDevice->conn_id_, leAudioDevice->ctp_hdls_.val_hdl,
@@ -3045,7 +3045,7 @@ private:
log::info("Group {} is doing autonomous release", group->group_id_);
SetTargetState(group, AseState::BTA_LE_AUDIO_ASE_STATE_IDLE);
state_machine_callbacks_->StatusReportCb(group->group_id_,
- GroupStreamStatus::RELEASING);
+ GroupStreamStatus::RELEASING_AUTONOMOUS);
}
}
diff --git a/system/bta/le_audio/state_machine_test.cc b/system/bta/le_audio/state_machine_test.cc
index 7fc2985b28..682bb9c722 100644
--- a/system/bta/le_audio/state_machine_test.cc
+++ b/system/bta/le_audio/state_machine_test.cc
@@ -4147,9 +4147,13 @@ TEST_F(StateMachineTest, testAutonomousReleaseMultiple) {
// Validate GroupStreamStatus
EXPECT_CALL(mock_callbacks_,
- StatusReportCb(leaudio_group_id, bluetooth::le_audio::GroupStreamStatus::RELEASING))
+ StatusReportCb(leaudio_group_id,
+ bluetooth::le_audio::GroupStreamStatus::RELEASING_AUTONOMOUS))
.Times(1);
EXPECT_CALL(mock_callbacks_,
+ StatusReportCb(leaudio_group_id, bluetooth::le_audio::GroupStreamStatus::RELEASING))
+ .Times(0);
+ EXPECT_CALL(mock_callbacks_,
StatusReportCb(leaudio_group_id, bluetooth::le_audio::GroupStreamStatus::IDLE))
.Times(1);
EXPECT_CALL(mock_callbacks_,
diff --git a/system/bta/ras/ras_client.cc b/system/bta/ras/ras_client.cc
index 196ec06a5e..b052717d14 100644
--- a/system/bta/ras/ras_client.cc
+++ b/system/bta/ras/ras_client.cc
@@ -51,6 +51,7 @@ using namespace bluetooth;
using namespace ::ras;
using namespace ::ras::feature;
using namespace ::ras::uuid;
+using bluetooth::ras::RasDisconnectReason;
using bluetooth::ras::VendorSpecificCharacteristic;
namespace {
@@ -287,6 +288,8 @@ public:
if (evt.status != GATT_SUCCESS) {
log::error("Failed to connect to server device {}", evt.remote_bda);
+ callbacks_->OnDisconnected(tracker->address_for_cs_,
+ RasDisconnectReason::SERVER_NOT_AVAILABLE);
return;
}
tracker->conn_id_ = evt.conn_id;
@@ -308,7 +311,7 @@ public:
BTA_GATTC_Close(evt.conn_id);
return;
}
- callbacks_->OnDisconnected(tracker->address_for_cs_);
+ callbacks_->OnDisconnected(tracker->address_for_cs_, RasDisconnectReason::GATT_DISCONNECT);
trackers_.remove(tracker);
}
@@ -338,6 +341,8 @@ public:
return;
} else if (!service_found) {
log::error("Can't find Ranging Service in the services list");
+ callbacks_->OnDisconnected(tracker->address_for_cs_,
+ RasDisconnectReason::SERVER_NOT_AVAILABLE);
return;
} else {
log::info("Found Ranging Service");
@@ -347,7 +352,10 @@ public:
if (UseCachedData(tracker)) {
log::info("Use cached data for Ras features and vendor specific characteristic");
- SubscribeCharacteristic(tracker, kRasControlPointCharacteristic);
+ if (!SubscribeCharacteristic(tracker, kRasControlPointCharacteristic)) {
+ callbacks_->OnDisconnected(tracker->address_for_cs_,
+ RasDisconnectReason::SERVER_NOT_AVAILABLE);
+ }
AllCharacteristicsReadComplete(tracker);
} else {
// Read Vendor Specific Uuid
@@ -370,6 +378,8 @@ public:
auto characteristic = tracker->FindCharacteristicByUuid(kRasFeaturesCharacteristic);
if (characteristic == nullptr) {
log::error("Can not find Characteristic for Ras Features");
+ callbacks_->OnDisconnected(tracker->address_for_cs_,
+ RasDisconnectReason::SERVER_NOT_AVAILABLE);
return;
}
BTA_GATTC_ReadCharacteristic(
@@ -380,7 +390,9 @@ public:
},
&gatt_read_callback_data_);
- SubscribeCharacteristic(tracker, kRasControlPointCharacteristic);
+ if (!SubscribeCharacteristic(tracker, kRasControlPointCharacteristic)) {
+ callbacks_->OnDisconnected(tracker->address_for_cs_, RasDisconnectReason::FATAL_ERROR);
+ }
}
}
@@ -628,23 +640,23 @@ public:
}
}
- void SubscribeCharacteristic(std::shared_ptr<RasTracker> tracker, const Uuid uuid) {
+ bool SubscribeCharacteristic(std::shared_ptr<RasTracker> tracker, const Uuid uuid) {
auto characteristic = tracker->FindCharacteristicByUuid(uuid);
if (characteristic == nullptr) {
log::warn("Can't find characteristic 0x{:04x}", uuid.As16Bit());
- return;
+ return false;
}
uint16_t ccc_handle = FindCccHandle(characteristic);
if (ccc_handle == GAP_INVALID_HANDLE) {
log::warn("Can't find Client Characteristic Configuration descriptor");
- return;
+ return false;
}
tGATT_STATUS register_status = BTA_GATTC_RegisterForNotifications(gatt_if_, tracker->address_,
characteristic->value_handle);
if (register_status != GATT_SUCCESS) {
log::error("Fail to register, {}", gatt_status_text(register_status));
- return;
+ return false;
}
std::vector<uint8_t> value(2);
@@ -664,6 +676,7 @@ public:
}
},
nullptr);
+ return true;
}
void UnsubscribeCharacteristic(std::shared_ptr<RasTracker> tracker, const Uuid uuid) {
@@ -790,14 +803,22 @@ public:
if (tracker->remote_supported_features_ & feature::kRealTimeRangingData) {
log::info("Subscribe Real-time Ranging Data");
tracker->ranging_type_ = REAL_TIME;
- SubscribeCharacteristic(tracker, kRasRealTimeRangingDataCharacteristic);
+ if (!SubscribeCharacteristic(tracker, kRasRealTimeRangingDataCharacteristic)) {
+ callbacks_->OnDisconnected(tracker->address_for_cs_,
+ RasDisconnectReason::SERVER_NOT_AVAILABLE);
+ return;
+ }
SetTimeOutAlarm(tracker, kFirstSegmentRangingDataTimeoutMs, TimeoutType::FIRST_SEGMENT);
} else {
log::info("Subscribe On-demand Ranging Data");
tracker->ranging_type_ = ON_DEMAND;
- SubscribeCharacteristic(tracker, kRasOnDemandDataCharacteristic);
- SubscribeCharacteristic(tracker, kRasRangingDataReadyCharacteristic);
- SubscribeCharacteristic(tracker, kRasRangingDataOverWrittenCharacteristic);
+ if (!SubscribeCharacteristic(tracker, kRasOnDemandDataCharacteristic) ||
+ !SubscribeCharacteristic(tracker, kRasRangingDataReadyCharacteristic) ||
+ !SubscribeCharacteristic(tracker, kRasRangingDataOverWrittenCharacteristic)) {
+ callbacks_->OnDisconnected(tracker->address_for_cs_,
+ RasDisconnectReason::SERVER_NOT_AVAILABLE);
+ return;
+ }
SetTimeOutAlarm(tracker, kRangingDataReadyTimeoutMs, TimeoutType::RANGING_DATA_READY);
}
auto characteristic = tracker->FindCharacteristicByUuid(kRasRealTimeRangingDataCharacteristic);
diff --git a/system/bta/ras/ras_utils_test.cc b/system/bta/ras/ras_utils_test.cc
new file mode 100644
index 0000000000..7acc658265
--- /dev/null
+++ b/system/bta/ras/ras_utils_test.cc
@@ -0,0 +1,179 @@
+/*
+ * Copyright 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <gtest/gtest.h>
+
+#include "bta/include/bta_ras_api.h"
+#include "bta/ras/ras_types.h"
+
+class RasUtilsTest : public ::testing::Test {};
+
+TEST(RasUtilsTest, GetUuidName) {
+ // Test known UUIDs
+ EXPECT_EQ(ras::uuid::getUuidName(bluetooth::Uuid::From16Bit(ras::uuid::kRangingService16Bit)),
+ "Ranging Service");
+ EXPECT_EQ(ras::uuid::getUuidName(
+ bluetooth::Uuid::From16Bit(ras::uuid::kRasFeaturesCharacteristic16bit)),
+ "RAS Features");
+ EXPECT_EQ(ras::uuid::getUuidName(bluetooth::Uuid::From16Bit(
+ ras::uuid::kRasRealTimeRangingDataCharacteristic16bit)),
+ "Real-time Ranging Data");
+ EXPECT_EQ(ras::uuid::getUuidName(
+ bluetooth::Uuid::From16Bit(ras::uuid::kRasOnDemandDataCharacteristic16bit)),
+ "On-demand Ranging Data");
+ EXPECT_EQ(ras::uuid::getUuidName(
+ bluetooth::Uuid::From16Bit(ras::uuid::kRasControlPointCharacteristic16bit)),
+ "RAS Control Point (RAS-CP)");
+ EXPECT_EQ(ras::uuid::getUuidName(
+ bluetooth::Uuid::From16Bit(ras::uuid::kRasRangingDataReadyCharacteristic16bit)),
+ "Ranging Data Ready");
+ EXPECT_EQ(ras::uuid::getUuidName(bluetooth::Uuid::From16Bit(
+ ras::uuid::kRasRangingDataOverWrittenCharacteristic16bit)),
+ "Ranging Data Overwritten");
+ EXPECT_EQ(ras::uuid::getUuidName(
+ bluetooth::Uuid::From16Bit(ras::uuid::kClientCharacteristicConfiguration16bit)),
+ "Client Characteristic Configuration");
+
+ // Test unknown UUID
+ EXPECT_EQ(ras::uuid::getUuidName(
+ bluetooth::Uuid::FromString("00001101-0000-1000-8000-00805F9B34FB")),
+ "Unknown UUID");
+}
+
+TEST(RasUtilsTest, ParseControlPointCommand) {
+ // Test successful parsing of valid commands
+ uint8_t valid_data_get_ranging_data[] = {0x00, 0x01, 0x02};
+ ras::ControlPointCommand command_get_ranging_data;
+ ASSERT_TRUE(ras::ParseControlPointCommand(&command_get_ranging_data, valid_data_get_ranging_data,
+ sizeof(valid_data_get_ranging_data)));
+ ASSERT_EQ(command_get_ranging_data.opcode_, ras::Opcode::GET_RANGING_DATA);
+ ASSERT_EQ(command_get_ranging_data.parameter_[0], 0x01);
+ ASSERT_EQ(command_get_ranging_data.parameter_[1], 0x02);
+
+ uint8_t valid_data_ack_ranging_data[] = {0x01, 0x03, 0x04};
+ ras::ControlPointCommand command_ack_ranging_data;
+ ASSERT_TRUE(ras::ParseControlPointCommand(&command_ack_ranging_data, valid_data_ack_ranging_data,
+ sizeof(valid_data_ack_ranging_data)));
+ ASSERT_EQ(command_ack_ranging_data.opcode_, ras::Opcode::ACK_RANGING_DATA);
+ ASSERT_EQ(command_ack_ranging_data.parameter_[0], 0x03);
+ ASSERT_EQ(command_ack_ranging_data.parameter_[1], 0x04);
+
+ uint8_t valid_data_retrieve_lost_ranging_data_segments[] = {0x02, 0x05, 0x06, 0x07, 0x08};
+ ras::ControlPointCommand command_retrieve_lost_ranging_data_segments;
+ ASSERT_TRUE(
+ ras::ParseControlPointCommand(&command_retrieve_lost_ranging_data_segments,
+ valid_data_retrieve_lost_ranging_data_segments,
+ sizeof(valid_data_retrieve_lost_ranging_data_segments)));
+ ASSERT_EQ(command_retrieve_lost_ranging_data_segments.opcode_,
+ ras::Opcode::RETRIEVE_LOST_RANGING_DATA_SEGMENTS);
+ ASSERT_EQ(command_retrieve_lost_ranging_data_segments.parameter_[0], 0x05);
+ ASSERT_EQ(command_retrieve_lost_ranging_data_segments.parameter_[1], 0x06);
+ ASSERT_EQ(command_retrieve_lost_ranging_data_segments.parameter_[2], 0x07);
+ ASSERT_EQ(command_retrieve_lost_ranging_data_segments.parameter_[3], 0x08);
+
+ uint8_t valid_data_abort_operation[] = {0x03};
+ ras::ControlPointCommand command_abort_operation;
+ ASSERT_TRUE(ras::ParseControlPointCommand(&command_abort_operation, valid_data_abort_operation,
+ sizeof(valid_data_abort_operation)));
+ ASSERT_EQ(command_abort_operation.opcode_, ras::Opcode::ABORT_OPERATION);
+
+ uint8_t valid_data_filter[] = {0x04, 0x09, 0x0A};
+ ras::ControlPointCommand command_filter;
+ ASSERT_TRUE(ras::ParseControlPointCommand(&command_filter, valid_data_filter,
+ sizeof(valid_data_filter)));
+ ASSERT_EQ(command_filter.opcode_, ras::Opcode::FILTER);
+ ASSERT_EQ(command_filter.parameter_[0], 0x09);
+ ASSERT_EQ(command_filter.parameter_[1], 0x0A);
+
+ // Test failed parsing of invalid commands
+ uint8_t invalid_data_short_get_ranging_data[] = {0x00, 0x01};
+ ras::ControlPointCommand command_invalid_short_get_ranging_data;
+ ASSERT_FALSE(ras::ParseControlPointCommand(&command_invalid_short_get_ranging_data,
+ invalid_data_short_get_ranging_data,
+ sizeof(invalid_data_short_get_ranging_data)));
+
+ uint8_t invalid_data_long_get_ranging_data[] = {0x00, 0x01, 0x02, 0x03};
+ ras::ControlPointCommand command_invalid_long_get_ranging_data;
+ ASSERT_FALSE(ras::ParseControlPointCommand(&command_invalid_long_get_ranging_data,
+ invalid_data_long_get_ranging_data,
+ sizeof(invalid_data_long_get_ranging_data)));
+
+ uint8_t invalid_data_unknown_opcode[] = {0x05, 0x01, 0x02};
+ ras::ControlPointCommand command_invalid_unknown_opcode;
+ ASSERT_FALSE(ras::ParseControlPointCommand(&command_invalid_unknown_opcode,
+ invalid_data_unknown_opcode,
+ sizeof(invalid_data_unknown_opcode)));
+}
+
+TEST(RasUtilsTest, GetOpcodeText) {
+ // Test known opcodes
+ EXPECT_EQ(ras::GetOpcodeText(ras::Opcode::GET_RANGING_DATA), "GET_RANGING_DATA");
+ EXPECT_EQ(ras::GetOpcodeText(ras::Opcode::ACK_RANGING_DATA), "ACK_RANGING_DATA");
+ EXPECT_EQ(ras::GetOpcodeText(ras::Opcode::RETRIEVE_LOST_RANGING_DATA_SEGMENTS),
+ "RETRIEVE_LOST_RANGING_DATA_SEGMENTS");
+ EXPECT_EQ(ras::GetOpcodeText(ras::Opcode::ABORT_OPERATION), "ABORT_OPERATION");
+ EXPECT_EQ(ras::GetOpcodeText(ras::Opcode::FILTER), "FILTER");
+
+ // Test unknown opcode (casting an invalid value to Opcode)
+ EXPECT_EQ(ras::GetOpcodeText(static_cast<ras::Opcode>(0x05)), "Unknown Opcode");
+}
+
+TEST(RasUtilsTest, GetResponseOpcodeValueText) {
+ // Test known response code values
+ EXPECT_EQ(ras::GetResponseOpcodeValueText(ras::ResponseCodeValue::RESERVED_FOR_FUTURE_USE),
+ "RESERVED_FOR_FUTURE_USE");
+ EXPECT_EQ(ras::GetResponseOpcodeValueText(ras::ResponseCodeValue::SUCCESS), "SUCCESS");
+ EXPECT_EQ(ras::GetResponseOpcodeValueText(ras::ResponseCodeValue::OP_CODE_NOT_SUPPORTED),
+ "OP_CODE_NOT_SUPPORTED");
+ EXPECT_EQ(ras::GetResponseOpcodeValueText(ras::ResponseCodeValue::INVALID_PARAMETER),
+ "INVALID_PARAMETER");
+ EXPECT_EQ(ras::GetResponseOpcodeValueText(ras::ResponseCodeValue::PERSISTED), "PERSISTED");
+ EXPECT_EQ(ras::GetResponseOpcodeValueText(ras::ResponseCodeValue::ABORT_UNSUCCESSFUL),
+ "ABORT_UNSUCCESSFUL");
+ EXPECT_EQ(ras::GetResponseOpcodeValueText(ras::ResponseCodeValue::PROCEDURE_NOT_COMPLETED),
+ "PROCEDURE_NOT_COMPLETED");
+ EXPECT_EQ(ras::GetResponseOpcodeValueText(ras::ResponseCodeValue::SERVER_BUSY), "SERVER_BUSY");
+ EXPECT_EQ(ras::GetResponseOpcodeValueText(ras::ResponseCodeValue::NO_RECORDS_FOUND),
+ "NO_RECORDS_FOUND");
+
+ // Test unknown response code value (casting an invalid value to ResponseCodeValue)
+ EXPECT_EQ(ras::GetResponseOpcodeValueText(static_cast<ras::ResponseCodeValue>(0x09)),
+ "Reserved for Future Use");
+}
+
+TEST(RasUtilsTest, IsRangingServiceCharacteristic) {
+ // Test true cases for Ranging Service characteristics
+ EXPECT_TRUE(ras::IsRangingServiceCharacteristic(
+ bluetooth::Uuid::From16Bit(ras::uuid::kRangingService16Bit)));
+ EXPECT_TRUE(ras::IsRangingServiceCharacteristic(
+ bluetooth::Uuid::From16Bit(ras::uuid::kRasFeaturesCharacteristic16bit)));
+ EXPECT_TRUE(ras::IsRangingServiceCharacteristic(
+ bluetooth::Uuid::From16Bit(ras::uuid::kRasRealTimeRangingDataCharacteristic16bit)));
+ EXPECT_TRUE(ras::IsRangingServiceCharacteristic(
+ bluetooth::Uuid::From16Bit(ras::uuid::kRasOnDemandDataCharacteristic16bit)));
+ EXPECT_TRUE(ras::IsRangingServiceCharacteristic(
+ bluetooth::Uuid::From16Bit(ras::uuid::kRasControlPointCharacteristic16bit)));
+ EXPECT_TRUE(ras::IsRangingServiceCharacteristic(
+ bluetooth::Uuid::From16Bit(ras::uuid::kRasRangingDataReadyCharacteristic16bit)));
+ EXPECT_TRUE(ras::IsRangingServiceCharacteristic(
+ bluetooth::Uuid::From16Bit(ras::uuid::kRasRangingDataOverWrittenCharacteristic16bit)));
+
+ // Test false cases for non-Ranging Service characteristics
+ EXPECT_FALSE(ras::IsRangingServiceCharacteristic(
+ bluetooth::Uuid::From16Bit(ras::uuid::kClientCharacteristicConfiguration16bit)));
+ EXPECT_FALSE(ras::IsRangingServiceCharacteristic(
+ bluetooth::Uuid::FromString("00001101-0000-1000-8000-00805F9B34FB"))); // Random UUID
+}
diff --git a/system/bta/test/bta_disc_test.cc b/system/bta/test/bta_disc_test.cc
index 58421f2962..3a1dbc9882 100644
--- a/system/bta/test/bta_disc_test.cc
+++ b/system/bta/test/bta_disc_test.cc
@@ -219,61 +219,7 @@ int gatt_service_cb_both_call_cnt = 0;
/* This test exercises the usual service discovery flow when bonding to
* dual-mode, CTKD capable device on LE transport.
*/
-TEST_F_WITH_FLAGS(BtaInitializedTest, bta_dm_disc_both_transports_flag_disabled,
- REQUIRES_FLAGS_DISABLED(ACONFIG_FLAG(TEST_BT, bta_dm_discover_both))) {
- bta_dm_disc_start(true);
-
- std::promise<void> gatt_triggered;
- int gatt_call_cnt = 0;
- base::RepeatingCallback<void(const RawAddress&)> gatt_performer =
- base::BindLambdaForTesting([&](const RawAddress& /*bd_addr*/) {
- gatt_call_cnt++;
- gatt_triggered.set_value();
- });
- bta_dm_disc_override_gatt_performer_for_testing(gatt_performer);
-
- int sdp_call_cnt = 0;
- base::RepeatingCallback<void(tBTA_DM_SDP_STATE*)> sdp_performer =
- base::BindLambdaForTesting([&](tBTA_DM_SDP_STATE* /*sdp_state*/) { sdp_call_cnt++; });
- bta_dm_disc_override_sdp_performer_for_testing(sdp_performer);
-
- gatt_service_cb_both_call_cnt = 0;
- service_cb_both_call_cnt = 0;
-
- bta_dm_disc_start_service_discovery(
- {[](RawAddress, std::vector<bluetooth::Uuid>&, bool) {}, nullptr,
- [](RawAddress /*addr*/, const std::vector<bluetooth::Uuid>&, tBTA_STATUS) {
- service_cb_both_call_cnt++;
- }},
- kRawAddress, BT_TRANSPORT_BR_EDR);
- EXPECT_EQ(sdp_call_cnt, 1);
-
- bta_dm_disc_start_service_discovery(
- {[](RawAddress, std::vector<bluetooth::Uuid>&, bool) { gatt_service_cb_both_call_cnt++; },
- nullptr, [](RawAddress /*addr*/, const std::vector<bluetooth::Uuid>&, tBTA_STATUS) {}},
- kRawAddress, BT_TRANSPORT_LE);
-
- // GATT discovery is queued, until SDP finishes
- EXPECT_EQ(gatt_call_cnt, 0);
-
- bta_dm_sdp_finished(kRawAddress, BTA_SUCCESS, {}, {});
- EXPECT_EQ(service_cb_both_call_cnt, 1);
-
- // SDP finished, wait until GATT is triggered.
- EXPECT_EQ(std::future_status::ready,
- gatt_triggered.get_future().wait_for(std::chrono::seconds(1)));
- bta_dm_gatt_finished(kRawAddress, BTA_SUCCESS);
- EXPECT_EQ(gatt_service_cb_both_call_cnt, 1);
-
- bta_dm_disc_override_sdp_performer_for_testing({});
- bta_dm_disc_override_gatt_performer_for_testing({});
-}
-
-/* This test exercises the usual service discovery flow when bonding to
- * dual-mode, CTKD capable device on LE transport.
- */
-TEST_F_WITH_FLAGS(BtaInitializedTest, bta_dm_disc_both_transports_flag_enabled,
- REQUIRES_FLAGS_ENABLED(ACONFIG_FLAG(TEST_BT, bta_dm_discover_both))) {
+TEST_F(BtaInitializedTest, bta_dm_disc_both_transports) {
bta_dm_disc_start(true);
int gatt_call_cnt = 0;
diff --git a/system/btif/include/btif_dm.h b/system/btif/include/btif_dm.h
index 15a5bfa1ae..9069c97b76 100644
--- a/system/btif/include/btif_dm.h
+++ b/system/btif/include/btif_dm.h
@@ -104,6 +104,7 @@ void btif_dm_clear_event_filter();
void btif_dm_clear_event_mask();
void btif_dm_clear_filter_accept_list();
void btif_dm_disconnect_all_acls();
+void btif_dm_disconnect_acl(const RawAddress& bd_addr, tBT_TRANSPORT transport);
void btif_dm_le_rand(bluetooth::hci::LeRandCallback callback);
void btif_dm_set_event_filter_connection_setup_all_devices();
@@ -150,6 +151,7 @@ void btif_dm_get_ble_local_keys(tBTA_DM_BLE_LOCAL_KEY_MASK* p_key_mask, Octet16*
tBTA_BLE_LOCAL_ID_KEYS* p_id_keys);
void btif_update_remote_properties(const RawAddress& bd_addr, BD_NAME bd_name, DEV_CLASS dev_class,
tBT_DEVICE_TYPE dev_type);
+bool btif_is_interesting_le_service(const bluetooth::Uuid& uuid);
bool check_cod_hid(const RawAddress& bd_addr);
bool check_cod_hid_major(const RawAddress& bd_addr, uint32_t cod);
diff --git a/system/btif/include/btif_storage.h b/system/btif/include/btif_storage.h
index 0c1043faae..01432bade6 100644
--- a/system/btif/include/btif_storage.h
+++ b/system/btif/include/btif_storage.h
@@ -446,6 +446,7 @@ bt_status_t btif_storage_set_hid_connection_policy(const tAclLinkSpec& link_spec
bt_status_t btif_storage_get_hid_connection_policy(const tAclLinkSpec& link_spec,
bool* reconnect_allowed);
+void btif_storage_migrate_services();
/******************************************************************************
* Exported for unit tests
*****************************************************************************/
diff --git a/system/btif/src/bluetooth.cc b/system/btif/src/bluetooth.cc
index 25a74ea47a..7bdf50781b 100644
--- a/system/btif/src/bluetooth.cc
+++ b/system/btif/src/bluetooth.cc
@@ -814,6 +814,16 @@ static int disconnect_all_acls() {
return BT_STATUS_SUCCESS;
}
+static int disconnect_acl(const RawAddress& bd_addr, int transport) {
+ log::verbose("{}", bd_addr);
+ if (!interface_ready()) {
+ return BT_STATUS_NOT_READY;
+ }
+
+ do_in_main_thread(base::BindOnce(btif_dm_disconnect_acl, bd_addr, to_bt_transport(transport)));
+ return BT_STATUS_SUCCESS;
+}
+
static void le_rand_btif_cb(uint64_t random_number) {
log::verbose("");
do_in_jni_thread(base::BindOnce(
@@ -1286,6 +1296,7 @@ EXPORT_SYMBOL bt_interface_t bluetoothInterface = {
.clear_event_mask = clear_event_mask,
.clear_filter_accept_list = clear_filter_accept_list,
.disconnect_all_acls = disconnect_all_acls,
+ .disconnect_acl = disconnect_acl,
.le_rand = le_rand,
.set_event_filter_inquiry_result_all_devices = set_event_filter_inquiry_result_all_devices,
.set_default_event_mask_except = set_default_event_mask_except,
diff --git a/system/btif/src/btif_a2dp_source.cc b/system/btif/src/btif_a2dp_source.cc
index d58e3de696..70634b9984 100644
--- a/system/btif/src/btif_a2dp_source.cc
+++ b/system/btif/src/btif_a2dp_source.cc
@@ -531,9 +531,15 @@ bool btif_a2dp_source_restart_session(const RawAddress& old_peer_address,
bool btif_a2dp_source_end_session(const RawAddress& peer_address) {
log::info("peer_address={} state={}", peer_address, btif_a2dp_source_cb.StateStr());
- local_thread()->DoInThread(FROM_HERE,
- base::BindOnce(&btif_a2dp_source_end_session_delayed, peer_address));
- btif_a2dp_source_cleanup_codec();
+ if (com::android::bluetooth::flags::a2dp_source_threading_fix()) {
+ btif_a2dp_source_cleanup_codec();
+ btif_a2dp_source_end_session_delayed(peer_address);
+ } else {
+
+ local_thread()->DoInThread(FROM_HERE,
+ base::BindOnce(&btif_a2dp_source_end_session_delayed, peer_address));
+ btif_a2dp_source_cleanup_codec();
+ }
return true;
}
diff --git a/system/btif/src/btif_core.cc b/system/btif/src/btif_core.cc
index f0d06373b6..8e481daed0 100644
--- a/system/btif/src/btif_core.cc
+++ b/system/btif/src/btif_core.cc
@@ -134,7 +134,12 @@ int btif_is_enabled(void) {
return (!btif_is_dut_mode()) && (stack_manager_get_interface()->get_stack_is_running());
}
-void btif_init_ok() { btif_dm_load_ble_local_keys(); }
+void btif_init_ok() {
+ btif_dm_load_ble_local_keys();
+ if (com::android::bluetooth::flags::separate_service_storage()) {
+ btif_storage_migrate_services();
+ }
+}
/*******************************************************************************
*
@@ -290,7 +295,7 @@ void btif_dut_mode_send(uint16_t opcode, uint8_t* buf, uint8_t len) {
****************************************************************************/
static bt_status_t btif_in_get_adapter_properties(void) {
- const static uint32_t NUM_ADAPTER_PROPERTIES = 5;
+ static const uint32_t NUM_ADAPTER_PROPERTIES = 5;
bt_property_t properties[NUM_ADAPTER_PROPERTIES];
uint32_t num_props = 0;
@@ -340,12 +345,13 @@ static bt_status_t btif_in_get_adapter_properties(void) {
}
static bt_status_t btif_in_get_remote_device_properties(RawAddress* bd_addr) {
- bt_property_t remote_properties[8];
+ bt_property_t remote_properties[9];
uint32_t num_props = 0;
bt_bdname_t name, alias;
uint32_t cod, devtype;
Uuid remote_uuids[BT_MAX_NUM_UUIDS];
+ Uuid remote_uuids_le[BT_MAX_NUM_UUIDS];
memset(remote_properties, 0, sizeof(remote_properties));
BTIF_STORAGE_FILL_PROPERTY(&remote_properties[num_props], BT_PROPERTY_BDNAME, sizeof(name),
@@ -369,10 +375,17 @@ static bt_status_t btif_in_get_remote_device_properties(RawAddress* bd_addr) {
num_props++;
BTIF_STORAGE_FILL_PROPERTY(&remote_properties[num_props], BT_PROPERTY_UUIDS, sizeof(remote_uuids),
- remote_uuids);
+ &remote_uuids);
btif_storage_get_remote_device_property(bd_addr, &remote_properties[num_props]);
num_props++;
+ if (com::android::bluetooth::flags::separate_service_storage()) {
+ BTIF_STORAGE_FILL_PROPERTY(&remote_properties[num_props], BT_PROPERTY_UUIDS_LE,
+ sizeof(remote_uuids_le), &remote_uuids_le);
+ btif_storage_get_remote_device_property(bd_addr, &remote_properties[num_props]);
+ num_props++;
+ }
+
GetInterfaceToProfiles()->events->invoke_remote_device_properties_cb(
BT_STATUS_SUCCESS, *bd_addr, num_props, remote_properties);
diff --git a/system/btif/src/btif_dm.cc b/system/btif/src/btif_dm.cc
index 5848ebba1c..6f83aee781 100644
--- a/system/btif/src/btif_dm.cc
+++ b/system/btif/src/btif_dm.cc
@@ -1517,15 +1517,16 @@ static void btif_dm_search_devices_evt(tBTA_DM_SEARCH_EVT event, tBTA_DM_SEARCH*
}
/* Returns true if |uuid| should be passed as device property */
-static bool btif_is_interesting_le_service(bluetooth::Uuid uuid) {
+bool btif_is_interesting_le_service(const bluetooth::Uuid& uuid) {
return uuid.As16Bit() == UUID_SERVCLASS_LE_HID || uuid == UUID_HEARING_AID || uuid == UUID_VC ||
uuid == UUID_CSIS || uuid == UUID_LE_AUDIO || uuid == UUID_LE_MIDI || uuid == UUID_HAS ||
uuid == UUID_BASS || uuid == UUID_BATTERY || uuid == ANDROID_HEADTRACKER_SERVICE_UUID;
}
-static bt_status_t btif_get_existing_uuids(RawAddress* bd_addr, Uuid* existing_uuids) {
+static bt_status_t btif_get_existing_uuids(RawAddress* bd_addr, Uuid* existing_uuids,
+ bt_property_type_t property_type = BT_PROPERTY_UUIDS) {
bt_property_t tmp_prop;
- BTIF_STORAGE_FILL_PROPERTY(&tmp_prop, BT_PROPERTY_UUIDS, sizeof(*existing_uuids), existing_uuids);
+ BTIF_STORAGE_FILL_PROPERTY(&tmp_prop, property_type, sizeof(*existing_uuids), existing_uuids);
return btif_storage_get_remote_device_property(bd_addr, &tmp_prop);
}
@@ -1537,9 +1538,10 @@ static bool btif_is_gatt_service_discovery_post_pairing(const RawAddress bd_addr
(pairing_cb.gatt_over_le == btif_dm_pairing_cb_t::ServiceDiscoveryState::SCHEDULED);
}
-static void btif_merge_existing_uuids(RawAddress& addr, std::set<Uuid>* uuids) {
+static void btif_merge_existing_uuids(RawAddress& addr, std::set<Uuid>* uuids,
+ bt_property_type_t property_type = BT_PROPERTY_UUIDS) {
Uuid existing_uuids[BT_MAX_NUM_UUIDS] = {};
- bt_status_t lookup_result = btif_get_existing_uuids(&addr, existing_uuids);
+ bt_status_t lookup_result = btif_get_existing_uuids(&addr, existing_uuids, property_type);
if (lookup_result == BT_STATUS_FAIL) {
return;
@@ -1550,18 +1552,14 @@ static void btif_merge_existing_uuids(RawAddress& addr, std::set<Uuid>* uuids) {
if (btif_should_ignore_uuid(uuid)) {
continue;
}
- if (btif_is_interesting_le_service(uuid)) {
- log::info("interesting le service {} insert", uuid.ToString());
- uuids->insert(uuid);
- }
+
+ uuids->insert(uuid);
}
}
static void btif_on_service_discovery_results(RawAddress bd_addr,
const std::vector<bluetooth::Uuid>& uuids_param,
tBTA_STATUS result) {
- bt_property_t prop;
- std::vector<uint8_t> property_value;
std::set<Uuid> uuids;
bool a2dp_sink_capable = false;
@@ -1589,8 +1587,12 @@ static void btif_on_service_discovery_results(RawAddress bd_addr,
pairing_cb.sdp_over_classic = btif_dm_pairing_cb_t::ServiceDiscoveryState::FINISHED;
}
- prop.type = BT_PROPERTY_UUIDS;
- prop.len = 0;
+ std::vector<uint8_t> bredr_property_value;
+ std::vector<uint8_t> le_property_value;
+ bt_property_t uuid_props[2] = {};
+ bt_property_t& bredr_prop = uuid_props[0];
+ bt_property_t& le_prop = uuid_props[1];
+
if ((result == BTA_SUCCESS) && !uuids_param.empty()) {
log::info("New UUIDs for {}:", bd_addr);
for (const auto& uuid : uuids_param) {
@@ -1610,13 +1612,35 @@ static void btif_on_service_discovery_results(RawAddress bd_addr,
for (auto& uuid : uuids) {
auto uuid_128bit = uuid.To128BitBE();
- property_value.insert(property_value.end(), uuid_128bit.begin(), uuid_128bit.end());
+ bredr_property_value.insert(bredr_property_value.end(), uuid_128bit.begin(),
+ uuid_128bit.end());
if (uuid == UUID_A2DP_SINK) {
a2dp_sink_capable = true;
}
}
- prop.val = (void*)property_value.data();
- prop.len = Uuid::kNumBytes128 * uuids.size();
+
+ bredr_prop = {BT_PROPERTY_UUIDS, static_cast<int>(Uuid::kNumBytes128 * uuids.size()),
+ (void*)bredr_property_value.data()};
+
+ if (com::android::bluetooth::flags::separate_service_storage()) {
+ bt_status_t ret = btif_storage_set_remote_device_property(&bd_addr, &bredr_prop);
+ ASSERTC(ret == BT_STATUS_SUCCESS, "storing remote classic services failed", ret);
+
+ std::set<Uuid> le_uuids;
+ if (results_for_bonding_device) {
+ btif_merge_existing_uuids(pairing_cb.static_bdaddr, &le_uuids, BT_PROPERTY_UUIDS_LE);
+ btif_merge_existing_uuids(pairing_cb.bd_addr, &le_uuids, BT_PROPERTY_UUIDS_LE);
+ } else {
+ btif_merge_existing_uuids(bd_addr, &le_uuids, BT_PROPERTY_UUIDS_LE);
+ }
+
+ for (auto& uuid : le_uuids) {
+ auto uuid_128bit = uuid.To128BitBE();
+ le_property_value.insert(le_property_value.end(), uuid_128bit.begin(), uuid_128bit.end());
+ }
+ le_prop = {BT_PROPERTY_UUIDS_LE, static_cast<int>(Uuid::kNumBytes128 * le_uuids.size()),
+ (void*)le_property_value.data()};
+ }
}
bool skip_reporting_wait_for_le = false;
@@ -1649,17 +1673,18 @@ static void btif_on_service_discovery_results(RawAddress bd_addr,
log::info("SDP failed, send {} EIR UUIDs to unblock bonding {}", num_eir_uuids, bd_addr);
for (auto eir_uuid : uuids_iter->second) {
auto uuid_128bit = eir_uuid.To128BitBE();
- property_value.insert(property_value.end(), uuid_128bit.begin(), uuid_128bit.end());
+ bredr_property_value.insert(bredr_property_value.end(), uuid_128bit.begin(),
+ uuid_128bit.end());
}
eir_uuids_cache.erase(uuids_iter);
}
if (num_eir_uuids > 0) {
- prop.val = (void*)property_value.data();
- prop.len = num_eir_uuids * Uuid::kNumBytes128;
+ bredr_prop.val = (void*)bredr_property_value.data();
+ bredr_prop.len = num_eir_uuids * Uuid::kNumBytes128;
} else {
log::warn("SDP failed and we have no EIR UUIDs to report either");
- prop.val = &uuid;
- prop.len = Uuid::kNumBytes128;
+ bredr_prop.val = &uuid;
+ bredr_prop.len = Uuid::kNumBytes128;
}
}
@@ -1677,9 +1702,10 @@ static void btif_on_service_discovery_results(RawAddress bd_addr,
uuids_param.size(), num_eir_uuids));
if (!uuids_param.empty() || num_eir_uuids != 0) {
- /* Also write this to the NVRAM */
- const bt_status_t ret = btif_storage_set_remote_device_property(&bd_addr, &prop);
- ASSERTC(ret == BT_STATUS_SUCCESS, "storing remote services failed", ret);
+ if (!com::android::bluetooth::flags::separate_service_storage()) {
+ const bt_status_t ret = btif_storage_set_remote_device_property(&bd_addr, &bredr_prop);
+ ASSERTC(ret == BT_STATUS_SUCCESS, "storing remote services failed", ret);
+ }
if (skip_reporting_wait_for_le) {
log::info(
@@ -1694,16 +1720,13 @@ static void btif_on_service_discovery_results(RawAddress bd_addr,
}
/* Send the event to the BTIF */
- GetInterfaceToProfiles()->events->invoke_remote_device_properties_cb(BT_STATUS_SUCCESS, bd_addr,
- 1, &prop);
+ GetInterfaceToProfiles()->events->invoke_remote_device_properties_cb(
+ BT_STATUS_SUCCESS, bd_addr, ARRAY_SIZE(uuid_props), uuid_props);
}
}
static void btif_on_gatt_results(RawAddress bd_addr, std::vector<bluetooth::Uuid>& services,
bool is_transport_le) {
- std::vector<bt_property_t> prop;
- std::vector<uint8_t> property_value;
- std::set<Uuid> uuids;
RawAddress static_addr_copy = pairing_cb.static_bdaddr;
bool lea_supported = is_le_audio_capable_during_service_discovery(bd_addr);
@@ -1739,6 +1762,7 @@ static void btif_on_gatt_results(RawAddress bd_addr, std::vector<bluetooth::Uuid
BTM_LogHistory(kBtmLogTag, bd_addr, "Discovered GATT services using SDP transport");
}
+ std::set<Uuid> uuids;
for (Uuid uuid : services) {
if (btif_is_interesting_le_service(uuid)) {
if (btif_should_ignore_uuid(uuid)) {
@@ -1767,46 +1791,28 @@ static void btif_on_gatt_results(RawAddress bd_addr, std::vector<bluetooth::Uuid
log::info("Will return Classic SDP results, if done, to unblock bonding");
}
- Uuid existing_uuids[BT_MAX_NUM_UUIDS] = {};
-
- // Look up UUIDs using pseudo address (either RPA or static address)
- bt_status_t existing_lookup_result = btif_get_existing_uuids(&bd_addr, existing_uuids);
-
- if (existing_lookup_result != BT_STATUS_FAIL) {
- log::info("Got some existing UUIDs by address {}", bd_addr);
-
- for (int i = 0; i < BT_MAX_NUM_UUIDS; i++) {
- Uuid uuid = existing_uuids[i];
- if (uuid.IsEmpty()) {
- continue;
- }
- uuids.insert(uuid);
+ if (!com::android::bluetooth::flags::separate_service_storage()) {
+ // Look up UUIDs using pseudo address (either RPA or static address)
+ btif_merge_existing_uuids(bd_addr, &uuids);
+ if (bd_addr != static_addr_copy) {
+ // Look up UUID using static address, if different than sudo address
+ btif_merge_existing_uuids(static_addr_copy, &uuids);
}
}
- if (bd_addr != static_addr_copy) {
- // Look up UUID using static address, if different than sudo address
- existing_lookup_result = btif_get_existing_uuids(&static_addr_copy, existing_uuids);
- if (existing_lookup_result != BT_STATUS_FAIL) {
- log::info("Got some existing UUIDs by static address {}", static_addr_copy);
- for (int i = 0; i < BT_MAX_NUM_UUIDS; i++) {
- Uuid uuid = existing_uuids[i];
- if (uuid.IsEmpty()) {
- continue;
- }
- uuids.insert(uuid);
- }
- }
- }
+ std::vector<bt_property_t> prop;
+ std::vector<uint8_t> property_value;
for (auto& uuid : uuids) {
auto uuid_128bit = uuid.To128BitBE();
property_value.insert(property_value.end(), uuid_128bit.begin(), uuid_128bit.end());
}
- prop.push_back(bt_property_t{BT_PROPERTY_UUIDS,
- static_cast<int>(Uuid::kNumBytes128 * uuids.size()),
- (void*)property_value.data()});
+ prop.push_back(bt_property_t{
+ (com::android::bluetooth::flags::separate_service_storage() && is_transport_le)
+ ? BT_PROPERTY_UUIDS_LE
+ : BT_PROPERTY_UUIDS,
+ static_cast<int>(Uuid::kNumBytes128 * uuids.size()), (void*)property_value.data()});
/* Also write this to the NVRAM */
bt_status_t ret = btif_storage_set_remote_device_property(&bd_addr, &prop[0]);
@@ -1817,8 +1823,7 @@ static void btif_on_gatt_results(RawAddress bd_addr, std::vector<bluetooth::Uuid
* send them with rest of SDP results in on_service_discovery_results */
return;
} else {
- if (pairing_cb.sdp_over_classic == btif_dm_pairing_cb_t::ServiceDiscoveryState::SCHEDULED &&
- com::android::bluetooth::flags::bta_dm_discover_both()) {
+ if (pairing_cb.sdp_over_classic == btif_dm_pairing_cb_t::ServiceDiscoveryState::SCHEDULED) {
/* Don't report services yet, they will be reported together once SDP
* finishes. */
log::info("will report services later, with SDP results {}", bd_addr);
@@ -1826,6 +1831,32 @@ static void btif_on_gatt_results(RawAddress bd_addr, std::vector<bluetooth::Uuid
}
}
+ if (!com::android::bluetooth::flags::separate_service_storage()) {
+ /* Send the event to the BTIF */
+ GetInterfaceToProfiles()->events->invoke_remote_device_properties_cb(BT_STATUS_SUCCESS, bd_addr,
+ prop.size(), prop.data());
+ return;
+ }
+
+ std::set<Uuid> bredr_uuids;
+ // Look up UUIDs using pseudo address (either RPA or static address)
+ btif_merge_existing_uuids(bd_addr, &bredr_uuids);
+ if (bd_addr != static_addr_copy) {
+ // Look up UUID using static address, if different than sudo address
+ btif_merge_existing_uuids(static_addr_copy, &bredr_uuids);
+ }
+
+ std::vector<uint8_t> bredr_property_value;
+
+ for (auto& uuid : bredr_uuids) {
+ auto uuid_128bit = uuid.To128BitBE();
+ bredr_property_value.insert(bredr_property_value.end(), uuid_128bit.begin(), uuid_128bit.end());
+ }
+
+ prop.push_back(bt_property_t{BT_PROPERTY_UUIDS,
+ static_cast<int>(Uuid::kNumBytes128 * bredr_uuids.size()),
+ (void*)bredr_property_value.data()});
+
/* Send the event to the BTIF */
GetInterfaceToProfiles()->events->invoke_remote_device_properties_cb(BT_STATUS_SUCCESS, bd_addr,
prop.size(), prop.data());
@@ -2517,6 +2548,9 @@ void btif_dm_cancel_bond(const RawAddress bd_addr) {
** 2. special handling for HID devices
*/
if (is_bonding_or_sdp()) {
+ // clear sdp_attempts
+ pairing_cb.sdp_attempts = 0;
+
if (com::android::bluetooth::flags::ignore_unrelated_cancel_bond() &&
(pairing_cb.bd_addr != bd_addr)) {
log::warn("Ignoring bond cancel for unrelated device: {} pairing: {}", bd_addr,
@@ -3896,6 +3930,30 @@ void btif_dm_clear_filter_accept_list() { BTA_DmClearFilterAcceptList(); }
void btif_dm_disconnect_all_acls() { BTA_DmDisconnectAllAcls(); }
+void btif_dm_disconnect_acl(const RawAddress& bd_addr, tBT_TRANSPORT transport) {
+ log::debug(" {}, transport {}", bd_addr, transport);
+
+ if (transport == BT_TRANSPORT_LE || transport == BT_TRANSPORT_AUTO) {
+ uint16_t acl_handle =
+ get_btm_client_interface().peer.BTM_GetHCIConnHandle(bd_addr, BT_TRANSPORT_LE);
+
+ log::debug("{}, le_acl_handle: {:#x}", bd_addr, acl_handle);
+ if (acl_handle != HCI_INVALID_HANDLE) {
+ acl_disconnect_from_handle(acl_handle, HCI_ERR_PEER_USER, "bt_btif_dm disconnect");
+ }
+ }
+
+ if (transport == BT_TRANSPORT_BR_EDR || transport == BT_TRANSPORT_AUTO) {
+ uint16_t acl_handle =
+ get_btm_client_interface().peer.BTM_GetHCIConnHandle(bd_addr, BT_TRANSPORT_BR_EDR);
+
+ log::debug("{}, bredr_acl_handle: {:#x}", bd_addr, acl_handle);
+ if (acl_handle != HCI_INVALID_HANDLE) {
+ acl_disconnect_from_handle(acl_handle, HCI_ERR_PEER_USER, "bt_btif_dm disconnect");
+ }
+ }
+}
+
void btif_dm_le_rand(bluetooth::hci::LeRandCallback callback) { BTA_DmLeRand(std::move(callback)); }
void btif_dm_set_event_filter_connection_setup_all_devices() {
diff --git a/system/btif/src/btif_sock_rfc.cc b/system/btif/src/btif_sock_rfc.cc
index de091e8b05..d23f012249 100644
--- a/system/btif/src/btif_sock_rfc.cc
+++ b/system/btif/src/btif_sock_rfc.cc
@@ -196,7 +196,7 @@ static rfc_slot_t* find_rfc_slot_by_pending_sdp(void) {
static bool is_requesting_sdp(void) {
for (size_t i = 0; i < ARRAY_SIZE(rfc_slots); ++i) {
if (rfc_slots[i].id && rfc_slots[i].f.doing_sdp_request) {
- log::info("slot id {} is doing sdp request", rfc_slots[i].id);
+ log::info("slot_id {} is doing sdp request", rfc_slots[i].id);
return true;
}
}
@@ -467,7 +467,7 @@ bt_status_t btsock_rfc_connect(const RawAddress* bd_addr, const Uuid* service_uu
}
if (!send_app_scn(slot)) {
- log::error("send_app_scn() failed, closing slot->id:{}", slot->id);
+ log::error("send_app_scn() failed, closing slot_id:{}", slot->id);
cleanup_rfc_slot(slot);
return BT_STATUS_SOCKET_ERROR;
}
@@ -536,7 +536,7 @@ static void cleanup_rfc_slot(rfc_slot_t* slot) {
close(slot->fd);
log::info(
"disconnected from RFCOMM socket connections for device: {}, scn: {}, "
- "app_uid: {}, id: {}, socket_id: {}",
+ "app_uid: {}, slot_id: {}, socket_id: {}",
slot->addr, slot->scn, slot->app_uid, slot->id, slot->socket_id);
btif_sock_connection_logger(
slot->addr, slot->id, BTSOCK_RFCOMM, SOCKET_CONNECTION_STATE_DISCONNECTED,
@@ -586,7 +586,7 @@ static bool send_app_scn(rfc_slot_t* slot) {
// already sent, just return success.
return true;
}
- log::debug("Sending scn for slot {}. bd_addr:{}", slot->id, slot->addr);
+ log::debug("Sending scn for slot_id {}. bd_addr:{}", slot->id, slot->addr);
slot->scn_notified = true;
return sock_send_all(slot->fd, (const uint8_t*)&slot->scn, sizeof(slot->scn)) ==
sizeof(slot->scn);
@@ -615,10 +615,10 @@ static void on_cl_rfc_init(tBTA_JV_RFCOMM_CL_INIT* p_init, uint32_t id) {
std::unique_lock<std::recursive_mutex> lock(slot_lock);
rfc_slot_t* slot = find_rfc_slot_by_id(id);
if (!slot) {
- log::error("RFCOMM slot with id {} not found. p_init->status={}", id,
+ log::error("RFCOMM slot_id {} not found. p_init->status={}", id,
bta_jv_status_text(p_init->status));
} else if (p_init->status != tBTA_JV_STATUS::SUCCESS) {
- log::warn("INIT unsuccessful, status {}. Cleaning up slot with id {}",
+ log::warn("INIT unsuccessful, status {}. Cleaning up slot_id {}",
bta_jv_status_text(p_init->status), slot->id);
cleanup_rfc_slot(slot);
} else {
@@ -630,10 +630,10 @@ static void on_srv_rfc_listen_started(tBTA_JV_RFCOMM_START* p_start, uint32_t id
std::unique_lock<std::recursive_mutex> lock(slot_lock);
rfc_slot_t* slot = find_rfc_slot_by_id(id);
if (!slot) {
- log::error("RFCOMM slot with id {} not found", id);
+ log::error("RFCOMM slot_id {} not found", id);
return;
} else if (p_start->status != tBTA_JV_STATUS::SUCCESS) {
- log::warn("START unsuccessful, status {}. Cleaning up slot with id {}",
+ log::warn("START unsuccessful, status {}. Cleaning up slot_id {}",
bta_jv_status_text(p_start->status), slot->id);
cleanup_rfc_slot(slot);
return;
@@ -702,7 +702,7 @@ static uint32_t on_srv_rfc_connect(tBTA_JV_RFCOMM_SRV_OPEN* p_open, uint32_t id)
rfc_slot_t* accept_rs;
rfc_slot_t* srv_rs = find_rfc_slot_by_id(id);
if (!srv_rs) {
- log::error("RFCOMM slot with id {} not found.", id);
+ log::error("RFCOMM slot_id {} not found.", id);
return 0;
}
@@ -719,7 +719,7 @@ static uint32_t on_srv_rfc_connect(tBTA_JV_RFCOMM_SRV_OPEN* p_open, uint32_t id)
log::info(
"connected to RFCOMM socket connections for device: {}, scn: {}, "
- "app_uid: {}, id: {}, socket_id: {}",
+ "app_uid: {}, slot_id: {}, socket_id: {}",
accept_rs->addr, accept_rs->scn, accept_rs->app_uid, id, accept_rs->socket_id);
btif_sock_connection_logger(accept_rs->addr, accept_rs->id, BTSOCK_RFCOMM,
SOCKET_CONNECTION_STATE_CONNECTED,
@@ -779,12 +779,12 @@ static void on_cli_rfc_connect(tBTA_JV_RFCOMM_OPEN* p_open, uint32_t id) {
std::unique_lock<std::recursive_mutex> lock(slot_lock);
rfc_slot_t* slot = find_rfc_slot_by_id(id);
if (!slot) {
- log::error("RFCOMM slot with id {} not found.", id);
+ log::error("RFCOMM slot_id {} not found.", id);
return;
}
if (p_open->status != tBTA_JV_STATUS::SUCCESS) {
- log::warn("CONNECT unsuccessful, status {}. Cleaning up slot with id {}",
+ log::warn("CONNECT unsuccessful, status {}. Cleaning up slot_id {}",
bta_jv_status_text(p_open->status), slot->id);
cleanup_rfc_slot(slot);
return;
@@ -906,7 +906,7 @@ static void on_rfc_close(tBTA_JV_RFCOMM_CLOSE* /* p_close */, uint32_t id) {
static void on_rfc_write_done(tBTA_JV_RFCOMM_WRITE* p, uint32_t id) {
if (p->status != tBTA_JV_STATUS::SUCCESS) {
- log::error("error writing to RFCOMM socket with slot {}.", p->req_id);
+ log::error("error writing to RFCOMM socket, req_id:{}.", p->req_id);
return;
}
@@ -915,7 +915,7 @@ static void on_rfc_write_done(tBTA_JV_RFCOMM_WRITE* p, uint32_t id) {
rfc_slot_t* slot = find_rfc_slot_by_id(id);
if (!slot) {
- log::error("RFCOMM slot with id {} not found.", id);
+ log::error("RFCOMM slot_id {} not found.", id);
return;
}
app_uid = slot->app_uid;
@@ -931,7 +931,7 @@ static void on_rfc_outgoing_congest(tBTA_JV_RFCOMM_CONG* p, uint32_t id) {
rfc_slot_t* slot = find_rfc_slot_by_id(id);
if (!slot) {
- log::error("RFCOMM slot with id {} not found.", id);
+ log::error("RFCOMM slot_id {} not found.", id);
return;
}
@@ -946,39 +946,39 @@ static uint32_t rfcomm_cback(tBTA_JV_EVT event, tBTA_JV* p_data, uint32_t rfcomm
switch (event) {
case BTA_JV_RFCOMM_START_EVT:
- log::info("handling {}, rfcomm_slot_id:{}", bta_jv_event_text(event), rfcomm_slot_id);
+ log::info("handling {}, slot_id:{}", bta_jv_event_text(event), rfcomm_slot_id);
on_srv_rfc_listen_started(&p_data->rfc_start, rfcomm_slot_id);
break;
case BTA_JV_RFCOMM_CL_INIT_EVT:
- log::info("handling {}, rfcomm_slot_id:{}", bta_jv_event_text(event), rfcomm_slot_id);
+ log::info("handling {}, slot_id:{}", bta_jv_event_text(event), rfcomm_slot_id);
on_cl_rfc_init(&p_data->rfc_cl_init, rfcomm_slot_id);
break;
case BTA_JV_RFCOMM_OPEN_EVT:
- log::info("handling {}, rfcomm_slot_id:{}", bta_jv_event_text(event), rfcomm_slot_id);
+ log::info("handling {}, slot_id:{}", bta_jv_event_text(event), rfcomm_slot_id);
BTA_JvSetPmProfile(p_data->rfc_open.handle, BTA_JV_PM_ID_1, BTA_JV_CONN_OPEN);
on_cli_rfc_connect(&p_data->rfc_open, rfcomm_slot_id);
break;
case BTA_JV_RFCOMM_SRV_OPEN_EVT:
- log::info("handling {}, rfcomm_slot_id:{}", bta_jv_event_text(event), rfcomm_slot_id);
+ log::info("handling {}, slot_id:{}", bta_jv_event_text(event), rfcomm_slot_id);
BTA_JvSetPmProfile(p_data->rfc_srv_open.handle, BTA_JV_PM_ALL, BTA_JV_CONN_OPEN);
id = on_srv_rfc_connect(&p_data->rfc_srv_open, rfcomm_slot_id);
break;
case BTA_JV_RFCOMM_CLOSE_EVT:
- log::info("handling {}, rfcomm_slot_id:{}", bta_jv_event_text(event), rfcomm_slot_id);
+ log::info("handling {}, slot_id:{}", bta_jv_event_text(event), rfcomm_slot_id);
on_rfc_close(&p_data->rfc_close, rfcomm_slot_id);
break;
case BTA_JV_RFCOMM_WRITE_EVT:
- log::verbose("handling {}, rfcomm_slot_id:{}", bta_jv_event_text(event), rfcomm_slot_id);
+ log::verbose("handling {}, slot_id:{}", bta_jv_event_text(event), rfcomm_slot_id);
on_rfc_write_done(&p_data->rfc_write, rfcomm_slot_id);
break;
case BTA_JV_RFCOMM_CONG_EVT:
- log::verbose("handling {}, rfcomm_slot_id:{}", bta_jv_event_text(event), rfcomm_slot_id);
+ log::verbose("handling {}, slot_id:{}", bta_jv_event_text(event), rfcomm_slot_id);
on_rfc_outgoing_congest(&p_data->rfc_cong, rfcomm_slot_id);
break;
@@ -987,20 +987,20 @@ static uint32_t rfcomm_cback(tBTA_JV_EVT event, tBTA_JV* p_data, uint32_t rfcomm
break;
default:
- log::warn("unhandled event {}, slot id: {}", bta_jv_event_text(event), rfcomm_slot_id);
+ log::warn("unhandled event {}, slot_id: {}", bta_jv_event_text(event), rfcomm_slot_id);
break;
}
return id;
}
static void jv_dm_cback(tBTA_JV_EVT event, tBTA_JV* p_data, uint32_t id) {
- log::info("handling event:{}, id:{}", bta_jv_event_text(event), id);
+ log::info("handling event:{}, slot_id:{}", bta_jv_event_text(event), id);
switch (event) {
case BTA_JV_GET_SCN_EVT: {
std::unique_lock<std::recursive_mutex> lock(slot_lock);
rfc_slot_t* rs = find_rfc_slot_by_id(id);
if (!rs) {
- log::error("RFCOMM slot with id {} not found. event:{}", id, bta_jv_event_text(event));
+ log::error("RFCOMM slot with slot_id {} not found. event:{}", id, bta_jv_event_text(event));
break;
}
if (p_data->scn == 0) {
@@ -1061,7 +1061,7 @@ static void jv_dm_cback(tBTA_JV_EVT event, tBTA_JV* p_data, uint32_t id) {
rfc_slot_t* slot = find_rfc_slot_by_id(id);
if (!slot) {
- log::error("RFCOMM slot with id {} not found. event:{}", id, bta_jv_event_text(event));
+ log::error("RFCOMM slot_id {} not found. event:{}", id, bta_jv_event_text(event));
break;
}
@@ -1104,7 +1104,7 @@ static void jv_dm_cback(tBTA_JV_EVT event, tBTA_JV* p_data, uint32_t id) {
}
default:
- log::debug("unhandled event:{}, slot id:{}", bta_jv_event_text(event), id);
+ log::debug("unhandled event:{}, slot_id:{}", bta_jv_event_text(event), id);
break;
}
}
@@ -1112,18 +1112,18 @@ static void jv_dm_cback(tBTA_JV_EVT event, tBTA_JV* p_data, uint32_t id) {
static void handle_discovery_comp(tBTA_JV_STATUS status, int scn, uint32_t id) {
rfc_slot_t* slot = find_rfc_slot_by_id(id);
if (!slot) {
- log::error("RFCOMM slot with id {} not found. event: BTA_JV_DISCOVERY_COMP_EVT", id);
+ log::error("RFCOMM slot_id {} not found. event: BTA_JV_DISCOVERY_COMP_EVT", id);
return;
}
if (!slot->f.doing_sdp_request) {
- log::error("SDP response returned but RFCOMM slot {} did not request SDP record.", id);
+ log::error("SDP response returned but RFCOMM slot_id {} did not request SDP record.", id);
return;
}
if (status != tBTA_JV_STATUS::SUCCESS || !scn) {
log::error(
- "SDP service discovery completed for slot id: {} with the result "
+ "SDP service discovery completed for slot_id: {} with the result "
"status: {}, scn: {}",
id, bta_jv_status_text(status), scn);
cleanup_rfc_slot(slot);
@@ -1146,10 +1146,7 @@ static void handle_discovery_comp(tBTA_JV_STATUS status, int scn, uint32_t id) {
if (BTA_JvRfcommConnect(slot->security, scn, slot->addr, rfcomm_cback, slot->id, cfg,
slot->app_uid) != tBTA_JV_STATUS::SUCCESS) {
- log::warn(
- "BTA_JvRfcommConnect() returned BTA_JV_FAILURE for RFCOMM slot with "
- "id: {}",
- id);
+ log::warn("BTA_JvRfcommConnect() returned BTA_JV_FAILURE for RFCOMM slot_id:{}", id);
cleanup_rfc_slot(slot);
return;
}
@@ -1158,7 +1155,7 @@ static void handle_discovery_comp(tBTA_JV_STATUS status, int scn, uint32_t id) {
slot->f.doing_sdp_request = false;
if (!send_app_scn(slot)) {
- log::warn("send_app_scn() failed, closing slot->id {}", slot->id);
+ log::warn("send_app_scn() failed, closing slot_id {}", slot->id);
cleanup_rfc_slot(slot);
return;
}
@@ -1233,7 +1230,7 @@ static bool flush_incoming_que_on_wr_signal(rfc_slot_t* slot) {
static bool btsock_rfc_read_signaled_on_connected_socket(int /* fd */, int flags, uint32_t /* id */,
rfc_slot_t* slot) {
if (!slot->f.connected) {
- log::error("socket signaled for read while disconnected, slot: {}, channel: {}", slot->id,
+ log::error("socket signaled for read while disconnected, slot_id: {}, channel: {}", slot->id,
slot->scn);
return false;
}
@@ -1260,7 +1257,7 @@ static bool btsock_rfc_read_signaled_on_listen_socket(int fd, int /* flags */, u
return false;
}
slot->is_accepting = accept_signal.is_accepting;
- log::info("Server socket: {}, is_accepting: {}", slot->id, slot->is_accepting);
+ log::info("Server socket slot_id: {}, is_accepting: {}", slot->id, slot->is_accepting);
}
return true;
}
@@ -1270,7 +1267,7 @@ static void btsock_rfc_signaled_flagged(int fd, int flags, uint32_t id) {
std::unique_lock<std::recursive_mutex> lock(slot_lock);
rfc_slot_t* slot = find_rfc_slot_by_id(id);
if (!slot) {
- log::warn("RFCOMM slot with id {} not found.", id);
+ log::warn("RFCOMM slot_id {} not found.", id);
return;
}
@@ -1294,7 +1291,7 @@ static void btsock_rfc_signaled_flagged(int fd, int flags, uint32_t id) {
if (!slot->f.connected || !flush_incoming_que_on_wr_signal(slot)) {
log::error(
"socket signaled for write while disconnected (or write failure), "
- "slot: {}, channel: {}",
+ "slot_id: {}, channel: {}",
slot->id, slot->scn);
need_close = true;
}
@@ -1335,7 +1332,7 @@ void btsock_rfc_signaled(int fd, int flags, uint32_t id) {
BTA_JvRfcommWrite(slot->rfc_handle, slot->id);
}
} else {
- log::error("socket signaled for read while disconnected, slot: {}, channel: {}", slot->id,
+ log::error("socket signaled for read while disconnected, slot_id: {}, channel: {}", slot->id,
slot->scn);
need_close = true;
}
@@ -1346,7 +1343,7 @@ void btsock_rfc_signaled(int fd, int flags, uint32_t id) {
if (!slot->f.connected || !flush_incoming_que_on_wr_signal(slot)) {
log::error(
"socket signaled for write while disconnected (or write failure), "
- "slot: {}, channel: {}",
+ "slot_id: {}, channel: {}",
slot->id, slot->scn);
need_close = true;
}
@@ -1372,7 +1369,7 @@ int bta_co_rfc_data_incoming(uint32_t id, BT_HDR* p_buf) {
std::unique_lock<std::recursive_mutex> lock(slot_lock);
rfc_slot_t* slot = find_rfc_slot_by_id(id);
if (!slot) {
- log::error("RFCOMM slot with id {} not found.", id);
+ log::error("RFCOMM slot_id {} not found.", id);
return 0;
}
@@ -1412,7 +1409,7 @@ int bta_co_rfc_data_outgoing_size(uint32_t id, int* size) {
std::unique_lock<std::recursive_mutex> lock(slot_lock);
rfc_slot_t* slot = find_rfc_slot_by_id(id);
if (!slot) {
- log::error("RFCOMM slot with id {} not found.", id);
+ log::error("RFCOMM slot_id {} not found.", id);
return false;
}
@@ -1430,7 +1427,7 @@ int bta_co_rfc_data_outgoing(uint32_t id, uint8_t* buf, uint16_t size) {
std::unique_lock<std::recursive_mutex> lock(slot_lock);
rfc_slot_t* slot = find_rfc_slot_by_id(id);
if (!slot) {
- log::error("RFCOMM slot with id {} not found.", id);
+ log::error("RFCOMM slot_id {} not found.", id);
return false;
}
diff --git a/system/btif/src/btif_storage.cc b/system/btif/src/btif_storage.cc
index e51756483c..4704362f2c 100644
--- a/system/btif/src/btif_storage.cc
+++ b/system/btif/src/btif_storage.cc
@@ -47,6 +47,7 @@
#include "btif/include/btif_api.h"
#include "btif/include/btif_config.h"
+#include "btif/include/btif_dm.h"
#include "btif/include/btif_util.h"
#include "btif/include/core_callbacks.h"
#include "btif/include/stack_manager_t.h"
@@ -179,15 +180,18 @@ static bool prop2cfg(const RawAddress* remote_bd_addr, bt_property_t* prop) {
case BT_PROPERTY_TYPE_OF_DEVICE:
btif_config_set_int(bdstr, BTIF_STORAGE_KEY_DEV_TYPE, *reinterpret_cast<int*>(prop->val));
break;
- case BT_PROPERTY_UUIDS: {
+ case BT_PROPERTY_UUIDS:
+ case BT_PROPERTY_UUIDS_LE: {
std::string val;
size_t cnt = (prop->len) / sizeof(Uuid);
for (size_t i = 0; i < cnt; i++) {
val += (reinterpret_cast<Uuid*>(prop->val) + i)->ToString() + " ";
}
- btif_config_set_str(bdstr, BTIF_STORAGE_KEY_REMOTE_SERVICE, val);
- break;
- }
+ std::string key = (prop->type == BT_PROPERTY_UUIDS_LE) ? BTIF_STORAGE_KEY_REMOTE_SERVICE_LE
+ : BTIF_STORAGE_KEY_REMOTE_SERVICE;
+ btif_config_set_str(bdstr, key, val);
+ } break;
+
case BT_PROPERTY_REMOTE_VERSION_INFO: {
bt_remote_version_t* info = reinterpret_cast<bt_remote_version_t*>(prop->val);
@@ -300,10 +304,15 @@ static bool cfg2prop(const RawAddress* remote_bd_addr, bt_property_t* prop) {
reinterpret_cast<int*>(prop->val));
}
break;
- case BT_PROPERTY_UUIDS: {
+ case BT_PROPERTY_UUIDS:
+ case BT_PROPERTY_UUIDS_LE: {
char value[1280];
int size = sizeof(value);
- if (btif_config_get_str(bdstr, BTIF_STORAGE_KEY_REMOTE_SERVICE, value, &size)) {
+
+ std::string key = (prop->type == BT_PROPERTY_UUIDS_LE) ? BTIF_STORAGE_KEY_REMOTE_SERVICE_LE
+ : BTIF_STORAGE_KEY_REMOTE_SERVICE;
+
+ if (btif_config_get_str(bdstr, key, value, &size)) {
Uuid* p_uuid = reinterpret_cast<Uuid*>(prop->val);
size_t num_uuids = btif_split_uuids_string(value, p_uuid, BT_MAX_NUM_UUIDS);
prop->len = num_uuids * sizeof(Uuid);
@@ -938,13 +947,14 @@ bt_status_t btif_storage_load_bonded_devices(void) {
uint32_t i = 0;
bt_property_t adapter_props[6];
uint32_t num_props = 0;
- bt_property_t remote_properties[10];
+ bt_property_t remote_properties[11];
RawAddress addr;
bt_bdname_t name, alias, model_name;
bt_scan_mode_t mode;
uint32_t disc_timeout;
Uuid local_uuids[BT_MAX_NUM_UUIDS];
Uuid remote_uuids[BT_MAX_NUM_UUIDS];
+ Uuid remote_uuids_le[BT_MAX_NUM_UUIDS];
bt_status_t status;
remove_devices_with_sample_ltk();
@@ -1026,10 +1036,16 @@ bt_status_t btif_storage_load_bonded_devices(void) {
sizeof(devtype), &remote_properties[num_props]);
num_props++;
- btif_storage_get_remote_prop(p_remote_addr, BT_PROPERTY_UUIDS, remote_uuids,
+ btif_storage_get_remote_prop(p_remote_addr, BT_PROPERTY_UUIDS, &remote_uuids,
sizeof(remote_uuids), &remote_properties[num_props]);
num_props++;
+ if (com::android::bluetooth::flags::separate_service_storage()) {
+ btif_storage_get_remote_prop(p_remote_addr, BT_PROPERTY_UUIDS_LE, &remote_uuids_le,
+ sizeof(remote_uuids_le), &remote_properties[num_props]);
+ num_props++;
+ }
+
// Floss needs appearance for metrics purposes
uint16_t appearance = 0;
if (btif_storage_get_remote_prop(p_remote_addr, BT_PROPERTY_APPEARANCE, &appearance,
@@ -1438,6 +1454,48 @@ void btif_storage_remove_gatt_cl_db_hash(const RawAddress& bd_addr) {
bd_addr));
}
+// TODO(b/369381361) Remove this function after all devices are migrated
+void btif_storage_migrate_services() {
+ for (const auto& mac_address : btif_config_get_paired_devices()) {
+ auto addr_str = mac_address.ToString();
+
+ int device_type = BT_DEVICE_TYPE_UNKNOWN;
+ btif_config_get_int(addr_str, BTIF_STORAGE_KEY_DEV_TYPE, &device_type);
+
+ if ((device_type == BT_DEVICE_TYPE_BREDR) ||
+ btif_config_exist(addr_str, BTIF_STORAGE_KEY_REMOTE_SERVICE_LE)) {
+ /* Classic only, or already migrated entries don't need migration */
+ continue;
+ }
+
+ bt_property_t remote_uuids_prop;
+ Uuid remote_uuids[BT_MAX_NUM_UUIDS];
+ BTIF_STORAGE_FILL_PROPERTY(&remote_uuids_prop, BT_PROPERTY_UUIDS, sizeof(remote_uuids),
+ remote_uuids);
+ btif_storage_get_remote_device_property(&mac_address, &remote_uuids_prop);
+
+ log::info("Will migrate Services => ServicesLe for {}", mac_address.ToStringForLogging());
+
+ std::vector<uint8_t> property_value;
+ for (auto& uuid : remote_uuids) {
+ if (!btif_is_interesting_le_service(uuid)) {
+ continue;
+ }
+
+ log::info("interesting LE service: {}", uuid);
+ auto uuid_128bit = uuid.To128BitBE();
+ property_value.insert(property_value.end(), uuid_128bit.begin(), uuid_128bit.end());
+ }
+
+ bt_property_t le_uuids_prop{BT_PROPERTY_UUIDS_LE, static_cast<int>(property_value.size()),
+ (void*)property_value.data()};
+
+ /* Write LE services to storage */
+ btif_storage_set_remote_device_property(&mac_address, &le_uuids_prop);
+ log::info("Migration finished for {}", mac_address.ToStringForLogging());
+ }
+}
+
void btif_debug_linkkey_type_dump(int fd) {
dprintf(fd, "\nLink Key Types:\n");
for (const auto& bd_addr : btif_config_get_paired_devices()) {
diff --git a/system/btif/src/btif_util.cc b/system/btif/src/btif_util.cc
index 85c0069eaa..c6cf544d4a 100644
--- a/system/btif/src/btif_util.cc
+++ b/system/btif/src/btif_util.cc
@@ -129,6 +129,7 @@ std::string dump_property_type(bt_property_type_t type) {
CASE_RETURN_STRING(BT_PROPERTY_ADAPTER_DISCOVERABLE_TIMEOUT);
CASE_RETURN_STRING(BT_PROPERTY_ADAPTER_BONDED_DEVICES);
CASE_RETURN_STRING(BT_PROPERTY_REMOTE_FRIENDLY_NAME);
+ CASE_RETURN_STRING(BT_PROPERTY_UUIDS_LE);
default:
RETURN_UNKNOWN_TYPE_STRING(bt_property_type_t, type);
}
diff --git a/system/btif/test/btif_core_test.cc b/system/btif/test/btif_core_test.cc
index 66881a92d8..1bffc9ce3a 100644
--- a/system/btif/test/btif_core_test.cc
+++ b/system/btif/test/btif_core_test.cc
@@ -320,6 +320,7 @@ TEST_F(BtifUtilsTest, dump_property_type) {
"BT_PROPERTY_ADAPTER_DISCOVERABLE_TIMEOUT"),
std::make_pair(BT_PROPERTY_ADAPTER_BONDED_DEVICES, "BT_PROPERTY_ADAPTER_BONDED_DEVICES"),
std::make_pair(BT_PROPERTY_REMOTE_FRIENDLY_NAME, "BT_PROPERTY_REMOTE_FRIENDLY_NAME"),
+ std::make_pair(BT_PROPERTY_UUIDS_LE, "BT_PROPERTY_UUIDS_LE"),
};
for (const auto& type : types) {
EXPECT_TRUE(dump_property_type(type.first).starts_with(type.second));
diff --git a/system/gd/hci/distance_measurement_manager.cc b/system/gd/hci/distance_measurement_manager.cc
index c0e996b12e..696f33e621 100644
--- a/system/gd/hci/distance_measurement_manager.cc
+++ b/system/gd/hci/distance_measurement_manager.cc
@@ -542,7 +542,8 @@ struct DistanceMeasurementManager::impl : bluetooth::hal::RangingHalCallback {
}
}
- void handle_ras_client_disconnected_event(const Address address) {
+ void handle_ras_client_disconnected_event(const Address address,
+ const ras::RasDisconnectReason& ras_disconnect_reason) {
log::info("address:{}", address);
for (auto it = cs_requester_trackers_.begin(); it != cs_requester_trackers_.end();) {
if (it->second.address == address) {
@@ -550,8 +551,13 @@ struct DistanceMeasurementManager::impl : bluetooth::hal::RangingHalCallback {
it->second.repeating_alarm->Cancel();
it->second.repeating_alarm.reset();
}
- distance_measurement_callbacks_->OnDistanceMeasurementStopped(
- address, REASON_NO_LE_CONNECTION, METHOD_CS);
+ DistanceMeasurementErrorCode reason = REASON_NO_LE_CONNECTION;
+ if (ras_disconnect_reason == ras::RasDisconnectReason::SERVER_NOT_AVAILABLE) {
+ reason = REASON_FEATURE_NOT_SUPPORTED_REMOTE;
+ } else if (ras_disconnect_reason == ras::RasDisconnectReason::FATAL_ERROR) {
+ reason = REASON_INTERNAL_ERROR;
+ }
+ distance_measurement_callbacks_->OnDistanceMeasurementStopped(address, reason, METHOD_CS);
gatt_mtus_.erase(it->first);
it = cs_requester_trackers_.erase(it); // erase and get the next iterator
} else {
@@ -930,9 +936,6 @@ struct DistanceMeasurementManager::impl : bluetooth::hal::RangingHalCallback {
req_it->second.remote_support_phase_based_ranging =
event_view.GetOptionalSubfeaturesSupported().phase_based_ranging_ == 0x01;
req_it->second.remote_num_antennas_supported_ = event_view.GetNumAntennasSupported();
- req_it->second.setup_complete = true;
- log::info("Setup phase complete, connection_handle: {}, address: {}", connection_handle,
- req_it->second.address);
req_it->second.retry_counter_for_create_config = 0;
req_it->second.remote_supported_sw_time_ = event_view.GetTSwTimeSupported();
send_le_cs_create_config(connection_handle, req_it->second.requesting_config_id);
@@ -1031,6 +1034,10 @@ struct DistanceMeasurementManager::impl : bluetooth::hal::RangingHalCallback {
if (!live_tracker->local_start) {
// reset the responder state, as no other event to set the state.
live_tracker->state = CsTrackerState::WAIT_FOR_CONFIG_COMPLETE;
+ } else {
+ live_tracker->setup_complete = true;
+ log::info("connection_handle: {}, address: {}, config_id: {}", connection_handle,
+ live_tracker->address, config_id);
}
live_tracker->used_config_id = config_id;
@@ -2596,8 +2603,9 @@ void DistanceMeasurementManager::HandleConnIntervalUpdated(const Address& addres
conn_interval);
}
-void DistanceMeasurementManager::HandleRasClientDisconnectedEvent(const Address& address) {
- CallOn(pimpl_.get(), &impl::handle_ras_client_disconnected_event, address);
+void DistanceMeasurementManager::HandleRasClientDisconnectedEvent(
+ const Address& address, const ras::RasDisconnectReason& ras_disconnect_reason) {
+ CallOn(pimpl_.get(), &impl::handle_ras_client_disconnected_event, address, ras_disconnect_reason);
}
void DistanceMeasurementManager::HandleVendorSpecificReply(
diff --git a/system/gd/hci/distance_measurement_manager.h b/system/gd/hci/distance_measurement_manager.h
index 5d3484ff74..d8c3d49aea 100644
--- a/system/gd/hci/distance_measurement_manager.h
+++ b/system/gd/hci/distance_measurement_manager.h
@@ -18,6 +18,7 @@
#include <bluetooth/log.h>
#include "address.h"
+#include "bta/include/bta_ras_api.h"
#include "hal/ranging_hal.h"
#include "hci/hci_packets.h"
#include "module.h"
@@ -92,7 +93,8 @@ public:
const Address& address, uint16_t connection_handle, uint16_t att_handle,
const std::vector<hal::VendorSpecificCharacteristic>& vendor_specific_data,
uint16_t conn_interval);
- void HandleRasClientDisconnectedEvent(const Address& address);
+ void HandleRasClientDisconnectedEvent(const Address& address,
+ const ras::RasDisconnectReason& ras_disconnect_reason);
void HandleVendorSpecificReply(
const Address& address, uint16_t connection_handle,
const std::vector<hal::VendorSpecificCharacteristic>& vendor_specific_reply);
diff --git a/system/gd/hci/le_advertising_manager.h b/system/gd/hci/le_advertising_manager.h
index 55245a5cfa..fe614861e2 100644
--- a/system/gd/hci/le_advertising_manager.h
+++ b/system/gd/hci/le_advertising_manager.h
@@ -47,8 +47,8 @@ class AdvertisingConfig {
public:
std::vector<GapData> advertisement;
std::vector<GapData> scan_response;
- uint16_t interval_min;
- uint16_t interval_max;
+ uint32_t interval_min;
+ uint32_t interval_max;
AdvertisingType advertising_type;
AdvertiserAddressType requested_advertiser_address_type;
PeerAddressType peer_address_type;
diff --git a/system/gd/rust/topshim/le_audio/le_audio_shim.cc b/system/gd/rust/topshim/le_audio/le_audio_shim.cc
index 77d7a5c60d..53a0f7d98f 100644
--- a/system/gd/rust/topshim/le_audio/le_audio_shim.cc
+++ b/system/gd/rust/topshim/le_audio/le_audio_shim.cc
@@ -162,6 +162,8 @@ static BtLeAudioGroupStreamStatus to_rust_btle_audio_group_stream_status(
return BtLeAudioGroupStreamStatus::Streaming;
case le_audio::GroupStreamStatus::RELEASING:
return BtLeAudioGroupStreamStatus::Releasing;
+ case le_audio::GroupStreamStatus::RELEASING_AUTONOMOUS:
+ return BtLeAudioGroupStreamStatus::ReleasingAutonomous;
case le_audio::GroupStreamStatus::SUSPENDING:
return BtLeAudioGroupStreamStatus::Suspending;
case le_audio::GroupStreamStatus::SUSPENDED:
diff --git a/system/gd/rust/topshim/src/profiles/le_audio.rs b/system/gd/rust/topshim/src/profiles/le_audio.rs
index 266f24f7fb..651ad82ecd 100644
--- a/system/gd/rust/topshim/src/profiles/le_audio.rs
+++ b/system/gd/rust/topshim/src/profiles/le_audio.rs
@@ -109,6 +109,7 @@ pub mod ffi {
Idle = 0,
Streaming,
Releasing,
+ ReleasingAutonomous,
Suspending,
Suspended,
ConfiguredAutonomous,
@@ -413,11 +414,12 @@ impl From<BtLeAudioGroupStreamStatus> for i32 {
BtLeAudioGroupStreamStatus::Idle => 0,
BtLeAudioGroupStreamStatus::Streaming => 1,
BtLeAudioGroupStreamStatus::Releasing => 2,
- BtLeAudioGroupStreamStatus::Suspending => 3,
- BtLeAudioGroupStreamStatus::Suspended => 4,
- BtLeAudioGroupStreamStatus::ConfiguredAutonomous => 5,
- BtLeAudioGroupStreamStatus::ConfiguredByUser => 6,
- BtLeAudioGroupStreamStatus::Destroyed => 7,
+ BtLeAudioGroupStreamStatus::ReleasingAutonomous => 3,
+ BtLeAudioGroupStreamStatus::Suspending => 4,
+ BtLeAudioGroupStreamStatus::Suspended => 5,
+ BtLeAudioGroupStreamStatus::ConfiguredAutonomous => 6,
+ BtLeAudioGroupStreamStatus::ConfiguredByUser => 7,
+ BtLeAudioGroupStreamStatus::Destroyed => 8,
_ => panic!("Invalid value {:?} to BtLeAudioGroupStreamStatus", value),
}
}
@@ -429,11 +431,12 @@ impl From<i32> for BtLeAudioGroupStreamStatus {
0 => BtLeAudioGroupStreamStatus::Idle,
1 => BtLeAudioGroupStreamStatus::Streaming,
2 => BtLeAudioGroupStreamStatus::Releasing,
- 3 => BtLeAudioGroupStreamStatus::Suspending,
- 4 => BtLeAudioGroupStreamStatus::Suspended,
- 5 => BtLeAudioGroupStreamStatus::ConfiguredAutonomous,
- 6 => BtLeAudioGroupStreamStatus::ConfiguredByUser,
- 7 => BtLeAudioGroupStreamStatus::Destroyed,
+ 3 => BtLeAudioGroupStreamStatus::ReleasingAutonomous,
+ 4 => BtLeAudioGroupStreamStatus::Suspending,
+ 5 => BtLeAudioGroupStreamStatus::Suspended,
+ 6 => BtLeAudioGroupStreamStatus::ConfiguredAutonomous,
+ 7 => BtLeAudioGroupStreamStatus::ConfiguredByUser,
+ 8 => BtLeAudioGroupStreamStatus::Destroyed,
_ => panic!("Invalid value {} to BtLeAudioGroupStreamStatus", value),
}
}
diff --git a/system/gd/storage/config_keys.h b/system/gd/storage/config_keys.h
index 4629a494ba..d3659b6a96 100644
--- a/system/gd/storage/config_keys.h
+++ b/system/gd/storage/config_keys.h
@@ -111,6 +111,7 @@
#define BTIF_STORAGE_KEY_PIN_LENGTH "PinLength"
#define BTIF_STORAGE_KEY_PRODUCT_ID "ProductId"
#define BTIF_STORAGE_KEY_REMOTE_SERVICE "Service"
+#define BTIF_STORAGE_KEY_REMOTE_SERVICE_LE "ServiceLe"
#define BTIF_STORAGE_KEY_REMOTE_VER_MFCT "Manufacturer"
#define BTIF_STORAGE_KEY_REMOTE_VER_SUBVER "LmpSubVer"
#define BTIF_STORAGE_KEY_REMOTE_VER_VER "LmpVer"
diff --git a/system/include/hardware/bluetooth.h b/system/include/hardware/bluetooth.h
index a8a169a58d..fa15353d9f 100644
--- a/system/include/hardware/bluetooth.h
+++ b/system/include/hardware/bluetooth.h
@@ -299,7 +299,7 @@ typedef enum {
*/
BT_PROPERTY_TYPE_OF_DEVICE,
/**
- * Description - Bluetooth Service Record
+ * Description - Bluetooth Service Record, UUIDs on BREDR transport
* Access mode - Only GET.
* Data type - bt_service_record_t
*/
@@ -427,6 +427,14 @@ typedef enum {
*/
BT_PROPERTY_LPP_OFFLOAD_FEATURES,
+ /**
+ * Description - Bluetooth Service 128-bit UUIDs on LE transport
+ * Access mode - Only GET.
+ * Data type - Array of bluetooth::Uuid (Array size inferred from property
+ * length).
+ */
+ BT_PROPERTY_UUIDS_LE,
+
BT_PROPERTY_REMOTE_DEVICE_TIMESTAMP = 0xFF,
} bt_property_type_t;
@@ -915,6 +923,11 @@ typedef struct {
int (*disconnect_all_acls)();
/**
+ * Call to disconnect ACL connection to device
+ */
+ int (*disconnect_acl)(const RawAddress& bd_addr, int transport);
+
+ /**
* Call to retrieve a generated random
*/
int (*le_rand)();
diff --git a/system/include/hardware/bt_le_audio.h b/system/include/hardware/bt_le_audio.h
index 91ce17c388..94c3762596 100644
--- a/system/include/hardware/bt_le_audio.h
+++ b/system/include/hardware/bt_le_audio.h
@@ -70,6 +70,7 @@ enum class GroupStreamStatus {
IDLE = 0,
STREAMING,
RELEASING,
+ RELEASING_AUTONOMOUS,
SUSPENDING,
SUSPENDED,
CONFIGURED_AUTONOMOUS,
diff --git a/system/main/shim/distance_measurement_manager.cc b/system/main/shim/distance_measurement_manager.cc
index cdd069b215..fd7eb3591d 100644
--- a/system/main/shim/distance_measurement_manager.cc
+++ b/system/main/shim/distance_measurement_manager.cc
@@ -252,9 +252,10 @@ public:
bluetooth::ToGdAddress(address), GetConnectionHandleAndRole(address), conn_interval);
}
- void OnDisconnected(const RawAddress& address) {
+ void OnDisconnected(const RawAddress& address,
+ const ras::RasDisconnectReason& ras_disconnect_reason) {
bluetooth::shim::GetDistanceMeasurementManager()->HandleRasClientDisconnectedEvent(
- bluetooth::ToGdAddress(address));
+ bluetooth::ToGdAddress(address), ras_disconnect_reason);
}
// Must be called from main_thread
diff --git a/system/rust/src/core/shared_box.rs b/system/rust/src/core/shared_box.rs
index 5dd1118185..accd1ae6ec 100644
--- a/system/rust/src/core/shared_box.rs
+++ b/system/rust/src/core/shared_box.rs
@@ -79,7 +79,7 @@ impl<T: ?Sized> Clone for WeakBox<T> {
/// A strong reference to the contents within a SharedBox<>.
pub struct WeakBoxRef<'a, T: ?Sized>(&'a T, Weak<T>);
-impl<'a, T: ?Sized> WeakBoxRef<'a, T> {
+impl<T: ?Sized> WeakBoxRef<'_, T> {
/// Downgrade to a weak reference (with static lifetime) to the contents
/// within the underlying SharedBox<>
pub fn downgrade(&self) -> WeakBox<T> {
@@ -87,7 +87,7 @@ impl<'a, T: ?Sized> WeakBoxRef<'a, T> {
}
}
-impl<'a, T: ?Sized> Deref for WeakBoxRef<'a, T> {
+impl<T: ?Sized> Deref for WeakBoxRef<'_, T> {
type Target = T;
fn deref(&self) -> &Self::Target {
@@ -95,7 +95,7 @@ impl<'a, T: ?Sized> Deref for WeakBoxRef<'a, T> {
}
}
-impl<'a, T: ?Sized> Clone for WeakBoxRef<'a, T> {
+impl<T: ?Sized> Clone for WeakBoxRef<'_, T> {
fn clone(&self) -> Self {
Self(self.0, self.1.clone())
}
diff --git a/system/stack/Android.bp b/system/stack/Android.bp
index 384c31f010..783117ddc3 100644
--- a/system/stack/Android.bp
+++ b/system/stack/Android.bp
@@ -349,6 +349,10 @@ cc_defaults {
defaults: [
"bluetooth_cflags",
],
+ cflags: [
+ "-Wno-missing-prototypes",
+ "-Wno-unused-parameter",
+ ],
include_dirs: [
"packages/modules/Bluetooth/system",
"packages/modules/Bluetooth/system/gd",
@@ -563,6 +567,7 @@ cc_fuzz {
":TestCommonMockFunctions",
":TestCommonStackConfig",
":TestFakeOsi",
+ ":TestMockBtaDm",
":TestMockBtif",
":TestMockDevice",
":TestMockMainShim",
@@ -945,6 +950,7 @@ cc_test {
srcs: [
":TestCommonMainHandler",
":TestCommonMockFunctions",
+ ":TestMockBtaDm",
":TestMockBtif",
":TestMockDevice",
":TestMockMainShim",
diff --git a/system/stack/avrc/avrc_api.cc b/system/stack/avrc/avrc_api.cc
index 13184ed058..6b3c1700cb 100644
--- a/system/stack/avrc/avrc_api.cc
+++ b/system/stack/avrc/avrc_api.cc
@@ -25,6 +25,7 @@
#include <android_bluetooth_sysprop.h>
#include <bluetooth/log.h>
+#include <com_android_bluetooth_flags.h>
#include <string.h>
#include <cstdint>
@@ -1009,7 +1010,12 @@ uint16_t AVRC_GetControlProfileVersion() {
uint16_t AVRC_GetProfileVersion() {
uint16_t profile_version = AVRC_REV_1_4;
char avrcp_version[PROPERTY_VALUE_MAX] = {0};
- osi_property_get(AVRC_VERSION_PROPERTY, avrcp_version, AVRC_DEFAULT_VERSION);
+
+ if (!com::android::bluetooth::flags::avrcp_16_default()) {
+ osi_property_get(AVRC_VERSION_PROPERTY, avrcp_version, AVRC_1_5_STRING);
+ } else {
+ osi_property_get(AVRC_VERSION_PROPERTY, avrcp_version, AVRC_1_6_STRING);
+ }
if (!strncmp(AVRC_1_6_STRING, avrcp_version, sizeof(AVRC_1_6_STRING))) {
profile_version = AVRC_REV_1_6;
diff --git a/system/stack/btm/btm_ble_gap.cc b/system/stack/btm/btm_ble_gap.cc
index 16e46168ba..808c9491d1 100644
--- a/system/stack/btm/btm_ble_gap.cc
+++ b/system/stack/btm/btm_ble_gap.cc
@@ -1909,7 +1909,7 @@ static DEV_CLASS btm_ble_appearance_to_cod(uint16_t appearance) {
dev_class[1] = BTM_COD_MAJOR_PERIPHERAL;
dev_class[2] = BTM_COD_MINOR_DIGITAL_PAN;
break;
- case BTM_BLE_APPEARANCE_UKNOWN:
+ case BTM_BLE_APPEARANCE_UNKNOWN:
case BTM_BLE_APPEARANCE_GENERIC_CLOCK:
case BTM_BLE_APPEARANCE_GENERIC_TAG:
case BTM_BLE_APPEARANCE_GENERIC_KEYRING:
diff --git a/system/stack/fuzzers/avrc_fuzzer.cc b/system/stack/fuzzers/avrc_fuzzer.cc
index be0c80f32c..d2c4acf4f8 100644
--- a/system/stack/fuzzers/avrc_fuzzer.cc
+++ b/system/stack/fuzzers/avrc_fuzzer.cc
@@ -33,10 +33,6 @@
#include "test/mock/mock_stack_l2cap_api.h"
#include "types/bluetooth/uuid.h"
-// TODO(b/369381361) Enfore -Wmissing-prototypes
-#pragma GCC diagnostic ignored "-Wmissing-prototypes"
-#pragma GCC diagnostic ignored "-Wunused-parameter"
-
using bluetooth::Uuid;
using namespace bluetooth;
diff --git a/system/stack/fuzzers/bnep_fuzzer.cc b/system/stack/fuzzers/bnep_fuzzer.cc
index 79a70556e4..6976ccd602 100644
--- a/system/stack/fuzzers/bnep_fuzzer.cc
+++ b/system/stack/fuzzers/bnep_fuzzer.cc
@@ -31,10 +31,6 @@
#include "test/mock/mock_stack_l2cap_ble.h"
#include "types/bluetooth/uuid.h"
-// TODO(b/369381361) Enfore -Wmissing-prototypes
-#pragma GCC diagnostic ignored "-Wmissing-prototypes"
-#pragma GCC diagnostic ignored "-Wunused-parameter"
-
using bluetooth::Uuid;
namespace {
diff --git a/system/stack/fuzzers/gatt_fuzzer.cc b/system/stack/fuzzers/gatt_fuzzer.cc
index 3a9fb81276..064a203a81 100644
--- a/system/stack/fuzzers/gatt_fuzzer.cc
+++ b/system/stack/fuzzers/gatt_fuzzer.cc
@@ -33,11 +33,8 @@
#include "test/mock/mock_stack_l2cap_api.h"
#include "test/mock/mock_stack_l2cap_ble.h"
-// TODO(b/369381361) Enfore -Wmissing-prototypes
-#pragma GCC diagnostic ignored "-Wmissing-prototypes"
-#pragma GCC diagnostic ignored "-Wunused-parameter"
-
using bluetooth::Uuid;
+
bt_status_t do_in_main_thread(base::OnceCallback<void()>) {
// this is not properly mocked, so we use abort to catch if this is used in
// any test cases
diff --git a/system/stack/fuzzers/l2cap_fuzzer.cc b/system/stack/fuzzers/l2cap_fuzzer.cc
index 004e5b0920..6cb4d5170f 100644
--- a/system/stack/fuzzers/l2cap_fuzzer.cc
+++ b/system/stack/fuzzers/l2cap_fuzzer.cc
@@ -42,9 +42,6 @@
#include "test/mock/mock_stack_acl.h"
#include "test/mock/mock_stack_btm_devctl.h"
-// TODO(b/369381361) Enfore -Wmissing-prototypes
-#pragma GCC diagnostic ignored "-Wmissing-prototypes"
-
using bluetooth::Uuid;
using testing::Return;
using namespace bluetooth;
diff --git a/system/stack/fuzzers/rfcomm_fuzzer.cc b/system/stack/fuzzers/rfcomm_fuzzer.cc
index 1445156aec..3418f3a6ed 100644
--- a/system/stack/fuzzers/rfcomm_fuzzer.cc
+++ b/system/stack/fuzzers/rfcomm_fuzzer.cc
@@ -39,10 +39,6 @@
#include "test/mock/mock_stack_l2cap_interface.h"
#include "test/rfcomm/stack_rfcomm_test_utils.h"
-// TODO(b/369381361) Enfore -Wmissing-prototypes
-#pragma GCC diagnostic ignored "-Wmissing-prototypes"
-#pragma GCC diagnostic ignored "-Wunused-parameter"
-
using ::testing::_;
using ::testing::NiceMock;
using ::testing::Return;
diff --git a/system/stack/fuzzers/sdp_fuzzer.cc b/system/stack/fuzzers/sdp_fuzzer.cc
index bd5b6f77c2..de994d415e 100644
--- a/system/stack/fuzzers/sdp_fuzzer.cc
+++ b/system/stack/fuzzers/sdp_fuzzer.cc
@@ -31,10 +31,6 @@
#include "test/mock/mock_stack_l2cap_api.h"
#include "types/bluetooth/uuid.h"
-// TODO(b/369381361) Enfore -Wmissing-prototypes
-#pragma GCC diagnostic ignored "-Wmissing-prototypes"
-#pragma GCC diagnostic ignored "-Wunused-parameter"
-
namespace {
#define SDP_DB_SIZE 0x10000
diff --git a/system/stack/fuzzers/smp_fuzzer.cc b/system/stack/fuzzers/smp_fuzzer.cc
index a99010254a..b4dad542ea 100644
--- a/system/stack/fuzzers/smp_fuzzer.cc
+++ b/system/stack/fuzzers/smp_fuzzer.cc
@@ -33,13 +33,10 @@
#include "test/mock/mock_stack_l2cap_api.h"
#include "test/mock/mock_stack_l2cap_ble.h"
-// TODO(b/369381361) Enfore -Wmissing-prototypes
-#pragma GCC diagnostic ignored "-Wmissing-prototypes"
-#pragma GCC diagnostic ignored "-Wunused-parameter"
-
bluetooth::common::MessageLoopThread* main_thread_ptr = nullptr;
bluetooth::common::MessageLoopThread* get_main_thread() { return main_thread_ptr; }
+
namespace {
#define SDP_DB_SIZE 0x10000
diff --git a/system/stack/include/avrc_api.h b/system/stack/include/avrc_api.h
index 589ddb5021..b2f683cc0c 100644
--- a/system/stack/include/avrc_api.h
+++ b/system/stack/include/avrc_api.h
@@ -133,10 +133,6 @@
#define AVRC_1_3_STRING "avrcp13"
#endif
-#ifndef AVRC_DEFAULT_VERSION
-#define AVRC_DEFAULT_VERSION AVRC_1_5_STRING
-#endif
-
/* Configurable dynamic avrcp version enable key*/
#ifndef AVRC_DYNAMIC_AVRCP_ENABLE_PROPERTY
#define AVRC_DYNAMIC_AVRCP_ENABLE_PROPERTY "persist.bluetooth.dynamic_avrcp.enable"
diff --git a/system/stack/include/ble_appearance.h b/system/stack/include/ble_appearance.h
new file mode 100644
index 0000000000..6dbb32996c
--- /dev/null
+++ b/system/stack/include/ble_appearance.h
@@ -0,0 +1,446 @@
+/******************************************************************************
+ *
+ * Copyright (C) 2025 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.
+ *
+ ******************************************************************************/
+
+#ifndef BLE_APPEARANCE_H
+#define BLE_APPEARANCE_H
+
+/* BLE appearance values as per BT spec assigned numbers */
+/* Category[15:6] 0x000 */
+#define BLE_APPEARANCE_UNKNOWN 0x0000
+
+/* Category[15:6] 0x001 */
+#define BLE_APPEARANCE_GENERIC_PHONE 0x0040
+
+/* Category[15:6] 0x002 */
+#define BLE_APPEARANCE_GENERIC_COMPUTER 0x0080
+
+#define BLE_APPEARANCE_DESKTOP_WORKSTATION 0x81
+#define BLE_APPEARANCE_SERVER_CLASS_COMPUTER 0x82
+#define BLE_APPEARANCE_LAPTOP 0x83
+#define BLE_APPEARANCE_HANDHELD_PC_PDA 0x84
+#define BLE_APPEARANCE_PALM_SIZE_PC_PDA 0x85
+#define BLE_APPEARANCE_WEARABLE_COMPUTER_WATCH_SIZE 0x86
+#define BLE_APPEARANCE_TABLET 0x87
+#define BLE_APPEARANCE_DOCKING_STATION 0x88
+#define BLE_APPEARANCE_ALL_IN_ONE 0x89
+#define BLE_APPEARANCE_BLADE_SERVER 0x8A
+#define BLE_APPEARANCE_CONVERTIBLE 0x8B
+#define BLE_APPEARANCE_DETACHABLE 0x8C
+#define BLE_APPEARANCE_IOT_GATEWAY 0x8D
+#define BLE_APPEARANCE_MINI_PC 0x8E
+#define BLE_APPEARANCE_STICK_PC 0x8F
+
+/* Category[15:6] 0x003 */
+#define BLE_APPEARANCE_GENERIC_WATCH 0x00C0
+#define BLE_APPEARANCE_SPORTS_WATCH 0x00C1
+#define BLE_APPEARANCE_SMART_WATCH 0x00C2
+
+/* Category[15:6] 0x004 */
+#define BLE_APPEARANCE_GENERIC_CLOCK 0x0100
+
+/* Category[15:6] 0x005 */
+#define BLE_APPEARANCE_GENERIC_DISPLAY 0x0140
+
+/* Category[15:6] 0x006 */
+#define BLE_APPEARANCE_GENERIC_REMOTE 0x0180
+
+/* Category[15:6] 0x007 */
+#define BLE_APPEARANCE_GENERIC_EYEGLASSES 0x01C0
+
+/* Category[15:6] 0x008 */
+#define BLE_APPEARANCE_GENERIC_TAG 0x0200
+
+/* Category[15:6] 0x009 */
+#define BLE_APPEARANCE_GENERIC_KEYRING 0x0240
+
+/* Category[15:6] 0x00A */
+#define BLE_APPEARANCE_GENERIC_MEDIA_PLAYER 0x0280
+
+/* Category[15:6] 0x00B */
+#define BLE_APPEARANCE_GENERIC_BARCODE_SCANNER 0x02C0
+
+/* Category[15:6] 0x00C */
+#define BLE_APPEARANCE_GENERIC_THERMOMETER 0x0300
+#define BLE_APPEARANCE_THERMOMETER_EAR 0x0301
+
+/* Category[15:6] 0x00D */
+#define BLE_APPEARANCE_GENERIC_HEART_RATE 0x0340
+#define BLE_APPEARANCE_HEART_RATE_BELT 0x0341
+
+/* Category[15:6] 0x00E */
+#define BLE_APPEARANCE_GENERIC_BLOOD_PRESSURE 0x0380
+#define BLE_APPEARANCE_BLOOD_PRESSURE_ARM 0x0381
+#define BLE_APPEARANCE_BLOOD_PRESSURE_WRIST 0x0382
+
+/* Category[15:6] 0x00F */
+#define BLE_APPEARANCE_GENERIC_HID 0x03C0
+#define BLE_APPEARANCE_HID_KEYBOARD 0x03C1
+#define BLE_APPEARANCE_HID_MOUSE 0x03C2
+#define BLE_APPEARANCE_HID_JOYSTICK 0x03C3
+#define BLE_APPEARANCE_HID_GAMEPAD 0x03C4
+#define BLE_APPEARANCE_HID_DIGITIZER_TABLET 0x03C5
+#define BLE_APPEARANCE_HID_CARD_READER 0x03C6
+#define BLE_APPEARANCE_HID_DIGITAL_PEN 0x03C7
+#define BLE_APPEARANCE_HID_BARCODE_SCANNER 0x03C8
+#define BLE_APPEARANCE_HID_TOUCHPAD 0x03C9
+#define BLE_APPEARANCE_HID_PRESENTATION_REMOTE 0x03CA
+
+/* Category[15:6] 0x010 */
+#define BLE_APPEARANCE_GENERIC_GLUCOSE 0x0400
+
+/* Category[15:6] 0x011 */
+#define BLE_APPEARANCE_GENERIC_WALKING 0x0440
+#define BLE_APPEARANCE_WALKING_IN_SHOE 0x0441
+#define BLE_APPEARANCE_WALKING_ON_SHOE 0x0442
+#define BLE_APPEARANCE_WALKING_ON_HIP 0x0443
+
+/* Category[15:6] 0x012 */
+#define BLE_APPEARANCE_GENERIC_CYCLING 0x0480
+#define BLE_APPEARANCE_CYCLING_COMPUTER 0x0481
+#define BLE_APPEARANCE_CYCLING_SPEED 0x0482
+#define BLE_APPEARANCE_CYCLING_CADENCE 0x0483
+#define BLE_APPEARANCE_CYCLING_POWER 0x0484
+#define BLE_APPEARANCE_CYCLING_SPEED_CADENCE 0x0485
+
+/* Category[15:6] 0x013 */
+#define BLE_APPEARANCE_GENERIC_CONTROL_DEVICE 0x04C0
+#define BLE_APPEARANCE_SWITCH 0x04C1
+#define BLE_APPEARANCE_MULTI_SWITCH 0x04C2
+#define BLE_APPEARANCE_SWITCH_BUTTON 0x04C3
+#define BLE_APPEARANCE_SWITCH_SLIDER 0x04C4
+#define BLE_APPEARANCE_ROTARY_SWITCH 0x04C5
+#define BLE_APPEARANCE_TOUCH_PANEL 0x04C6
+#define BLE_APPEARANCE_SINGLE_SWITCH 0x04C7
+#define BLE_APPEARANCE_DOUBLE_SWITCH 0x04C8
+#define BLE_APPEARANCE_TRIPLE_SWITCH 0x04C9
+#define BLE_APPEARANCE_BATTERY_SWITCH 0x04CA
+#define BLE_APPEARANCE_ENERGY_HARVESTING_SWITCH 0x04CB
+#define BLE_APPEARANCE_SWITCH_PUSH_BUTTON 0x04CC
+#define BLE_APPEARANCE_SWITCH_DIAL 0x04CD
+
+/* Category[15:6] 0x014 */
+#define BLE_APPEARANCE_GENERIC_NETWORK_DEVICE 0x0500
+#define BLE_APPEARANCE_NETWORK_DEVICE_ACCESS_POINT 0x0501
+#define BLE_APPEARANCE_NETWORK_DEVICE_MESH_DEVICE 0x0502
+#define BLE_APPEARANCE_NETWORK_DEVICE_MESH_NETWORK_PROXY 0x0503
+
+/* Category[15:6] 0x015 */
+#define BLE_APPEARANCE_GENERIC_SENSOR 0x0540
+#define BLE_APPEARANCE_MOTION_SENSOR 0x0541
+#define BLE_APPEARANCE_AIR_QUALITY_SENSOR 0x0542
+#define BLE_APPEARANCE_TEMPERATURE_SENSOR 0x0543
+#define BLE_APPEARANCE_HUMIDITY_SENSOR 0x0544
+#define BLE_APPEARANCE_LEAK_SENSOR 0x05
+#define BLE_APPEARANCE_SMOKE_SENSOR 0x0546
+#define BLE_APPEARANCE_OCCUPANCY_SENSOR 0x0547
+#define BLE_APPEARANCE_CONTACT_SENSOR 0x0548
+#define BLE_APPEARANCE_CARBON_MONOXIDE_SENSOR 0x0549
+#define BLE_APPEARANCE_CARBON_DIOXIDE_SENSOR 0x054A
+#define BLE_APPEARANCE_AMBIENT_LIGHT_SENSOR 0x054B
+#define BLE_APPEARANCE_ENERGY_SENSOR 0x054C
+#define BLE_APPEARANCE_COLOR_LIGHT_SENSOR 0x054D
+#define BLE_APPEARANCE_RAIN_SENSOR 0x054E
+#define BLE_APPEARANCE_FIRE_SENSOR 0x054F
+#define BLE_APPEARANCE_WIND_SENSOR 0x0550
+#define BLE_APPEARANCE_PROXIMITY_SENSOR 0x0551
+#define BLE_APPEARANCE_MULTI_SENSOR 0x0552
+#define BLE_APPEARANCE_FLUSH_MOUNTED_SENSOR 0x0553
+#define BLE_APPEARANCE_CEILING_MOUNTED_SENSOR 0x0554
+#define BLE_APPEARANCE_WALL_MOUNTED_SENSOR 0x0555
+#define BLE_APPEARANCE_MULTISENSOR 0x0556
+#define BLE_APPEARANCE_SENSOR_ENERGY_METER 0x0557
+#define BLE_APPEARANCE_SENSOR_FLAME_DETECTOR 0x0558
+#define BLE_APPEARANCE_VEHICLE_TIRE_PRESSURE_SENSOR 0x0559
+
+/* Category[15:6] 0x016 */
+#define BLE_APPEARANCE_GENERIC_LIGHT_FIXTURE 0x0580
+#define BLE_APPEARANCE_WALL_LIGHT 0x0581
+#define BLE_APPEARANCE_CEILING_LIGHT 0x0582
+#define BLE_APPEARANCE_FLOOR_LIGHT 0x0583
+#define BLE_APPEARANCE_CABINET_LIGHT 0x0584
+#define BLE_APPEARANCE_DESK_LIGHT 0x0585
+#define BLE_APPEARANCE_TROFFER_LIGHT 0x0586
+#define BLE_APPEARANCE_PENDANT_LIGHT 0x0587
+#define BLE_APPEARANCE_IN_GROUND_LIGHT 0x0588
+#define BLE_APPEARANCE_FLOOD_LIGHT 0x0589
+#define BLE_APPEARANCE_UNDERWATER_LIGHT 0x058A
+#define BLE_APPEARANCE_BOLLARD_WITH_LIGHT 0x058B
+#define BLE_APPEARANCE_PATHWAY_LIGHT 0x058C
+#define BLE_APPEARANCE_GARDEN_LIGHT 0x058D
+#define BLE_APPEARANCE_POLE_TOP_LIGHT 0x058E
+#define BLE_APPEARANCE_SPOTLIGHT 0x058F
+#define BLE_APPEARANCE_LINEAR_LIGHT 0x0590
+#define BLE_APPEARANCE_STREET_LIGHT 0x0591
+#define BLE_APPEARANCE_SHELVES_LIGHT 0x0592
+#define BLE_APPEARANCE_BAY_LIGHT 0x0593
+#define BLE_APPEARANCE_EMERGENCY_EXIT_LIGHT 0x0594
+#define BLE_APPEARANCE_LIGHT_CONTROLLER 0x0595
+#define BLE_APPEARANCE_LIGHT_DRIVER 0x0596
+#define BLE_APPEARANCE_BULB 0x0597
+#define BLE_APPEARANCE_LOW_BAY_LIGHT 0x0598
+#define BLE_APPEARANCE_HIGH_BAY_LIGHT 0x0599
+
+/* Category[15:6] 0x017 */
+#define BLE_APPEARANCE_GENERIC_FAN 0x05C0
+#define BLE_APPEARANCE_CEILING_FAN 0x05C1
+#define BLE_APPEARANCE_AXIAL_FAN 0x05C2
+#define BLE_APPEARANCE_EXHAUST_FAN 0x05C3
+#define BLE_APPEARANCE_PEDESTAL_FAN 0x05C4
+#define BLE_APPEARANCE_DESK_FAN 0x05C5
+#define BLE_APPEARANCE_WALL_FAN 0x05C6
+
+/* Category[15:6] 0x018 */
+#define BLE_APPEARANCE_GENERIC_HVAC 0x0600
+#define BLE_APPEARANCE_HVAC_THERMOSTAT 0x0601
+#define BLE_APPEARANCE_HVAC_HUMIDIFIER 0x0602
+#define BLE_APPEARANCE_HVAC_DEHUMIDIFIER 0x0603
+#define BLE_APPEARANCE_HVAC_HEATER 0x0604
+#define BLE_APPEARANCE_HVAC_RADIATOR 0x0605
+#define BLE_APPEARANCE_HVAC_BOILER 0x0606
+#define BLE_APPEARANCE_HVAC_HEAT_PUMP 0x0607
+#define BLE_APPEARANCE_HVAC_INFRARED_HEATER 0x0608
+#define BLE_APPEARANCE_HVAC_RADIANT_PANEL_HEATER 0x0609
+#define BLE_APPEARANCE_HVAC_FAN_HEATER 0x060A
+#define BLE_APPEARANCE_HVAC_AIR_CURTAIN 0x060B
+
+/* Category[15:6] 0x019 */
+#define BLE_APPEARANCE_GENERIC_AIR_CONDITIONING 0x0640
+
+/* Category[15:6] 0x01A */
+#define BLE_APPEARANCE_GENERIC_HUMIDIFIER 0x0680
+
+/* Category[15:6] 0x01B */
+#define BLE_APPEARANCE_GENERIC_HEATING 0x06C0
+#define BLE_APPEARANCE_HEATING_RADIATOR 0x06C1
+#define BLE_APPEARANCE_HEATING_BOILER 0x06C2
+#define BLE_APPEARANCE_HEATING_HEAT_PUMP 0x06C3
+#define BLE_APPEARANCE_HEATING_INFRARED_HEATER 0x06C4
+#define BLE_APPEARANCE_HEATING_RADIANT_PANEL_HEATER 0x06C5
+#define BLE_APPEARANCE_HEATING_FAN_HEATER 0x06C6
+#define BLE_APPEARANCE_HEATING_AIR_CURTAIN 0x06C7
+
+/* Category[15:6] 0x01C */
+#define BLE_APPEARANCE_GENERIC_ACCESS_CONTROL 0x0700
+#define BLE_APPEARANCE_ACCESS_DOOR 0x0701
+#define BLE_APPEARANCE_ACCESS_CONTROL_GARAGE_DOOR 0x0702
+#define BLE_APPEARANCE_ACCESS_CONTROL_EMERGENCY_EXIT_DOOR 0x0703
+#define BLE_APPEARANCE_ACCESS_CONTROL_ACCESS_LOCK 0x0704
+#define BLE_APPEARANCE_ACCESS_CONTROL_ELEVATOR 0x0705
+#define BLE_APPEARANCE_ACCESS_CONTROL_WINDOW 0x0706
+#define BLE_APPEARANCE_ACCESS_CONTROL_ENTRANCE_GATE 0x0707
+#define BLE_APPEARANCE_ACCESS_CONTROL_DOOR_LOCK 0x0708
+#define BLE_APPEARANCE_ACCESS_CONTROL_LOCKER 0x0709
+
+/* Category[15:6] 0x01D */
+#define BLE_APPEARANCE_GENERIC_MOTORIZED_DEVICE 0x0740
+#define BLE_APPEARANCE_MOTORIZED_GATE 0x0741
+#define BLE_APPEARANCE_MOTORIZED_AWNING 0x0742
+#define BLE_APPEARANCE_MOTORIZED_BLINDS_OR_SHADES 0x0743
+#define BLE_APPEARANCE_MOTORIZED_CURTAINS 0x0744
+#define BLE_APPEARANCE_MOTORIZED_SCREEN 0x0745
+
+/* Category[15:6] 0x01E */
+#define BLE_APPEARANCE_GENERIC_POWER_DEVICE 0x0780
+#define BLE_APPEARANCE_POWER_OUTLET 0x0781
+#define BLE_APPEARANCE_POWER_STRIP 0x0782
+#define BLE_APPEARANCE_POWER_PLUG 0x0783
+#define BLE_APPEARANCE_POWER_SUPPLY 0x0784
+
+/* Category[15:6] 0x01F */
+#define BLE_APPEARANCE_GENERIC_LIGHT_SOURCE 0x07C0
+#define BLE_APPEARANCE_LIGHT_SOURCE_INCANDESCENT_LIGHT_BULB 0x07C1
+#define BLE_APPEARANCE_LIGHT_SOURCE_LED_LAMP 0x07C2
+#define BLE_APPEARANCE_LIGHT_SOURCE_HID_LAMP 0x07C3
+#define BLE_APPEARANCE_LIGHT_SOURCE_FLUORESCENT_LAMP 0x07C4
+#define BLE_APPEARANCE_LIGHT_SOURCE_LED_ARRAY 0x07C5
+#define BLE_APPEARANCE_LIGHT_SOURCE_MULTI_COLOR_LED_ARRAY 0x07C6
+#define BLE_APPEARANCE_LIGHT_SOURCE_LOW_VOLTAGE_HALOGEN 0x07C7
+#define BLE_APPEARANCE_LIGHT_SOURCE_ORGANIC_LIGHT_EMITTING_DIODE_OLED 0x07C8
+
+/* Category[15:6] 0x020 */
+#define BLE_APPEARANCE_GENERIC_WINDOW_COVERING 0x0800
+#define BLE_APPEARANCE_WINDOW_COVERING_WINDOW_SHADES 0x0801
+#define BLE_APPEARANCE_WINDOW_COVERING_WINDOW_BLINDS 0x0802
+#define BLE_APPEARANCE_WINDOW_COVERING_WINDOW_AWNING 0x0803
+#define BLE_APPEARANCE_WINDOW_COVERING_WINDOW_CURTAIN 0x0804
+#define BLE_APPEARANCE_WINDOW_COVERING_EXTERIOR_SHUTTER 0x0805
+#define BLE_APPEARANCE_WINDOW_COVERING_EXTERIOR_SCREEN 0x0806
+
+/* Category[15:6] 0x021 */
+#define BLE_APPEARANCE_GENERIC_AUDIO_SINK 0x0840
+#define BLE_APPEARANCE_AUDIO_SINK_STANDALONE_SPEAKER 0x0841
+#define BLE_APPEARANCE_AUDIO_SINK_SOUNDBAR 0x0842
+#define BLE_APPEARANCE_AUDIO_SINK_BOOKSHELF_SPEAKER 0x0843
+#define BLE_APPEARANCE_AUDIO_SINK_STANDMOUNTED_SPEAKER 0x0844
+#define BLE_APPEARANCE_AUDIO_SINK_SPEAKERPHONE 0x0845
+
+/* Category[15:6] 0x022 */
+#define BLE_APPEARANCE_GENERIC_AUDIO_SOURCE 0x0880
+#define BLE_APPEARANCE_AUDIO_SOURCE_MICROPHONE 0x0881
+#define BLE_APPEARANCE_AUDIO_SOURCE_ALARM 0x0882
+#define BLE_APPEARANCE_AUDIO_SOURCE_BELL 0x0883
+#define BLE_APPEARANCE_AUDIO_SOURCE_HORN 0x0884
+#define BLE_APPEARANCE_AUDIO_SOURCE_BROADCASTING_DEVICE 0x0885
+#define BLE_APPEARANCE_AUDIO_SOURCE_SERVICE_DESK 0x0886
+#define BLE_APPEARANCE_AUDIO_SOURCE_KIOSK 0x0887
+#define BLE_APPEARANCE_AUDIO_SOURCE_BROADCASTING_ROOM 0x0888
+#define BLE_APPEARANCE_AUDIO_SOURCE_AUDITORIUM 0x0889
+
+/* Category[15:6] 0x023 */
+#define BLE_APPEARANCE_GENERIC_MOTORIZED_VEHICLE 0x08C0
+#define BLE_APPEARANCE_MOTORIZED_VEHICLE_CAR 0x08C1
+#define BLE_APPEARANCE_MOTORIZED_VEHICLE_LARGE_GOODS 0x08C2
+#define BLE_APPEARANCE_MOTORIZED_VEHICLE_2_WHEELED 0x08C3
+#define BLE_APPEARANCE_MOTORIZED_VEHICLE_MOTORBIKE 0x08C4
+#define BLE_APPEARANCE_MOTORIZED_VEHICLE_SCOOTER 0x08C5
+#define BLE_APPEARANCE_MOTORIZED_VEHICLE_MOPED 0x08C6
+#define BLE_APPEARANCE_MOTORIZED_VEHICLE_3_WHEELED 0x08C7
+#define BLE_APPEARANCE_MOTORIZED_VEHICLE_LIGHT_VEHICLE 0x08C8
+#define BLE_APPEARANCE_MOTORIZED_VEHICLE_QUAD_BIKE 0x08C9
+#define BLE_APPEARANCE_MOTORIZED_VEHICLE_MINIBUS 0x08CA
+#define BLE_APPEARANCE_MOTORIZED_VEHICLE_BUS 0x08CB
+#define BLE_APPEARANCE_MOTORIZED_VEHICLE_TROLLEY 0x08CC
+#define BLE_APPEARANCE_MOTORIZED_VEHICLE_AGRICULTURAL_VEHICLE 0x08CD
+#define BLE_APPEARANCE_MOTORIZED_VEHICLE_CAMPER_CARAVAN 0x08CE
+#define BLE_APPEARANCE_MOTORIZED_VEHICLE_RECREATIONAL_VEHICLE_MOTOR_HOME 0x08CF
+
+/* Category[15:6] 0x024 */
+#define BLE_APPEARANCE_GENERIC_DOMESTIC_APPLIANCE 0x0900
+#define BLE_APPEARANCE_DOMESTIC_APPLIANCE_REFRIGERATOR 0x0901
+#define BLE_APPEARANCE_DOMESTIC_APPLIANCE_FREEZER 0x0902
+#define BLE_APPEARANCE_DOMESTIC_APPLIANCE_OVEN 0x0903
+#define BLE_APPEARANCE_DOMESTIC_APPLIANCE_MICROWAVE 0x0904
+#define BLE_APPEARANCE_DOMESTIC_APPLIANCE_TOASTER 0x0905
+#define BLE_APPEARANCE_DOMESTIC_APPLIANCE_WASHING_MACHINE 0x0906
+#define BLE_APPEARANCE_DOMESTIC_APPLIANCE_DRYER 0x0907
+#define BLE_APPEARANCE_DOMESTIC_APPLIANCE_COFFEE_MAKER 0x0908
+#define BLE_APPEARANCE_DOMESTIC_APPLIANCE_CLOTHES_IRON 0x0909
+#define BLE_APPEARANCE_DOMESTIC_APPLIANCE_CURLING_IRON 0x090A
+#define BLE_APPEARANCE_DOMESTIC_APPLIANCE_HAIR_DRYER 0x090B
+#define BLE_APPEARANCE_DOMESTIC_APPLIANCE_VACUUM_CLEANER 0x090C
+#define BLE_APPEARANCE_DOMESTIC_APPLIANCE_ROBOTIC_VACUUM_CLEANER 0x090D
+#define BLE_APPEARANCE_DOMESTIC_APPLIANCE_RICE_COOKER 0x090E
+#define BLE_APPEARANCE_DOMESTIC_APPLIANCE_CLOTHES_STEAMER 0x090F
+
+/* Category[15:6] 0x025 */
+#define BLE_APPEARANCE_GENERIC_WEARABLE_AUDIO_DEVICE 0x0940
+#define BLE_APPEARANCE_WEARABLE_AUDIO_DEVICE_EARBUD 0x0941
+#define BLE_APPEARANCE_WEARABLE_AUDIO_DEVICE_HEADSET 0x0942
+#define BLE_APPEARANCE_WEARABLE_AUDIO_DEVICE_HEADPHONES 0x0943
+#define BLE_APPEARANCE_WEARABLE_AUDIO_DEVICE_NECK_BAND 0x0944
+
+/* Category[15:6] 0x026 */
+#define BLE_APPEARANCE_GENERIC_AIRCRAFT 0x0980
+#define BLE_APPEARANCE_AIRCRAFT_LIGHT 0x0981
+#define BLE_APPEARANCE_AIRCRAFT_MICROLIGHT 0x0982
+#define BLE_APPEARANCE_AIRCRAFT_PARAGLIDER 0x0983
+#define BLE_APPEARANCE_AIRCRAFT_LARGE_PASSENGER 0x0984
+
+/* Category[15:6] 0x027 */
+#define BLE_APPEARANCE_GENERIC_AV_EQUIPMENT 0x09C0
+#define BLE_APPEARANCE_AV_EQUIPMENT_AMPLIFIER 0x09C1
+#define BLE_APPEARANCE_AV_EQUIPMENT_RECEIVER 0x09C2
+#define BLE_APPEARANCE_AV_EQUIPMENT_RADIO 0x09C3
+#define BLE_APPEARANCE_AV_EQUIPMENT_TUNER 0x09C4
+#define BLE_APPEARANCE_AV_EQUIPMENT_TURNTABLE 0x09C5
+#define BLE_APPEARANCE_AV_EQUIPMENT_CD_PLAYER 0x09C6
+#define BLE_APPEARANCE_AV_EQUIPMENT_DVD_PLAYER 0x09C7
+#define BLE_APPEARANCE_AV_EQUIPMENT_BLU_RAY_PLAYER 0x09C8
+#define BLE_APPEARANCE_AV_EQUIPMENT_OPTICAL_DISC_PLAYER 0x09C9
+#define BLE_APPEARANCE_AV_EQUIPMENT_SET_TOP_BOX 0x09CA
+
+/* Category[15:6] 0x028 */
+#define BLE_APPEARANCE_GENERIC_DISPLAY_EQUIPMENT 0x0A00
+#define BLE_APPEARANCE_DISPLAY_EQUIPMENT_TELEVISION 0x0A01
+#define BLE_APPEARANCE_DISPLAY_EQUIPMENT_MONITOR 0x0A02
+#define BLE_APPEARANCE_DISPLAY_EQUIPMENT_PROJECTOR 0x0A03
+
+/* Category[15:6] 0x029 */
+#define BLE_APPEARANCE_GENERIC_HEARING_AID 0x0A40
+#define BLE_APPEARANCE_HEARING_AID_IN_EAR 0x0A41
+#define BLE_APPEARANCE_HEARING_AID_BEHIND_EAR 0x0A42
+#define BLE_APPEARANCE_HEARING_AID_COCHLLEAR_IMPLANT 0x0A43
+
+/* Category[15:6] 0x02A */
+#define BLE_APPEARANCE_GENERIC_GAMING 0x0A80
+#define BLE_APPEARANCE_GAMING_HOME_VIDEO_GAME_CONSOLE 0x0A81
+#define BLE_APPEARANCE_GAMING_PORTABLE_HANDHELD_CONSOLE 0x0A82
+
+/* Category[15:6] 0x02B */
+#define BLE_APPEARANCE_GENERIC_SIGNAGE 0x0AC0
+#define BLE_APPEARANCE_SIGNAGE_DIGITAL 0x0AC1
+#define BLE_APPEARANCE_SIGNAGE_ELECTRONIC_LABEL 0x0AC2
+
+/* Category[15:6] 0x031 */
+#define BLE_APPEARANCE_GENERIC_PULSE_OXIMETER 0x0C40
+#define BLE_APPEARANCE_PULSE_OXIMETER_FINGERTIP 0x0C41
+#define BLE_APPEARANCE_PULSE_OXIMETER_WRIST 0x0C42
+
+/* Category[15:6] 0x032 */
+#define BLE_APPEARANCE_GENERIC_WEIGHT 0x0C80
+
+/* Category[15:6] 0x033 */
+#define BLE_APPEARANCE_GENERIC_PERSONAL_MOBILITY_DEVICE 0x0CC0
+#define BLE_APPEARANCE_PERSONAL_MOBILITY_DEVICE_POWERED_WHEELCHAIR 0x0CC1
+#define BLE_APPEARANCE_PERSONAL_MOBILITY_DEVICE_MOBILITY_SCOOTER 0x0CC2
+
+/* Category[15:6] 0x034 */
+#define BLE_APPEARANCE_GENERIC_CONTINUOUS_GLUCOSE_MONITOR 0x0D00
+
+/* Category[15:6] 0x035 */
+#define BLE_APPEARANCE_GENERIC_INSULIN_PUMP 0x0D40
+#define BLE_APPEARANCE_INSULIN_PUMP_DURABLE 0x0D41
+#define BLE_APPEARANCE_INSULIN_PUMP_PATCH 0x0D44
+#define BLE_APPEARANCE_INSULIN_PUMP_PEN 0x0D48
+
+/* Category[15:6] 0x036 */
+#define BLE_APPEARANCE_GENERIC_MEDICATION_DELIVERY 0x0D80
+
+/* Category[15:6] 0x037 */
+#define BLE_APPEARANCE_GENERIC_SPIROMETER 0x0DC0
+#define BLE_APPEARANCE_SPIROMETER_HANDHELD 0x0DC1
+
+/* Category[15:6] 0x051 */
+#define BLE_APPEARANCE_GENERIC_OUTDOOR_SPORTS 0x1440
+#define BLE_APPEARANCE_OUTDOOR_SPORTS_LOCATION 0x1441
+#define BLE_APPEARANCE_OUTDOOR_SPORTS_LOCATION_AND_NAV 0x1442
+#define BLE_APPEARANCE_OUTDOOR_SPORTS_LOCATION_POD 0x1443
+#define BLE_APPEARANCE_OUTDOOR_SPORTS_LOCATION_POD_AND_NAV 0x1444
+
+/* Category[15:6] 0x052 */
+#define BLE_APPEARANCE_GENERIC_INDUSTRIAL_MEASUREMENT_DEVICE 0x1480
+#define BLE_APPEARANCE_INDUSTRIAL_MEASUREMENT_DEVICE_TORQUE_TESTING 0x1481
+#define BLE_APPEARANCE_INDUSTRIAL_MEASUREMENT_DEVICE_CALIPER 0x1482
+#define BLE_APPEARANCE_INDUSTRIAL_MEASUREMENT_DEVICE_DIAL_INDICATOR 0x1483
+#define BLE_APPEARANCE_INDUSTRIAL_MEASUREMENT_DEVICE_MICROMETER 0x1484
+#define BLE_APPEARANCE_INDUSTRIAL_MEASUREMENT_DEVICE_HEIGHT_GAUGE 0x1485
+#define BLE_APPEARANCE_INDUSTRIAL_MEASUREMENT_DEVICE_FORCE_GAUGE 0x1486
+
+/* Category[15:6] 0x053 */
+#define BLE_APPEARANCE_GENERIC_INDUSTRIAL_TOOLS 0x14C0
+#define BLE_APPEARANCE_INDUSTRIAL_TOOLS_MACHINE_TOOL_HOLDER 0x14C1
+#define BLE_APPEARANCE_INDUSTRIAL_TOOLS_GENERIC_CLAMPING_DEVICE 0x14C2
+#define BLE_APPEARANCE_INDUSTRIAL_TOOLS_CLAMPING_JAWS_JAWS_CHUCK 0x14C3
+#define BLE_APPEARANCE_INDUSTRIAL_TOOLS_CLAMPING_COLLET_CHUCK 0x14C4
+#define BLE_APPEARANCE_INDUSTRIAL_TOOLS_CLAMPING_MANDREL 0x14C5
+#define BLE_APPEARANCE_INDUSTRIAL_TOOLS_VISE 0x14C6
+#define BLE_APPEARANCE_INDUSTRIAL_TOOLS_ZERO_POINT_CLAMPING_SYSTEM 0x14C7
+#define BLE_APPEARANCE_INDUSTRIAL_TOOLS_TORQUE_WRENCH 0x14C8
+#define BLE_APPEARANCE_INDUSTRIAL_TOOLS_TORQUE_SCREWDRIVER 0x14C9
+
+#endif // BLE_APPEARANCE_H \ No newline at end of file
diff --git a/system/stack/include/bt_dev_class.h b/system/stack/include/bt_dev_class.h
index 1455f66fd2..ca951532ce 100644
--- a/system/stack/include/bt_dev_class.h
+++ b/system/stack/include/bt_dev_class.h
@@ -26,72 +26,222 @@ typedef std::array<uint8_t, kDevClassLength> DEV_CLASS; /* Device class */
inline constexpr DEV_CLASS kDevClassEmpty = {};
+/***************************
+ * major device class field
+ * Note: All values are deduced by basing BIT_X to BIT_8, values as per
+ * BT-spec assigned-numbers.
+ ***************************/
+#define COD_MAJOR_MISC 0x00
+#define COD_MAJOR_COMPUTER 0x01 // BIT8
+#define COD_MAJOR_PHONE 0x02 // BIT9
+#define COD_MAJOR_LAN_NAP 0x03 // BIT8 | BIT9
+#define COD_MAJOR_AUDIO 0x04 // BIT10
+#define COD_MAJOR_PERIPHERAL 0x05 // BIT8 | BIT10
+#define COD_MAJOR_IMAGING 0x06 // BIT9 | BIT10
+#define COD_MAJOR_WEARABLE 0x07 // BIT8 | BIT9 | BIT10
+#define COD_MAJOR_TOY 0x08 // BIT11
+#define COD_MAJOR_HEALTH 0x09 // BIT8 | BIT11
+#define COD_MAJOR_UNCLASSIFIED 0x1F // BIT8 | BIT9 | BIT10 | BIT11 | BIT12
+
+/***************************
+ * service class fields
+ * Note: All values are deduced by basing BIT_X to BIT_8, values as per
+ * BT-spec assigned-numbers.
+ ***************************/
+#define COD_SERVICE_LMTD_DISCOVER 0x0020 // BIT13 (eg. 13-8 = BIT5 = 0x0020)
+#define COD_SERVICE_LE_AUDIO 0x0040 // BIT14
+#define COD_SERVICE_POSITIONING 0x0100 // BIT16
+#define COD_SERVICE_NETWORKING 0x0200 // BIT17
+#define COD_SERVICE_RENDERING 0x0400 // BIT18
+#define COD_SERVICE_CAPTURING 0x0800 // BIT19
+#define COD_SERVICE_OBJ_TRANSFER 0x1000 // BIT20
+#define COD_SERVICE_AUDIO 0x2000 // BIT21
+#define COD_SERVICE_TELEPHONY 0x4000 // BIT22
+#define COD_SERVICE_INFORMATION 0x8000 // BIT23
+
+/***************************
+ * minor device class field
+ * Note: LSB[1:0] (2 bits) is don't care for minor device class.
+ ***************************/
+/* Minor Device class field - Computer Major Class (COD_MAJOR_COMPUTER) */
+#define COD_MAJOR_COMPUTER_MINOR_UNCATEGORIZED 0x00
+#define COD_MAJOR_COMPUTER_MINOR_DESKTOP_WORKSTATION 0x04 // BIT2
+#define COD_MAJOR_COMPUTER_MINOR_SERVER_CLASS_COMPUTER 0x08 // BIT3
+#define COD_MAJOR_COMPUTER_MINOR_LAPTOP 0x0C // BIT2 | BIT3
+#define COD_MAJOR_COMPUTER_MINOR_HANDHELD_PC_PDA 0x10 // BIT4
+#define COD_MAJOR_COMPUTER_MINOR_PALM_SIZE_PC_PDA 0x14 // BIT2 | BIT4
+#define COD_MAJOR_COMPUTER_MINOR_WEARABLE_COMPUTER_WATCH_SIZE 0x18 // BIT3 | BIT4
+#define COD_MAJOR_COMPUTER_MINOR_TABLET 0x1C // BIT2 | BIT3 | BIT4
+
+/* Minor Device class field - Phone Major Class (COD_MAJOR_PHONE) */
+#define COD_MAJOR_PHONE_MINOR_UNCATEGORIZED 0x00
+#define COD_MAJOR_PHONE_MINOR_CELLULAR 0x04 // BIT2
+#define COD_MAJOR_PHONE_MINOR_CORDLESS 0x08 // BIT3
+#define COD_MAJOR_PHONE_MINOR_SMARTPHONE 0x0C // BIT2 | BIT3
+#define COD_MAJOR_PHONE_MINOR_WIRED_MODEM_OR_VOICE_GATEWAY 0x10 // BIT4
+#define COD_MAJOR_PHONE_MINOR_COMMON_ISDN_ACCESS 0x14 // BIT2 | BIT4
+
+/*
+ * Minor Device class field -
+ * LAN/Network Access Point Major Class (COD_MAJOR_LAN_NAP)
+ */
+#define COD_MAJOR_LAN_NAP_MINOR_FULLY_AVAILABLE 0x00
+#define COD_MAJOR_LAN_NAP_MINOR_1_TO_17_PER_UTILIZED 0x20 // BIT5
+#define COD_MAJOR_LAN_NAP_MINOR_17_TO_33_PER_UTILIZED 0x40 // BIT6
+#define COD_MAJOR_LAN_NAP_MINOR_33_TO_50_PER_UTILIZED 0x60 // BIT5 | BIT6
+#define COD_MAJOR_LAN_NAP_MINOR_50_TO_67_PER_UTILIZED 0x80 // BIT7
+#define COD_MAJOR_LAN_NAP_MINOR_67_TO_83_PER_UTILIZED 0xA0 // BIT5 | BIT7
+#define COD_MAJOR_LAN_NAP_MINOR_83_TO_99_PER_UTILIZED 0xC0 // BIT6 | BIT7
+#define COD_MAJOR_LAN_NAP_MINOR_NO_SERVICE_AVAILABLE 0xE0 // BIT5 | BIT6 | BIT7
+
+/* Minor Device class field - Audio/Video Major Class (COD_MAJOR_AUDIO) */
+/* 0x00 is used as unclassified for all minor device classes */
+#define COD_MINOR_UNCATEGORIZED 0x00
+#define COD_MAJOR_AUDIO_MINOR_WEARABLE_HEADSET 0x04 // BIT2
+#define COD_MAJOR_AUDIO_MINOR_CONFM_HANDSFREE 0x08 // BIT3
+#define COD_MAJOR_AUDIO_MINOR_MICROPHONE 0x10 // BIT4
+#define COD_MAJOR_AUDIO_MINOR_LOUDSPEAKER 0x14 // BIT2 | BIT4
+#define COD_MAJOR_AUDIO_MINOR_HEADPHONES 0x18 // BIT3 | BIT4
+#define COD_MAJOR_AUDIO_MINOR_PORTABLE_AUDIO 0x1C // BIT2 | BIT3 | BIT4
+#define COD_MAJOR_AUDIO_MINOR_CAR_AUDIO 0x20 // BIT5
+#define COD_MAJOR_AUDIO_MINOR_SET_TOP_BOX 0x24 // BIT2 | BIT5
+#define COD_MAJOR_AUDIO_MINOR_HIFI_AUDIO 0x28 // BIT3 | BIT5
+#define COD_MAJOR_AUDIO_MINOR_VCR 0x2C // BIT2 | BIT3 | BIT5
+#define COD_MAJOR_AUDIO_MINOR_VIDEO_CAMERA 0x30 // BIT4 | BIT5
+#define COD_MAJOR_AUDIO_MINOR_CAMCORDER 0x34 // BIT2 | BIT4 | BIT5
+#define COD_MAJOR_AUDIO_MINOR_VIDEO_MONITOR 0x38 // BIT3 | BIT4 | BIT5
+#define COD_MAJOR_AUDIO_MINOR_VIDEO_DISPLAY_AND_LOUDSPEAKER 0x3C // BIT2 |
+ // BIT3 | BIT4 | BIT5
+#define COD_MAJOR_AUDIO_MINOR_VIDEO_CONFERENCING 0x40 // BIT6
+#define COD_MAJOR_AUDIO_MINOR_GAMING_OR_TOY 0x48 // BIT3 | BIT6
+
+/* Minor Device class field - Peripheral Major Class (COD_MAJOR_PERIPHERAL) */
+/* Bits 6-7 independently specify mouse, keyboard, or combo mouse/keyboard */
+#define COD_MAJOR_PERIPH_MINOR_KEYBOARD 0x40 // BIT6
+#define COD_MAJOR_PERIPH_MINOR_POINTING 0x80 // BIT7
+#define COD_MAJOR_PERIPH_MINOR_KEYBOARD_AND_POINTING_DEVICE 0xC0 // BIT6 | BIT7
+
+/* Bits 2-5 OR'd with selection from bits 6-7 */
+#define COD_MAJOR_PERIPH_MINOR_JOYSTICK 0x04 // BIT2
+#define COD_MAJOR_PERIPH_MINOR_GAMEPAD 0x08 // BIT3
+#define COD_MAJOR_PERIPH_MINOR_REMOTE_CONTROL 0x0C // BIT2 | BIT3
+#define COD_MAJOR_PERIPH_MINOR_SENSING_DEVICE 0x10 // BIT4
+#define COD_MAJOR_PERIPH_MINOR_DIGITIZING_TABLET 0x14 // BIT2 | BIT4
+#define COD_MAJOR_PERIPH_MINOR_CARD_READER 0x18 /* e.g. SIM card reader, BIT3 | BIT4 */
+#define COD_MAJOR_PERIPH_MINOR_DIGITAL_PEN 0x1C // Pen, BIT2 | BIT3 | BIT4
+#define COD_MAJOR_PERIPH_MINOR_HANDHELD_SCANNER 0x20 // e.g. Barcode, RFID, BIT5
+#define COD_MAJOR_PERIPH_MINOR_HANDHELD_GESTURAL_INP_DEVICE 0x24
+ // e.g. "wand" form factor, BIT2 | BIT5
+
+/* Minor Device class field - Imaging Major Class (COD_MAJOR_IMAGING)
+ *
+ * Bits 5-7 independently specify display, camera, scanner, or printer
+ * Note: Apart from the set bit, all other bits are don't care.
+ */
+#define COD_MAJOR_IMAGING_MINOR_DISPLAY 0x10 // BIT4
+#define COD_MAJOR_IMAGING_MINOR_CAMERA 0x20 // BIT5
+#define COD_MAJOR_IMAGING_MINOR_SCANNER 0x40 // BIT6
+#define COD_MAJOR_IMAGING_MINOR_PRINTER 0x80 // BIT7
+
+/* Minor Device class field - Wearable Major Class (COD_MAJOR_WEARABLE) */
+#define COD_MAJOR_WEARABLE_MINOR_WRIST_WATCH 0x04 // BIT2
+#define COD_MAJOR_WEARABLE_MINOR_PAGER 0x08 // BIT3
+#define COD_MJAOR_WEARABLE_MINOR_JACKET 0x0C // BIT2 | BIT3
+#define COD_MAJOR_WEARABLE_MINOR_HELMET 0x10 // BIT4
+#define COD_MAJOR_WEARABLE_MINOR_GLASSES 0x14 // BIT2 | BIT4
+#define COD_MAJOR_WEARABLE_MINOR_PIN 0x18
+ // e.g. Label pin, broach, badge BIT3 | BIT4
+
+/* Minor Device class field - Toy Major Class (COD_MAJOR_TOY) */
+#define COD_MAJOR_TOY_MINOR_ROBOT 0x04 // BIT2
+#define COD_MAJOR_TOY_MINOR_VEHICLE 0x08 // BIT3
+#define COD_MAJOR_TOY_MINOR_DOLL_OR_ACTION_FIGURE 0x0C // BIT2 | BIT3
+#define COD_MAJOR_TOY_MINOR_CONTROLLER 0x10 // BIT4
+#define COD_MAJOR_TOY_MINOR_GAME 0x14 // BIT2 | BIT4
+
+/* Minor Device class field - Health Major Class (COD_MAJOR_HEALTH) */
+#define COD_MAJOR_HEALTH_MINOR_BLOOD_MONITOR 0x04 // Blood pressure monitor, BIT2
+#define COD_MAJOR_HEALTH_MINOR_THERMOMETER 0x08 // BIT3
+#define COD_MAJOR_HEALTH_MINOR_WEIGHING_SCALE 0x0C // BIT2 | BIT3
+#define COD_MAJOR_HEALTH_MINOR_GLUCOSE_METER 0x10 // BIT4
+#define COD_MAJOR_HEALTH_MINOR_PULSE_OXIMETER 0x14 // BIT2 | BIT4
+#define COD_MAJOR_HEALTH_MINOR_HEART_PULSE_MONITOR 0x18 // BIT3 | BIT4
+#define COD_MAJOR_HEALTH_MINOR_HEALTH_DATA_DISPLAY 0x1C // BIT2 | BIT3 | BIT4
+#define COD_MAJO_HEALTH_MINOR_STEP_COUNTER 0x20 // BIT5
+#define COD_MAJOR_HEALTH_MINOR_BODY_COMPOSITION_ANALYZER 0x24 // BIT2 | BIT5
+#define COD_MAJOR_HEALTH_MINOR_PEAK_FLOW_MONITOR 0x28 // BIT3 | BIT5
+#define COD_MAJOR_HEALTH_MINOR_MEDICATION_MONITOR 0x2C // BIT2 | BIT3 | BIT5
+#define COD_MAJOR_HEALTH_MINOR_KNEE_PROSTHESIS 0x30 // BIT4 | BIT5
+#define COD_MAJOR_HEALTH_MINOR_ANKLE_PROSTHESIS 0x34 // BIT3 | BIT4 | BIT5
+#define COD_MAJOR_HEALTH_MINOR_GENERIC_HEALTH_MANAGER 0x38 // BIT2 | BIT3 | BIT4 | BIT5
+#define COD_MAJOR_HEALTH_MINOR_PERSONAL_MOBILITY_DEVICE 0x3C // BIT4 | BIT5
+
/* 0x00 is used as unclassified for all minor device classes */
-#define BTM_COD_MINOR_UNCLASSIFIED 0x00
-#define BTM_COD_MINOR_WEARABLE_HEADSET 0x04
-#define BTM_COD_MINOR_CONFM_HANDSFREE 0x08
-#define BTM_COD_MINOR_CAR_AUDIO 0x20
-#define BTM_COD_MINOR_SET_TOP_BOX 0x24
+#define BTM_COD_MINOR_UNCLASSIFIED COD_MINOR_UNCATEGORIZED
+#define BTM_COD_MINOR_WEARABLE_HEADSET COD_MAJOR_AUDIO_MINOR_WEARABLE_HEADSET
+#define BTM_COD_MINOR_CONFM_HANDSFREE COD_MAJOR_AUDIO_MINOR_CONFM_HANDSFREE
+#define BTM_COD_MINOR_CAR_AUDIO COD_MAJOR_AUDIO_MINOR_CAR_AUDIO
+#define BTM_COD_MINOR_SET_TOP_BOX COD_MAJOR_AUDIO_MINOR_SET_TOP_BOX
/* minor device class field for Peripheral Major Class */
/* Bits 6-7 independently specify mouse, keyboard, or combo mouse/keyboard */
-#define BTM_COD_MINOR_KEYBOARD 0x40
-#define BTM_COD_MINOR_POINTING 0x80
+#define BTM_COD_MINOR_KEYBOARD COD_MAJOR_PERIPH_MINOR_KEYBOARD
+#define BTM_COD_MINOR_POINTING COD_MAJOR_PERIPH_MINOR_POINTING
/* Bits 2-5 OR'd with selection from bits 6-7 */
/* #define BTM_COD_MINOR_UNCLASSIFIED 0x00 */
-#define BTM_COD_MINOR_JOYSTICK 0x04
-#define BTM_COD_MINOR_GAMEPAD 0x08
-#define BTM_COD_MINOR_REMOTE_CONTROL 0x0C
-#define BTM_COD_MINOR_DIGITIZING_TABLET 0x14
-#define BTM_COD_MINOR_CARD_READER 0x18 /* e.g. SIM card reader */
-#define BTM_COD_MINOR_DIGITAL_PAN 0x1C
+#define BTM_COD_MINOR_JOYSTICK COD_MAJOR_PERIPH_MINOR_JOYSTICK
+#define BTM_COD_MINOR_GAMEPAD COD_MAJOR_PERIPH_MINOR_GAMEPAD
+#define BTM_COD_MINOR_REMOTE_CONTROL COD_MAJOR_PERIPH_MINOR_REMOTE_CONTROL
+#define BTM_COD_MINOR_DIGITIZING_TABLET COD_MAJOR_PERIPH_MINOR_DIGITIZING_TABLET
+#define BTM_COD_MINOR_CARD_READER COD_MAJOR_PERIPH_MINOR_CARD_READER
+#define BTM_COD_MINOR_DIGITAL_PAN COD_MAJOR_PERIPH_MINOR_DIGITAL_PEN
/* minor device class field for Imaging Major Class */
/* Bits 5-7 independently specify display, camera, scanner, or printer */
-#define BTM_COD_MINOR_DISPLAY 0x10
+#define BTM_COD_MINOR_DISPLAY COD_MAJOR_IMAGING_MINOR_DISPLAY
/* Bits 2-3 Reserved */
/* #define BTM_COD_MINOR_UNCLASSIFIED 0x00 */
/* minor device class field for Wearable Major Class */
/* Bits 2-7 meaningful */
-#define BTM_COD_MINOR_WRIST_WATCH 0x04
-#define BTM_COD_MINOR_GLASSES 0x14
+#define BTM_COD_MINOR_WRIST_WATCH COD_MAJOR_WEARABLE_MINOR_WRIST_WATCH
+#define BTM_COD_MINOR_GLASSES COD_MAJOR_WEARABLE_MINOR_GLASSES
/* minor device class field for Health Major Class */
/* Bits 2-7 meaningful */
-#define BTM_COD_MINOR_BLOOD_MONITOR 0x04
-#define BTM_COD_MINOR_THERMOMETER 0x08
-#define BTM_COD_MINOR_WEIGHING_SCALE 0x0C
-#define BTM_COD_MINOR_GLUCOSE_METER 0x10
-#define BTM_COD_MINOR_PULSE_OXIMETER 0x14
-#define BTM_COD_MINOR_HEART_PULSE_MONITOR 0x18
-#define BTM_COD_MINOR_STEP_COUNTER 0x20
+#define BTM_COD_MINOR_BLOOD_MONITOR COD_MAJOR_HEALTH_MINOR_BLOOD_MONITOR
+#define BTM_COD_MINOR_THERMOMETER COD_MAJOR_HEALTH_MINOR_THERMOMETER
+#define BTM_COD_MINOR_WEIGHING_SCALE COD_MAJOR_HEALTH_MINOR_WEIGHING_SCALE
+#define BTM_COD_MINOR_GLUCOSE_METER COD_MAJOR_HEALTH_MINOR_GLUCOSE_METER
+#define BTM_COD_MINOR_PULSE_OXIMETER COD_MAJOR_HEALTH_MINOR_PULSE_OXIMETER
+#define BTM_COD_MINOR_HEART_PULSE_MONITOR COD_MAJOR_HEALTH_MINOR_HEART_PULSE_MONITOR
+#define BTM_COD_MINOR_STEP_COUNTER COD_MAJO_HEALTH_MINOR_STEP_COUNTER
/***************************
* major device class field
***************************/
-#define BTM_COD_MAJOR_COMPUTER 0x01
-#define BTM_COD_MAJOR_PHONE 0x02
-#define BTM_COD_MAJOR_AUDIO 0x04
-#define BTM_COD_MAJOR_PERIPHERAL 0x05
-#define BTM_COD_MAJOR_IMAGING 0x06
-#define BTM_COD_MAJOR_WEARABLE 0x07
-#define BTM_COD_MAJOR_HEALTH 0x09
-#define BTM_COD_MAJOR_UNCLASSIFIED 0x1F
+#define BTM_COD_MAJOR_COMPUTER COD_MAJOR_COMPUTER
+#define BTM_COD_MAJOR_PHONE COD_MAJOR_PHONE
+#define BTM_COD_MAJOR_AUDIO COD_MAJOR_AUDIO
+#define BTM_COD_MAJOR_PERIPHERAL COD_MAJOR_PERIPHERAL
+#define BTM_COD_MAJOR_IMAGING COD_MAJOR_IMAGING
+#define BTM_COD_MAJOR_WEARABLE COD_MAJOR_WEARABLE
+#define BTM_COD_MAJOR_HEALTH COD_MAJOR_HEALTH
+#define BTM_COD_MAJOR_UNCLASSIFIED COD_MAJOR_UNCLASSIFIED
/***************************
* service class fields
***************************/
-#define BTM_COD_SERVICE_LMTD_DISCOVER 0x0020
-#define BTM_COD_SERVICE_LE_AUDIO 0x0040
-#define BTM_COD_SERVICE_POSITIONING 0x0100
-#define BTM_COD_SERVICE_NETWORKING 0x0200
-#define BTM_COD_SERVICE_RENDERING 0x0400
-#define BTM_COD_SERVICE_CAPTURING 0x0800
-#define BTM_COD_SERVICE_OBJ_TRANSFER 0x1000
-#define BTM_COD_SERVICE_AUDIO 0x2000
-#define BTM_COD_SERVICE_TELEPHONY 0x4000
-#define BTM_COD_SERVICE_INFORMATION 0x8000
+#define BTM_COD_SERVICE_LMTD_DISCOVER COD_SERVICE_LMTD_DISCOVER
+#define BTM_COD_SERVICE_LE_AUDIO COD_SERVICE_LE_AUDIO
+#define BTM_COD_SERVICE_POSITIONING COD_SERVICE_POSITIONING
+#define BTM_COD_SERVICE_NETWORKING COD_SERVICE_NETWORKING
+#define BTM_COD_SERVICE_RENDERING COD_SERVICE_RENDERING
+#define BTM_COD_SERVICE_CAPTURING COD_SERVICE_CAPTURING
+#define BTM_COD_SERVICE_OBJ_TRANSFER COD_SERVICE_OBJ_TRANSFER
+#define BTM_COD_SERVICE_AUDIO COD_SERVICE_AUDIO
+#define BTM_COD_SERVICE_TELEPHONY COD_SERVICE_TELEPHONY
+#define BTM_COD_SERVICE_INFORMATION COD_SERVICE_INFORMATION
/* the COD masks */
#define BTM_COD_MINOR_CLASS_MASK 0xFC
diff --git a/system/stack/include/btm_ble_api_types.h b/system/stack/include/btm_ble_api_types.h
index 9a1679768b..d89028d27f 100644
--- a/system/stack/include/btm_ble_api_types.h
+++ b/system/stack/include/btm_ble_api_types.h
@@ -29,6 +29,7 @@
#include "stack/include/bt_octets.h"
#include "stack/include/btm_status.h"
#include "stack/include/hci_error_code.h"
+#include "stack/include/ble_appearance.h"
#include "types/ble_address_with_type.h"
#include "types/raw_address.h"
@@ -216,60 +217,70 @@ typedef uint8_t BLE_SIGNATURE[BTM_BLE_AUTH_SIGN_LEN]; /* Device address */
#endif
/* Appearance Values Reported with BTM_BLE_AD_TYPE_APPEARANCE */
-#define BTM_BLE_APPEARANCE_UKNOWN 0x0000
-#define BTM_BLE_APPEARANCE_GENERIC_PHONE 0x0040
-#define BTM_BLE_APPEARANCE_GENERIC_COMPUTER 0x0080
-#define BTM_BLE_APPEARANCE_GENERIC_WATCH 0x00C0
-#define BTM_BLE_APPEARANCE_SPORTS_WATCH 0x00C1
-#define BTM_BLE_APPEARANCE_GENERIC_CLOCK 0x0100
-#define BTM_BLE_APPEARANCE_GENERIC_DISPLAY 0x0140
-#define BTM_BLE_APPEARANCE_GENERIC_REMOTE 0x0180
-#define BTM_BLE_APPEARANCE_GENERIC_EYEGLASSES 0x01C0
-#define BTM_BLE_APPEARANCE_GENERIC_TAG 0x0200
-#define BTM_BLE_APPEARANCE_GENERIC_KEYRING 0x0240
-#define BTM_BLE_APPEARANCE_GENERIC_MEDIA_PLAYER 0x0280
-#define BTM_BLE_APPEARANCE_GENERIC_BARCODE_SCANNER 0x02C0
-#define BTM_BLE_APPEARANCE_GENERIC_THERMOMETER 0x0300
-#define BTM_BLE_APPEARANCE_THERMOMETER_EAR 0x0301
-#define BTM_BLE_APPEARANCE_GENERIC_HEART_RATE 0x0340
-#define BTM_BLE_APPEARANCE_HEART_RATE_BELT 0x0341
-#define BTM_BLE_APPEARANCE_GENERIC_BLOOD_PRESSURE 0x0380
-#define BTM_BLE_APPEARANCE_BLOOD_PRESSURE_ARM 0x0381
-#define BTM_BLE_APPEARANCE_BLOOD_PRESSURE_WRIST 0x0382
-#define BTM_BLE_APPEARANCE_GENERIC_HID 0x03C0
-#define BTM_BLE_APPEARANCE_HID_KEYBOARD 0x03C1
-#define BTM_BLE_APPEARANCE_HID_MOUSE 0x03C2
-#define BTM_BLE_APPEARANCE_HID_JOYSTICK 0x03C3
-#define BTM_BLE_APPEARANCE_HID_GAMEPAD 0x03C4
-#define BTM_BLE_APPEARANCE_HID_DIGITIZER_TABLET 0x03C5
-#define BTM_BLE_APPEARANCE_HID_CARD_READER 0x03C6
-#define BTM_BLE_APPEARANCE_HID_DIGITAL_PEN 0x03C7
-#define BTM_BLE_APPEARANCE_HID_BARCODE_SCANNER 0x03C8
-#define BTM_BLE_APPEARANCE_GENERIC_GLUCOSE 0x0400
-#define BTM_BLE_APPEARANCE_GENERIC_WALKING 0x0440
-#define BTM_BLE_APPEARANCE_WALKING_IN_SHOE 0x0441
-#define BTM_BLE_APPEARANCE_WALKING_ON_SHOE 0x0442
-#define BTM_BLE_APPEARANCE_WALKING_ON_HIP 0x0443
-#define BTM_BLE_APPEARANCE_GENERIC_CYCLING 0x0480
-#define BTM_BLE_APPEARANCE_CYCLING_COMPUTER 0x0481
-#define BTM_BLE_APPEARANCE_CYCLING_SPEED 0x0482
-#define BTM_BLE_APPEARANCE_CYCLING_CADENCE 0x0483
-#define BTM_BLE_APPEARANCE_CYCLING_POWER 0x0484
-#define BTM_BLE_APPEARANCE_CYCLING_SPEED_CADENCE 0x0485
-#define BTM_BLE_APPEARANCE_GENERIC_WEARABLE_AUDIO_DEVICE 0x0940
-#define BTM_BLE_APPEARANCE_WEARABLE_AUDIO_DEVICE_EARBUD 0x0941
-#define BTM_BLE_APPEARANCE_WEARABLE_AUDIO_DEVICE_HEADSET 0x0942
-#define BTM_BLE_APPEARANCE_WEARABLE_AUDIO_DEVICE_HEADPHONES 0x0943
-#define BTM_BLE_APPEARANCE_WEARABLE_AUDIO_DEVICE_NECK_BAND 0x0944
-#define BTM_BLE_APPEARANCE_GENERIC_PULSE_OXIMETER 0x0C40
-#define BTM_BLE_APPEARANCE_PULSE_OXIMETER_FINGERTIP 0x0C41
-#define BTM_BLE_APPEARANCE_PULSE_OXIMETER_WRIST 0x0C42
-#define BTM_BLE_APPEARANCE_GENERIC_WEIGHT 0x0C80
-#define BTM_BLE_APPEARANCE_GENERIC_OUTDOOR_SPORTS 0x1440
-#define BTM_BLE_APPEARANCE_OUTDOOR_SPORTS_LOCATION 0x1441
-#define BTM_BLE_APPEARANCE_OUTDOOR_SPORTS_LOCATION_AND_NAV 0x1442
-#define BTM_BLE_APPEARANCE_OUTDOOR_SPORTS_LOCATION_POD 0x1443
-#define BTM_BLE_APPEARANCE_OUTDOOR_SPORTS_LOCATION_POD_AND_NAV 0x1444
+#define BTM_BLE_APPEARANCE_UNKNOWN BLE_APPEARANCE_UNKNOWN
+#define BTM_BLE_APPEARANCE_GENERIC_PHONE BLE_APPEARANCE_GENERIC_PHONE
+#define BTM_BLE_APPEARANCE_GENERIC_COMPUTER BLE_APPEARANCE_GENERIC_COMPUTER
+#define BTM_BLE_APPEARANCE_GENERIC_WATCH BLE_APPEARANCE_GENERIC_WATCH
+#define BTM_BLE_APPEARANCE_SPORTS_WATCH BLE_APPEARANCE_SPORTS_WATCH
+#define BTM_BLE_APPEARANCE_GENERIC_CLOCK BLE_APPEARANCE_GENERIC_CLOCK
+#define BTM_BLE_APPEARANCE_GENERIC_DISPLAY BLE_APPEARANCE_GENERIC_DISPLAY
+#define BTM_BLE_APPEARANCE_GENERIC_REMOTE BLE_APPEARANCE_GENERIC_REMOTE
+#define BTM_BLE_APPEARANCE_GENERIC_EYEGLASSES BLE_APPEARANCE_GENERIC_EYEGLASSES
+#define BTM_BLE_APPEARANCE_GENERIC_TAG BLE_APPEARANCE_GENERIC_TAG
+#define BTM_BLE_APPEARANCE_GENERIC_KEYRING BLE_APPEARANCE_GENERIC_KEYRING
+#define BTM_BLE_APPEARANCE_GENERIC_MEDIA_PLAYER BLE_APPEARANCE_GENERIC_MEDIA_PLAYER
+#define BTM_BLE_APPEARANCE_GENERIC_BARCODE_SCANNER BLE_APPEARANCE_GENERIC_BARCODE_SCANNER
+#define BTM_BLE_APPEARANCE_GENERIC_THERMOMETER BLE_APPEARANCE_GENERIC_THERMOMETER
+#define BTM_BLE_APPEARANCE_THERMOMETER_EAR BLE_APPEARANCE_THERMOMETER_EAR
+#define BTM_BLE_APPEARANCE_GENERIC_HEART_RATE BLE_APPEARANCE_GENERIC_HEART_RATE
+#define BTM_BLE_APPEARANCE_HEART_RATE_BELT BLE_APPEARANCE_HEART_RATE_BELT
+#define BTM_BLE_APPEARANCE_GENERIC_BLOOD_PRESSURE BLE_APPEARANCE_GENERIC_BLOOD_PRESSURE
+#define BTM_BLE_APPEARANCE_BLOOD_PRESSURE_ARM BLE_APPEARANCE_BLOOD_PRESSURE_ARM
+#define BTM_BLE_APPEARANCE_BLOOD_PRESSURE_WRIST BLE_APPEARANCE_BLOOD_PRESSURE_WRIST
+#define BTM_BLE_APPEARANCE_GENERIC_HID BLE_APPEARANCE_GENERIC_HID
+#define BTM_BLE_APPEARANCE_HID_KEYBOARD BLE_APPEARANCE_HID_KEYBOARD
+#define BTM_BLE_APPEARANCE_HID_MOUSE BLE_APPEARANCE_HID_MOUSE
+#define BTM_BLE_APPEARANCE_HID_JOYSTICK BLE_APPEARANCE_HID_JOYSTICK
+#define BTM_BLE_APPEARANCE_HID_GAMEPAD BLE_APPEARANCE_HID_GAMEPAD
+#define BTM_BLE_APPEARANCE_HID_DIGITIZER_TABLET BLE_APPEARANCE_HID_DIGITIZER_TABLET
+#define BTM_BLE_APPEARANCE_HID_CARD_READER BLE_APPEARANCE_HID_CARD_READER
+#define BTM_BLE_APPEARANCE_HID_DIGITAL_PEN BLE_APPEARANCE_HID_DIGITAL_PEN
+#define BTM_BLE_APPEARANCE_HID_BARCODE_SCANNER BLE_APPEARANCE_HID_BARCODE_SCANNER
+#define BTM_BLE_APPEARANCE_GENERIC_GLUCOSE BLE_APPEARANCE_GENERIC_GLUCOSE
+#define BTM_BLE_APPEARANCE_GENERIC_WALKING BLE_APPEARANCE_GENERIC_WALKING
+#define BTM_BLE_APPEARANCE_WALKING_IN_SHOE BLE_APPEARANCE_WALKING_IN_SHOE
+#define BTM_BLE_APPEARANCE_WALKING_ON_SHOE BLE_APPEARANCE_WALKING_ON_SHOE
+#define BTM_BLE_APPEARANCE_WALKING_ON_HIP BLE_APPEARANCE_WALKING_ON_HIP
+#define BTM_BLE_APPEARANCE_GENERIC_CYCLING BLE_APPEARANCE_GENERIC_CYCLING
+#define BTM_BLE_APPEARANCE_CYCLING_COMPUTER BLE_APPEARANCE_CYCLING_COMPUTER
+#define BTM_BLE_APPEARANCE_CYCLING_SPEED BLE_APPEARANCE_CYCLING_SPEED
+#define BTM_BLE_APPEARANCE_CYCLING_CADENCE BLE_APPEARANCE_CYCLING_CADENCE
+#define BTM_BLE_APPEARANCE_CYCLING_POWER BLE_APPEARANCE_CYCLING_POWER
+#define BTM_BLE_APPEARANCE_CYCLING_SPEED_CADENCE BLE_APPEARANCE_CYCLING_SPEED_CADENCE
+#define BTM_BLE_APPEARANCE_GENERIC_WEARABLE_AUDIO_DEVICE \
+ BLE_APPEARANCE_GENERIC_WEARABLE_AUDIO_DEVICE
+#define BTM_BLE_APPEARANCE_WEARABLE_AUDIO_DEVICE_EARBUD \
+ BLE_APPEARANCE_WEARABLE_AUDIO_DEVICE_EARBUD
+#define BTM_BLE_APPEARANCE_WEARABLE_AUDIO_DEVICE_HEADSET \
+ BLE_APPEARANCE_WEARABLE_AUDIO_DEVICE_HEADSET
+#define BTM_BLE_APPEARANCE_WEARABLE_AUDIO_DEVICE_HEADPHONES \
+ BLE_APPEARANCE_WEARABLE_AUDIO_DEVICE_HEADPHONES
+#define BTM_BLE_APPEARANCE_WEARABLE_AUDIO_DEVICE_NECK_BAND \
+ BLE_APPEARANCE_WEARABLE_AUDIO_DEVICE_NECK_BAND
+#define BTM_BLE_APPEARANCE_GENERIC_PULSE_OXIMETER BLE_APPEARANCE_GENERIC_PULSE_OXIMETER
+#define BTM_BLE_APPEARANCE_PULSE_OXIMETER_FINGERTIP BLE_APPEARANCE_PULSE_OXIMETER_FINGERTIP
+#define BTM_BLE_APPEARANCE_PULSE_OXIMETER_WRIST BLE_APPEARANCE_PULSE_OXIMETER_WRIST
+#define BTM_BLE_APPEARANCE_GENERIC_WEIGHT BLE_APPEARANCE_GENERIC_WEIGHT
+#define BTM_BLE_APPEARANCE_GENERIC_OUTDOOR_SPORTS \
+ BLE_APPEARANCE_GENERIC_OUTDOOR_SPORTS
+#define BTM_BLE_APPEARANCE_OUTDOOR_SPORTS_LOCATION \
+ BLE_APPEARANCE_OUTDOOR_SPORTS_LOCATION
+#define BTM_BLE_APPEARANCE_OUTDOOR_SPORTS_LOCATION_AND_NAV \
+ BLE_APPEARANCE_OUTDOOR_SPORTS_LOCATION_AND_NAV
+#define BTM_BLE_APPEARANCE_OUTDOOR_SPORTS_LOCATION_POD \
+ BLE_APPEARANCE_OUTDOOR_SPORTS_LOCATION_POD
+#define BTM_BLE_APPEARANCE_OUTDOOR_SPORTS_LOCATION_POD_AND_NAV \
+ BLE_APPEARANCE_OUTDOOR_SPORTS_LOCATION_POD_AND_NAV
/* Structure returned with Rand/Encrypt complete callback */
typedef struct {
diff --git a/system/stack/include/smp_status.h b/system/stack/include/smp_status.h
index 86670a5161..1d222ab419 100644
--- a/system/stack/include/smp_status.h
+++ b/system/stack/include/smp_status.h
@@ -41,25 +41,27 @@ typedef enum : uint8_t {
SMP_NUMERIC_COMPAR_FAIL = 0x0C,
SMP_BR_PARING_IN_PROGR = 0x0D,
SMP_XTRANS_DERIVE_NOT_ALLOW = 0x0E,
- SMP_MAX_FAIL_RSN_PER_SPEC = SMP_XTRANS_DERIVE_NOT_ALLOW,
+ SMP_KEY_REJECTED = 0x0F,
+ SMP_BUSY = 0x10, /*device is not ready to perform a pairing procedure*/
+ SMP_MAX_FAIL_RSN_PER_SPEC = SMP_BUSY,
/* self defined error code */
- SMP_PAIR_INTERNAL_ERR = (SMP_MAX_FAIL_RSN_PER_SPEC + 0x01), /* 0x0F */
+ SMP_PAIR_INTERNAL_ERR = (SMP_MAX_FAIL_RSN_PER_SPEC + 0x01), /* 0x11 */
/* Unknown IO capability, unable to decide association model */
- SMP_UNKNOWN_IO_CAP = (SMP_MAX_FAIL_RSN_PER_SPEC + 0x02), /* 0x10 */
+ SMP_UNKNOWN_IO_CAP = (SMP_MAX_FAIL_RSN_PER_SPEC + 0x02), /* 0x12 */
- SMP_BUSY = (SMP_MAX_FAIL_RSN_PER_SPEC + 0x05), /* 0x13 */
- SMP_ENC_FAIL = (SMP_MAX_FAIL_RSN_PER_SPEC + 0x06), /* 0x14 */
- SMP_STARTED = (SMP_MAX_FAIL_RSN_PER_SPEC + 0x07), /* 0x15 */
- SMP_RSP_TIMEOUT = (SMP_MAX_FAIL_RSN_PER_SPEC + 0x08), /* 0x16 */
+ SMP_IMPL_BUSY = (SMP_MAX_FAIL_RSN_PER_SPEC + 0x05), /* 0x15 */
+ SMP_ENC_FAIL = (SMP_MAX_FAIL_RSN_PER_SPEC + 0x06), /* 0x16 */
+ SMP_STARTED = (SMP_MAX_FAIL_RSN_PER_SPEC + 0x07), /* 0x17 */
+ SMP_RSP_TIMEOUT = (SMP_MAX_FAIL_RSN_PER_SPEC + 0x08), /* 0x18 */
/* Unspecified failure reason */
- SMP_FAIL = (SMP_MAX_FAIL_RSN_PER_SPEC + 0x0A), /* 0x18 */
+ SMP_FAIL = (SMP_MAX_FAIL_RSN_PER_SPEC + 0x0A), /* 0x1A */
- SMP_CONN_TOUT = (SMP_MAX_FAIL_RSN_PER_SPEC + 0x0B), /* 0x19 */
- SMP_SIRK_DEVICE_INVALID = (SMP_MAX_FAIL_RSN_PER_SPEC + 0x0C), /* 0x1a */
- SMP_USER_CANCELLED = (SMP_MAX_FAIL_RSN_PER_SPEC + 0x0D), /* 0x1b */
+ SMP_CONN_TOUT = (SMP_MAX_FAIL_RSN_PER_SPEC + 0x0B), /* 0x1B */
+ SMP_SIRK_DEVICE_INVALID = (SMP_MAX_FAIL_RSN_PER_SPEC + 0x0C), /* 0x1C */
+ SMP_USER_CANCELLED = (SMP_MAX_FAIL_RSN_PER_SPEC + 0x0D), /* 0x1D */
} tSMP_STATUS;
inline std::string smp_status_text(const tSMP_STATUS& status) {
@@ -79,9 +81,11 @@ inline std::string smp_status_text(const tSMP_STATUS& status) {
CASE_RETURN_TEXT(SMP_NUMERIC_COMPAR_FAIL);
CASE_RETURN_TEXT(SMP_BR_PARING_IN_PROGR);
CASE_RETURN_TEXT(SMP_XTRANS_DERIVE_NOT_ALLOW);
+ CASE_RETURN_TEXT(SMP_KEY_REJECTED);
+ CASE_RETURN_TEXT(SMP_BUSY);
CASE_RETURN_TEXT(SMP_PAIR_INTERNAL_ERR);
CASE_RETURN_TEXT(SMP_UNKNOWN_IO_CAP);
- CASE_RETURN_TEXT(SMP_BUSY);
+ CASE_RETURN_TEXT(SMP_IMPL_BUSY);
CASE_RETURN_TEXT(SMP_ENC_FAIL);
CASE_RETURN_TEXT(SMP_STARTED);
CASE_RETURN_TEXT(SMP_RSP_TIMEOUT);
diff --git a/system/stack/rfcomm/port_rfc.cc b/system/stack/rfcomm/port_rfc.cc
index e7813a240f..14a9cb5bf0 100644
--- a/system/stack/rfcomm/port_rfc.cc
+++ b/system/stack/rfcomm/port_rfc.cc
@@ -1052,7 +1052,7 @@ void port_rfc_closed(tPORT* p_port, uint8_t res) {
p_port->rfc.sm_cb.state = RFC_STATE_CLOSED;
log::info(
- "RFCOMM connection closed, index={}, state={}, reason={}[{}], "
+ "RFCOMM connection closed, port_handle={}, state={}, reason={}[{}], "
"UUID=0x{:x}, bd_addr={}, is_server={}",
p_port->handle, p_port->state, PORT_GetResultString(res), res, p_port->uuid,
p_port->bd_addr, p_port->is_server);
diff --git a/system/stack/rfcomm/rfc_port_fsm.cc b/system/stack/rfcomm/rfc_port_fsm.cc
index 040315d276..be47e22617 100644
--- a/system/stack/rfcomm/rfc_port_fsm.cc
+++ b/system/stack/rfcomm/rfc_port_fsm.cc
@@ -452,12 +452,12 @@ void rfc_port_sm_orig_wait_sec_check(tPORT* p_port, tRFC_PORT_EVENT event, void*
void rfc_port_sm_opened(tPORT* p_port, tRFC_PORT_EVENT event, void* p_data) {
switch (event) {
case RFC_PORT_EVENT_OPEN:
- log::error("RFC_PORT_EVENT_OPEN bd_addr:{} handle:{} dlci:{} scn:{}", p_port->bd_addr,
+ log::error("RFC_PORT_EVENT_OPEN bd_addr:{} port_handle:{} dlci:{} scn:{}", p_port->bd_addr,
p_port->handle, p_port->dlci, p_port->scn);
return;
case RFC_PORT_EVENT_CLOSE:
- log::info("RFC_PORT_EVENT_CLOSE bd_addr:{}, handle:{} dlci:{} scn:{}", p_port->bd_addr,
+ log::info("RFC_PORT_EVENT_CLOSE bd_addr:{}, port_handle:{} dlci:{} scn:{}", p_port->bd_addr,
p_port->handle, p_port->dlci, p_port->scn);
rfc_port_timer_start(p_port, RFC_DISC_TIMEOUT);
rfc_send_disc(p_port->rfc.p_mcb, p_port->dlci);
@@ -466,7 +466,7 @@ void rfc_port_sm_opened(tPORT* p_port, tRFC_PORT_EVENT event, void* p_data) {
return;
case RFC_PORT_EVENT_CLEAR:
- log::warn("RFC_PORT_EVENT_CLEAR bd_addr:{} handle:{} dlci:{} scn:{}", p_port->bd_addr,
+ log::warn("RFC_PORT_EVENT_CLEAR bd_addr:{} port_handle:{} dlci:{} scn:{}", p_port->bd_addr,
p_port->handle, p_port->dlci, p_port->scn);
rfc_port_closed(p_port);
return;
@@ -475,7 +475,7 @@ void rfc_port_sm_opened(tPORT* p_port, tRFC_PORT_EVENT event, void* p_data) {
// Send credits in the frame. Pass them in the layer specific member of the hdr.
// There might be an initial case when we reduced rx_max and credit_rx is still bigger.
// Make sure that we do not send 255
- log::verbose("RFC_PORT_EVENT_DATA bd_addr:{} handle:{} dlci:{} scn:{}", p_port->bd_addr,
+ log::verbose("RFC_PORT_EVENT_DATA bd_addr:{} port_handle:{} dlci:{} scn:{}", p_port->bd_addr,
p_port->handle, p_port->dlci, p_port->scn);
if ((p_port->rfc.p_mcb->flow == PORT_FC_CREDIT) &&
(((BT_HDR*)p_data)->len < p_port->peer_mtu) && (!p_port->rx.user_fc) &&
@@ -490,25 +490,25 @@ void rfc_port_sm_opened(tPORT* p_port, tRFC_PORT_EVENT event, void* p_data) {
return;
case RFC_PORT_EVENT_UA:
- log::verbose("RFC_PORT_EVENT_UA bd_addr:{} handle:{} dlci:{} scn:{}", p_port->bd_addr,
+ log::verbose("RFC_PORT_EVENT_UA bd_addr:{} port_handle:{} dlci:{} scn:{}", p_port->bd_addr,
p_port->handle, p_port->dlci, p_port->scn);
return;
case RFC_PORT_EVENT_SABME:
- log::verbose("RFC_PORT_EVENT_SABME bd_addr:{} handle:{} dlci:{} scn:{}", p_port->bd_addr,
+ log::verbose("RFC_PORT_EVENT_SABME bd_addr:{} port_handle:{} dlci:{} scn:{}", p_port->bd_addr,
p_port->handle, p_port->dlci, p_port->scn);
rfc_send_ua(p_port->rfc.p_mcb, p_port->dlci);
return;
case RFC_PORT_EVENT_DM:
- log::info("RFC_EVENT_DM bd_addr:{} handle:{} dlci:{} scn:{}", p_port->bd_addr, p_port->handle,
- p_port->dlci, p_port->scn);
+ log::info("RFC_EVENT_DM bd_addr:{} port_handle:{} dlci:{} scn:{}", p_port->bd_addr,
+ p_port->handle, p_port->dlci, p_port->scn);
PORT_DlcReleaseInd(p_port->rfc.p_mcb, p_port->dlci);
rfc_port_closed(p_port);
return;
case RFC_PORT_EVENT_DISC:
- log::info("RFC_PORT_EVENT_DISC bd_addr:{} handle:{} dlci:{} scn:{}", p_port->bd_addr,
+ log::info("RFC_PORT_EVENT_DISC bd_addr:{} port_handle:{} dlci:{} scn:{}", p_port->bd_addr,
p_port->handle, p_port->dlci, p_port->scn);
p_port->rfc.sm_cb.state = RFC_STATE_CLOSED;
rfc_send_ua(p_port->rfc.p_mcb, p_port->dlci);
@@ -522,19 +522,19 @@ void rfc_port_sm_opened(tPORT* p_port, tRFC_PORT_EVENT event, void* p_data) {
return;
case RFC_PORT_EVENT_UIH:
- log::verbose("RFC_PORT_EVENT_UIH bd_addr:{}, handle:{} dlci:{} scn:{}", p_port->bd_addr,
+ log::verbose("RFC_PORT_EVENT_UIH bd_addr:{}, port_handle:{} dlci:{} scn:{}", p_port->bd_addr,
p_port->handle, p_port->dlci, p_port->scn);
rfc_port_uplink_data(p_port, (BT_HDR*)p_data);
return;
case RFC_PORT_EVENT_TIMEOUT:
PORT_TimeOutCloseMux(p_port->rfc.p_mcb);
- log::error("RFC_PORT_EVENT_TIMEOUT bd_addr:{} handle:{} dlci:{} scn:{}", p_port->bd_addr,
+ log::error("RFC_PORT_EVENT_TIMEOUT bd_addr:{} port_handle:{} dlci:{} scn:{}", p_port->bd_addr,
p_port->handle, p_port->dlci, p_port->scn);
return;
default:
- log::error("Received unexpected event:{} bd_addr:{} handle:{} dlci:{} scn:{}",
+ log::error("Received unexpected event:{} bd_addr:{} port_handle:{} dlci:{} scn:{}",
rfcomm_port_event_text(event), p_port->bd_addr, p_port->handle, p_port->dlci,
p_port->scn);
break;
@@ -560,7 +560,7 @@ void rfc_port_sm_disc_wait_ua(tPORT* p_port, tRFC_PORT_EVENT event, void* p_data
return;
case RFC_PORT_EVENT_CLEAR:
- log::warn("RFC_PORT_EVENT_CLEAR, handle:{}", p_port->handle);
+ log::warn("RFC_PORT_EVENT_CLEAR, port_handle:{}", p_port->handle);
rfc_port_closed(p_port);
return;
@@ -573,7 +573,7 @@ void rfc_port_sm_disc_wait_ua(tPORT* p_port, tRFC_PORT_EVENT event, void* p_data
FALLTHROUGH_INTENDED; /* FALLTHROUGH */
case RFC_PORT_EVENT_DM:
- log::warn("RFC_EVENT_DM|RFC_EVENT_UA[{}], handle:{}", event, p_port->handle);
+ log::warn("RFC_EVENT_DM|RFC_EVENT_UA[{}], port_handle:{}", event, p_port->handle);
if (com::android::bluetooth::flags::rfcomm_always_disc_initiator_in_disc_wait_ua()) {
// If we got a DM in RFC_STATE_DISC_WAIT_UA, it's likely that both ends
// attempt to DISC at the same time and both get a DM.
@@ -602,7 +602,7 @@ void rfc_port_sm_disc_wait_ua(tPORT* p_port, tRFC_PORT_EVENT event, void* p_data
return;
case RFC_PORT_EVENT_TIMEOUT:
- log::error("RFC_EVENT_TIMEOUT, handle:{}", p_port->handle);
+ log::error("RFC_EVENT_TIMEOUT, port_handle:{}", p_port->handle);
rfc_port_closed(p_port);
return;
default:
diff --git a/system/stack/rfcomm/rfc_port_if.cc b/system/stack/rfcomm/rfc_port_if.cc
index af6906fbb5..4fdd46059d 100644
--- a/system/stack/rfcomm/rfc_port_if.cc
+++ b/system/stack/rfcomm/rfc_port_if.cc
@@ -333,7 +333,12 @@ void RFCOMM_LineStatusReq(tRFC_MCB* p_mcb, uint8_t dlci, uint8_t status) {
*
******************************************************************************/
void RFCOMM_DlcReleaseReq(tRFC_MCB* p_mcb, uint8_t dlci) {
- rfc_port_sm_execute(port_find_mcb_dlci_port(p_mcb, dlci), RFC_PORT_EVENT_CLOSE, nullptr);
+ tPORT* p_port = port_find_mcb_dlci_port(p_mcb, dlci);
+ if (p_port == nullptr) {
+ log::warn("Unable to find DLCI port dlci:{}", dlci);
+ return;
+ }
+ rfc_port_sm_execute(p_port, RFC_PORT_EVENT_CLOSE, nullptr);
}
/*******************************************************************************
diff --git a/system/stack/smp/smp_act.cc b/system/stack/smp/smp_act.cc
index c625217c7c..fd77fabbed 100644
--- a/system/stack/smp/smp_act.cc
+++ b/system/stack/smp/smp_act.cc
@@ -23,6 +23,7 @@
#include <cstring>
+#include "bta/dm/bta_dm_sec_int.h"
#include "btif/include/btif_common.h"
#include "btif/include/core_callbacks.h"
#include "btif/include/stack_manager_t.h"
@@ -34,6 +35,7 @@
#include "stack/btm/btm_ble_sec.h"
#include "stack/btm/btm_dev.h"
#include "stack/btm/btm_sec.h"
+#include "stack/include/acl_api.h"
#include "stack/include/bt_octets.h"
#include "stack/include/bt_types.h"
#include "stack/include/btm_client_interface.h"
@@ -548,6 +550,21 @@ void smp_proc_pair_cmd(tSMP_CB* p_cb, tSMP_INT_DATA* p_data) {
/* erase all keys if it is peripheral proc pairing req */
if (p_dev_rec && (p_cb->role == HCI_ROLE_PERIPHERAL)) {
+ if (com::android::bluetooth::flags::key_missing_ble_peripheral()) {
+ tBTM_SEC_DEV_REC* p_rec = btm_find_dev(p_cb->pairing_bda);
+ /* If we bonded, but not encrypted, it's a key missing - disconnect.
+ * If we are bonded, its key upgrade and ok to continue.
+ * If we are not bonded, its new device pairing and ok.
+ */
+ if (p_rec != NULL && p_rec->sec_rec.is_le_link_key_known() &&
+ !p_rec->sec_rec.is_le_device_encrypted()) {
+ log::warn("bonded unencrypted central wants to pair {}", p_cb->pairing_bda);
+ bta_dm_remote_key_missing(p_cb->pairing_bda);
+ acl_disconnect_from_handle(p_rec->ble_hci_handle, HCI_ERR_AUTH_FAILURE,
+ "bonded unencrypted central wants to pair");
+ return;
+ }
+ }
btm_sec_clear_ble_keys(p_dev_rec);
}
diff --git a/system/stack/smp/smp_api.cc b/system/stack/smp/smp_api.cc
index 0d66b0a064..a46f113aec 100644
--- a/system/stack/smp/smp_api.cc
+++ b/system/stack/smp/smp_api.cc
@@ -89,7 +89,7 @@ tSMP_STATUS SMP_Pair(const RawAddress& bd_addr, tBLE_ADDR_TYPE addr_type) {
if (p_cb->state != SMP_STATE_IDLE || p_cb->flags & SMP_PAIR_FLAGS_WE_STARTED_DD ||
p_cb->smp_over_br) {
/* pending security on going, reject this one */
- return SMP_BUSY;
+ return SMP_IMPL_BUSY;
} else {
p_cb->flags = SMP_PAIR_FLAGS_WE_STARTED_DD;
p_cb->pairing_bda = bd_addr;
@@ -135,7 +135,7 @@ tSMP_STATUS SMP_BR_PairWith(const RawAddress& bd_addr) {
if (p_cb->state != SMP_STATE_IDLE || p_cb->smp_over_br ||
p_cb->flags & SMP_PAIR_FLAGS_WE_STARTED_DD) {
/* pending security on going, reject this one */
- return SMP_BUSY;
+ return SMP_IMPL_BUSY;
}
p_cb->role = HCI_ROLE_CENTRAL;
diff --git a/system/stack/smp/smp_utils.cc b/system/stack/smp/smp_utils.cc
index 81b005a78c..e5aefe3c6a 100644
--- a/system/stack/smp/smp_utils.cc
+++ b/system/stack/smp/smp_utils.cc
@@ -1231,7 +1231,7 @@ void smp_reject_unexpected_pairing_command(const RawAddress& bd_addr) {
p = (uint8_t*)(p_buf + 1) + L2CAP_MIN_OFFSET;
UINT8_TO_STREAM(p, SMP_OPCODE_PAIRING_FAILED);
- UINT8_TO_STREAM(p, SMP_PAIR_NOT_SUPPORT);
+ UINT8_TO_STREAM(p, SMP_BUSY);
p_buf->offset = L2CAP_MIN_OFFSET;
p_buf->len = SMP_PAIR_FAIL_SIZE;
diff --git a/system/stack/test/stack_smp_test.cc b/system/stack/test/stack_smp_test.cc
index 41d61e738b..8f020ae1c5 100644
--- a/system/stack/test/stack_smp_test.cc
+++ b/system/stack/test/stack_smp_test.cc
@@ -375,11 +375,13 @@ TEST(SmpStatusText, smp_status_text) {
std::make_pair(SMP_NUMERIC_COMPAR_FAIL, "SMP_NUMERIC_COMPAR_FAIL"),
std::make_pair(SMP_BR_PARING_IN_PROGR, "SMP_BR_PARING_IN_PROGR"),
std::make_pair(SMP_XTRANS_DERIVE_NOT_ALLOW, "SMP_XTRANS_DERIVE_NOT_ALLOW"),
+ std::make_pair(SMP_KEY_REJECTED, "SMP_KEY_REJECTED"),
+ std::make_pair(SMP_BUSY, "SMP_BUSY"),
std::make_pair(SMP_MAX_FAIL_RSN_PER_SPEC,
- "SMP_XTRANS_DERIVE_NOT_ALLOW"), // NOTE: Dup
+ "SMP_BUSY"), // NOTE: Dup
std::make_pair(SMP_PAIR_INTERNAL_ERR, "SMP_PAIR_INTERNAL_ERR"),
std::make_pair(SMP_UNKNOWN_IO_CAP, "SMP_UNKNOWN_IO_CAP"),
- std::make_pair(SMP_BUSY, "SMP_BUSY"),
+ std::make_pair(SMP_IMPL_BUSY, "SMP_IMPL_BUSY"),
std::make_pair(SMP_ENC_FAIL, "SMP_ENC_FAIL"),
std::make_pair(SMP_STARTED, "SMP_STARTED"),
std::make_pair(SMP_RSP_TIMEOUT, "SMP_RSP_TIMEOUT"),
diff --git a/system/test/headless/property.cc b/system/test/headless/property.cc
index fe6e21f1ed..2b5df50ff2 100644
--- a/system/test/headless/property.cc
+++ b/system/test/headless/property.cc
@@ -99,6 +99,10 @@ std::map<::bt_property_type_t,
return new headless::property::void_t(data, len,
BT_PROPERTY_REMOTE_DEVICE_TIMESTAMP);
}},
+ {BT_PROPERTY_UUIDS_LE,
+ [](const uint8_t* data, const size_t len) -> headless::bt_property_t* {
+ return new headless::property::uuid_t(data, len);
+ }},
};
} // namespace
diff --git a/system/test/headless/property.h b/system/test/headless/property.h
index 6af17a244f..1113495f24 100644
--- a/system/test/headless/property.h
+++ b/system/test/headless/property.h
@@ -52,6 +52,7 @@ inline std::string bt_property_type_text(const ::bt_property_type_t type) {
CASE_RETURN_TEXT(BT_PROPERTY_REMOTE_MODEL_NUM);
CASE_RETURN_TEXT(BT_PROPERTY_REMOTE_DEVICE_TIMESTAMP);
CASE_RETURN_TEXT(BT_PROPERTY_REMOTE_ADDR_TYPE);
+ CASE_RETURN_TEXT(BT_PROPERTY_UUIDS_LE);
CASE_RETURN_TEXT(BT_PROPERTY_RESERVED_0x14);
default:
RETURN_UNKNOWN_TYPE_STRING(::bt_property_type_t, type);
diff --git a/tools/rootcanal/hal/bluetooth_hci.cc b/tools/rootcanal/hal/bluetooth_hci.cc
index b08d59995b..b36c18687d 100644
--- a/tools/rootcanal/hal/bluetooth_hci.cc
+++ b/tools/rootcanal/hal/bluetooth_hci.cc
@@ -53,24 +53,6 @@ bool BtTestConsoleEnabled() {
} // namespace
-class BluetoothDeathRecipient : public hidl_death_recipient {
-public:
- BluetoothDeathRecipient(const sp<IBluetoothHci> hci) : mHci(hci) {}
-
- void serviceDied(uint64_t /* cookie */,
- const wp<::android::hidl::base::V1_0::IBase>& /* who */) override {
- ALOGE("BluetoothDeathRecipient::serviceDied - Bluetooth service died");
- has_died_ = true;
- mHci->close();
- }
- sp<IBluetoothHci> mHci;
- bool getHasDied() const { return has_died_; }
- void setHasDied(bool has_died) { has_died_ = has_died; }
-
-private:
- bool has_died_{false};
-};
-
BluetoothHci::BluetoothHci() : death_recipient_(new BluetoothDeathRecipient(this)) {}
Return<void> BluetoothHci::initialize(const sp<V1_0::IBluetoothHciCallbacks>& cb) {
diff --git a/tools/rootcanal/hal/bluetooth_hci.h b/tools/rootcanal/hal/bluetooth_hci.h
index 95bda38957..95e3e45334 100644
--- a/tools/rootcanal/hal/bluetooth_hci.h
+++ b/tools/rootcanal/hal/bluetooth_hci.h
@@ -18,6 +18,7 @@
#include <android/hardware/bluetooth/1.1/IBluetoothHci.h>
#include <hidl/MQDescriptor.h>
+#include <log/log.h>
#include "hci_packetizer.h"
#include "model/controller/dual_mode_controller.h"
@@ -44,6 +45,25 @@ using android::net::ConnectCallback;
using rootcanal::Device;
using rootcanal::Phy;
+class BluetoothDeathRecipient : public hidl_death_recipient {
+public:
+ BluetoothDeathRecipient(const sp<IBluetoothHci> hci) : mHci(hci) {}
+
+ void serviceDied(uint64_t /* cookie */,
+ const wp<::android::hidl::base::V1_0::IBase>& /* who */) override {
+ ALOGE("BluetoothDeathRecipient::serviceDied - Bluetooth service died");
+ has_died_ = true;
+ mHci->close();
+ }
+
+ sp<IBluetoothHci> mHci;
+ bool getHasDied() const { return has_died_; }
+ void setHasDied(bool has_died) { has_died_ = has_died; }
+
+private:
+ bool has_died_{false};
+};
+
class BluetoothHci : public IBluetoothHci {
public:
BluetoothHci();
diff --git a/tools/rootcanal/model/controller/controller_properties.cc b/tools/rootcanal/model/controller/controller_properties.cc
index 26718a9747..3f6c918bfe 100644
--- a/tools/rootcanal/model/controller/controller_properties.cc
+++ b/tools/rootcanal/model/controller/controller_properties.cc
@@ -1743,6 +1743,40 @@ ControllerProperties::ControllerProperties(rootcanal::configuration::Controller
le_supported_states = 0x3ffffffffff;
break;
+ case ControllerPreset::INTEL_BE200:
+ // Configuration extracted with the helper script controller_info.py
+ supports_csr_vendor_command = true;
+ br_supported = true;
+ le_supported = true;
+ hci_version = bluetooth::hci::HciVersion::V_5_4;
+ hci_subversion = 0x4363;
+ lmp_version = bluetooth::hci::LmpVersion::V_5_4;
+ lmp_subversion = 0x4363;
+ company_identifier = 0x2;
+ supported_commands = std::array<uint8_t, 64>{
+ 0xbf, 0xff, 0xfb, 0x03, 0xcc, 0xff, 0x0f, 0xff, 0xbf, 0xff, 0xfc, 0x1f, 0xf2,
+ 0x0f, 0xe8, 0xfe, 0x3f, 0xf7, 0x8f, 0xff, 0x1c, 0x00, 0x04, 0x00, 0x61, 0xf7,
+ 0xff, 0xff, 0x7f, 0x38, 0x00, 0x00, 0xfe, 0xf0, 0xff, 0xff, 0xff, 0xe3, 0x80,
+ 0x07, 0x00, 0xe8, 0x1f, 0xfc, 0xff, 0xff, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ };
+ lmp_features = std::array<uint64_t, 3>{0x877bffdbfe0ffebf, 0x0, 0x300};
+ acl_data_packet_length = 1021;
+ total_num_acl_data_packets = 4;
+ sco_data_packet_length = 96;
+ total_num_sco_data_packets = 6;
+ num_supported_iac = 2;
+ le_features = 0x80059ff;
+ le_acl_data_packet_length = 251;
+ total_num_le_acl_data_packets = 3;
+ le_filter_accept_list_size = 25;
+ le_resolving_list_size = 25;
+ le_supported_states = 0x3ffffffffff;
+ le_max_advertising_data_length = 160;
+ le_num_supported_advertising_sets = 12;
+ le_periodic_advertiser_list_size = 12;
+ break;
+
default:
break;
}
diff --git a/tools/rootcanal/model/controller/dual_mode_controller.cc b/tools/rootcanal/model/controller/dual_mode_controller.cc
index 15f788b438..c7b5d0bbe2 100644
--- a/tools/rootcanal/model/controller/dual_mode_controller.cc
+++ b/tools/rootcanal/model/controller/dual_mode_controller.cc
@@ -3290,6 +3290,11 @@ void DualModeController::WriteLoopbackMode(CommandView command) {
ErrorCode::SUCCESS));
}
+void DualModeController::IntelDdcConfigWrite(CommandView /*command*/) {
+ send_event_(bluetooth::hci::CommandCompleteBuilder::Create(kNumCommandPackets, OpCode::INTEL_DDC_CONFIG_WRITE,
+ std::vector<uint8_t> { static_cast<uint8_t>(ErrorCode::SUCCESS) }));
+}
+
// Note: the list does not contain all defined opcodes.
// Notable exceptions:
// - Vendor commands
@@ -4274,6 +4279,8 @@ const std::unordered_map<OpCode, DualModeController::CommandHandler>
{OpCode::LE_GET_CONTROLLER_ACTIVITY_ENERGY_INFO,
&DualModeController::LeGetControllerActivityEnergyInfo},
{OpCode::LE_EX_SET_SCAN_PARAMETERS, &DualModeController::LeExSetScanParameters},
- {OpCode::GET_CONTROLLER_DEBUG_INFO, &DualModeController::GetControllerDebugInfo}};
+ {OpCode::GET_CONTROLLER_DEBUG_INFO, &DualModeController::GetControllerDebugInfo},
+ {OpCode::INTEL_DDC_CONFIG_WRITE, &DualModeController::IntelDdcConfigWrite},
+ };
} // namespace rootcanal
diff --git a/tools/rootcanal/model/controller/dual_mode_controller.h b/tools/rootcanal/model/controller/dual_mode_controller.h
index d0394a7bfd..c5574bda85 100644
--- a/tools/rootcanal/model/controller/dual_mode_controller.h
+++ b/tools/rootcanal/model/controller/dual_mode_controller.h
@@ -525,6 +525,7 @@ public:
void LeGetControllerActivityEnergyInfo(CommandView command);
void LeExSetScanParameters(CommandView command);
void GetControllerDebugInfo(CommandView command);
+ void IntelDdcConfigWrite(CommandView command);
// CSR vendor command.
// Implement the command specific to the CSR controller
diff --git a/tools/rootcanal/model/setup/test_command_handler.cc b/tools/rootcanal/model/setup/test_command_handler.cc
index 93315ae16e..59071842a5 100644
--- a/tools/rootcanal/model/setup/test_command_handler.cc
+++ b/tools/rootcanal/model/setup/test_command_handler.cc
@@ -246,6 +246,8 @@ void TestCommandHandler::SetDeviceConfiguration(const vector<std::string>& args)
preset = rootcanal::configuration::ControllerPreset::LAIRD_BL654;
} else if (args[1] == "csr_rck_pts_dongle") {
preset = rootcanal::configuration::ControllerPreset::CSR_RCK_PTS_DONGLE;
+ } else if (args[1] == "intel_be200") {
+ preset = rootcanal::configuration::ControllerPreset::INTEL_BE200;
} else {
response_string_ = "TestCommandHandler 'set_device_configuration' invalid configuration preset";
send_response_(response_string_);
diff --git a/tools/rootcanal/packets/hci_packets.pdl b/tools/rootcanal/packets/hci_packets.pdl
index 23abb93b29..8f3d3c1588 100644
--- a/tools/rootcanal/packets/hci_packets.pdl
+++ b/tools/rootcanal/packets/hci_packets.pdl
@@ -400,6 +400,7 @@ enum OpCode : 16 {
// VENDOR_SPECIFIC
// MSFT_OPCODE_xxxx below is needed for the tests.
MSFT_OPCODE_INTEL = 0xFC1E,
+ INTEL_DDC_CONFIG_WRITE = 0xFC8B,
LE_GET_VENDOR_CAPABILITIES = 0xFD53,
LE_BATCH_SCAN = 0xFD56,
LE_APCF = 0xFD57,
diff --git a/tools/rootcanal/proto/rootcanal/configuration.proto b/tools/rootcanal/proto/rootcanal/configuration.proto
index 0b6db15c89..0beb961291 100644
--- a/tools/rootcanal/proto/rootcanal/configuration.proto
+++ b/tools/rootcanal/proto/rootcanal/configuration.proto
@@ -24,6 +24,8 @@ enum ControllerPreset {
LAIRD_BL654 = 1;
// Official PTS dongle, CSR rck.
CSR_RCK_PTS_DONGLE = 2;
+ // Official PTS dongle, Intel BE200.
+ INTEL_BE200 = 3;
}
message ControllerFeatures {
diff --git a/tools/rootcanal/rust/src/lmp/procedure/mod.rs b/tools/rootcanal/rust/src/lmp/procedure/mod.rs
index 5f93e62da9..d73c5d39e9 100644
--- a/tools/rootcanal/rust/src/lmp/procedure/mod.rs
+++ b/tools/rootcanal/rust/src/lmp/procedure/mod.rs
@@ -66,7 +66,7 @@ pub trait Context {
/// Future for Context::receive_hci_command and Context::receive_lmp_packet
pub struct ReceiveFuture<'a, C: ?Sized, P>(fn(&'a C) -> Poll<P>, &'a C);
-impl<'a, C, O> Future for ReceiveFuture<'a, C, O>
+impl<C, O> Future for ReceiveFuture<'_, C, O>
where
C: Context,
{
@@ -80,7 +80,7 @@ where
/// Future for Context::receive_hci_command and Context::receive_lmp_packet
pub struct SendAcceptedLmpPacketFuture<'a, C: ?Sized>(&'a C, lmp::Opcode);
-impl<'a, C> Future for SendAcceptedLmpPacketFuture<'a, C>
+impl<C> Future for SendAcceptedLmpPacketFuture<'_, C>
where
C: Context,
{
diff --git a/tools/rootcanal/scripts/controller_info.py b/tools/rootcanal/scripts/controller_info.py
index 10a74a0daf..cca4f0970b 100755
--- a/tools/rootcanal/scripts/controller_info.py
+++ b/tools/rootcanal/scripts/controller_info.py
@@ -98,66 +98,74 @@ async def br_edr_properties(host: Host):
page2 = await host.expect_evt(hci.ReadLocalExtendedFeaturesComplete)
print(
- f"lmp_features: {{ 0x{page0.lmp_features:x}, 0x{page1.extended_lmp_features:x}, 0x{page2.extended_lmp_features:x} }}"
+ f"lmp_features = {{ 0x{page0.lmp_features:x}, 0x{page1.extended_lmp_features:x}, 0x{page2.extended_lmp_features:x} }};"
)
await host.send_cmd(hci.ReadBufferSize())
evt = await host.expect_evt(hci.ReadBufferSizeComplete)
- print(f"acl_data_packet_length: {evt.acl_data_packet_length}")
- print(f"total_num_acl_data_packets: {evt.total_num_acl_data_packets}")
- print(f"sco_data_packet_length: {evt.synchronous_data_packet_length}")
- print(f"total_num_sco_data_packets: {evt.total_num_synchronous_data_packets}")
+ print(f"acl_data_packet_length = {evt.acl_data_packet_length};")
+ print(f"total_num_acl_data_packets = {evt.total_num_acl_data_packets};")
+ print(f"sco_data_packet_length = {evt.synchronous_data_packet_length};")
+ print(f"total_num_sco_data_packets = {evt.total_num_synchronous_data_packets};")
await host.send_cmd(hci.ReadNumberOfSupportedIac())
evt = await host.expect_evt(hci.ReadNumberOfSupportedIacComplete)
- print(f"num_supported_iac: {evt.num_support_iac}")
+ print(f"num_supported_iac = {evt.num_support_iac};")
async def le_properties(host: Host):
- await host.send_cmd(hci.LeReadLocalSupportedFeatures())
- evt = await host.expect_evt(hci.LeReadLocalSupportedFeaturesComplete)
+ await host.send_cmd(hci.LeReadLocalSupportedFeaturesPage0())
+ evt = await host.expect_evt(hci.LeReadLocalSupportedFeaturesPage0Complete)
- print(f"le_features: 0x{evt.le_features:x}")
+ print(f"le_features = 0x{evt.le_features:x};")
- await host.send_cmd(hci.LeReadBufferSizeV2())
- evt = await host.expect_evt(hci.LeReadBufferSizeV2Complete)
+ try:
+ await host.send_cmd(hci.LeReadBufferSizeV2())
+ evt = await host.expect_evt(hci.LeReadBufferSizeV2Complete)
+
+ print(f"le_acl_data_packet_length = {evt.le_buffer_size.le_data_packet_length};")
+ print(f"total_num_le_acl_data_packets = {evt.le_buffer_size.total_num_le_packets};")
+ print(f"iso_data_packet_length = {evt.iso_buffer_size.le_data_packet_length};")
+ print(f"total_num_iso_data_packets = {evt.iso_buffer_size.total_num_le_packets};")
+
+ except Exception:
+ await host.send_cmd(hci.LeReadBufferSizeV1())
+ evt = await host.expect_evt(hci.LeReadBufferSizeV1Complete)
- print(f"le_acl_data_packet_length: {evt.le_buffer_size.le_data_packet_length}")
- print(f"total_num_le_acl_data_packets: {evt.le_buffer_size.total_num_le_packets}")
- print(f"iso_data_packet_length: {evt.iso_buffer_size.le_data_packet_length}")
- print(f"total_num_iso_data_packets: {evt.iso_buffer_size.total_num_le_packets}")
+ print(f"le_acl_data_packet_length = {evt.le_buffer_size.le_data_packet_length};")
+ print(f"total_num_le_acl_data_packets = {evt.le_buffer_size.total_num_le_packets};")
await host.send_cmd(hci.LeReadFilterAcceptListSize())
evt = await host.expect_evt(hci.LeReadFilterAcceptListSizeComplete)
- print(f"le_filter_accept_list_size: {evt.filter_accept_list_size}")
+ print(f"le_filter_accept_list_size = {evt.filter_accept_list_size};")
await host.send_cmd(hci.LeReadResolvingListSize())
evt = await host.expect_evt(hci.LeReadResolvingListSizeComplete)
- print(f"le_resolving_list_size: {evt.resolving_list_size}")
+ print(f"le_resolving_list_size = {evt.resolving_list_size};")
await host.send_cmd(hci.LeReadSupportedStates())
evt = await host.expect_evt(hci.LeReadSupportedStatesComplete)
- print(f"le_supported_states: 0x{evt.le_states:x}")
+ print(f"le_supported_states: 0x{evt.le_states:x};")
await host.send_cmd(hci.LeReadMaximumAdvertisingDataLength())
evt = await host.expect_evt(hci.LeReadMaximumAdvertisingDataLengthComplete)
- print(f"le_max_advertising_data_length: {evt.maximum_advertising_data_length}")
+ print(f"le_max_advertising_data_length = {evt.maximum_advertising_data_length};")
await host.send_cmd(hci.LeReadNumberOfSupportedAdvertisingSets())
evt = await host.expect_evt(hci.LeReadNumberOfSupportedAdvertisingSetsComplete)
- print(f"le_num_supported_advertising_sets: {evt.number_supported_advertising_sets}")
+ print(f"le_num_supported_advertising_sets = {evt.number_supported_advertising_sets};")
await host.send_cmd(hci.LeReadPeriodicAdvertiserListSize())
evt = await host.expect_evt(hci.LeReadPeriodicAdvertiserListSizeComplete)
- print(f"le_periodic_advertiser_list_size: {evt.periodic_advertiser_list_size}")
+ print(f"le_periodic_advertiser_list_size = {evt.periodic_advertiser_list_size};")
async def run(tcp_port: int):
@@ -170,16 +178,16 @@ async def run(tcp_port: int):
await host.send_cmd(hci.ReadLocalVersionInformation())
evt = await host.expect_evt(hci.ReadLocalVersionInformationComplete)
- print(f"hci_version: {evt.local_version_information.hci_version}")
- print(f"hci_subversion: 0x{evt.local_version_information.hci_revision:x}")
- print(f"lmp_version: {evt.local_version_information.lmp_version}")
- print(f"lmp_subversion: 0x{evt.local_version_information.lmp_subversion:x}")
- print(f"company_identifier: 0x{evt.local_version_information.manufacturer_name:x}")
+ print(f"hci_version = {evt.local_version_information.hci_version};")
+ print(f"hci_subversion = 0x{evt.local_version_information.hci_revision:x};")
+ print(f"lmp_version = {evt.local_version_information.lmp_version};")
+ print(f"lmp_subversion = 0x{evt.local_version_information.lmp_subversion:x};")
+ print(f"company_identifier = 0x{evt.local_version_information.manufacturer_name:x};")
await host.send_cmd(hci.ReadLocalSupportedCommands())
evt = await host.expect_evt(hci.ReadLocalSupportedCommandsComplete)
- print(f"supported_commands: {{ {', '.join([f'0x{b:x}' for b in evt.supported_commands])} }}")
+ print(f"supported_commands = {{ {', '.join([f'0x{b:02x}' for b in evt.supported_commands])} }};")
try:
await br_edr_properties(host)