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

Commit 78b8efbd authored by Ben's avatar Ben
Browse files

BumbleBluetoothTests: Add scan with PendingIntent tests

Create PendingIntentScanReceiver class that is statically registered in
the manifest to receive scan result broadcasts.

Create startBleScan_withPendingIntentAndStaticReceiverAndCallbackTypeAllMatches
test that scans using the statically registered
PendingIntentScanReceiver.

Create
startBleScan_withPendingIntentAndDynamicReceiverAndCallbackTypeAllMatches
test that scans with a dynamically registered BroadcastReceiver.

Bug: 306062963
Test: atest BumbleBluetoothTests:android.bluetooth.LeScanningTest
Change-Id: Ia34f8e08002bc66680ca95f7fb6428a54617a182
parent e85acfbb
Loading
Loading
Loading
Loading
+6 −0
Original line number Diff line number Diff line
@@ -9,6 +9,12 @@

    <application>
        <uses-library android:name="android.test.runner" />

        <receiver android:name="android.bluetooth.PendingIntentScanReceiver" >
            <intent-filter>
                <action android:name="android.bluetooth.test.ACTION_SCAN_RESULT" />
            </intent-filter>
        </receiver>
    </application>

    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+120 −17
Original line number Diff line number Diff line
@@ -19,11 +19,21 @@ package android.bluetooth;
import static com.google.common.io.BaseEncoding.base16;
import static com.google.common.truth.Truth.assertThat;

import static org.mockito.Mockito.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.verify;

import android.app.PendingIntent;
import android.bluetooth.le.BluetoothLeScanner;
import android.bluetooth.le.ScanCallback;
import android.bluetooth.le.ScanFilter;
import android.bluetooth.le.ScanResult;
import android.bluetooth.le.ScanSettings;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.ParcelUuid;
import android.util.Log;

@@ -35,6 +45,7 @@ import com.android.compatibility.common.util.AdoptShellPermissionsRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;

