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.RequiresPermission;
21import android.annotation.SystemApi;
22import android.app.ActivityThread;
23import android.bluetooth.BluetoothAdapter;
24import android.bluetooth.BluetoothGatt;
25import android.bluetooth.BluetoothGattCallbackWrapper;
26import android.bluetooth.IBluetoothGatt;
27import android.bluetooth.IBluetoothManager;
28import android.os.Handler;
29import android.os.Looper;
30import android.os.ParcelUuid;
31import android.os.RemoteException;
32import android.os.WorkSource;
33import android.util.Log;
34
35import java.util.ArrayList;
36import java.util.HashMap;
37import java.util.List;
38import java.util.Map;
39import 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 */
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    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     */
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)
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)
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 })
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 })
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
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)
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     */
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
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     */
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
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
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
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
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
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
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
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
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
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
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
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
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
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