• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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