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