• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2014 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5  * use this file except in compliance with the License. You may obtain a copy of
6  * 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, WITHOUT
12  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13  * License for the specific language governing permissions and limitations under
14  * the License.
15  */
16 
17 package android.bluetooth.le;
18 
19 import android.Manifest;
20 import android.annotation.RequiresPermission;
21 import android.annotation.SystemApi;
22 import android.app.ActivityThread;
23 import android.bluetooth.BluetoothAdapter;
24 import android.bluetooth.BluetoothGatt;
25 import android.bluetooth.BluetoothGattCallbackWrapper;
26 import android.bluetooth.IBluetoothGatt;
27 import android.bluetooth.IBluetoothManager;
28 import android.os.Handler;
29 import android.os.Looper;
30 import android.os.ParcelUuid;
31 import android.os.RemoteException;
32 import android.os.WorkSource;
33 import android.util.Log;
34 
35 import java.util.ArrayList;
36 import java.util.HashMap;
37 import java.util.List;
38 import java.util.Map;
39 import java.util.UUID;
40 
41 /**
42  * This class provides methods to perform scan related operations for Bluetooth LE devices. An
43  * application can scan for a particular type of Bluetooth LE devices using {@link ScanFilter}. It
44  * can also request different types of callbacks for delivering the result.
45  * <p>
46  * Use {@link BluetoothAdapter#getBluetoothLeScanner()} to get an instance of
47  * {@link BluetoothLeScanner}.
48  * <p>
49  * <b>Note:</b> Most of the scan methods here require
50  * {@link android.Manifest.permission#BLUETOOTH_ADMIN} permission.
51  *
52  * @see ScanFilter
53  */
54 public final class BluetoothLeScanner {
55 
56     private static final String TAG = "BluetoothLeScanner";
57     private static final boolean DBG = true;
58     private static final boolean VDBG = false;
59 
60     private final IBluetoothManager mBluetoothManager;
61     private final Handler mHandler;
62     private BluetoothAdapter mBluetoothAdapter;
63     private final Map<ScanCallback, BleScanCallbackWrapper> mLeScanClients;
64 
65     /**
66      * Use {@link BluetoothAdapter#getBluetoothLeScanner()} instead.
67      *
68      * @param bluetoothManager BluetoothManager that conducts overall Bluetooth Management.
69      * @hide
70      */
BluetoothLeScanner(IBluetoothManager bluetoothManager)71     public BluetoothLeScanner(IBluetoothManager bluetoothManager) {
72         mBluetoothManager = bluetoothManager;
73         mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
74         mHandler = new Handler(Looper.getMainLooper());
75         mLeScanClients = new HashMap<ScanCallback, BleScanCallbackWrapper>();
76     }
77 
78     /**
79      * Start Bluetooth LE scan with default parameters and no filters. The scan results will be
80      * delivered through {@code callback}.
81      * <p>
82      * Requires {@link android.Manifest.permission#BLUETOOTH_ADMIN} permission.
83      * An app must hold
84      * {@link android.Manifest.permission#ACCESS_COARSE_LOCATION ACCESS_COARSE_LOCATION} or
85      * {@link android.Manifest.permission#ACCESS_FINE_LOCATION ACCESS_FINE_LOCATION} permission
86      * in order to get results.
87      *
88      * @param callback Callback used to deliver scan results.
89      * @throws IllegalArgumentException If {@code callback} is null.
90      */
91     @RequiresPermission(Manifest.permission.BLUETOOTH_ADMIN)
startScan(final ScanCallback callback)92     public void startScan(final ScanCallback callback) {
93         startScan(null, new ScanSettings.Builder().build(), callback);
94     }
95 
96     /**
97      * Start Bluetooth LE scan. The scan results will be delivered through {@code callback}.
98      * <p>
99      * Requires {@link android.Manifest.permission#BLUETOOTH_ADMIN} permission.
100      * An app must hold
101      * {@link android.Manifest.permission#ACCESS_COARSE_LOCATION ACCESS_COARSE_LOCATION} or
102      * {@link android.Manifest.permission#ACCESS_FINE_LOCATION ACCESS_FINE_LOCATION} permission
103      * in order to get results.
104      *
105      * @param filters {@link ScanFilter}s for finding exact BLE devices.
106      * @param settings Settings for the scan.
107      * @param callback Callback used to deliver scan results.
108      * @throws IllegalArgumentException If {@code settings} or {@code callback} is null.
109      */
110     @RequiresPermission(Manifest.permission.BLUETOOTH_ADMIN)
startScan(List<ScanFilter> filters, ScanSettings settings, final ScanCallback callback)111     public void startScan(List<ScanFilter> filters, ScanSettings settings,
112             final ScanCallback callback) {
113         startScan(filters, settings, null, callback, null);
114     }
115 
116     /**
117      * Start Bluetooth LE scan. Same as {@link #startScan(ScanCallback)} but allows the caller to
118      * specify on behalf of which application(s) the work is being done.
119      *
120      * @param workSource {@link WorkSource} identifying the application(s) for which to blame for
121      *                   the scan.
122      * @param callback Callback used to deliver scan results.
123      * @hide
124      */
125     @SystemApi
126     @RequiresPermission(allOf = {
127             Manifest.permission.BLUETOOTH_ADMIN, Manifest.permission.UPDATE_DEVICE_STATS })
startScanFromSource(final WorkSource workSource, final ScanCallback callback)128     public void startScanFromSource(final WorkSource workSource, final ScanCallback callback) {
129         startScanFromSource(null, new ScanSettings.Builder().build(), workSource, callback);
130     }
131 
132     /**
133      * Start Bluetooth LE scan. Same as {@link #startScan(List, ScanSettings, ScanCallback)} but
134      * allows the caller to specify on behalf of which application(s) the work is being done.
135      *
136      * @param filters {@link ScanFilter}s for finding exact BLE devices.
137      * @param settings Settings for the scan.
138      * @param workSource {@link WorkSource} identifying the application(s) for which to blame for
139      *                   the scan.
140      * @param callback Callback used to deliver scan results.
141      * @hide
142      */
143     @SystemApi
144     @RequiresPermission(allOf = {
145             Manifest.permission.BLUETOOTH_ADMIN, Manifest.permission.UPDATE_DEVICE_STATS })
startScanFromSource(List<ScanFilter> filters, ScanSettings settings, final WorkSource workSource, final ScanCallback callback)146     public void startScanFromSource(List<ScanFilter> filters, ScanSettings settings,
147                                     final WorkSource workSource, final ScanCallback callback) {
148         startScan(filters, settings, workSource, callback, null);
149     }
150 
startScan(List<ScanFilter> filters, ScanSettings settings, final WorkSource workSource, final ScanCallback callback, List<List<ResultStorageDescriptor>> resultStorages)151     private void startScan(List<ScanFilter> filters, ScanSettings settings,
152                            final WorkSource workSource, final ScanCallback callback,
153                            List<List<ResultStorageDescriptor>> resultStorages) {
154         BluetoothLeUtils.checkAdapterStateOn(mBluetoothAdapter);
155         if (callback == null) {
156             throw new IllegalArgumentException("callback is null");
157         }
158         if (settings == null) {
159             throw new IllegalArgumentException("settings is null");
160         }
161         synchronized (mLeScanClients) {
162             if (mLeScanClients.containsKey(callback)) {
163                 postCallbackError(callback, ScanCallback.SCAN_FAILED_ALREADY_STARTED);
164                 return;
165             }
166             IBluetoothGatt gatt;
167             try {
168                 gatt = mBluetoothManager.getBluetoothGatt();
169             } catch (RemoteException e) {
170                 gatt = null;
171             }
172             if (gatt == null) {
173                 postCallbackError(callback, ScanCallback.SCAN_FAILED_INTERNAL_ERROR);
174                 return;
175             }
176             if (!isSettingsConfigAllowedForScan(settings)) {
177                 postCallbackError(callback,
178                         ScanCallback.SCAN_FAILED_FEATURE_UNSUPPORTED);
179                 return;
180             }
181             if (!isHardwareResourcesAvailableForScan(settings)) {
182                 postCallbackError(callback,
183                         ScanCallback.SCAN_FAILED_OUT_OF_HARDWARE_RESOURCES);
184                 return;
185             }
186             if (!isSettingsAndFilterComboAllowed(settings, filters)) {
187                 postCallbackError(callback,
188                         ScanCallback.SCAN_FAILED_FEATURE_UNSUPPORTED);
189                 return;
190             }
191             BleScanCallbackWrapper wrapper = new BleScanCallbackWrapper(gatt, filters,
192                     settings, workSource, callback, resultStorages);
193             wrapper.startRegisteration();
194         }
195     }
196 
197     /**
198      * Stops an ongoing Bluetooth LE scan.
199      * <p>
200      * Requires {@link android.Manifest.permission#BLUETOOTH_ADMIN} permission.
201      *
202      * @param callback
203      */
204     @RequiresPermission(Manifest.permission.BLUETOOTH_ADMIN)
stopScan(ScanCallback callback)205     public void stopScan(ScanCallback callback) {
206         BluetoothLeUtils.checkAdapterStateOn(mBluetoothAdapter);
207         synchronized (mLeScanClients) {
208             BleScanCallbackWrapper wrapper = mLeScanClients.remove(callback);
209             if (wrapper == null) {
210                 if (DBG) Log.d(TAG, "could not find callback wrapper");
211                 return;
212             }
213             wrapper.stopLeScan();
214         }
215     }
216 
217     /**
218      * Flush pending batch scan results stored in Bluetooth controller. This will return Bluetooth
219      * LE scan results batched on bluetooth controller. Returns immediately, batch scan results data
220      * will be delivered through the {@code callback}.
221      *
222      * @param callback Callback of the Bluetooth LE Scan, it has to be the same instance as the one
223      *            used to start scan.
224      */
flushPendingScanResults(ScanCallback callback)225     public void flushPendingScanResults(ScanCallback callback) {
226         BluetoothLeUtils.checkAdapterStateOn(mBluetoothAdapter);
227         if (callback == null) {
228             throw new IllegalArgumentException("callback cannot be null!");
229         }
230         synchronized (mLeScanClients) {
231             BleScanCallbackWrapper wrapper = mLeScanClients.get(callback);
232             if (wrapper == null) {
233                 return;
234             }
235             wrapper.flushPendingBatchResults();
236         }
237     }
238 
239     /**
240      * Start truncated scan.
241      *
242      * @hide
243      */
244     @SystemApi
startTruncatedScan(List<TruncatedFilter> truncatedFilters, ScanSettings settings, final ScanCallback callback)245     public void startTruncatedScan(List<TruncatedFilter> truncatedFilters, ScanSettings settings,
246             final ScanCallback callback) {
247         int filterSize = truncatedFilters.size();
248         List<ScanFilter> scanFilters = new ArrayList<ScanFilter>(filterSize);
249         List<List<ResultStorageDescriptor>> scanStorages =
250                 new ArrayList<List<ResultStorageDescriptor>>(filterSize);
251         for (TruncatedFilter filter : truncatedFilters) {
252             scanFilters.add(filter.getFilter());
253             scanStorages.add(filter.getStorageDescriptors());
254         }
255         startScan(scanFilters, settings, null, callback, scanStorages);
256     }
257 
258     /**
259      * Cleans up scan clients. Should be called when bluetooth is down.
260      *
261      * @hide
262      */
cleanup()263     public void cleanup() {
264         mLeScanClients.clear();
265     }
266 
267     /**
268      * Bluetooth GATT interface callbacks
269      */
270     private class BleScanCallbackWrapper extends BluetoothGattCallbackWrapper {
271         private static final int REGISTRATION_CALLBACK_TIMEOUT_MILLIS = 2000;
272 
273         private final ScanCallback mScanCallback;
274         private final List<ScanFilter> mFilters;
275         private final WorkSource mWorkSource;
276         private ScanSettings mSettings;
277         private IBluetoothGatt mBluetoothGatt;
278         private List<List<ResultStorageDescriptor>> mResultStorages;
279 
280         // mLeHandle 0: not registered
281         // -1: scan stopped or registration failed
282         // > 0: registered and scan started
283         private int mClientIf;
284 
BleScanCallbackWrapper(IBluetoothGatt bluetoothGatt, List<ScanFilter> filters, ScanSettings settings, WorkSource workSource, ScanCallback scanCallback, List<List<ResultStorageDescriptor>> resultStorages)285         public BleScanCallbackWrapper(IBluetoothGatt bluetoothGatt,
286                 List<ScanFilter> filters, ScanSettings settings,
287                 WorkSource workSource, ScanCallback scanCallback,
288                 List<List<ResultStorageDescriptor>> resultStorages) {
289             mBluetoothGatt = bluetoothGatt;
290             mFilters = filters;
291             mSettings = settings;
292             mWorkSource = workSource;
293             mScanCallback = scanCallback;
294             mClientIf = 0;
295             mResultStorages = resultStorages;
296         }
297 
startRegisteration()298         public void startRegisteration() {
299             synchronized (this) {
300                 // Scan stopped.
301                 if (mClientIf == -1) return;
302                 try {
303                     UUID uuid = UUID.randomUUID();
304                     mBluetoothGatt.registerClient(new ParcelUuid(uuid), this);
305                     wait(REGISTRATION_CALLBACK_TIMEOUT_MILLIS);
306                 } catch (InterruptedException | RemoteException e) {
307                     Log.e(TAG, "application registeration exception", e);
308                     postCallbackError(mScanCallback, ScanCallback.SCAN_FAILED_INTERNAL_ERROR);
309                 }
310                 if (mClientIf > 0) {
311                     mLeScanClients.put(mScanCallback, this);
312                 } else {
313                     // Registration timed out or got exception, reset clientIf to -1 so no
314                     // subsequent operations can proceed.
315                     if (mClientIf == 0) mClientIf = -1;
316                     postCallbackError(mScanCallback,
317                             ScanCallback.SCAN_FAILED_APPLICATION_REGISTRATION_FAILED);
318                 }
319             }
320         }
321 
stopLeScan()322         public void stopLeScan() {
323             synchronized (this) {
324                 if (mClientIf <= 0) {
325                     Log.e(TAG, "Error state, mLeHandle: " + mClientIf);
326                     return;
327                 }
328                 try {
329                     mBluetoothGatt.stopScan(mClientIf, false);
330                     mBluetoothGatt.unregisterClient(mClientIf);
331                 } catch (RemoteException e) {
332                     Log.e(TAG, "Failed to stop scan and unregister", e);
333                 }
334                 mClientIf = -1;
335             }
336         }
337 
flushPendingBatchResults()338         void flushPendingBatchResults() {
339             synchronized (this) {
340                 if (mClientIf <= 0) {
341                     Log.e(TAG, "Error state, mLeHandle: " + mClientIf);
342                     return;
343                 }
344                 try {
345                     mBluetoothGatt.flushPendingBatchResults(mClientIf, false);
346                 } catch (RemoteException e) {
347                     Log.e(TAG, "Failed to get pending scan results", e);
348                 }
349             }
350         }
351 
352         /**
353          * Application interface registered - app is ready to go
354          */
355         @Override
onClientRegistered(int status, int clientIf)356         public void onClientRegistered(int status, int clientIf) {
357             Log.d(TAG, "onClientRegistered() - status=" + status +
358                     " clientIf=" + clientIf + " mClientIf=" + mClientIf);
359             synchronized (this) {
360                 if (status == BluetoothGatt.GATT_SUCCESS) {
361                     try {
362                         if (mClientIf == -1) {
363                             // Registration succeeds after timeout, unregister client.
364                             mBluetoothGatt.unregisterClient(clientIf);
365                         } else {
366                             mClientIf = clientIf;
367                             mBluetoothGatt.startScan(mClientIf, false, mSettings, mFilters,
368                                     mWorkSource, mResultStorages,
369                                     ActivityThread.currentOpPackageName());
370                         }
371                     } catch (RemoteException e) {
372                         Log.e(TAG, "fail to start le scan: " + e);
373                         mClientIf = -1;
374                     }
375                 } else {
376                     // registration failed
377                     mClientIf = -1;
378                 }
379                 notifyAll();
380             }
381         }
382 
383         /**
384          * Callback reporting an LE scan result.
385          *
386          * @hide
387          */
388         @Override
onScanResult(final ScanResult scanResult)389         public void onScanResult(final ScanResult scanResult) {
390             if (VDBG) Log.d(TAG, "onScanResult() - " + scanResult.toString());
391 
392             // Check null in case the scan has been stopped
393             synchronized (this) {
394                 if (mClientIf <= 0) return;
395             }
396             Handler handler = new Handler(Looper.getMainLooper());
397             handler.post(new Runnable() {
398                 @Override
399                 public void run() {
400                     mScanCallback.onScanResult(ScanSettings.CALLBACK_TYPE_ALL_MATCHES, scanResult);
401                 }
402             });
403 
404         }
405 
406         @Override
onBatchScanResults(final List<ScanResult> results)407         public void onBatchScanResults(final List<ScanResult> results) {
408             Handler handler = new Handler(Looper.getMainLooper());
409             handler.post(new Runnable() {
410                 @Override
411                 public void run() {
412                     mScanCallback.onBatchScanResults(results);
413                 }
414             });
415         }
416 
417         @Override
onFoundOrLost(final boolean onFound, final ScanResult scanResult)418         public void onFoundOrLost(final boolean onFound, final ScanResult scanResult) {
419             if (VDBG) {
420                 Log.d(TAG, "onFoundOrLost() - onFound = " + onFound +
421                         " " + scanResult.toString());
422             }
423 
424             // Check null in case the scan has been stopped
425             synchronized (this) {
426                 if (mClientIf <= 0)
427                     return;
428             }
429             Handler handler = new Handler(Looper.getMainLooper());
430             handler.post(new Runnable() {
431                     @Override
432                 public void run() {
433                     if (onFound) {
434                         mScanCallback.onScanResult(ScanSettings.CALLBACK_TYPE_FIRST_MATCH,
435                                 scanResult);
436                     } else {
437                         mScanCallback.onScanResult(ScanSettings.CALLBACK_TYPE_MATCH_LOST,
438                                 scanResult);
439                     }
440                 }
441             });
442         }
443 
444         @Override
onScanManagerErrorCallback(final int errorCode)445         public void onScanManagerErrorCallback(final int errorCode) {
446             if (VDBG) {
447                 Log.d(TAG, "onScanManagerErrorCallback() - errorCode = " + errorCode);
448             }
449             synchronized (this) {
450                 if (mClientIf <= 0)
451                     return;
452             }
453             postCallbackError(mScanCallback, errorCode);
454         }
455     }
456 
postCallbackError(final ScanCallback callback, final int errorCode)457     private void postCallbackError(final ScanCallback callback, final int errorCode) {
458         mHandler.post(new Runnable() {
459             @Override
460             public void run() {
461                 callback.onScanFailed(errorCode);
462             }
463         });
464     }
465 
isSettingsConfigAllowedForScan(ScanSettings settings)466     private boolean isSettingsConfigAllowedForScan(ScanSettings settings) {
467         if (mBluetoothAdapter.isOffloadedFilteringSupported()) {
468             return true;
469         }
470         final int callbackType = settings.getCallbackType();
471         // Only support regular scan if no offloaded filter support.
472         if (callbackType == ScanSettings.CALLBACK_TYPE_ALL_MATCHES
473                 && settings.getReportDelayMillis() == 0) {
474             return true;
475         }
476         return false;
477     }
478 
isSettingsAndFilterComboAllowed(ScanSettings settings, List <ScanFilter> filterList)479     private boolean isSettingsAndFilterComboAllowed(ScanSettings settings,
480                         List <ScanFilter> filterList) {
481         final int callbackType = settings.getCallbackType();
482         // If onlost/onfound is requested, a non-empty filter is expected
483         if ((callbackType & (ScanSettings.CALLBACK_TYPE_FIRST_MATCH
484                         | ScanSettings.CALLBACK_TYPE_MATCH_LOST)) != 0) {
485             if (filterList == null) {
486                 return false;
487             }
488             for (ScanFilter filter : filterList) {
489                 if (filter.isAllFieldsEmpty()) {
490                     return false;
491                 }
492             }
493         }
494         return true;
495     }
496 
isHardwareResourcesAvailableForScan(ScanSettings settings)497     private boolean isHardwareResourcesAvailableForScan(ScanSettings settings) {
498         final int callbackType = settings.getCallbackType();
499         if ((callbackType & ScanSettings.CALLBACK_TYPE_FIRST_MATCH) != 0
500                 || (callbackType & ScanSettings.CALLBACK_TYPE_MATCH_LOST) != 0) {
501             // For onlost/onfound, we required hw support be available
502             return (mBluetoothAdapter.isOffloadedFilteringSupported() &&
503                     mBluetoothAdapter.isHardwareTrackingFiltersAvailable());
504         }
505         return true;
506     }
507 }
508