import java.util.ArrayList;
import java.util.List;
@@ -51,16 +62,27 @@ public class LeScanningTest {
    private static final String TAG = "LeScanningTest";
    private static final int TIMEOUT_SCANNING_MS = 2000;

    @Rule public final AdoptShellPermissionsRule mPermissionRule = new AdoptShellPermissionsRule();
    @Rule
    public final AdoptShellPermissionsRule mPermissionRule = new AdoptShellPermissionsRule();

    @Rule public final PandoraDevice mBumble = new PandoraDevice();
    @Rule
    public final PandoraDevice mBumble = new PandoraDevice();

    private static final String TEST_ADDRESS_RANDOM_STATIC = "F0:43:A8:23:10:11";

    // IRK must match what's defined in bumble_config.json
    private static final byte[] TEST_IRK = base16().decode("1F66F4B5F0C742F807DD0DDBF64E9213");

    private final String TEST_UUID_STRING = "00001805-0000-1000-8000-00805f9b34fb";
    private final Context mContext = ApplicationProvider.getApplicationContext();
    private final BluetoothManager mBluetoothManager =
            mContext.getSystemService(BluetoothManager.class);
    private final BluetoothAdapter mBluetoothAdapter = mBluetoothManager.getAdapter();
    private final BluetoothLeScanner mLeScanner = mBluetoothAdapter.getBluetoothLeScanner();

    private static final String TEST_UUID_STRING = "00001805-0000-1000-8000-00805f9b34fb";

    private static final String ACTION_DYNAMIC_RECEIVER_SCAN_RESULT =
            "android.bluetooth.test.ACTION_DYNAMIC_RECEIVER_SCAN_RESULT";

    @Test
    public void startBleScan_withCallbackTypeAllMatches() {
@@ -72,8 +94,9 @@ public class LeScanningTest {
                        .build();

        List<ScanResult> results =
                startScanning(scanFilter, ScanSettings.CALLBACK_TYPE_ALL_MATCHES).join();
                startScanning(scanFilter, ScanSettings.CALLBACK_TYPE_ALL_MATCHES);

        assertThat(results).isNotNull();
        assertThat(results.get(0).getScanRecord().getServiceUuids().get(0))
                .isEqualTo(ParcelUuid.fromString(TEST_UUID_STRING));
        assertThat(results.get(1).getScanRecord().getServiceUuids().get(0))
@@ -93,23 +116,99 @@ public class LeScanningTest {
                        .build();

        List<ScanResult> results =
                startScanning(scanFilter, ScanSettings.CALLBACK_TYPE_ALL_MATCHES).join();
                startScanning(scanFilter, ScanSettings.CALLBACK_TYPE_ALL_MATCHES);

        assertThat(results).isNotEmpty();
        assertThat(results.get(0).getDevice().getAddress()).isEqualTo(TEST_ADDRESS_RANDOM_STATIC);
    }

    private CompletableFuture<List<ScanResult>> startScanning(
            ScanFilter scanFilter, int callbackType) {
        CompletableFuture<List<ScanResult>> future = new CompletableFuture<>();
        List<ScanResult> scanResults = new ArrayList<>();
    @Test
    public void startBleScan_withPendingIntentAndDynamicReceiverAndCallbackTypeAllMatches() {
        BroadcastReceiver mockReceiver = mock(BroadcastReceiver.class);
        IntentFilter intentFilter = new IntentFilter(ACTION_DYNAMIC_RECEIVER_SCAN_RESULT);
        mContext.registerReceiver(mockReceiver, intentFilter);

        advertiseWithBumble(TEST_UUID_STRING, OwnAddressType.PUBLIC);

        ScanSettings scanSettings =
                new ScanSettings.Builder()
                        .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
                        .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES)
                        .build();

        android.content.Context context = ApplicationProvider.getApplicationContext();
        BluetoothManager bluetoothManager = context.getSystemService(BluetoothManager.class);
        BluetoothAdapter bluetoothAdapter = bluetoothManager.getAdapter();
        ScanFilter scanFilter =
                new ScanFilter.Builder()
                        .setServiceUuid(ParcelUuid.fromString(TEST_UUID_STRING))
                        .build();

        // NOTE: Intent.setClass() must not be called, or else scan results won't be received.
        Intent scanIntent = new Intent(ACTION_DYNAMIC_RECEIVER_SCAN_RESULT);
        PendingIntent pendingIntent =
                PendingIntent.getBroadcast(
                        mContext, 0, scanIntent, PendingIntent.FLAG_CANCEL_CURRENT);

        mLeScanner.startScan(List.of(scanFilter), scanSettings, pendingIntent);

        ArgumentCaptor<Intent> intent = ArgumentCaptor.forClass(Intent.class);
        verify(mockReceiver, timeout(TIMEOUT_SCANNING_MS)).onReceive(any(), intent.capture());

        // Start scanning
        BluetoothLeScanner leScanner = bluetoothAdapter.getBluetoothLeScanner();
        mLeScanner.stopScan(pendingIntent);
        mContext.unregisterReceiver(mockReceiver);

        assertThat(intent.getValue().getAction()).isEqualTo(ACTION_DYNAMIC_RECEIVER_SCAN_RESULT);
        assertThat(intent.getValue().getIntExtra(BluetoothLeScanner.EXTRA_CALLBACK_TYPE, -1))
                .isEqualTo(ScanSettings.CALLBACK_TYPE_ALL_MATCHES);

        List<ScanResult> results =
                intent.getValue()
                        .getParcelableArrayListExtra(
                                BluetoothLeScanner.EXTRA_LIST_SCAN_RESULT,
                                ScanResult.class);
        assertThat(results).isNotEmpty();
        assertThat(results.get(0).getScanRecord().getServiceUuids()).isNotEmpty();
        assertThat(results.get(0).getScanRecord().getServiceUuids().get(0))
                .isEqualTo(ParcelUuid.fromString(TEST_UUID_STRING));
        assertThat(results.get(0).getScanRecord().getServiceUuids())
                .containsExactly(ParcelUuid.fromString(TEST_UUID_STRING));
    }

    @Test
    public void startBleScan_withPendingIntentAndStaticReceiverAndCallbackTypeAllMatches() {
        advertiseWithBumble(TEST_UUID_STRING, OwnAddressType.PUBLIC);

        ScanSettings scanSettings =
                new ScanSettings.Builder()
                        .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
                        .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES)
                        .build();

        ArrayList<ScanFilter> scanFilters = new ArrayList<>();
        ScanFilter scanFilter =
                new ScanFilter.Builder()
                        .setServiceUuid(ParcelUuid.fromString(TEST_UUID_STRING))
                        .build();
        scanFilters.add(scanFilter);

        PendingIntent pendingIntent =
                PendingIntentScanReceiver.newBroadcastPendingIntent(mContext, 0);

        mLeScanner.startScan(scanFilters, scanSettings, pendingIntent);
        List<ScanResult> results =
                PendingIntentScanReceiver.nextScanResult()
                        .completeOnTimeout(null, TIMEOUT_SCANNING_MS, TimeUnit.MILLISECONDS)
                        .join();
        mLeScanner.stopScan(pendingIntent);
        PendingIntentScanReceiver.resetNextScanResultFuture();

        assertThat(results).isNotEmpty();
        assertThat(results.get(0).getScanRecord().getServiceUuids()).isNotEmpty();
        assertThat(results.get(0).getScanRecord().getServiceUuids())
                .containsExactly(ParcelUuid.fromString(TEST_UUID_STRING));
    }

    private List<ScanResult> startScanning(ScanFilter scanFilter, int callbackType) {
        CompletableFuture<List<ScanResult>> future = new CompletableFuture<>();
        List<ScanResult> scanResults = new ArrayList<>();

        ScanSettings scanSettings =
                new ScanSettings.Builder()
@@ -148,10 +247,14 @@ public class LeScanningTest {
                    }
                };

        leScanner.startScan(List.of(scanFilter), scanSettings, scanCallback);
        mLeScanner.startScan(List.of(scanFilter), scanSettings, scanCallback);

        List<ScanResult> result =
                future.completeOnTimeout(null, TIMEOUT_SCANNING_MS, TimeUnit.MILLISECONDS).join();

        mLeScanner.stopScan(scanCallback);

        // Make sure completableFuture object completes with null after some timeout
        return future.completeOnTimeout(null, TIMEOUT_SCANNING_MS, TimeUnit.MILLISECONDS);
        return result;
    }

    private void advertiseWithBumble(String serviceUuid, OwnAddressType addressType) {
+115 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 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;

import android.app.PendingIntent;
import android.bluetooth.le.BluetoothLeScanner;
import android.bluetooth.le.ScanResult;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.util.Log;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;

/**
 * PendingIntentScanReceiver is registered statically in the manifest file as a BroadcastReceiver
 * for the android.bluetooth.ACTION_SCAN_RESULT action. Tests can use nextScanResult() to get a
 * future that completes when scan results are next delivered.
 */
public class PendingIntentScanReceiver extends BroadcastReceiver {
    private static final String TAG = "PendingIntentScanReceiver";

    public static final String ACTION_SCAN_RESULT = "android.bluetooth.test.ACTION_SCAN_RESULT";

    private static Optional<CompletableFuture<List<ScanResult>>> sNextScanResultFuture =
            Optional.empty();

    /**
     * Constructs a new Intent associated with this class.
     *
     * @param context The context the to associate with the Intent.
     * @return The new Intent.
     */
    private static Intent newIntent(Context context) {
        Intent intent = new Intent();
        intent.setAction(PendingIntentScanReceiver.ACTION_SCAN_RESULT);
        intent.setClass(context, PendingIntentScanReceiver.class);
        return intent;
    }

    /**
     * Constructs a new PendingIntent associated with this class.
     *
     * @param context The context to associate the PendingIntent with.
     * @param requestCode The request code to uniquely identify this PendingIntent with.
     * @return
     */
    public static PendingIntent newBroadcastPendingIntent(Context context, int requestCode) {
        return PendingIntent.getBroadcast(
                context, requestCode, newIntent(context), PendingIntent.FLAG_CANCEL_CURRENT);
    }

    /**
     * Use this method for statically registered receivers.
     *
     * @return A future that will complete when the next scan result is received.
     */
    public static CompletableFuture<List<ScanResult>> nextScanResult()
            throws IllegalStateException {
        if (sNextScanResultFuture.isPresent()) {
            throw new IllegalStateException("scan result future already set");
        }
        sNextScanResultFuture = Optional.of(new CompletableFuture<List<ScanResult>>());
        return sNextScanResultFuture.get();
    }

    /** Clears the future waiting for the next static receiver scan result, if any. */
    public static void resetNextScanResultFuture() {
        sNextScanResultFuture = Optional.empty();
    }

    @Override
    public void onReceive(Context context, Intent intent) {
        Log.d(TAG, "onReceive() intent: " + intent);

        if (intent.getAction() != ACTION_SCAN_RESULT) {
            throw new RuntimeException();
        }

        int errorCode = intent.getIntExtra(BluetoothLeScanner.EXTRA_ERROR_CODE, 0);
        if (errorCode != 0) {
            Log.e(TAG, "onReceive() error: " + errorCode);
            throw new RuntimeException("onReceive() unexpected error: " + errorCode);
        }

        List<ScanResult> scanResults =
                intent.getParcelableExtra(
                        BluetoothLeScanner.EXTRA_LIST_SCAN_RESULT,
                        new ArrayList<ScanResult>().getClass());

        if (sNextScanResultFuture.isPresent()) {
            sNextScanResultFuture.get().complete(scanResults);
            sNextScanResultFuture = Optional.empty();
        } else {
            throw new IllegalStateException("scan result received but no future set");
        }
    }
}