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
17package android.bluetooth.le;
18
19import android.Manifest;
20import android.annotation.NonNull;
21import android.annotation.Nullable;
22import android.annotation.RequiresPermission;
23import android.annotation.SystemApi;
24import android.app.ActivityThread;
25import android.app.PendingIntent;
26import android.bluetooth.BluetoothAdapter;
27import android.bluetooth.BluetoothGatt;
28import android.bluetooth.IBluetoothGatt;
29import android.bluetooth.IBluetoothManager;
30import android.os.Handler;
31import android.os.Looper;
32import android.os.RemoteException;
33import android.os.WorkSource;
34import android.util.Log;
35
36import java.util.ArrayList;
37import java.util.HashMap;
38import java.util.List;
39import java.util.Map;
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 */
54public 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    /**
61     * Extra containing a list of ScanResults. It can have one or more results if there was no
62     * error. In case of error, {@link #EXTRA_ERROR_CODE} will contain the error code and this
63     * extra will not be available.
64     */
65    public static final String EXTRA_LIST_SCAN_RESULT
66            = "android.bluetooth.le.extra.LIST_SCAN_RESULT";
67
68    /**
69     * Optional extra indicating the error code, if any. The error code will be one of the
70     * SCAN_FAILED_* codes in {@link ScanCallback}.
71     */
72    public static final String EXTRA_ERROR_CODE = "android.bluetooth.le.extra.ERROR_CODE";
73
74    /**
75     * Optional extra indicating the callback type, which will be one of
76     * CALLBACK_TYPE_* constants in {@link ScanSettings}.
77     * @see ScanCallback#onScanResult(int, ScanResult)
78     */
79    public static final String EXTRA_CALLBACK_TYPE = "android.bluetooth.le.extra.CALLBACK_TYPE";
80
81    private final IBluetoothManager mBluetoothManager;
82    private final Handler mHandler;
83    private BluetoothAdapter mBluetoothAdapter;
84    private final Map<ScanCallback, BleScanCallbackWrapper> mLeScanClients;
85
86    /**
87     * Use {@link BluetoothAdapter#getBluetoothLeScanner()} instead.
88     *
89     * @param bluetoothManager BluetoothManager that conducts overall Bluetooth Management.
90     * @hide
91     */
92    public BluetoothLeScanner(IBluetoothManager bluetoothManager) {
93        mBluetoothManager = bluetoothManager;
94        mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
95        mHandler = new Handler(Looper.getMainLooper());
96        mLeScanClients = new HashMap<ScanCallback, BleScanCallbackWrapper>();
97    }
98
99    /**
100     * Start Bluetooth LE scan with default parameters and no filters. The scan results will be
101     * delivered through {@code callback}.
102     * <p>
103     * An app must hold
104     * {@link android.Manifest.permission#ACCESS_COARSE_LOCATION ACCESS_COARSE_LOCATION} or
105     * {@link android.Manifest.permission#ACCESS_FINE_LOCATION ACCESS_FINE_LOCATION} permission
106     * in order to get results.
107     *
108     * @param callback Callback used to deliver scan results.
109     * @throws IllegalArgumentException If {@code callback} is null.
110     */
111    @RequiresPermission(Manifest.permission.BLUETOOTH_ADMIN)
112    public void startScan(final ScanCallback callback) {
113        startScan(null, new ScanSettings.Builder().build(), callback);
114    }
115
116    /**
117     * Start Bluetooth LE scan. The scan results will be delivered through {@code callback}.
118     * <p>
119     * An app must hold
120     * {@link android.Manifest.permission#ACCESS_COARSE_LOCATION ACCESS_COARSE_LOCATION} or
121     * {@link android.Manifest.permission#ACCESS_FINE_LOCATION ACCESS_FINE_LOCATION} permission
122     * in order to get results.
123     *
124     * @param filters {@link ScanFilter}s for finding exact BLE devices.
125     * @param settings Settings for the scan.
126     * @param callback Callback used to deliver scan results.
127     * @throws IllegalArgumentException If {@code settings} or {@code callback} is null.
128     */
129    @RequiresPermission(Manifest.permission.BLUETOOTH_ADMIN)
130    public void startScan(List<ScanFilter> filters, ScanSettings settings,
131            final ScanCallback callback) {
132        startScan(filters, settings, null, callback, /*callbackIntent=*/ null, null);
133    }
134
135    /**
136     * Start Bluetooth LE scan using a {@link PendingIntent}. The scan results will be delivered via
137     * the PendingIntent. Use this method of scanning if your process is not always running and it
138     * should be started when scan results are available.
139     * <p>
140     * An app must hold
141     * {@link android.Manifest.permission#ACCESS_COARSE_LOCATION ACCESS_COARSE_LOCATION} or
142     * {@link android.Manifest.permission#ACCESS_FINE_LOCATION ACCESS_FINE_LOCATION} permission
143     * in order to get results.
144     * <p>
145     * When the PendingIntent is delivered, the Intent passed to the receiver or activity
146     * will contain one or more of the extras {@link #EXTRA_CALLBACK_TYPE},
147     * {@link #EXTRA_ERROR_CODE} and {@link #EXTRA_LIST_SCAN_RESULT} to indicate the result of
148     * the scan.
149     *
150     * @param filters Optional list of ScanFilters for finding exact BLE devices.
151     * @param settings Optional settings for the scan.
152     * @param callbackIntent The PendingIntent to deliver the result to.
153     * @return Returns 0 for success or an error code from {@link ScanCallback} if the scan request
154     * could not be sent.
155     * @see #stopScan(PendingIntent)
156     */
157    @RequiresPermission(Manifest.permission.BLUETOOTH_ADMIN)
158    public int startScan(@Nullable List<ScanFilter> filters, @Nullable ScanSettings settings,
159            @NonNull PendingIntent callbackIntent) {
160        return startScan(filters,
161                settings != null ? settings : new ScanSettings.Builder().build(),
162                null, null, callbackIntent, null);
163    }
164
165    /**
166     * Start Bluetooth LE scan. Same as {@link #startScan(ScanCallback)} but allows the caller to
167     * specify on behalf of which application(s) the work is being done.
168     *
169     * @param workSource {@link WorkSource} identifying the application(s) for which to blame for
170     *                   the scan.
171     * @param callback Callback used to deliver scan results.
172     * @hide
173     */
174    @SystemApi
175    @RequiresPermission(allOf = {
176            Manifest.permission.BLUETOOTH_ADMIN, Manifest.permission.UPDATE_DEVICE_STATS })
177    public void startScanFromSource(final WorkSource workSource, final ScanCallback callback) {
178        startScanFromSource(null, new ScanSettings.Builder().build(), workSource, callback);
179    }
180
181    /**
182     * Start Bluetooth LE scan. Same as {@link #startScan(List, ScanSettings, ScanCallback)} but
183     * allows the caller to specify on behalf of which application(s) the work is being done.
184     *
185     * @param filters {@link ScanFilter}s for finding exact BLE devices.
186     * @param settings Settings for the scan.
187     * @param workSource {@link WorkSource} identifying the application(s) for which to blame for
188     *                   the scan.
189     * @param callback Callback used to deliver scan results.
190     * @hide
191     */
192    @SystemApi
193    @RequiresPermission(allOf = {
194            Manifest.permission.BLUETOOTH_ADMIN, Manifest.permission.UPDATE_DEVICE_STATS })
195    public void startScanFromSource(List<ScanFilter> filters, ScanSettings settings,
196                                    final WorkSource workSource, final ScanCallback callback) {
197        startScan(filters, settings, workSource, callback, null, null);
198    }
199
200    private int startScan(List<ScanFilter> filters, ScanSettings settings,
201                           final WorkSource workSource, final ScanCallback callback,
202                           final PendingIntent callbackIntent,
203                           List<List<ResultStorageDescriptor>> resultStorages) {
204        BluetoothLeUtils.checkAdapterStateOn(mBluetoothAdapter);
205        if (callback == null && callbackIntent == null) {
206            throw new IllegalArgumentException("callback is null");
207        }
208        if (settings == null) {
209            throw new IllegalArgumentException("settings is null");
210        }
211        synchronized (mLeScanClients) {
212            if (callback != null && mLeScanClients.containsKey(callback)) {
213                return postCallbackErrorOrReturn(callback,
214                            ScanCallback.SCAN_FAILED_ALREADY_STARTED);
215            }
216            IBluetoothGatt gatt;
217            try {
218                gatt = mBluetoothManager.getBluetoothGatt();
219            } catch (RemoteException e) {
220                gatt = null;
221            }
222            if (gatt == null) {
223                return postCallbackErrorOrReturn(callback, ScanCallback.SCAN_FAILED_INTERNAL_ERROR);
224            }
225            if (!isSettingsConfigAllowedForScan(settings)) {
226                return postCallbackErrorOrReturn(callback,
227                            ScanCallback.SCAN_FAILED_FEATURE_UNSUPPORTED);
228            }
229            if (!isHardwareResourcesAvailableForScan(settings)) {
230                return postCallbackErrorOrReturn(callback,
231                            ScanCallback.SCAN_FAILED_OUT_OF_HARDWARE_RESOURCES);
232            }
233            if (!isSettingsAndFilterComboAllowed(settings, filters)) {
234                return postCallbackErrorOrReturn(callback,
235                        ScanCallback.SCAN_FAILED_FEATURE_UNSUPPORTED);
236            }
237            if (callback != null) {
238                BleScanCallbackWrapper wrapper = new BleScanCallbackWrapper(gatt, filters,
239                        settings, workSource, callback, resultStorages);
240                wrapper.startRegistration();
241            } else {
242                try {
243                    gatt.startScanForIntent(callbackIntent, settings, filters,
244                            ActivityThread.currentOpPackageName());
245                } catch (RemoteException e) {
246                    return ScanCallback.SCAN_FAILED_INTERNAL_ERROR;
247                }
248            }
249        }
250        return ScanCallback.NO_ERROR;
251    }
252
253    /**
254     * Stops an ongoing Bluetooth LE scan.
255     *
256     * @param callback
257     */
258    @RequiresPermission(Manifest.permission.BLUETOOTH_ADMIN)
259    public void stopScan(ScanCallback callback) {
260        BluetoothLeUtils.checkAdapterStateOn(mBluetoothAdapter);
261        synchronized (mLeScanClients) {
262            BleScanCallbackWrapper wrapper = mLeScanClients.remove(callback);
263            if (wrapper == null) {
264                if (DBG) Log.d(TAG, "could not find callback wrapper");
265                return;
266            }
267            wrapper.stopLeScan();
268        }
269    }
270
271    /**
272     * Stops an ongoing Bluetooth LE scan started using a PendingIntent.
273     *
274     * @param callbackIntent The PendingIntent that was used to start the scan.
275     * @see #startScan(List, ScanSettings, PendingIntent)
276     */
277    @RequiresPermission(Manifest.permission.BLUETOOTH_ADMIN)
278    public void stopScan(PendingIntent callbackIntent) {
279        BluetoothLeUtils.checkAdapterStateOn(mBluetoothAdapter);
280        IBluetoothGatt gatt;
281        try {
282            gatt = mBluetoothManager.getBluetoothGatt();
283            gatt.stopScanForIntent(callbackIntent, ActivityThread.currentOpPackageName());
284        } catch (RemoteException e) {
285        }
286    }
287
288    /**
289     * Flush pending batch scan results stored in Bluetooth controller. This will return Bluetooth
290     * LE scan results batched on bluetooth controller. Returns immediately, batch scan results data
291     * will be delivered through the {@code callback}.
292     *
293     * @param callback Callback of the Bluetooth LE Scan, it has to be the same instance as the one
294     *            used to start scan.
295     */
296    public void flushPendingScanResults(ScanCallback callback) {
297        BluetoothLeUtils.checkAdapterStateOn(mBluetoothAdapter);
298        if (callback == null) {
299            throw new IllegalArgumentException("callback cannot be null!");
300        }
301        synchronized (mLeScanClients) {
302            BleScanCallbackWrapper wrapper = mLeScanClients.get(callback);
303            if (wrapper == null) {
304                return;
305            }
306            wrapper.flushPendingBatchResults();
307        }
308    }
309
310    /**
311     * Start truncated scan.
312     *
313     * @hide
314     */
315    @SystemApi
316    public void startTruncatedScan(List<TruncatedFilter> truncatedFilters, ScanSettings settings,
317            final ScanCallback callback) {
318        int filterSize = truncatedFilters.size();
319        List<ScanFilter> scanFilters = new ArrayList<ScanFilter>(filterSize);
320        List<List<ResultStorageDescriptor>> scanStorages =
321                new ArrayList<List<ResultStorageDescriptor>>(filterSize);
322        for (TruncatedFilter filter : truncatedFilters) {
323            scanFilters.add(filter.getFilter());
324            scanStorages.add(filter.getStorageDescriptors());
325        }
326        startScan(scanFilters, settings, null, callback, null, scanStorages);
327    }
328
329    /**
330     * Cleans up scan clients. Should be called when bluetooth is down.
331     *
332     * @hide
333     */
334    public void cleanup() {
335        mLeScanClients.clear();
336    }
337
338    /**
339     * Bluetooth GATT interface callbacks
340     */
341    private class BleScanCallbackWrapper extends IScannerCallback.Stub {
342        private static final int REGISTRATION_CALLBACK_TIMEOUT_MILLIS = 2000;
343
344        private final ScanCallback mScanCallback;
345        private final List<ScanFilter> mFilters;
346        private final WorkSource mWorkSource;
347        private ScanSettings mSettings;
348        private IBluetoothGatt mBluetoothGatt;
349        private List<List<ResultStorageDescriptor>> mResultStorages;
350
351        // mLeHandle 0: not registered
352        // -2: registration failed because app is scanning to frequently
353        // -1: scan stopped or registration failed
354        // > 0: registered and scan started
355        private int mScannerId;
356
357        public BleScanCallbackWrapper(IBluetoothGatt bluetoothGatt,
358                List<ScanFilter> filters, ScanSettings settings,
359                WorkSource workSource, ScanCallback scanCallback,
360                List<List<ResultStorageDescriptor>> resultStorages) {
361            mBluetoothGatt = bluetoothGatt;
362            mFilters = filters;
363            mSettings = settings;
364            mWorkSource = workSource;
365            mScanCallback = scanCallback;
366            mScannerId = 0;
367            mResultStorages = resultStorages;
368        }
369
370        public void startRegistration() {
371            synchronized (this) {
372                // Scan stopped.
373                if (mScannerId == -1 || mScannerId == -2) return;
374                try {
375                    mBluetoothGatt.registerScanner(this, mWorkSource);
376                    wait(REGISTRATION_CALLBACK_TIMEOUT_MILLIS);
377                } catch (InterruptedException | RemoteException e) {
378                    Log.e(TAG, "application registeration exception", e);
379                    postCallbackError(mScanCallback, ScanCallback.SCAN_FAILED_INTERNAL_ERROR);
380                }
381                if (mScannerId > 0) {
382                    mLeScanClients.put(mScanCallback, this);
383                } else {
384                    // Registration timed out or got exception, reset scannerId to -1 so no
385                    // subsequent operations can proceed.
386                    if (mScannerId == 0) mScannerId = -1;
387
388                    // If scanning too frequently, don't report anything to the app.
389                    if (mScannerId == -2) return;
390
391                    postCallbackError(mScanCallback,
392                            ScanCallback.SCAN_FAILED_APPLICATION_REGISTRATION_FAILED);
393                }
394            }
395        }
396
397        public void stopLeScan() {
398            synchronized (this) {
399                if (mScannerId <= 0) {
400                    Log.e(TAG, "Error state, mLeHandle: " + mScannerId);
401                    return;
402                }
403                try {
404                    mBluetoothGatt.stopScan(mScannerId);
405                    mBluetoothGatt.unregisterScanner(mScannerId);
406                } catch (RemoteException e) {
407                    Log.e(TAG, "Failed to stop scan and unregister", e);
408                }
409                mScannerId = -1;
410            }
411        }
412
413        void flushPendingBatchResults() {
414            synchronized (this) {
415                if (mScannerId <= 0) {
416                    Log.e(TAG, "Error state, mLeHandle: " + mScannerId);
417                    return;
418                }
419                try {
420                    mBluetoothGatt.flushPendingBatchResults(mScannerId);
421                } catch (RemoteException e) {
422                    Log.e(TAG, "Failed to get pending scan results", e);
423                }
424            }
425        }
426
427        /**
428         * Application interface registered - app is ready to go
429         */
430        @Override
431        public void onScannerRegistered(int status, int scannerId) {
432            Log.d(TAG, "onScannerRegistered() - status=" + status +
433                    " scannerId=" + scannerId + " mScannerId=" + mScannerId);
434            synchronized (this) {
435                if (status == BluetoothGatt.GATT_SUCCESS) {
436                    try {
437                        if (mScannerId == -1) {
438                            // Registration succeeds after timeout, unregister client.
439                            mBluetoothGatt.unregisterClient(scannerId);
440                        } else {
441                            mScannerId = scannerId;
442                            mBluetoothGatt.startScan(mScannerId, mSettings, mFilters,
443                                    mResultStorages,
444                                    ActivityThread.currentOpPackageName());
445                        }
446                    } catch (RemoteException e) {
447                        Log.e(TAG, "fail to start le scan: " + e);
448                        mScannerId = -1;
449                    }
450                } else if (status == ScanCallback.SCAN_FAILED_SCANNING_TOO_FREQUENTLY) {
451                    // applicaiton was scanning too frequently
452                    mScannerId = -2;
453                } else {
454                    // registration failed
455                    mScannerId = -1;
456                }
457                notifyAll();
458            }
459        }
460
461        /**
462         * Callback reporting an LE scan result.
463         *
464         * @hide
465         */
466        @Override
467        public void onScanResult(final ScanResult scanResult) {
468            if (VDBG) Log.d(TAG, "onScanResult() - " + scanResult.toString());
469
470            // Check null in case the scan has been stopped
471            synchronized (this) {
472                if (mScannerId <= 0) return;
473            }
474            Handler handler = new Handler(Looper.getMainLooper());
475            handler.post(new Runnable() {
476                @Override
477                public void run() {
478                    mScanCallback.onScanResult(ScanSettings.CALLBACK_TYPE_ALL_MATCHES, scanResult);
479                }
480            });
481        }
482
483        @Override
484        public void onBatchScanResults(final List<ScanResult> results) {
485            Handler handler = new Handler(Looper.getMainLooper());
486            handler.post(new Runnable() {
487                @Override
488                public void run() {
489                    mScanCallback.onBatchScanResults(results);
490                }
491            });
492        }
493
494        @Override
495        public void onFoundOrLost(final boolean onFound, final ScanResult scanResult) {
496            if (VDBG) {
497                Log.d(TAG, "onFoundOrLost() - onFound = " + onFound +
498                        " " + scanResult.toString());
499            }
500
501            // Check null in case the scan has been stopped
502            synchronized (this) {
503                if (mScannerId <= 0)
504                    return;
505            }
506            Handler handler = new Handler(Looper.getMainLooper());
507            handler.post(new Runnable() {
508                    @Override
509                public void run() {
510                    if (onFound) {
511                        mScanCallback.onScanResult(ScanSettings.CALLBACK_TYPE_FIRST_MATCH,
512                                scanResult);
513                    } else {
514                        mScanCallback.onScanResult(ScanSettings.CALLBACK_TYPE_MATCH_LOST,
515                                scanResult);
516                    }
517                }
518            });
519        }
520
521        @Override
522        public void onScanManagerErrorCallback(final int errorCode) {
523            if (VDBG) {
524                Log.d(TAG, "onScanManagerErrorCallback() - errorCode = " + errorCode);
525            }
526            synchronized (this) {
527                if (mScannerId <= 0)
528                    return;
529            }
530            postCallbackError(mScanCallback, errorCode);
531        }
532    }
533
534    private int postCallbackErrorOrReturn(final ScanCallback callback, final int errorCode) {
535        if (callback == null) {
536            return errorCode;
537        } else {
538            postCallbackError(callback, errorCode);
539            return ScanCallback.NO_ERROR;
540        }
541    }
542
543    private void postCallbackError(final ScanCallback callback, final int errorCode) {
544        mHandler.post(new Runnable() {
545            @Override
546            public void run() {
547                callback.onScanFailed(errorCode);
548            }
549        });
550    }
551
552    private boolean isSettingsConfigAllowedForScan(ScanSettings settings) {
553        if (mBluetoothAdapter.isOffloadedFilteringSupported()) {
554            return true;
555        }
556        final int callbackType = settings.getCallbackType();
557        // Only support regular scan if no offloaded filter support.
558        if (callbackType == ScanSettings.CALLBACK_TYPE_ALL_MATCHES
559                && settings.getReportDelayMillis() == 0) {
560            return true;
561        }
562        return false;
563    }
564
565    private boolean isSettingsAndFilterComboAllowed(ScanSettings settings,
566                        List <ScanFilter> filterList) {
567        final int callbackType = settings.getCallbackType();
568        // If onlost/onfound is requested, a non-empty filter is expected
569        if ((callbackType & (ScanSettings.CALLBACK_TYPE_FIRST_MATCH
570                        | ScanSettings.CALLBACK_TYPE_MATCH_LOST)) != 0) {
571            if (filterList == null) {
572                return false;
573            }
574            for (ScanFilter filter : filterList) {
575                if (filter.isAllFieldsEmpty()) {
576                    return false;
577                }
578            }
579        }
580        return true;
581    }
582
583    private boolean isHardwareResourcesAvailableForScan(ScanSettings settings) {
584        final int callbackType = settings.getCallbackType();
585        if ((callbackType & ScanSettings.CALLBACK_TYPE_FIRST_MATCH) != 0
586                || (callbackType & ScanSettings.CALLBACK_TYPE_MATCH_LOST) != 0) {
587            // For onlost/onfound, we required hw support be available
588            return (mBluetoothAdapter.isOffloadedFilteringSupported() &&
589                    mBluetoothAdapter.isHardwareTrackingFiltersAvailable());
590        }
591        return true;
592    }
593}
594