Loading flags/Android.bp +1 −0 Original line number Diff line number Diff line Loading @@ -18,6 +18,7 @@ java_aconfig_library { "//cts/tests/tests/bluetooth:__subpackages__", "//packages/modules/Bluetooth/android/app", "//packages/modules/Bluetooth/framework", "//packages/modules/Bluetooth/framework/tests/bumble", "//packages/modules/Bluetooth/framework/tests/util", "//packages/modules/Bluetooth/service:__subpackages__", ], Loading framework/tests/bumble/Android.bp +1 −0 Original line number Diff line number Diff line Loading @@ -25,6 +25,7 @@ android_test_helper_app { "androidx.test.ext.truth", "androidx.test.rules", "bluetooth-test-util-lib", "bluetooth_flags_java_lib", "compatibility-device-util-axt", "flag-junit", "grpc-java-lite", Loading framework/tests/bumble/src/android/bluetooth/pairing/PairingTest.java +358 −10 Original line number Diff line number Diff line Loading @@ -22,8 +22,10 @@ import static androidx.test.espresso.intent.matcher.IntentMatchers.hasExtra; 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 org.mockito.Mockito.verifyNoMoreInteractions; import android.bluetooth.BluetoothAdapter; Loading @@ -36,15 +38,22 @@ import android.content.BroadcastReceiver; 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; import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.util.Log; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.platform.app.InstrumentationRegistry; import com.android.bluetooth.flags.Flags; import com.android.compatibility.common.util.AdoptShellPermissionsRule; 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; Loading @@ -57,30 +66,50 @@ import org.mockito.MockitoAnnotations; import org.mockito.hamcrest.MockitoHamcrest; import java.time.Duration; import java.util.Arrays; import java.util.HashMap; import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; import pandora.GattProto; import pandora.HostProto.AdvertiseRequest; import pandora.HostProto.AdvertiseResponse; import pandora.HostProto.OwnAddressType; import pandora.SecurityProto.LESecurityLevel; import pandora.SecurityProto.PairingEvent; import pandora.SecurityProto.PairingEventAnswer; import pandora.SecurityProto.SecureRequest; import pandora.SecurityProto.SecureResponse; @RunWith(AndroidJUnit4.class) public class PairingTest { private static final String TAG = "PairingTest"; private static final Duration BOND_INTENT_TIMEOUT = Duration.ofSeconds(10); private static final ParcelUuid BATTERY_UUID = ParcelUuid.fromString("0000180F-0000-1000-8000-00805F9B34FB"); private static final Context sTargetContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); private static final BluetoothAdapter sAdapter = sTargetContext.getSystemService(BluetoothManager.class).getAdapter(); @Rule public final AdoptShellPermissionsRule mPermissionRule = new AdoptShellPermissionsRule(); @Rule(order = 0) public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); @Rule(order = 1) public final AdoptShellPermissionsRule mPermissionRule = new AdoptShellPermissionsRule(); @Rule public final PandoraDevice mBumble = new PandoraDevice(); @Rule(order = 2) public final PandoraDevice mBumble = new PandoraDevice(); @Rule @Rule(order = 3) public final EnableBluetoothRule mEnableBluetoothRule = new EnableBluetoothRule(false /* enableTestMode */, true /* toggleBluetooth */); @Mock private BroadcastReceiver mReceiver; private final Map<String, Integer> mActionRegistrationCounts = new HashMap<>(); private InOrder mInOrder = null; private BluetoothDevice mBumbleDevice; Loading @@ -90,12 +119,30 @@ public class PairingTest { @Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); mInOrder = inOrder(mReceiver); IntentFilter filter = new IntentFilter(); filter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED); filter.addAction(BluetoothDevice.ACTION_PAIRING_REQUEST); sTargetContext.registerReceiver(mReceiver, filter); 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); mBumbleDevice = mBumble.getRemoteDevice(); Set<BluetoothDevice> bondedDevices = sAdapter.getBondedDevices(); Loading @@ -111,7 +158,10 @@ public class PairingTest { removeBond(mBumbleDevice); } mBumbleDevice = null; if (getTotalActionRegistrationCounts() > 0) { sTargetContext.unregisterReceiver(mReceiver); mActionRegistrationCounts.clear(); } } /** Loading @@ -129,6 +179,9 @@ public class PairingTest { */ @Test public void testBrEdrPairing_phoneInitiatedBrEdrInquiryOnlyJustWorks() { registerIntentActions( BluetoothDevice.ACTION_BOND_STATE_CHANGED, BluetoothDevice.ACTION_PAIRING_REQUEST); StreamObserver<PairingEventAnswer> pairingEventAnswerObserver = mBumble.security() .withDeadlineAfter(BOND_INTENT_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS) Loading Loading @@ -159,15 +212,219 @@ public class PairingTest { hasExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.BOND_BONDED)); verifyNoMoreInteractions(mReceiver); unregisterIntentActions( BluetoothDevice.ACTION_BOND_STATE_CHANGED, BluetoothDevice.ACTION_PAIRING_REQUEST); } /** * Test if parallel GATT service discovery interrupts cancelling LE pairing * * <p>Prerequisites: * * <ol> * <li>Bumble and Android are not bonded * <li>Bumble has GATT services in addition to GAP and GATT services * </ol> * * <p>Steps: * * <ol> * <li>Bumble is discoverable and connectable over LE * <li>Android connects to Bumble over LE * <li>Android starts GATT service discovery * <li>Bumble initiates pairing * <li>Android does not confirm the pairing immediately * <li>Service discovery completes * <li>Android cancels the pairing * </ol> * * Expectation: Pairing gets cancelled instead of getting timed out */ @Test @RequiresFlagsEnabled(Flags.FLAG_RESET_PAIRING_ONLY_FOR_RELATED_SERVICE_DISCOVERY) public void testCancelBondLe_WithGattServiceDiscovery() { registerIntentActions(BluetoothDevice.ACTION_BOND_STATE_CHANGED); // Outgoing GATT service discovery and incoming LE pairing in parallel StreamObserverSpliterator<SecureResponse> responseObserver = helper_OutgoingGattServiceDiscoveryWithIncomingLePairing(); // Cancel pairing from Android assertThat(mBumbleDevice.cancelBondProcess()).isTrue(); SecureResponse secureResponse = responseObserver.iterator().next(); assertThat(secureResponse.hasPairingFailure()).isTrue(); // Pairing should be cancelled in a moment instead of timing out in 30 // seconds verifyIntentReceived( 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); } /** * Test if parallel GATT service discovery interrupts the LE pairing * * <p>Prerequisites: * * <ol> * <li>Bumble and Android are not bonded * <li>Bumble has GATT services in addition to GAP and GATT services * </ol> * * <p>Steps: * * <ol> * <li>Bumble is discoverable and connectable over LE * <li>Android connects to Bumble over LE * <li>Android starts GATT service discovery * <li>Bumble starts pairing * <li>Service discovery completes * <li>Android does confirms the pairing * <li>Pairing is successful * </ol> * * Expectation: Pairing succeeds */ @Test @RequiresFlagsEnabled(Flags.FLAG_RESET_PAIRING_ONLY_FOR_RELATED_SERVICE_DISCOVERY) public void testBondLe_WithGattServiceDiscovery() { registerIntentActions(BluetoothDevice.ACTION_BOND_STATE_CHANGED); // Outgoing GATT service discovery and incoming LE pairing in parallel StreamObserverSpliterator<SecureResponse> responseObserver = helper_OutgoingGattServiceDiscoveryWithIncomingLePairing(); // Approve pairing from Android assertThat(mBumbleDevice.setPairingConfirmation(true)).isTrue(); SecureResponse secureResponse = responseObserver.iterator().next(); assertThat(secureResponse.hasSuccess()).isTrue(); // Ensure that pairing succeeds verifyIntentReceived( 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); } /* Starts outgoing GATT service discovery and incoming LE pairing in parallel */ private StreamObserverSpliterator<SecureResponse> helper_OutgoingGattServiceDiscoveryWithIncomingLePairing() { // Setup intent filters registerIntentActions( BluetoothDevice.ACTION_BOND_STATE_CHANGED, BluetoothDevice.ACTION_PAIRING_REQUEST, BluetoothDevice.ACTION_UUID, BluetoothDevice.ACTION_ACL_CONNECTED); // Register lots of interesting GATT services on Bumble for (int i = 0; i < 40; i++) { mBumble.gattBlocking() .registerService( GattProto.RegisterServiceRequest.newBuilder() .setService( GattProto.GattServiceParams.newBuilder() .setUuid(BATTERY_UUID.toString()) .build()) .build()); } // Start GATT service discovery, this will establish LE ACL assertThat(mBumbleDevice.fetchUuidsWithSdp(BluetoothDevice.TRANSPORT_LE)).isTrue(); // Make Bumble connectable AdvertiseResponse advertiseResponse = mBumble.hostBlocking() .advertise( AdvertiseRequest.newBuilder() .setLegacy(true) .setConnectable(true) .setOwnAddressType(OwnAddressType.PUBLIC) .build()) .next(); // Todo: Unexpected empty ACTION_UUID intent is generated verifyIntentReceived(hasAction(BluetoothDevice.ACTION_UUID)); // Wait for connection on Android verifyIntentReceived( hasAction(BluetoothDevice.ACTION_ACL_CONNECTED), hasExtra(BluetoothDevice.EXTRA_TRANSPORT, BluetoothDevice.TRANSPORT_LE)); // Start pairing from Bumble StreamObserverSpliterator<SecureResponse> responseObserver = new StreamObserverSpliterator<>(); mBumble.security() .secure( SecureRequest.newBuilder() .setConnection(advertiseResponse.getConnection()) .setLe(LESecurityLevel.LE_LEVEL3) .build(), responseObserver); // Wait for incoming pairing notification on Android // TODO: Order of these events is not deterministic verifyIntentReceivedUnordered( hasAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED), hasExtra(BluetoothDevice.EXTRA_DEVICE, mBumbleDevice), hasExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.BOND_BONDING)); verifyIntentReceivedUnordered( hasAction(BluetoothDevice.ACTION_PAIRING_REQUEST), hasExtra(BluetoothDevice.EXTRA_DEVICE, mBumbleDevice), hasExtra( BluetoothDevice.EXTRA_PAIRING_VARIANT, BluetoothDevice.PAIRING_VARIANT_CONSENT)); // Allow participating in the incoming pairing on Android assertThat(mBumbleDevice.setPairingConfirmation(true)).isTrue(); // Wait for pairing approval notification on Android verifyIntentReceivedUnordered( 2, hasAction(BluetoothDevice.ACTION_PAIRING_REQUEST), hasExtra(BluetoothDevice.EXTRA_DEVICE, mBumbleDevice), hasExtra( BluetoothDevice.EXTRA_PAIRING_VARIANT, BluetoothDevice.PAIRING_VARIANT_CONSENT)); // Wait for GATT service discovery to complete on Android // so that ACTION_UUID is received here. verifyIntentReceived( hasAction(BluetoothDevice.ACTION_UUID), hasExtra( BluetoothDevice.EXTRA_UUID, Matchers.arrayContainingInAnyOrder(BATTERY_UUID))); unregisterIntentActions( BluetoothDevice.ACTION_BOND_STATE_CHANGED, BluetoothDevice.ACTION_PAIRING_REQUEST, BluetoothDevice.ACTION_UUID, BluetoothDevice.ACTION_ACL_CONNECTED); return responseObserver; } private void removeBond(BluetoothDevice device) { registerIntentActions(BluetoothDevice.ACTION_BOND_STATE_CHANGED); assertThat(device.removeBond()).isTrue(); verifyIntentReceived( 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); } @SafeVarargs Loading @@ -175,4 +432,95 @@ public class PairingTest { mInOrder.verify(mReceiver, timeout(BOND_INTENT_TIMEOUT.toMillis())) .onReceive(any(Context.class), MockitoHamcrest.argThat(AllOf.allOf(matchers))); } @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))); } @SafeVarargs private void verifyIntentReceivedUnordered(Matcher<Intent>... matchers) { verifyIntentReceivedUnordered(1, matchers); } /** * 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); } /** * 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); } } /** * 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); } } Loading
flags/Android.bp +1 −0 Original line number Diff line number Diff line Loading @@ -18,6 +18,7 @@ java_aconfig_library { "//cts/tests/tests/bluetooth:__subpackages__", "//packages/modules/Bluetooth/android/app", "//packages/modules/Bluetooth/framework", "//packages/modules/Bluetooth/framework/tests/bumble", "//packages/modules/Bluetooth/framework/tests/util", "//packages/modules/Bluetooth/service:__subpackages__", ], Loading
framework/tests/bumble/Android.bp +1 −0 Original line number Diff line number Diff line Loading @@ -25,6 +25,7 @@ android_test_helper_app { "androidx.test.ext.truth", "androidx.test.rules", "bluetooth-test-util-lib", "bluetooth_flags_java_lib", "compatibility-device-util-axt", "flag-junit", "grpc-java-lite", Loading
framework/tests/bumble/src/android/bluetooth/pairing/PairingTest.java +358 −10 Original line number Diff line number Diff line Loading @@ -22,8 +22,10 @@ import static androidx.test.espresso.intent.matcher.IntentMatchers.hasExtra; 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 org.mockito.Mockito.verifyNoMoreInteractions; import android.bluetooth.BluetoothAdapter; Loading @@ -36,15 +38,22 @@ import android.content.BroadcastReceiver; 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; import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.util.Log; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.platform.app.InstrumentationRegistry; import com.android.bluetooth.flags.Flags; import com.android.compatibility.common.util.AdoptShellPermissionsRule; 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; Loading @@ -57,30 +66,50 @@ import org.mockito.MockitoAnnotations; import org.mockito.hamcrest.MockitoHamcrest; import java.time.Duration; import java.util.Arrays; import java.util.HashMap; import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; import pandora.GattProto; import pandora.HostProto.AdvertiseRequest; import pandora.HostProto.AdvertiseResponse; import pandora.HostProto.OwnAddressType; import pandora.SecurityProto.LESecurityLevel; import pandora.SecurityProto.PairingEvent; import pandora.SecurityProto.PairingEventAnswer; import pandora.SecurityProto.SecureRequest; import pandora.SecurityProto.SecureResponse; @RunWith(AndroidJUnit4.class) public class PairingTest { private static final String TAG = "PairingTest"; private static final Duration BOND_INTENT_TIMEOUT = Duration.ofSeconds(10); private static final ParcelUuid BATTERY_UUID = ParcelUuid.fromString("0000180F-0000-1000-8000-00805F9B34FB"); private static final Context sTargetContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); private static final BluetoothAdapter sAdapter = sTargetContext.getSystemService(BluetoothManager.class).getAdapter(); @Rule public final AdoptShellPermissionsRule mPermissionRule = new AdoptShellPermissionsRule(); @Rule(order = 0) public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); @Rule(order = 1) public final AdoptShellPermissionsRule mPermissionRule = new AdoptShellPermissionsRule(); @Rule public final PandoraDevice mBumble = new PandoraDevice(); @Rule(order = 2) public final PandoraDevice mBumble = new PandoraDevice(); @Rule @Rule(order = 3) public final EnableBluetoothRule mEnableBluetoothRule = new EnableBluetoothRule(false /* enableTestMode */, true /* toggleBluetooth */); @Mock private BroadcastReceiver mReceiver; private final Map<String, Integer> mActionRegistrationCounts = new HashMap<>(); private InOrder mInOrder = null; private BluetoothDevice mBumbleDevice; Loading @@ -90,12 +119,30 @@ public class PairingTest { @Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); mInOrder = inOrder(mReceiver); IntentFilter filter = new IntentFilter(); filter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED); filter.addAction(BluetoothDevice.ACTION_PAIRING_REQUEST); sTargetContext.registerReceiver(mReceiver, filter); 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); mBumbleDevice = mBumble.getRemoteDevice(); Set<BluetoothDevice> bondedDevices = sAdapter.getBondedDevices(); Loading @@ -111,7 +158,10 @@ public class PairingTest { removeBond(mBumbleDevice); } mBumbleDevice = null; if (getTotalActionRegistrationCounts() > 0) { sTargetContext.unregisterReceiver(mReceiver); mActionRegistrationCounts.clear(); } } /** Loading @@ -129,6 +179,9 @@ public class PairingTest { */ @Test public void testBrEdrPairing_phoneInitiatedBrEdrInquiryOnlyJustWorks() { registerIntentActions( BluetoothDevice.ACTION_BOND_STATE_CHANGED, BluetoothDevice.ACTION_PAIRING_REQUEST); StreamObserver<PairingEventAnswer> pairingEventAnswerObserver = mBumble.security() .withDeadlineAfter(BOND_INTENT_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS) Loading Loading @@ -159,15 +212,219 @@ public class PairingTest { hasExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.BOND_BONDED)); verifyNoMoreInteractions(mReceiver); unregisterIntentActions( BluetoothDevice.ACTION_BOND_STATE_CHANGED, BluetoothDevice.ACTION_PAIRING_REQUEST); } /** * Test if parallel GATT service discovery interrupts cancelling LE pairing * * <p>Prerequisites: * * <ol> * <li>Bumble and Android are not bonded * <li>Bumble has GATT services in addition to GAP and GATT services * </ol> * * <p>Steps: * * <ol> * <li>Bumble is discoverable and connectable over LE * <li>Android connects to Bumble over LE * <li>Android starts GATT service discovery * <li>Bumble initiates pairing * <li>Android does not confirm the pairing immediately * <li>Service discovery completes * <li>Android cancels the pairing * </ol> * * Expectation: Pairing gets cancelled instead of getting timed out */ @Test @RequiresFlagsEnabled(Flags.FLAG_RESET_PAIRING_ONLY_FOR_RELATED_SERVICE_DISCOVERY) public void testCancelBondLe_WithGattServiceDiscovery() { registerIntentActions(BluetoothDevice.ACTION_BOND_STATE_CHANGED); // Outgoing GATT service discovery and incoming LE pairing in parallel StreamObserverSpliterator<SecureResponse> responseObserver = helper_OutgoingGattServiceDiscoveryWithIncomingLePairing(); // Cancel pairing from Android assertThat(mBumbleDevice.cancelBondProcess()).isTrue(); SecureResponse secureResponse = responseObserver.iterator().next(); assertThat(secureResponse.hasPairingFailure()).isTrue(); // Pairing should be cancelled in a moment instead of timing out in 30 // seconds verifyIntentReceived( 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); } /** * Test if parallel GATT service discovery interrupts the LE pairing * * <p>Prerequisites: * * <ol> * <li>Bumble and Android are not bonded * <li>Bumble has GATT services in addition to GAP and GATT services * </ol> * * <p>Steps: * * <ol> * <li>Bumble is discoverable and connectable over LE * <li>Android connects to Bumble over LE * <li>Android starts GATT service discovery * <li>Bumble starts pairing * <li>Service discovery completes * <li>Android does confirms the pairing * <li>Pairing is successful * </ol> * * Expectation: Pairing succeeds */ @Test @RequiresFlagsEnabled(Flags.FLAG_RESET_PAIRING_ONLY_FOR_RELATED_SERVICE_DISCOVERY) public void testBondLe_WithGattServiceDiscovery() { registerIntentActions(BluetoothDevice.ACTION_BOND_STATE_CHANGED); // Outgoing GATT service discovery and incoming LE pairing in parallel StreamObserverSpliterator<SecureResponse> responseObserver = helper_OutgoingGattServiceDiscoveryWithIncomingLePairing(); // Approve pairing from Android assertThat(mBumbleDevice.setPairingConfirmation(true)).isTrue(); SecureResponse secureResponse = responseObserver.iterator().next(); assertThat(secureResponse.hasSuccess()).isTrue(); // Ensure that pairing succeeds verifyIntentReceived( 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); } /* Starts outgoing GATT service discovery and incoming LE pairing in parallel */ private StreamObserverSpliterator<SecureResponse> helper_OutgoingGattServiceDiscoveryWithIncomingLePairing() { // Setup intent filters registerIntentActions( BluetoothDevice.ACTION_BOND_STATE_CHANGED, BluetoothDevice.ACTION_PAIRING_REQUEST, BluetoothDevice.ACTION_UUID, BluetoothDevice.ACTION_ACL_CONNECTED); // Register lots of interesting GATT services on Bumble for (int i = 0; i < 40; i++) { mBumble.gattBlocking() .registerService( GattProto.RegisterServiceRequest.newBuilder() .setService( GattProto.GattServiceParams.newBuilder() .setUuid(BATTERY_UUID.toString()) .build()) .build()); } // Start GATT service discovery, this will establish LE ACL assertThat(mBumbleDevice.fetchUuidsWithSdp(BluetoothDevice.TRANSPORT_LE)).isTrue(); // Make Bumble connectable AdvertiseResponse advertiseResponse = mBumble.hostBlocking() .advertise( AdvertiseRequest.newBuilder() .setLegacy(true) .setConnectable(true) .setOwnAddressType(OwnAddressType.PUBLIC) .build()) .next(); // Todo: Unexpected empty ACTION_UUID intent is generated verifyIntentReceived(hasAction(BluetoothDevice.ACTION_UUID)); // Wait for connection on Android verifyIntentReceived( hasAction(BluetoothDevice.ACTION_ACL_CONNECTED), hasExtra(BluetoothDevice.EXTRA_TRANSPORT, BluetoothDevice.TRANSPORT_LE)); // Start pairing from Bumble StreamObserverSpliterator<SecureResponse> responseObserver = new StreamObserverSpliterator<>(); mBumble.security() .secure( SecureRequest.newBuilder() .setConnection(advertiseResponse.getConnection()) .setLe(LESecurityLevel.LE_LEVEL3) .build(), responseObserver); // Wait for incoming pairing notification on Android // TODO: Order of these events is not deterministic verifyIntentReceivedUnordered( hasAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED), hasExtra(BluetoothDevice.EXTRA_DEVICE, mBumbleDevice), hasExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.BOND_BONDING)); verifyIntentReceivedUnordered( hasAction(BluetoothDevice.ACTION_PAIRING_REQUEST), hasExtra(BluetoothDevice.EXTRA_DEVICE, mBumbleDevice), hasExtra( BluetoothDevice.EXTRA_PAIRING_VARIANT, BluetoothDevice.PAIRING_VARIANT_CONSENT)); // Allow participating in the incoming pairing on Android assertThat(mBumbleDevice.setPairingConfirmation(true)).isTrue(); // Wait for pairing approval notification on Android verifyIntentReceivedUnordered( 2, hasAction(BluetoothDevice.ACTION_PAIRING_REQUEST), hasExtra(BluetoothDevice.EXTRA_DEVICE, mBumbleDevice), hasExtra( BluetoothDevice.EXTRA_PAIRING_VARIANT, BluetoothDevice.PAIRING_VARIANT_CONSENT)); // Wait for GATT service discovery to complete on Android // so that ACTION_UUID is received here. verifyIntentReceived( hasAction(BluetoothDevice.ACTION_UUID), hasExtra( BluetoothDevice.EXTRA_UUID, Matchers.arrayContainingInAnyOrder(BATTERY_UUID))); unregisterIntentActions( BluetoothDevice.ACTION_BOND_STATE_CHANGED, BluetoothDevice.ACTION_PAIRING_REQUEST, BluetoothDevice.ACTION_UUID, BluetoothDevice.ACTION_ACL_CONNECTED); return responseObserver; } private void removeBond(BluetoothDevice device) { registerIntentActions(BluetoothDevice.ACTION_BOND_STATE_CHANGED); assertThat(device.removeBond()).isTrue(); verifyIntentReceived( 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); } @SafeVarargs Loading @@ -175,4 +432,95 @@ public class PairingTest { mInOrder.verify(mReceiver, timeout(BOND_INTENT_TIMEOUT.toMillis())) .onReceive(any(Context.class), MockitoHamcrest.argThat(AllOf.allOf(matchers))); } @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))); } @SafeVarargs private void verifyIntentReceivedUnordered(Matcher<Intent>... matchers) { verifyIntentReceivedUnordered(1, matchers); } /** * 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); } /** * 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); } } /** * 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); } }