diff options
-rw-r--r-- | TEST_MAPPING | 6 | ||||
-rw-r--r-- | android/app/src/com/android/bluetooth/gatt/GattService.java | 4 | ||||
-rw-r--r-- | android/app/src/com/android/bluetooth/le_scan/ScanManager.java | 3 | ||||
-rw-r--r-- | flags/framework.aconfig | 10 | ||||
-rw-r--r-- | flags/gatt.aconfig | 11 | ||||
-rw-r--r-- | framework/tests/bumble/src/android/bluetooth/pairing/PairingTest.java | 669 | ||||
-rw-r--r-- | framework/tests/bumble/src/android/bluetooth/pairing/utils/IntentReceiver.java | 400 | ||||
-rw-r--r-- | system/bta/Android.bp | 89 | ||||
-rw-r--r-- | system/bta/le_audio/client.cc | 19 | ||||
-rw-r--r-- | system/bta/le_audio/le_audio_client_test.cc | 24 | ||||
-rw-r--r-- | system/bta/le_audio/state_machine.cc | 2 | ||||
-rw-r--r-- | system/bta/le_audio/state_machine_test.cc | 6 | ||||
-rw-r--r-- | system/bta/ras/ras_utils_test.cc | 179 | ||||
-rw-r--r-- | system/gd/rust/topshim/le_audio/le_audio_shim.cc | 2 | ||||
-rw-r--r-- | system/gd/rust/topshim/src/profiles/le_audio.rs | 23 | ||||
-rw-r--r-- | system/include/hardware/bt_le_audio.h | 1 |
16 files changed, 1061 insertions, 387 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/src/com/android/bluetooth/gatt/GattService.java b/android/app/src/com/android/bluetooth/gatt/GattService.java index d582f63f85..cc2e759f61 100644 --- a/android/app/src/com/android/bluetooth/gatt/GattService.java +++ b/android/app/src/com/android/bluetooth/gatt/GattService.java @@ -1505,6 +1505,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); + } } /************************************************************************** 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..78f759fb13 100644 --- a/android/app/src/com/android/bluetooth/le_scan/ScanManager.java +++ b/android/app/src/com/android/bluetooth/le_scan/ScanManager.java @@ -208,7 +208,6 @@ public class ScanManager { mRegularScanClients.clear(); mBatchClients.clear(); mSuspendedScanClients.clear(); - mScanNative.cleanup(); if (mActivityManager != null) { try { @@ -225,6 +224,8 @@ public class ScanManager { // Shut down the thread mHandler.removeCallbacksAndMessages(null); + mScanNative.cleanup(); + try { mAdapterService.unregisterReceiver(mLocationReceiver); } catch (IllegalArgumentException e) { 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/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/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/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/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..8d8ca50ab0 100644 --- a/system/bta/le_audio/state_machine.cc +++ b/system/bta/le_audio/state_machine.cc @@ -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_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/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/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, |