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 static com.google.common.truth.Truth.assertThat; 20 21 import android.app.PendingIntent; 22 import android.bluetooth.BluetoothAdapter; 23 import android.bluetooth.BluetoothManager; 24 import android.bluetooth.le.BluetoothLeScanner; 25 import android.bluetooth.le.ScanCallback; 26 import android.bluetooth.le.ScanFilter; 27 import android.bluetooth.le.ScanRecord; 28 import android.bluetooth.le.ScanResult; 29 import android.bluetooth.le.ScanSettings; 30 import android.content.Context; 31 import android.content.Intent; 32 import android.os.ParcelUuid; 33 import android.os.SystemClock; 34 import android.util.Log; 35 import android.util.SparseArray; 36 37 import androidx.test.ext.junit.runners.AndroidJUnit4; 38 import androidx.test.filters.MediumTest; 39 import androidx.test.platform.app.InstrumentationRegistry; 40 41 import com.android.compatibility.common.util.CddTest; 42 43 import org.junit.After; 44 import org.junit.Assume; 45 import org.junit.Before; 46 import org.junit.Ignore; 47 import org.junit.Test; 48 import org.junit.runner.RunWith; 49 50 import java.util.ArrayList; 51 import java.util.Collection; 52 import java.util.Collections; 53 import java.util.Comparator; 54 import java.util.HashSet; 55 import java.util.List; 56 import java.util.Map; 57 import java.util.Set; 58 import java.util.concurrent.ConcurrentLinkedQueue; 59 import java.util.concurrent.CountDownLatch; 60 import java.util.concurrent.TimeUnit; 61 62 /** 63 * Test cases for Bluetooth LE scans. 64 * 65 * <p>To run the test, the device must be placed in an environment that has at least 3 beacons, all 66 * placed less than 5 meters away from the DUT. 67 * 68 * <p>Run 'run cts --class android.bluetooth.cts.BluetoothLeScanTest' in cts-tradefed to run the 69 * test cases. 70 */ 71 @RunWith(AndroidJUnit4.class) 72 public class BluetoothLeScanTest { 73 private static final String TAG = BluetoothLeScanTest.class.getSimpleName(); 74 75 private static final int SCAN_DURATION_MILLIS = 10000; 76 private static final int BATCH_SCAN_REPORT_DELAY_MILLIS = 20000; 77 private static final int SCAN_STOP_TIMEOUT = 2000; 78 private CountDownLatch mFlushBatchScanLatch; 79 80 private Context mContext; 81 private BluetoothAdapter mBluetoothAdapter; 82 private BluetoothLeScanner mScanner; 83 // Whether location is on before running the tests. 84 private boolean mLocationOn; 85 86 @Before setUp()87 public void setUp() { 88 mContext = InstrumentationRegistry.getInstrumentation().getContext(); 89 90 Assume.assumeTrue(TestUtils.isBleSupported(mContext)); 91 92 InstrumentationRegistry.getInstrumentation() 93 .getUiAutomation() 94 .adoptShellPermissionIdentity(android.Manifest.permission.BLUETOOTH_CONNECT); 95 BluetoothManager manager = 96 (BluetoothManager) mContext.getSystemService(Context.BLUETOOTH_SERVICE); 97 mBluetoothAdapter = manager.getAdapter(); 98 if (!mBluetoothAdapter.isEnabled()) { 99 assertThat(BTAdapterUtils.enableAdapter(mBluetoothAdapter, mContext)).isTrue(); 100 } 101 mScanner = mBluetoothAdapter.getBluetoothLeScanner(); 102 mLocationOn = TestUtils.isLocationOn(mContext); 103 if (!mLocationOn) { 104 TestUtils.enableLocation(mContext); 105 } 106 InstrumentationRegistry.getInstrumentation() 107 .getUiAutomation() 108 .grantRuntimePermission( 109 "android.bluetooth.cts", android.Manifest.permission.ACCESS_FINE_LOCATION); 110 } 111 112 @After tearDown()113 public void tearDown() { 114 if (!mLocationOn) { 115 TestUtils.disableLocation(mContext); 116 } 117 InstrumentationRegistry.getInstrumentation() 118 .getUiAutomation() 119 .dropShellPermissionIdentity(); 120 } 121 122 /** Basic test case for BLE scans. Checks BLE scan timestamp is within correct range. */ 123 @CddTest(requirements = {"7.4.3/C-2-1", "7.4.3/C-3-2"}) 124 @MediumTest 125 @Test basicBleScan()126 public void basicBleScan() { 127 InstrumentationRegistry.getInstrumentation() 128 .getUiAutomation() 129 .adoptShellPermissionIdentity(android.Manifest.permission.BLUETOOTH_SCAN); 130 long scanStartMillis = SystemClock.elapsedRealtime(); 131 Collection<ScanResult> scanResults = scan(); 132 long scanEndMillis = SystemClock.elapsedRealtime(); 133 Log.d(TAG, "scan result size:" + scanResults.size()); 134 assertThat(scanResults).isNotEmpty(); 135 verifyTimestamp(scanResults, scanStartMillis, scanEndMillis); 136 } 137 138 /** 139 * Test of scan filters. Ensures only beacons matching certain type of scan filters were 140 * reported. 141 */ 142 @CddTest(requirements = {"7.4.3/C-2-1", "7.4.3/C-3-2"}) 143 @MediumTest 144 @Test scanFilter()145 public void scanFilter() { 146 InstrumentationRegistry.getInstrumentation() 147 .getUiAutomation() 148 .adoptShellPermissionIdentity( 149 android.Manifest.permission.BLUETOOTH_CONNECT, 150 android.Manifest.permission.BLUETOOTH_SCAN); 151 List<ScanFilter> filters = new ArrayList<>(); 152 ScanFilter filter = createScanFilter(); 153 if (filter == null) { 154 Log.d(TAG, "no appropriate filter can be set"); 155 return; 156 } 157 filters.add(filter); 158 159 BleScanCallback filterLeScanCallback = new BleScanCallback(); 160 ScanSettings settings = 161 new ScanSettings.Builder().setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY).build(); 162 mScanner.startScan(filters, settings, filterLeScanCallback); 163 TestUtils.sleep(SCAN_DURATION_MILLIS); 164 mScanner.stopScan(filterLeScanCallback); 165 TestUtils.sleep(SCAN_STOP_TIMEOUT); 166 Collection<ScanResult> scanResults = filterLeScanCallback.getScanResults(); 167 for (ScanResult result : scanResults) { 168 assertThat(filter.matches(result)).isTrue(); 169 } 170 } 171 172 @CddTest(requirements = {"7.4.3/C-2-1", "7.4.3/C-3-2"}) 173 @MediumTest 174 @Test scanFromSourceWithoutFilters()175 public void scanFromSourceWithoutFilters() { 176 InstrumentationRegistry.getInstrumentation() 177 .getUiAutomation() 178 .adoptShellPermissionIdentity( 179 android.Manifest.permission.BLUETOOTH_CONNECT, 180 android.Manifest.permission.BLUETOOTH_PRIVILEGED, 181 android.Manifest.permission.BLUETOOTH_SCAN, 182 android.Manifest.permission.UPDATE_DEVICE_STATS); 183 BleScanCallback filterLeScanCallback = new BleScanCallback(); 184 mScanner.startScanFromSource(null, filterLeScanCallback); 185 TestUtils.sleep(SCAN_DURATION_MILLIS); 186 mScanner.stopScan(filterLeScanCallback); 187 TestUtils.sleep(SCAN_STOP_TIMEOUT); 188 assertThat(filterLeScanCallback.getScanResults()).isNotEmpty(); 189 } 190 191 @CddTest(requirements = {"7.4.3/C-2-1", "7.4.3/C-3-2"}) 192 @MediumTest 193 @Test scanFromSourceWithFilters()194 public void scanFromSourceWithFilters() { 195 InstrumentationRegistry.getInstrumentation() 196 .getUiAutomation() 197 .adoptShellPermissionIdentity( 198 android.Manifest.permission.BLUETOOTH_CONNECT, 199 android.Manifest.permission.BLUETOOTH_PRIVILEGED, 200 android.Manifest.permission.BLUETOOTH_SCAN, 201 android.Manifest.permission.UPDATE_DEVICE_STATS); 202 BleScanCallback filterLeScanCallback = new BleScanCallback(); 203 ScanSettings settings = 204 new ScanSettings.Builder().setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY).build(); 205 mScanner.startScanFromSource(null, settings, null, filterLeScanCallback); 206 TestUtils.sleep(SCAN_DURATION_MILLIS); 207 mScanner.stopScan(filterLeScanCallback); 208 TestUtils.sleep(SCAN_STOP_TIMEOUT); 209 assertThat(filterLeScanCallback.getScanResults()).isNotEmpty(); 210 } 211 212 // Create a scan filter based on the nearby beacon with highest signal strength. createScanFilter()213 private ScanFilter createScanFilter() { 214 // Get a list of nearby beacons. 215 List<ScanResult> scanResults = new ArrayList<>(scan()); 216 assertThat(scanResults).isNotEmpty(); 217 // Find the beacon with strongest signal strength, which is the target device for filter 218 // scan. 219 Collections.sort(scanResults, new RssiComparator()); 220 ScanResult result = scanResults.get(0); 221 ScanRecord record = result.getScanRecord(); 222 if (record == null) { 223 return null; 224 } 225 Map<ParcelUuid, byte[]> serviceData = record.getServiceData(); 226 if (serviceData != null && !serviceData.isEmpty()) { 227 ParcelUuid uuid = serviceData.keySet().iterator().next(); 228 return new ScanFilter.Builder() 229 .setServiceData(uuid, new byte[] {0}, new byte[] {0}) 230 .build(); 231 } 232 SparseArray<byte[]> manufacturerSpecificData = record.getManufacturerSpecificData(); 233 if (manufacturerSpecificData != null && manufacturerSpecificData.size() > 0) { 234 return new ScanFilter.Builder() 235 .setManufacturerData( 236 manufacturerSpecificData.keyAt(0), new byte[] {0}, new byte[] {0}) 237 .build(); 238 } 239 List<ParcelUuid> serviceUuids = record.getServiceUuids(); 240 if (serviceUuids != null && !serviceUuids.isEmpty()) { 241 return new ScanFilter.Builder().setServiceUuid(serviceUuids.get(0)).build(); 242 } 243 return null; 244 } 245 246 /** Test of opportunistic BLE scans. */ 247 @CddTest(requirements = {"7.4.3/C-2-1", "7.4.3/C-3-2"}) 248 @MediumTest 249 @Test 250 @Ignore("b/70865144 - Test fails because it obtains results from GmsCore explicit scan.") opportunisticScan()251 public void opportunisticScan() { 252 InstrumentationRegistry.getInstrumentation() 253 .getUiAutomation() 254 .adoptShellPermissionIdentity(android.Manifest.permission.BLUETOOTH_SCAN); 255 256 ScanSettings opportunisticScanSettings = 257 new ScanSettings.Builder() 258 .setScanMode(ScanSettings.SCAN_MODE_OPPORTUNISTIC) 259 .build(); 260 BleScanCallback emptyScanCallback = new BleScanCallback(); 261 assertThat(emptyScanCallback.getScanResults()).isEmpty(); 262 263 // No scans are really started with opportunistic scans only. 264 mScanner.startScan( 265 Collections.<ScanFilter>emptyList(), opportunisticScanSettings, emptyScanCallback); 266 TestUtils.sleep(SCAN_DURATION_MILLIS); 267 assertThat(emptyScanCallback.getScanResults()).isEmpty(); 268 269 BleScanCallback regularScanCallback = new BleScanCallback(); 270 ScanSettings regularScanSettings = 271 new ScanSettings.Builder().setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY).build(); 272 List<ScanFilter> filters = new ArrayList<>(); 273 ScanFilter filter = createScanFilter(); 274 if (filter != null) { 275 filters.add(filter); 276 } else { 277 Log.d(TAG, "no appropriate filter can be set"); 278 } 279 mScanner.startScan(filters, regularScanSettings, regularScanCallback); 280 TestUtils.sleep(SCAN_DURATION_MILLIS); 281 // With normal BLE scan client, opportunistic scan client will get scan results. 282 assertThat(emptyScanCallback.getScanResults()).isNotEmpty(); 283 284 // No more scan results for opportunistic scan clients once the normal BLE scan clients 285 // stops. 286 mScanner.stopScan(regularScanCallback); 287 // In case we got scan results before scan was completely stopped. 288 TestUtils.sleep(SCAN_STOP_TIMEOUT); 289 emptyScanCallback.clear(); 290 TestUtils.sleep(SCAN_DURATION_MILLIS); 291 assertThat(emptyScanCallback.getScanResults()).isEmpty(); 292 } 293 294 /** Test case for BLE Batch scan. */ 295 @CddTest(requirements = {"7.4.3/C-2-1", "7.4.3/C-3-2"}) 296 @MediumTest 297 @Test batchScan()298 public void batchScan() { 299 Assume.assumeTrue(isBleBatchScanSupported()); 300 InstrumentationRegistry.getInstrumentation() 301 .getUiAutomation() 302 .adoptShellPermissionIdentity( 303 android.Manifest.permission.BLUETOOTH_CONNECT, 304 android.Manifest.permission.BLUETOOTH_SCAN); 305 306 ScanSettings batchScanSettings = 307 new ScanSettings.Builder() 308 .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) 309 .setReportDelay(BATCH_SCAN_REPORT_DELAY_MILLIS) 310 .build(); 311 BleScanCallback batchScanCallback = new BleScanCallback(); 312 mScanner.startScan(Collections.emptyList(), batchScanSettings, batchScanCallback); 313 TestUtils.sleep(SCAN_DURATION_MILLIS); 314 mScanner.flushPendingScanResults(batchScanCallback); 315 mFlushBatchScanLatch = new CountDownLatch(1); 316 Collection<ScanResult> results = batchScanCallback.getBatchScanResults(); 317 try { 318 mFlushBatchScanLatch.await(5, TimeUnit.SECONDS); 319 } catch (InterruptedException e) { 320 // Nothing to do. 321 Log.e(TAG, "interrupted!"); 322 } 323 assertThat(results).isNotEmpty(); 324 long scanEndMillis = SystemClock.elapsedRealtime(); 325 mScanner.stopScan(batchScanCallback); 326 verifyTimestamp(results, 0, scanEndMillis); 327 } 328 329 /** Test case for starting a scan with a PendingIntent. */ 330 @CddTest(requirements = {"7.4.3/C-2-1", "7.4.3/C-3-2"}) 331 @MediumTest 332 @Test startScanPendingIntent_nullnull()333 public void startScanPendingIntent_nullnull() throws Exception { 334 Assume.assumeTrue(isBleBatchScanSupported()); 335 InstrumentationRegistry.getInstrumentation() 336 .getUiAutomation() 337 .adoptShellPermissionIdentity( 338 android.Manifest.permission.BLUETOOTH_CONNECT, 339 android.Manifest.permission.BLUETOOTH_SCAN); 340 341 Intent broadcastIntent = new Intent(); 342 broadcastIntent.setClass(mContext, BluetoothScanReceiver.class); 343 PendingIntent pi = 344 PendingIntent.getBroadcast( 345 mContext, 1, broadcastIntent, PendingIntent.FLAG_IMMUTABLE); 346 CountDownLatch latch = BluetoothScanReceiver.createCountDownLatch(); 347 mScanner.startScan(null, null, pi); 348 boolean gotResults = latch.await(20, TimeUnit.SECONDS); 349 mScanner.stopScan(pi); 350 assertThat(gotResults).isTrue(); 351 } 352 353 /** Test case for starting a scan with a PendingIntent. */ 354 @CddTest(requirements = {"7.4.3/C-2-1", "7.4.3/C-3-2"}) 355 @MediumTest 356 @Test startScanPendingIntent()357 public void startScanPendingIntent() throws Exception { 358 Assume.assumeTrue(isBleBatchScanSupported()); 359 InstrumentationRegistry.getInstrumentation() 360 .getUiAutomation() 361 .adoptShellPermissionIdentity( 362 android.Manifest.permission.BLUETOOTH_CONNECT, 363 android.Manifest.permission.BLUETOOTH_SCAN); 364 365 ScanSettings batchScanSettings = 366 new ScanSettings.Builder() 367 .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) 368 .setReportDelay(0) 369 .build(); 370 ScanFilter filter = createScanFilter(); 371 ArrayList<ScanFilter> filters = null; 372 if (filter != null) { 373 filters = new ArrayList<>(); 374 filters.add(filter); 375 } else { 376 Log.d(TAG, "Could not add a filter"); 377 } 378 Intent broadcastIntent = new Intent(); 379 broadcastIntent.setClass(mContext, BluetoothScanReceiver.class); 380 PendingIntent pi = 381 PendingIntent.getBroadcast( 382 mContext, 1, broadcastIntent, PendingIntent.FLAG_IMMUTABLE); 383 CountDownLatch latch = BluetoothScanReceiver.createCountDownLatch(); 384 mScanner.startScan(filters, batchScanSettings, pi); 385 boolean gotResults = latch.await(20, TimeUnit.SECONDS); 386 mScanner.stopScan(pi); 387 assertThat(gotResults).isTrue(); 388 } 389 390 // Verify timestamp of all scan results are within [scanStartMillis, scanEndMillis]. verifyTimestamp( Collection<ScanResult> results, long scanStartMillis, long scanEndMillis)391 private void verifyTimestamp( 392 Collection<ScanResult> results, long scanStartMillis, long scanEndMillis) { 393 for (ScanResult result : results) { 394 long timestampMillis = TimeUnit.NANOSECONDS.toMillis(result.getTimestampNanos()); 395 assertThat(timestampMillis).isAtLeast(scanStartMillis); 396 assertThat(timestampMillis).isAtMost(scanEndMillis); 397 } 398 } 399 400 // Perform a BLE scan to get results of nearby BLE devices. scan()401 private Set<ScanResult> scan() { 402 BleScanCallback regularLeScanCallback = new BleScanCallback(); 403 mScanner.startScan(regularLeScanCallback); 404 TestUtils.sleep(SCAN_DURATION_MILLIS); 405 mScanner.stopScan(regularLeScanCallback); 406 TestUtils.sleep(SCAN_STOP_TIMEOUT); 407 return regularLeScanCallback.getScanResults(); 408 } 409 410 // Returns whether offloaded scan batching is supported. isBleBatchScanSupported()411 private boolean isBleBatchScanSupported() { 412 return mBluetoothAdapter.isOffloadedScanBatchingSupported(); 413 } 414 415 // Helper class for BLE scan callback. 416 private class BleScanCallback extends ScanCallback { 417 private final Set<ScanResult> mResults = new HashSet<>(); 418 private final Collection<ScanResult> mBatchScanResults = new ConcurrentLinkedQueue<>(); 419 420 @Override onScanResult(int callbackType, ScanResult result)421 public void onScanResult(int callbackType, ScanResult result) { 422 if (callbackType == ScanSettings.CALLBACK_TYPE_ALL_MATCHES) { 423 mResults.add(result); 424 } 425 } 426 427 @Override onBatchScanResults(List<ScanResult> results)428 public void onBatchScanResults(List<ScanResult> results) { 429 // In case onBatchScanResults are called due to buffer full, we want to collect all 430 // scan results. 431 mBatchScanResults.addAll(results); 432 if (mFlushBatchScanLatch != null) { 433 mFlushBatchScanLatch.countDown(); 434 } 435 } 436 437 // Clear regular and batch scan results. clear()438 public synchronized void clear() { 439 mResults.clear(); 440 mBatchScanResults.clear(); 441 } 442 443 // Return regular BLE scan results accumulated so far. getScanResults()444 synchronized Set<ScanResult> getScanResults() { 445 return Collections.unmodifiableSet(mResults); 446 } 447 448 // Return batch scan results. getBatchScanResults()449 synchronized Collection<ScanResult> getBatchScanResults() { 450 return Collections.unmodifiableCollection(mBatchScanResults); 451 } 452 } 453 454 private class RssiComparator implements Comparator<ScanResult> { 455 456 @Override compare(ScanResult lhs, ScanResult rhs)457 public int compare(ScanResult lhs, ScanResult rhs) { 458 return rhs.getRssi() - lhs.getRssi(); 459 } 460 } 461 } 462