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