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.util.Log;
33
34import java.util.ArrayList;
35import java.util.HashMap;
36import java.util.List;
37import java.util.Map;
38import 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 */
53public 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     */
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)
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)
113    public void startScan(List<ScanFilter> filters, ScanSettings settings,
114            final ScanCallback callback) {
115        startScan(filters, settings, callback, null);
116    }
117
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)
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     */
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
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     */
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
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
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
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
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
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
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
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
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
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
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
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
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
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