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