blob: cc444a6f42353c523f7554b4af2b3ae0830c72b1 [file] [log] [blame]
/*
* Copyright (C) 2015 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.cts;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothManager;
import android.bluetooth.le.BluetoothLeScanner;
import android.bluetooth.le.ScanCallback;
import android.bluetooth.le.ScanFilter;
import android.bluetooth.le.ScanRecord;
import android.bluetooth.le.ScanResult;
import android.bluetooth.le.ScanSettings;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.ParcelUuid;
import android.os.SystemClock;
import android.test.AndroidTestCase;
import android.test.suitebuilder.annotation.MediumTest;
import android.util.Log;
import android.util.SparseArray;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
* Test cases for Bluetooth LE scans.
* <p>
* To run the test, the device must be placed in an environment that has at least 3 beacons, all
* placed less than 5 meters away from the DUT.
* <p>
* Run 'run cts --class android.bluetooth.cts.BluetoothLeScanTest' in cts-tradefed to run the test
* cases.
*/
public class BluetoothLeScanTest extends AndroidTestCase {
private static final String TAG = "BluetoothLeScanTest";
private static final int SCAN_DURATION_MILLIS = 5000;
private static final int BATCH_SCAN_REPORT_DELAY_MILLIS = 20000;
private BluetoothAdapter mBluetoothAdapter;
private BluetoothLeScanner mScanner;
@Override
public void setUp() {
if (!isBleSupported())
return;
BluetoothManager manager = (BluetoothManager) mContext.getSystemService(
Context.BLUETOOTH_SERVICE);
mBluetoothAdapter = manager.getAdapter();
if (!mBluetoothAdapter.isEnabled()) {
// Note it's not reliable to listen for Adapter.ACTION_STATE_CHANGED broadcast and check
// bluetooth state.
mBluetoothAdapter.enable();
sleep(3000);
}
mScanner = mBluetoothAdapter.getBluetoothLeScanner();
}
/**
* Basic test case for BLE scans. Checks BLE scan timestamp is within correct range.
*/
@MediumTest
public void testBasicBleScan() {
if (!isBleSupported())
return;
long scanStartMillis = SystemClock.elapsedRealtime();
Collection<ScanResult> scanResults = scan();
long scanEndMillis = SystemClock.elapsedRealtime();
assertTrue("Scan results shouldn't be empty", !scanResults.isEmpty());
verifyTimestamp(scanResults, scanStartMillis, scanEndMillis);
}
/**
* Test of scan filters. Ensures only beacons matching certain type of scan filters were
* reported.
*/
@MediumTest
public void testScanFilter() {
if (!isBleSupported())
return;
List<ScanFilter> filters = new ArrayList<ScanFilter>();
ScanFilter filter = createScanFilter();
if (filter == null) {
Log.d(TAG, "no appropriate filter can be set");
return;
}
filters.add(filter);
BleScanCallback filterLeScanCallback = new BleScanCallback();
ScanSettings settings = new ScanSettings.Builder().setScanMode(
ScanSettings.SCAN_MODE_LOW_LATENCY).build();
mScanner.startScan(filters, settings, filterLeScanCallback);
sleep(SCAN_DURATION_MILLIS);
mScanner.stopScan(filterLeScanCallback);
sleep(1000);
Collection<ScanResult> scanResults = filterLeScanCallback.getScanResults();
for (ScanResult result : scanResults) {
assertTrue(filter.matches(result));
}
}
// Create a scan filter based on the nearby beacon with highest signal strength.
private ScanFilter createScanFilter() {
// Get a list of nearby beacons.
List<ScanResult> scanResults = new ArrayList<ScanResult>(scan());
assertTrue("Scan results shouldn't be empty", !scanResults.isEmpty());
// Find the beacon with strongest signal strength, which is the target device for filter
// scan.
Collections.sort(scanResults, new RssiComparator());
ScanResult result = scanResults.get(0);
ScanRecord record = result.getScanRecord();
if (record == null) {
return null;
}
Map<ParcelUuid, byte[]> serviceData = record.getServiceData();
if (serviceData != null && !serviceData.isEmpty()) {
ParcelUuid uuid = serviceData.keySet().iterator().next();
return new ScanFilter.Builder().setServiceData(uuid, new byte[] { 0 },
new byte[] { 0 }).build();
}
SparseArray<byte[]> manufacturerSpecificData = record.getManufacturerSpecificData();
if (manufacturerSpecificData != null && manufacturerSpecificData.size() > 0) {
return new ScanFilter.Builder().setManufacturerData(manufacturerSpecificData.keyAt(0),
new byte[] { 0 }, new byte[] { 0 }).build();
}
List<ParcelUuid> serviceUuids = record.getServiceUuids();
if (serviceUuids != null && !serviceUuids.isEmpty()) {
return new ScanFilter.Builder().setServiceUuid(serviceUuids.get(0)).build();
}
return null;
}
/**
* Test of opportunistic BLE scans.
*/
@MediumTest
public void testOpportunisticScan() {
if (!isBleSupported())
return;
ScanSettings opportunisticScanSettings = new ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_OPPORTUNISTIC)
.build();
BleScanCallback emptyScanCallback = new BleScanCallback();
// No scans are really started with opportunistic scans only.
mScanner.startScan(Collections.<ScanFilter> emptyList(), opportunisticScanSettings,
emptyScanCallback);
sleep(SCAN_DURATION_MILLIS);
assertTrue(emptyScanCallback.getScanResults().isEmpty());
BleScanCallback regularScanCallback = new BleScanCallback();
ScanSettings regularScanSettings = new ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY).build();
List<ScanFilter> filters = new ArrayList<>();
ScanFilter filter = createScanFilter();
if (filter != null) {
filters.add(filter);
} else {
Log.d(TAG, "no appropriate filter can be set");
}
mScanner.startScan(filters, regularScanSettings, regularScanCallback);
sleep(SCAN_DURATION_MILLIS);
// With normal BLE scan client, opportunistic scan client will get scan results.
assertTrue("opportunistic scan results shouldn't be empty",
!emptyScanCallback.getScanResults().isEmpty());
// No more scan results for opportunistic scan clients once the normal BLE scan clients
// stops.
mScanner.stopScan(regularScanCallback);
// In case we got scan results before scan was completely stopped.
sleep(1000);
emptyScanCallback.clear();
sleep(SCAN_DURATION_MILLIS);
assertTrue("opportunistic scan shouldn't have scan results",
emptyScanCallback.getScanResults().isEmpty());
}
/**
* Test case for BLE Batch scan.
*/
@MediumTest
public void testBatchScan() {
if (!isBleSupported() || !isBleBatchScanSupported()) {
Log.d(TAG, "BLE or BLE batching not suppported");
return;
}
ScanSettings batchScanSettings = new ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
.setReportDelay(BATCH_SCAN_REPORT_DELAY_MILLIS).build();
BleScanCallback batchScanCallback = new BleScanCallback();
mScanner.startScan(Collections.<ScanFilter> emptyList(), batchScanSettings,
batchScanCallback);
sleep(SCAN_DURATION_MILLIS);
mScanner.flushPendingScanResults(batchScanCallback);
sleep(1000);
List<ScanResult> results = batchScanCallback.getBatchScanResults();
assertTrue(!results.isEmpty());
long scanEndMillis = SystemClock.elapsedRealtime();
mScanner.stopScan(batchScanCallback);
verifyTimestamp(results, 0, scanEndMillis);
}
// Verify timestamp of all scan results are within [scanStartMillis, scanEndMillis].
private void verifyTimestamp(Collection<ScanResult> results, long scanStartMillis,
long scanEndMillis) {
for (ScanResult result : results) {
long timestampMillis = TimeUnit.NANOSECONDS.toMillis(result.getTimestampNanos());
assertTrue("Invalid timestamp: " + timestampMillis + " should be >= " + scanStartMillis,
timestampMillis >= scanStartMillis);
assertTrue("Invalid timestamp: " + timestampMillis + " should be <= " + scanEndMillis,
timestampMillis <= scanEndMillis);
}
}
// Helper class for BLE scan callback.
private class BleScanCallback extends ScanCallback {
private Set<ScanResult> mResults = new HashSet<ScanResult>();
private List<ScanResult> mBatchScanResults = new ArrayList<ScanResult>();
@Override
public void onScanResult(int callbackType, ScanResult result) {
if (callbackType == ScanSettings.CALLBACK_TYPE_ALL_MATCHES) {
mResults.add(result);
}
}
@Override
public void onBatchScanResults(List<ScanResult> results) {
// In case onBatchScanResults are called due to buffer full, we want to collect all
// scan results.
mBatchScanResults.addAll(results);
}
// Clear regular and batch scan results.
synchronized public void clear() {
mResults.clear();
mBatchScanResults.clear();
}
// Return regular BLE scan results accumulated so far.
synchronized Set<ScanResult> getScanResults() {
return Collections.unmodifiableSet(mResults);
}
// Return batch scan results.
synchronized List<ScanResult> getBatchScanResults() {
return Collections.unmodifiableList(mBatchScanResults);
}
}
private class RssiComparator implements Comparator<ScanResult> {
@Override
public int compare(ScanResult lhs, ScanResult rhs) {
return rhs.getRssi() - lhs.getRssi();
}
}
// Perform a BLE scan to get results of nearby BLE devices.
private Set<ScanResult> scan() {
BleScanCallback regularLeScanCallback = new BleScanCallback();
mScanner.startScan(regularLeScanCallback);
sleep(SCAN_DURATION_MILLIS);
mScanner.stopScan(regularLeScanCallback);
sleep(1000);
return regularLeScanCallback.getScanResults();
}
// Put the current thread to sleep.
private void sleep(int sleepMillis) {
try {
Thread.sleep(sleepMillis);
} catch (InterruptedException e) {
Log.e(TAG, "interrupted", e);
}
}
// Check if Bluetooth LE feature is supported on DUT.
private boolean isBleSupported() {
return getContext().getPackageManager()
.hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE);
}
// Returns whether offloaded scan batching is supported.
private boolean isBleBatchScanSupported() {
return mBluetoothAdapter.isOffloadedScanBatchingSupported();
}
}