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