1 /* 2 * Copyright (C) 2015 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.bluetooth.cts; 18 19 import android.app.PendingIntent; 20 import android.bluetooth.BluetoothAdapter; 21 import android.bluetooth.BluetoothManager; 22 import android.bluetooth.le.BluetoothLeScanner; 23 import android.bluetooth.le.ScanCallback; 24 import android.bluetooth.le.ScanFilter; 25 import android.bluetooth.le.ScanRecord; 26 import android.bluetooth.le.ScanResult; 27 import android.bluetooth.le.ScanSettings; 28 import android.content.Context; 29 import android.content.Intent; 30 import android.content.pm.PackageManager; 31 import android.os.ParcelUuid; 32 import android.os.SystemClock; 33 import android.test.AndroidTestCase; 34 import android.test.suitebuilder.annotation.MediumTest; 35 import android.util.Log; 36 import android.util.SparseArray; 37 38 import androidx.test.InstrumentationRegistry; 39 40 import java.util.ArrayList; 41 import java.util.Collection; 42 import java.util.Collections; 43 import java.util.Comparator; 44 import java.util.HashSet; 45 import java.util.List; 46 import java.util.Map; 47 import java.util.Set; 48 import java.util.concurrent.CountDownLatch; 49 import java.util.concurrent.TimeUnit; 50 51 /** 52 * Test cases for Bluetooth LE scans. 53 * <p> 54 * To run the test, the device must be placed in an environment that has at least 3 beacons, all 55 * placed less than 5 meters away from the DUT. 56 * <p> 57 * Run 'run cts --class android.bluetooth.cts.BluetoothLeScanTest' in cts-tradefed to run the test 58 * cases. 59 */ 60 public class BluetoothLeScanTest extends AndroidTestCase { 61 62 private static final String TAG = "BluetoothLeScanTest"; 63 64 private static final int SCAN_DURATION_MILLIS = 10000; 65 private static final int BATCH_SCAN_REPORT_DELAY_MILLIS = 20000; 66 private static final int SCAN_STOP_TIMEOUT = 2000; 67 private CountDownLatch mFlushBatchScanLatch; 68 69 private BluetoothAdapter mBluetoothAdapter; 70 private BluetoothLeScanner mScanner; 71 // Whether location is on before running the tests. 72 private boolean mLocationOn; 73 74 @Override setUp()75 public void setUp() { 76 if (!TestUtils.isBleSupported(getContext())) { 77 return; 78 } 79 BluetoothManager manager = (BluetoothManager) mContext.getSystemService( 80 Context.BLUETOOTH_SERVICE); 81 mBluetoothAdapter = manager.getAdapter(); 82 if (!mBluetoothAdapter.isEnabled()) { 83 assertTrue(BTAdapterUtils.enableAdapter(mBluetoothAdapter, mContext)); 84 } 85 mScanner = mBluetoothAdapter.getBluetoothLeScanner(); 86 mLocationOn = TestUtils.isLocationOn(getContext()); 87 if (!mLocationOn) { 88 TestUtils.enableLocation(getContext()); 89 } 90 InstrumentationRegistry.getInstrumentation().getUiAutomation().grantRuntimePermission( 91 "android.bluetooth.cts", android.Manifest.permission.ACCESS_FINE_LOCATION); 92 } 93 94 @Override tearDown()95 public void tearDown() { 96 if (!TestUtils.isBleSupported(getContext())) { 97 // mBluetoothAdapter == null. 98 return; 99 } 100 101 if (!mLocationOn) { 102 TestUtils.disableLocation(getContext()); 103 } 104 assertTrue(BTAdapterUtils.disableAdapter(mBluetoothAdapter, mContext)); 105 } 106 107 /** 108 * Basic test case for BLE scans. Checks BLE scan timestamp is within correct range. 109 */ 110 @MediumTest testBasicBleScan()111 public void testBasicBleScan() { 112 if (!TestUtils.isBleSupported(getContext())) { 113 return; 114 } 115 long scanStartMillis = SystemClock.elapsedRealtime(); 116 Collection<ScanResult> scanResults = scan(); 117 long scanEndMillis = SystemClock.elapsedRealtime(); 118 Log.d(TAG, "scan result size:" + scanResults.size()); 119 assertTrue("Scan results shouldn't be empty", !scanResults.isEmpty()); 120 verifyTimestamp(scanResults, scanStartMillis, scanEndMillis); 121 } 122 123 /** 124 * Test of scan filters. Ensures only beacons matching certain type of scan filters were 125 * reported. 126 */ 127 @MediumTest testScanFilter()128 public void testScanFilter() { 129 if (!TestUtils.isBleSupported(getContext())) { 130 return; 131 } 132 133 List<ScanFilter> filters = new ArrayList<ScanFilter>(); 134 ScanFilter filter = createScanFilter(); 135 if (filter == null) { 136 Log.d(TAG, "no appropriate filter can be set"); 137 return; 138 } 139 filters.add(filter); 140 141 BleScanCallback filterLeScanCallback = new BleScanCallback(); 142 ScanSettings settings = new ScanSettings.Builder().setScanMode( 143 ScanSettings.SCAN_MODE_LOW_LATENCY).build(); 144 mScanner.startScan(filters, settings, filterLeScanCallback); 145 TestUtils.sleep(SCAN_DURATION_MILLIS); 146 mScanner.stopScan(filterLeScanCallback); 147 TestUtils.sleep(SCAN_STOP_TIMEOUT); 148 Collection<ScanResult> scanResults = filterLeScanCallback.getScanResults(); 149 for (ScanResult result : scanResults) { 150 assertTrue(filter.matches(result)); 151 } 152 } 153 154 // Create a scan filter based on the nearby beacon with highest signal strength. createScanFilter()155 private ScanFilter createScanFilter() { 156 // Get a list of nearby beacons. 157 List<ScanResult> scanResults = new ArrayList<ScanResult>(scan()); 158 assertTrue("Scan results shouldn't be empty", !scanResults.isEmpty()); 159 // Find the beacon with strongest signal strength, which is the target device for filter 160 // scan. 161 Collections.sort(scanResults, new RssiComparator()); 162 ScanResult result = scanResults.get(0); 163 ScanRecord record = result.getScanRecord(); 164 if (record == null) { 165 return null; 166 } 167 Map<ParcelUuid, byte[]> serviceData = record.getServiceData(); 168 if (serviceData != null && !serviceData.isEmpty()) { 169 ParcelUuid uuid = serviceData.keySet().iterator().next(); 170 return new ScanFilter.Builder().setServiceData(uuid, new byte[]{0}, 171 new byte[]{0}).build(); 172 } 173 SparseArray<byte[]> manufacturerSpecificData = record.getManufacturerSpecificData(); 174 if (manufacturerSpecificData != null && manufacturerSpecificData.size() > 0) { 175 return new ScanFilter.Builder().setManufacturerData(manufacturerSpecificData.keyAt(0), 176 new byte[]{0}, new byte[]{0}).build(); 177 } 178 List<ParcelUuid> serviceUuids = record.getServiceUuids(); 179 if (serviceUuids != null && !serviceUuids.isEmpty()) { 180 return new ScanFilter.Builder().setServiceUuid(serviceUuids.get(0)).build(); 181 } 182 return null; 183 } 184 185 // /** 186 // * Test of opportunistic BLE scans. 187 // * Temporarily disable this test because it is interfered by the GmsCore; 188 // * it fails when it obtains results from GmsCore explicit scan. 189 // * TODO(b/70865144): re-enable this test. 190 // */ 191 // @MediumTest 192 // public void testOpportunisticScan() { 193 // if (!TestUtils.isBleSupported(getContext())) { 194 // return; 195 // } 196 // ScanSettings opportunisticScanSettings = new ScanSettings.Builder() 197 // .setScanMode(ScanSettings.SCAN_MODE_OPPORTUNISTIC) 198 // .build(); 199 // BleScanCallback emptyScanCallback = new BleScanCallback(); 200 // assertTrue("opportunistic scan shouldn't have scan results", 201 // emptyScanCallback.getScanResults().isEmpty()); 202 // 203 // // No scans are really started with opportunistic scans only. 204 // mScanner.startScan(Collections.<ScanFilter>emptyList(), opportunisticScanSettings, 205 // emptyScanCallback); 206 // sleep(SCAN_DURATION_MILLIS); 207 // Log.d(TAG, "result: " + emptyScanCallback.getScanResults()); 208 // assertTrue("opportunistic scan shouldn't have scan results", 209 // emptyScanCallback.getScanResults().isEmpty()); 210 // 211 // BleScanCallback regularScanCallback = new BleScanCallback(); 212 // ScanSettings regularScanSettings = new ScanSettings.Builder() 213 // .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY).build(); 214 // List<ScanFilter> filters = new ArrayList<>(); 215 // ScanFilter filter = createScanFilter(); 216 // if (filter != null) { 217 // filters.add(filter); 218 // } else { 219 // Log.d(TAG, "no appropriate filter can be set"); 220 // } 221 // mScanner.startScan(filters, regularScanSettings, regularScanCallback); 222 // sleep(SCAN_DURATION_MILLIS); 223 // // With normal BLE scan client, opportunistic scan client will get scan results. 224 // assertTrue("opportunistic scan results shouldn't be empty", 225 // !emptyScanCallback.getScanResults().isEmpty()); 226 // 227 // // No more scan results for opportunistic scan clients once the normal BLE scan clients 228 // // stops. 229 // mScanner.stopScan(regularScanCallback); 230 // // In case we got scan results before scan was completely stopped. 231 // sleep(SCAN_STOP_TIMEOUT); 232 // emptyScanCallback.clear(); 233 // sleep(SCAN_DURATION_MILLIS); 234 // assertTrue("opportunistic scan shouldn't have scan results", 235 // emptyScanCallback.getScanResults().isEmpty()); 236 // } 237 238 /** 239 * Test case for BLE Batch scan. 240 */ 241 @MediumTest testBatchScan()242 public void testBatchScan() { 243 if (!TestUtils.isBleSupported(getContext()) || !isBleBatchScanSupported()) { 244 Log.d(TAG, "BLE or BLE batching not suppported"); 245 return; 246 } 247 ScanSettings batchScanSettings = new ScanSettings.Builder() 248 .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) 249 .setReportDelay(BATCH_SCAN_REPORT_DELAY_MILLIS).build(); 250 BleScanCallback batchScanCallback = new BleScanCallback(); 251 mScanner.startScan(Collections.<ScanFilter>emptyList(), batchScanSettings, 252 batchScanCallback); 253 TestUtils.sleep(SCAN_DURATION_MILLIS); 254 mScanner.flushPendingScanResults(batchScanCallback); 255 mFlushBatchScanLatch = new CountDownLatch(1); 256 List<ScanResult> results = batchScanCallback.getBatchScanResults(); 257 try { 258 mFlushBatchScanLatch.await(5, TimeUnit.SECONDS); 259 } catch (InterruptedException e) { 260 // Nothing to do. 261 Log.e(TAG, "interrupted!"); 262 } 263 assertTrue(!results.isEmpty()); 264 long scanEndMillis = SystemClock.elapsedRealtime(); 265 mScanner.stopScan(batchScanCallback); 266 verifyTimestamp(results, 0, scanEndMillis); 267 } 268 269 /** 270 * Test case for starting a scan with a PendingIntent. 271 */ 272 @MediumTest testStartScanPendingIntent_nullnull()273 public void testStartScanPendingIntent_nullnull() throws Exception { 274 if (!TestUtils.isBleSupported(getContext()) || !isBleBatchScanSupported()) { 275 Log.d(TAG, "BLE or BLE batching not suppported"); 276 return; 277 } 278 Intent broadcastIntent = new Intent(); 279 broadcastIntent.setClass(mContext, BluetoothScanReceiver.class); 280 PendingIntent pi = PendingIntent.getBroadcast(mContext, 1, broadcastIntent, 281 PendingIntent.FLAG_IMMUTABLE); 282 CountDownLatch latch = BluetoothScanReceiver.createCountDownLatch(); 283 mScanner.startScan(null, null, pi); 284 boolean gotResults = latch.await(20, TimeUnit.SECONDS); 285 mScanner.stopScan(pi); 286 assertTrue("Scan results not received", gotResults); 287 } 288 289 /** 290 * Test case for starting a scan with a PendingIntent. 291 */ 292 @MediumTest testStartScanPendingIntent()293 public void testStartScanPendingIntent() throws Exception { 294 if (!TestUtils.isBleSupported(getContext()) || !isBleBatchScanSupported()) { 295 Log.d(TAG, "BLE or BLE batching not suppported"); 296 return; 297 } 298 ScanSettings batchScanSettings = new ScanSettings.Builder() 299 .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) 300 .setReportDelay(0).build(); 301 ScanFilter filter = createScanFilter(); 302 ArrayList<ScanFilter> filters = null; 303 if (filter != null) { 304 filters = new ArrayList<>(); 305 filters.add(filter); 306 } else { 307 Log.d(TAG, "Could not add a filter"); 308 } 309 Intent broadcastIntent = new Intent(); 310 broadcastIntent.setClass(mContext, BluetoothScanReceiver.class); 311 PendingIntent pi = PendingIntent.getBroadcast(mContext, 1, broadcastIntent, 312 PendingIntent.FLAG_IMMUTABLE); 313 CountDownLatch latch = BluetoothScanReceiver.createCountDownLatch(); 314 mScanner.startScan(filters, batchScanSettings, pi); 315 boolean gotResults = latch.await(20, TimeUnit.SECONDS); 316 mScanner.stopScan(pi); 317 assertTrue("Scan results not received", gotResults); 318 } 319 320 // Verify timestamp of all scan results are within [scanStartMillis, scanEndMillis]. verifyTimestamp(Collection<ScanResult> results, long scanStartMillis, long scanEndMillis)321 private void verifyTimestamp(Collection<ScanResult> results, long scanStartMillis, 322 long scanEndMillis) { 323 for (ScanResult result : results) { 324 long timestampMillis = TimeUnit.NANOSECONDS.toMillis(result.getTimestampNanos()); 325 assertTrue("Invalid timestamp: " + timestampMillis + " should be >= " + scanStartMillis, 326 timestampMillis >= scanStartMillis); 327 assertTrue("Invalid timestamp: " + timestampMillis + " should be <= " + scanEndMillis, 328 timestampMillis <= scanEndMillis); 329 } 330 } 331 332 // Helper class for BLE scan callback. 333 private class BleScanCallback extends ScanCallback { 334 private Set<ScanResult> mResults = new HashSet<ScanResult>(); 335 private List<ScanResult> mBatchScanResults = new ArrayList<ScanResult>(); 336 337 @Override onScanResult(int callbackType, ScanResult result)338 public void onScanResult(int callbackType, ScanResult result) { 339 if (callbackType == ScanSettings.CALLBACK_TYPE_ALL_MATCHES) { 340 mResults.add(result); 341 } 342 } 343 344 @Override onBatchScanResults(List<ScanResult> results)345 public void onBatchScanResults(List<ScanResult> results) { 346 // In case onBatchScanResults are called due to buffer full, we want to collect all 347 // scan results. 348 mBatchScanResults.addAll(results); 349 if (mFlushBatchScanLatch != null) { 350 mFlushBatchScanLatch.countDown(); 351 } 352 } 353 354 // Clear regular and batch scan results. clear()355 synchronized public void clear() { 356 mResults.clear(); 357 mBatchScanResults.clear(); 358 } 359 360 // Return regular BLE scan results accumulated so far. getScanResults()361 synchronized Set<ScanResult> getScanResults() { 362 return Collections.unmodifiableSet(mResults); 363 } 364 365 // Return batch scan results. getBatchScanResults()366 synchronized List<ScanResult> getBatchScanResults() { 367 return Collections.unmodifiableList(mBatchScanResults); 368 } 369 } 370 371 private class RssiComparator implements Comparator<ScanResult> { 372 373 @Override compare(ScanResult lhs, ScanResult rhs)374 public int compare(ScanResult lhs, ScanResult rhs) { 375 return rhs.getRssi() - lhs.getRssi(); 376 } 377 378 } 379 380 // Perform a BLE scan to get results of nearby BLE devices. scan()381 private Set<ScanResult> scan() { 382 BleScanCallback regularLeScanCallback = new BleScanCallback(); 383 mScanner.startScan(regularLeScanCallback); 384 TestUtils.sleep(SCAN_DURATION_MILLIS); 385 mScanner.stopScan(regularLeScanCallback); 386 TestUtils.sleep(SCAN_STOP_TIMEOUT); 387 return regularLeScanCallback.getScanResults(); 388 } 389 390 // Returns whether offloaded scan batching is supported. isBleBatchScanSupported()391 private boolean isBleBatchScanSupported() { 392 return mBluetoothAdapter.isOffloadedScanBatchingSupported(); 393 } 394 395 } 396