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    private static final boolean VDBG = false;
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        if (callback == null) {
85            throw new IllegalArgumentException("callback is null");
86        }
87        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        BluetoothLeUtils.checkAdapterStateOn(mBluetoothAdapter);
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            wrapper.startRegisteration();
134        }
135    }
136
137    /**
138     * Stops an ongoing Bluetooth LE scan.
139     * <p>
140     * Requires {@link android.Manifest.permission#BLUETOOTH_ADMIN} permission.
141     *
142     * @param callback
143     */
144    public void stopScan(ScanCallback callback) {
145        BluetoothLeUtils.checkAdapterStateOn(mBluetoothAdapter);
146        synchronized (mLeScanClients) {
147            BleScanCallbackWrapper wrapper = mLeScanClients.remove(callback);
148            if (wrapper == null) {
149                if (DBG) Log.d(TAG, "could not find callback wrapper");
150                return;
151            }
152            wrapper.stopLeScan();
153        }
154    }
155
156    /**
157     * Flush pending batch scan results stored in Bluetooth controller. This will return Bluetooth
158     * LE scan results batched on bluetooth controller. Returns immediately, batch scan results data
159     * will be delivered through the {@code callback}.
160     *
161     * @param callback Callback of the Bluetooth LE Scan, it has to be the same instance as the one
162     *            used to start scan.
163     */
164    public void flushPendingScanResults(ScanCallback callback) {
165        BluetoothLeUtils.checkAdapterStateOn(mBluetoothAdapter);
166        if (callback == null) {
167            throw new IllegalArgumentException("callback cannot be null!");
168        }
169        synchronized (mLeScanClients) {
170            BleScanCallbackWrapper wrapper = mLeScanClients.get(callback);
171            if (wrapper == null) {
172                return;
173            }
174            wrapper.flushPendingBatchResults();
175        }
176    }
177
178    /**
179     * Start truncated scan.
180     *
181     * @hide
182     */
183    @SystemApi
184    public void startTruncatedScan(List<TruncatedFilter> truncatedFilters, ScanSettings settings,
185            final ScanCallback callback) {
186        int filterSize = truncatedFilters.size();
187        List<ScanFilter> scanFilters = new ArrayList<ScanFilter>(filterSize);
188        List<List<ResultStorageDescriptor>> scanStorages =
189                new ArrayList<List<ResultStorageDescriptor>>(filterSize);
190        for (TruncatedFilter filter : truncatedFilters) {
191            scanFilters.add(filter.getFilter());
192            scanStorages.add(filter.getStorageDescriptors());
193        }
194        startScan(scanFilters, settings, callback, scanStorages);
195    }
196
197    /**
198     * Cleans up scan clients. Should be called when bluetooth is down.
199     *
200     * @hide
201     */
202    public void cleanup() {
203        mLeScanClients.clear();
204    }
205
206    /**
207     * Bluetooth GATT interface callbacks
208     */
209    private class BleScanCallbackWrapper extends BluetoothGattCallbackWrapper {
210        private static final int REGISTRATION_CALLBACK_TIMEOUT_MILLIS = 2000;
211
212        private final ScanCallback mScanCallback;
213        private final List<ScanFilter> mFilters;
214        private ScanSettings mSettings;
215        private IBluetoothGatt mBluetoothGatt;
216        private List<List<ResultStorageDescriptor>> mResultStorages;
217
218        // mLeHandle 0: not registered
219        // -1: scan stopped
220        // > 0: registered and scan started
221        private int mClientIf;
222
223        public BleScanCallbackWrapper(IBluetoothGatt bluetoothGatt,
224                List<ScanFilter> filters, ScanSettings settings,
225                ScanCallback scanCallback, List<List<ResultStorageDescriptor>> resultStorages) {
226            mBluetoothGatt = bluetoothGatt;
227            mFilters = filters;
228            mSettings = settings;
229            mScanCallback = scanCallback;
230            mClientIf = 0;
231            mResultStorages = resultStorages;
232        }
233
234        public void startRegisteration() {
235            synchronized (this) {
236                // Scan stopped.
237                if (mClientIf == -1) return;
238                try {
239                    UUID uuid = UUID.randomUUID();
240                    mBluetoothGatt.registerClient(new ParcelUuid(uuid), this);
241                    wait(REGISTRATION_CALLBACK_TIMEOUT_MILLIS);
242                } catch (InterruptedException | RemoteException e) {
243                    Log.e(TAG, "application registeration exception", e);
244                    postCallbackError(mScanCallback, ScanCallback.SCAN_FAILED_INTERNAL_ERROR);
245                }
246                if (mClientIf > 0) {
247                    mLeScanClients.put(mScanCallback, this);
248                } else {
249                    postCallbackError(mScanCallback,
250                            ScanCallback.SCAN_FAILED_APPLICATION_REGISTRATION_FAILED);
251                }
252            }
253        }
254
255        public void stopLeScan() {
256            synchronized (this) {
257                if (mClientIf <= 0) {
258                    Log.e(TAG, "Error state, mLeHandle: " + mClientIf);
259                    return;
260                }
261                try {
262                    mBluetoothGatt.stopScan(mClientIf, false);
263                    mBluetoothGatt.unregisterClient(mClientIf);
264                } catch (RemoteException e) {
265                    Log.e(TAG, "Failed to stop scan and unregister", e);
266                }
267                mClientIf = -1;
268            }
269        }
270
271        void flushPendingBatchResults() {
272            synchronized (this) {
273                if (mClientIf <= 0) {
274                    Log.e(TAG, "Error state, mLeHandle: " + mClientIf);
275                    return;
276                }
277                try {
278                    mBluetoothGatt.flushPendingBatchResults(mClientIf, false);
279                } catch (RemoteException e) {
280                    Log.e(TAG, "Failed to get pending scan results", e);
281                }
282            }
283        }
284
285        /**
286         * Application interface registered - app is ready to go
287         */
288        @Override
289        public void onClientRegistered(int status, int clientIf) {
290            Log.d(TAG, "onClientRegistered() - status=" + status +
291                    " clientIf=" + clientIf);
292            synchronized (this) {
293                if (mClientIf == -1) {
294                    if (DBG) Log.d(TAG, "onClientRegistered LE scan canceled");
295                }
296
297                if (status == BluetoothGatt.GATT_SUCCESS) {
298                    mClientIf = clientIf;
299                    try {
300                        mBluetoothGatt.startScan(mClientIf, false, mSettings, mFilters,
301                                mResultStorages);
302                    } catch (RemoteException e) {
303                        Log.e(TAG, "fail to start le scan: " + e);
304                        mClientIf = -1;
305                    }
306                } else {
307                    // registration failed
308                    mClientIf = -1;
309                }
310                notifyAll();
311            }
312        }
313
314        /**
315         * Callback reporting an LE scan result.
316         *
317         * @hide
318         */
319        @Override
320        public void onScanResult(final ScanResult scanResult) {
321            if (VDBG) Log.d(TAG, "onScanResult() - " + scanResult.toString());
322
323            // Check null in case the scan has been stopped
324            synchronized (this) {
325                if (mClientIf <= 0) return;
326            }
327            Handler handler = new Handler(Looper.getMainLooper());
328            handler.post(new Runnable() {
329                @Override
330                public void run() {
331                    mScanCallback.onScanResult(ScanSettings.CALLBACK_TYPE_ALL_MATCHES, scanResult);
332                }
333            });
334
335        }
336
337        @Override
338        public void onBatchScanResults(final List<ScanResult> results) {
339            Handler handler = new Handler(Looper.getMainLooper());
340            handler.post(new Runnable() {
341                @Override
342                public void run() {
343                    mScanCallback.onBatchScanResults(results);
344                }
345            });
346        }
347
348        @Override
349        public void onFoundOrLost(final boolean onFound, final ScanResult scanResult) {
350            if (VDBG) {
351                Log.d(TAG, "onFoundOrLost() - onFound = " + onFound +
352                        " " + scanResult.toString());
353            }
354
355            // Check null in case the scan has been stopped
356            synchronized (this) {
357                if (mClientIf <= 0)
358                    return;
359            }
360            Handler handler = new Handler(Looper.getMainLooper());
361            handler.post(new Runnable() {
362                    @Override
363                public void run() {
364                    if (onFound) {
365                        mScanCallback.onScanResult(ScanSettings.CALLBACK_TYPE_FIRST_MATCH,
366                                scanResult);
367                    } else {
368                        mScanCallback.onScanResult(ScanSettings.CALLBACK_TYPE_MATCH_LOST,
369                                scanResult);
370                    }
371                }
372            });
373        }
374    }
375
376    private void postCallbackError(final ScanCallback callback, final int errorCode) {
377        mHandler.post(new Runnable() {
378            @Override
379            public void run() {
380                callback.onScanFailed(errorCode);
381            }
382        });
383    }
384
385    private boolean isSettingsConfigAllowedForScan(ScanSettings settings) {
386        if (mBluetoothAdapter.isOffloadedFilteringSupported()) {
387            return true;
388        }
389        final int callbackType = settings.getCallbackType();
390        // Only support regular scan if no offloaded filter support.
391        if (callbackType == ScanSettings.CALLBACK_TYPE_ALL_MATCHES
392                && settings.getReportDelayMillis() == 0) {
393            return true;
394        }
395        return false;
396    }
397}
398