Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit b8873cc2 authored by Charlie Boutier's avatar Charlie Boutier
Browse files

BumbleBluetoothTests: Add documentation

Test: atest BumbleBluetoothTests
Bug: 303700011
Change-Id: Ic8e4df856b8d8a9a3e52562e4ed136f8c8b24d8c
parent 8a786736
Loading
Loading
Loading
Loading
+37.5 KiB
Loading image diff...
+209 −0
Original line number Diff line number Diff line
# Writing a BumbleBluetoothTests: A Comprehensive Guide

This guide seeks to demystify the process using `testDiscoverDkGattService` as an example.
By the end, you should have a blueprint for constructing similar tests for your Bluetooth
functionalities.

The BumbleBluetoothTests source code can be found in the Android codebase [here][bumble-bluetooth-tests-code].

Pandora APIs are implemented both on Android in a [PandoraServer][pandora-server-code] app and on
[BumblePandoraServer][bumble-github-pandora-server]. The communication between the virtual Android
DUT and the virtual Bumble Reference device is made through [Rootcanal][rootcanal-code], a virtual
Bluetooth Controller.


## Prerequisites

Before diving in, ensure you are acquainted with:
- [Android Junit4][android-junit4] for unit testing
- [Mockito](https://site.mockito.org/) for mocking dependencies
- [Java gRPC documentation][grpc-java-doc]
- [Pandora stable APIs][pandora-stable-apis]
- [Pandora experimental APIs][pandora-experimental-apis]

You must have a running Cuttlefish instance. If not, you can run the following commands from the
root of your Android repository:

```shell
cd $ANDROID_BUILD_TOP
source build/envsetup.sh
lunch aosp_cf_x86_64_phone-userdebug
acloud create # Create a remote instance using the latest know good build image.
acloud create --local-image # OR: Create a remote instance using a local image.
acloud create --local-image --local-instance # OR: Create a local instance using a local image.
```

Note: For Googlers, from an internal Android repository, use the `cf_x86_64_phone-userdebug` target
instead.

## Run existing tests

You can run all the exisiting BumbleBluetoothTests by doing so:
```shell
atest BumbleBluetoothTests
```

If you wish to run a specific test file:
```shell
atest BumbleBluetoothTests:<package_name>.<test_file_name>
atest BumbleBluetoothTests:android.bluetooth.DckTest
```

And to run a specific test from a test file:
```shell
atest BumbleBluetoothTests:<package_name>.<test_file_name>#<test_name>
atest BumbleBluetoothTests:android.bluetooth.DckTest#testDiscoverDkGattService
```

## Crafting the test: Step by Step

### 0. Create the test file

You can either choose to build your new test in Kotlin or in Java. It is totally up to you.
However for this example we will build one in Kotlin.

Let say we are creating a DCK test.

First, create your file under `p/m/Blueooth/frameworks/bumble/src/android/bluetooth` like so:
```shell
cd p/m/Bluetooth/frameworks/bumble/src/android/bluetooth
touch DckTest.kt # We usualy name our test file <test_suite_name>Test.kt/.java
```

Then add the minimum requirements:
```kotlin
import androidx.test.ext.junit.runners.AndroidJUnit4

import org.junit.Rule
import org.junit.Test

@RunWith(AndroidJUnit4::class)
public class DckTest {
  private val TAG = "DckTest"

  // A Rule live from a test setup through it's teardown.
  // Gives shell permissions during the test.
  @Rule @JvmField val mPermissionRule = AdoptShellPermissionsRule()

  // Setup a Bumble Pandora device for the duration of the test.
  // Acting as a Pandora client, it can be interacted with through the Pandora APIs.
  @Rule @JvmField val mBumble = PandoraDevice()

  @Test
  fun testDiscoverDkGattService() {
    // Test implementation
  }
}
```

### 1. Register with Service via gRPC

Here, we're dealing with Bumble's DCK (Digital Car Key) service. First, we need Bumble to register
the Dck Gatt service.

```kotlin
//- `dckBlocking()` is likely a stub accessing the DCK service over gRPC in a synchronous manner.
//- `withDeadline(Deadline.after(TIMEOUT, TimeUnit.MILLISECONDS))` sets a timeout for the call.
//- `register(Empty.getDefaultInstance())` communicates our registration to the server.
mBumble
    .dckBlocking()
    .withDeadline(Deadline.after(TIMEOUT, TimeUnit.MILLISECONDS))
    .register(Empty.getDefaultInstance())
```

### 2. Advertise Bluetooth Capabilities

If our device wants to be discoverable and indicate its capabilities, it would "advertise" these
capabilities. Here, it's done via another gRPC call.

```kotlin
mBumble
    .hostBlocking()
    .advertise(
        AdvertiseRequest.newBuilder()
            .setLegacy(true) // As of now, Bumble only support legacy advertising (b/266124496).
            .setConnectable(true)
            .setOwnAddressType(OwnAddressType.RANDOM) // Ask Bumble to advertise it's `RANDOM` address.
            .build()
    )
```

### 3. Fetch a Known Remote Bluetooth Device

To keep things straightforward, the Bumble RANDOM address is set to a predefined constant.
Typically, an LE scan would be conducted to identify the Bumble device, matching it based on its
Advertising data.

```kotlin
val bumbleDevice =
    bluetoothAdapter.getRemoteLeDevice(
        Utils.BUMBLE_RANDOM_ADDRESS,
        BluetoothDevice.ADDRESS_TYPE_RANDOM // Specify address type as RANDOM because the device advertises with this address type.
    )
```

### 4. Create Mock Callback for GATT Events

Interactions over Bluetooth often involve callback mechanisms. Here, we're mocking the callback
with Mockito to verify later that expected events occurred.
```kotlin
val gattCallback = mock(BluetoothGattCallback::class.java)
```
### 5. Initiate and Verify Connection

To bond with Bumble, we initiate a connection and then verify that the connection is successful.
```kotlin
var bumbleGatt = bumbleDevice.connectGatt(context, false, gattCallback)
verify(gattCallback, timeout(TIMEOUT))
    .onConnectionStateChange(
        any(),
        eq(BluetoothGatt.GATT_SUCCESS),
        eq(BluetoothProfile.STATE_CONNECTED)
    )
```
### 6. Discover and Verify GATT Services

After connecting, we seek to find out the services offered by Bumble and affirm their successful
discovery.

```kotlin
bumbleGatt.discoverServices()
verify(gattCallback, timeout(TIMEOUT))
    .onServicesDiscovered(any(), eq(BluetoothGatt.GATT_SUCCESS))

```

### 7. Confirm Service Availability

Ensure that the specific service (in this example, CCC_DK_UUID) is present on the remote device.

```kotlin
assertThat(bumbleGatt.getService(CCC_DK_UUID)).isNotNull()
```
### 8. Disconnect and Confirm
Finally, after our operations, we disconnect and ensure it is done gracefully.

```kotlin
bumbleGatt.disconnect()
verify(gattCallback, timeout(TIMEOUT))
    .onConnectionStateChange(
        any(),
        eq(BluetoothGatt.GATT_SUCCESS),
        eq(BluetoothProfile.STATE_DISCONNECTED)
    )
```

## Conclusion

This tutorial provided a step-by-step guide on testing some Bluetooth functionalities on top of the
Android Bluetooth frameworks, leveraging both gRPC and Bluetooth GATT interactions. For the detailed
implementation and the full code, refer to our [source code][bumble-bluetooth-tests-code].

[android-junit4]: https://developer.android.com/reference/androidx/test/runner/AndroidJUnit4
[bumble-bluetooth-tests-code]: https://cs.android.com/android/platform/superproject/+/main:packages/modules/Bluetooth/framework/tests/bumble/
[bumble-github-pandora-server]: https://github.com/google/bumble/tree/main/bumble/pandora
[grpc-java-doc]: https://grpc.io/docs/languages/java/
[pandora-experimental-apis]: https://cs.android.com/android/platform/superproject/main/+/main:packages/modules/Bluetooth/pandora/interfaces/pandora_experimental/
[pandora-server-code]: https://cs.android.com/android/platform/superproject/main/+/main:packages/modules/Bluetooth/android/pandora/server/
[pandora-stable-apis]: https://cs.android.com/android/platform/superproject/main/+/main:external/pandora/bt-test-interfaces/
[rootcanal-code]: https://cs.android.com/android/platform/superproject/main/+/main:packages/modules/Bluetooth/tools/rootcanal
 No newline at end of file
+75 −0
Original line number Diff line number Diff line
# BumbleBluetoothTests

Bumble Bluetooth tests are instrumented Android-specific multi-device tests using a reference
peer device implementing the Pandora APIs.

## Architecture

BumbleBluetoothTests is an Android APK that offers enhanced control over Android compared to Avatar
by interacting directly with the Device Under Test (DUT) via Android APIs. Instead of mocking every
API call, it communicates with actual reference devices using gRPC and limits peer device interactions
to the Pandora APIs.

Here is an overview of the BumbleBluetoothTests architecture:
![BumbleBluetoothTests architecture](asset/java-bumble-test-setup.png)

A simple LE connection test looks like this:

```kotlin
// Setup a Bumble Pandora device for the duration of the test.
// Acting as a Pandora client, it can be interacted with through the Pandora APIs.
@Rule @JvmField val mBumble = PandoraDevice()

/**
 * Tests the Bluetooth GATT connection process with a mock callback.
 * This verifies both successful connection and disconnection events for a
 * remote Bluetooth device.
 *
 * @throws Exception if there's an unexpected error during the test execution.
 */
@Test
fun testGattConnect() {
    // 1. Advertise the host's Bluetooth capabilities using another
    //    gRPC call:
    // - `hostBlocking()` accesses another gRPC service related to the host.
    //   The following `advertise(...)` sends an advertise request to the server, setting
    //   specific attributes.
    mBumble
        .hostBlocking()
        .advertise(
            AdvertiseRequest.newBuilder()
                .setLegacy(true)
                .setConnectable(true)
                .setOwnAddressType(OwnAddressType.RANDOM)
                .build()
        )

    // 2. Create a mock callback to handle Bluetooth GATT (Generic Attribute Profile) related events.
    val gattCallback = mock(BluetoothGattCallback::class.java)

    // 3. Fetch a remote Bluetooth device instance (here, Bumble).
    val bumbleDevice =
        bluetoothAdapter.getRemoteLeDevice(
            Utils.BUMBLE_RANDOM_ADDRESS,
            BluetoothDevice.ADDRESS_TYPE_RANDOM // Specify address type as RANDOM because the device advertises with this address type.
        )

    // 4. Connect to the Bumble device and expect a successful connection callback.
    var bumbleGatt = bumbleDevice.connectGatt(context, false, gattCallback)
    verify(gattCallback, timeout(TIMEOUT))
        .onConnectionStateChange(
            any(),
            eq(BluetoothGatt.GATT_SUCCESS),
            eq(BluetoothProfile.STATE_CONNECTED)
        )

    // 5. Disconnect from the Bumble device and expect a successful disconnection callback.
    bumbleGatt.disconnect()
    verify(gattCallback, timeout(TIMEOUT))
        .onConnectionStateChange(
            any(),
            eq(BluetoothGatt.GATT_SUCCESS),
            eq(BluetoothProfile.STATE_DISCONNECTED)
        )
}
```
+51 −3
Original line number Diff line number Diff line
@@ -48,33 +48,77 @@ public class DckTest {
    // CCC DK Specification R3 1.2.0 r14 section 19.2.1.2 Bluetooth Le Pairing
    private val CCC_DK_UUID = UUID.fromString("0000FFF5-0000-1000-8000-00805f9b34fb")

    // A Rule live from a test setup through it's teardown.
    // Gives shell permissions during the test.
    @Rule @JvmField val mPermissionRule = AdoptShellPermissionsRule()

    // Setup a Bumble Pandora device for the duration of the test.
    // Acting as a Pandora client, it can be interacted with through the Pandora APIs.
    @Rule @JvmField val mBumble = PandoraDevice()

    /**
     * Tests the discovery of the Digital Car Key (DCK) GATT service via Bluetooth on an Android
     * device.
     *
     * <p>This test method goes through the following steps:
     * <ul>
     *     <li>1. Register the Dck Gatt service on Bumble over a gRPC call.</li>
     *     <li>2. Advertises the host's (potentially the car's) Bluetooth capabilities through a gRPC call.</li>
     *     <li>3. Fetches a remote LE (Low Energy) Bluetooth device instance.</li>
     *     <li>4. Sets up a mock GATT callback for Bluetooth related events.</li>
     *     <li>5. Connects to the Bumble device and verifies a successful connection.</li>
     *     <li>6. Discovers the GATT services offered by Bumble and checks for a successful service discovery.</li>
     *     <li>7. Validates the presence of the required GATT service (CCC_DK_UUID) on the Bumble device.</li>
     *     <li>8. Disconnects from the Bumble device and ensures a successful disconnection.</li>
     * </ul>
     * </p>
     *
     * @throws AssertionError if any of the assertions (like service discovery or connection checks) fail.
     * @see BluetoothGatt
     * @see BluetoothGattCallback
     */
    @Test
    fun testDiscoverDkGattService() {
        // 1. Register Bumble's DCK (Digital Car Key) service via a gRPC call:
        // - `dckBlocking()` is likely a stub that accesses the DCK service over gRPC in a
        //   blocking/synchronous manner.
        // - `withDeadline(Deadline.after(TIMEOUT, TimeUnit.MILLISECONDS))` sets a timeout for the
        //   gRPC call.
        // - `register(Empty.getDefaultInstance())` sends a registration request to the server.
        mBumble
            .dckBlocking()
            .withDeadline(Deadline.after(TIMEOUT, TimeUnit.MILLISECONDS))
            .register(Empty.getDefaultInstance())

        // 2. Advertise the host's (presumably the car's) Bluetooth capabilities using another
        //    gRPC call:
        // - `hostBlocking()` accesses another gRPC service related to the host.
        //   The following `advertise(...)` sends an advertise request to the server, setting
        //   specific attributes.
        mBumble
            .hostBlocking()
            .advertise(
                AdvertiseRequest.newBuilder()
                    .setLegacy(true)
                    .setLegacy(true) // As of now, Bumble only support legacy advertising (b/266124496).
                    .setConnectable(true)
                    .setOwnAddressType(OwnAddressType.RANDOM)
                    .setOwnAddressType(OwnAddressType.RANDOM) // Ask Bumble to advertise it's `RANDOM` address.
                    .build()
            )

        // 3. Fetch a remote Bluetooth device instance (here, Bumble).
        val bumbleDevice =
            bluetoothAdapter.getRemoteLeDevice(
                // To keep things straightforward, the Bumble RANDOM address is set to a predefined constant.
                // Typically, an LE scan would be conducted to identify the Bumble device, matching it based on its
                // Advertising data.
                Utils.BUMBLE_RANDOM_ADDRESS,
                BluetoothDevice.ADDRESS_TYPE_RANDOM
                BluetoothDevice.ADDRESS_TYPE_RANDOM // Specify address type as RANDOM because the device advertises with this address type.
            )

        // 4. Create a mock callback to handle Bluetooth GATT (Generic Attribute Profile) related events.
        val gattCallback = mock(BluetoothGattCallback::class.java)

        // 5. Connect to the Bumble device and expect a successful connection callback.
        var bumbleGatt = bumbleDevice.connectGatt(context, false, gattCallback)
        verify(gattCallback, timeout(TIMEOUT))
            .onConnectionStateChange(
@@ -83,11 +127,15 @@ public class DckTest {
                eq(BluetoothProfile.STATE_CONNECTED)
            )

        // 6. Discover GATT services offered by Bumble and expect successful service discovery.
        bumbleGatt.discoverServices()
        verify(gattCallback, timeout(TIMEOUT))
            .onServicesDiscovered(any(), eq(BluetoothGatt.GATT_SUCCESS))

        // 7. Check if the required service (CCC_DK_UUID) is available on Bumble.
        assertThat(bumbleGatt.getService(CCC_DK_UUID)).isNotNull()

        // 8. Disconnect from the Bumble device and expect a successful disconnection callback.
        bumbleGatt.disconnect()
        verify(gattCallback, timeout(TIMEOUT))
            .onConnectionStateChange(