/* * Copyright (C) 2014 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. */ package android.bluetooth.le; import android.Manifest; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.SystemApi; import android.app.ActivityThread; import android.app.PendingIntent; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothGatt; import android.bluetooth.IBluetoothGatt; import android.bluetooth.IBluetoothManager; import android.os.Handler; import android.os.Looper; import android.os.RemoteException; import android.os.WorkSource; import android.util.Log; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; /** * This class provides methods to perform scan related operations for Bluetooth LE devices. An * application can scan for a particular type of Bluetooth LE devices using {@link ScanFilter}. It * can also request different types of callbacks for delivering the result. *

* Use {@link BluetoothAdapter#getBluetoothLeScanner()} to get an instance of * {@link BluetoothLeScanner}. *

* Note: Most of the scan methods here require * {@link android.Manifest.permission#BLUETOOTH_ADMIN} permission. * * @see ScanFilter */ public final class BluetoothLeScanner { private static final String TAG = "BluetoothLeScanner"; private static final boolean DBG = true; private static final boolean VDBG = false; /** * Extra containing a list of ScanResults. It can have one or more results if there was no * error. In case of error, {@link #EXTRA_ERROR_CODE} will contain the error code and this * extra will not be available. */ public static final String EXTRA_LIST_SCAN_RESULT = "android.bluetooth.le.extra.LIST_SCAN_RESULT"; /** * Optional extra indicating the error code, if any. The error code will be one of the * SCAN_FAILED_* codes in {@link ScanCallback}. */ public static final String EXTRA_ERROR_CODE = "android.bluetooth.le.extra.ERROR_CODE"; /** * Optional extra indicating the callback type, which will be one of * CALLBACK_TYPE_* constants in {@link ScanSettings}. * * @see ScanCallback#onScanResult(int, ScanResult) */ public static final String EXTRA_CALLBACK_TYPE = "android.bluetooth.le.extra.CALLBACK_TYPE"; private final IBluetoothManager mBluetoothManager; private final Handler mHandler; private BluetoothAdapter mBluetoothAdapter; private final Map mLeScanClients; /** * Use {@link BluetoothAdapter#getBluetoothLeScanner()} instead. * * @param bluetoothManager BluetoothManager that conducts overall Bluetooth Management. * @hide */ public BluetoothLeScanner(IBluetoothManager bluetoothManager) { mBluetoothManager = bluetoothManager; mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); mHandler = new Handler(Looper.getMainLooper()); mLeScanClients = new HashMap(); } /** * Start Bluetooth LE scan with default parameters and no filters. The scan results will be * delivered through {@code callback}. For unfiltered scans, scanning is stopped on screen * off to save power. Scanning is resumed when screen is turned on again. To avoid this, use * {@link #startScan(List, ScanSettings, ScanCallback)} with desired {@link ScanFilter}. *

* An app must hold * {@link android.Manifest.permission#ACCESS_COARSE_LOCATION ACCESS_COARSE_LOCATION} or * {@link android.Manifest.permission#ACCESS_FINE_LOCATION ACCESS_FINE_LOCATION} permission * in order to get results. * * @param callback Callback used to deliver scan results. * @throws IllegalArgumentException If {@code callback} is null. */ @RequiresPermission(Manifest.permission.BLUETOOTH_ADMIN) public void startScan(final ScanCallback callback) { startScan(null, new ScanSettings.Builder().build(), callback); } /** * Start Bluetooth LE scan. The scan results will be delivered through {@code callback}. * For unfiltered scans, scanning is stopped on screen off to save power. Scanning is * resumed when screen is turned on again. To avoid this, do filetered scanning by * using proper {@link ScanFilter}. *

* An app must hold * {@link android.Manifest.permission#ACCESS_COARSE_LOCATION ACCESS_COARSE_LOCATION} or * {@link android.Manifest.permission#ACCESS_FINE_LOCATION ACCESS_FINE_LOCATION} permission * in order to get results. * * @param filters {@link ScanFilter}s for finding exact BLE devices. * @param settings Settings for the scan. * @param callback Callback used to deliver scan results. * @throws IllegalArgumentException If {@code settings} or {@code callback} is null. */ @RequiresPermission(Manifest.permission.BLUETOOTH_ADMIN) public void startScan(List filters, ScanSettings settings, final ScanCallback callback) { startScan(filters, settings, null, callback, /*callbackIntent=*/ null, null); } /** * Start Bluetooth LE scan using a {@link PendingIntent}. The scan results will be delivered via * the PendingIntent. Use this method of scanning if your process is not always running and it * should be started when scan results are available. *

* An app must hold * {@link android.Manifest.permission#ACCESS_COARSE_LOCATION ACCESS_COARSE_LOCATION} or * {@link android.Manifest.permission#ACCESS_FINE_LOCATION ACCESS_FINE_LOCATION} permission * in order to get results. *

* When the PendingIntent is delivered, the Intent passed to the receiver or activity * will contain one or more of the extras {@link #EXTRA_CALLBACK_TYPE}, * {@link #EXTRA_ERROR_CODE} and {@link #EXTRA_LIST_SCAN_RESULT} to indicate the result of * the scan. * * @param filters Optional list of ScanFilters for finding exact BLE devices. * @param settings Optional settings for the scan. * @param callbackIntent The PendingIntent to deliver the result to. * @return Returns 0 for success or an error code from {@link ScanCallback} if the scan request * could not be sent. * @see #stopScan(PendingIntent) */ @RequiresPermission(Manifest.permission.BLUETOOTH_ADMIN) public int startScan(@Nullable List filters, @Nullable ScanSettings settings, @NonNull PendingIntent callbackIntent) { return startScan(filters, settings != null ? settings : new ScanSettings.Builder().build(), null, null, callbackIntent, null); } /** * Start Bluetooth LE scan. Same as {@link #startScan(ScanCallback)} but allows the caller to * specify on behalf of which application(s) the work is being done. * * @param workSource {@link WorkSource} identifying the application(s) for which to blame for * the scan. * @param callback Callback used to deliver scan results. * @hide */ @SystemApi @RequiresPermission(allOf = { Manifest.permission.BLUETOOTH_ADMIN, Manifest.permission.UPDATE_DEVICE_STATS}) public void startScanFromSource(final WorkSource workSource, final ScanCallback callback) { startScanFromSource(null, new ScanSettings.Builder().build(), workSource, callback); } /** * Start Bluetooth LE scan. Same as {@link #startScan(List, ScanSettings, ScanCallback)} but * allows the caller to specify on behalf of which application(s) the work is being done. * * @param filters {@link ScanFilter}s for finding exact BLE devices. * @param settings Settings for the scan. * @param workSource {@link WorkSource} identifying the application(s) for which to blame for * the scan. * @param callback Callback used to deliver scan results. * @hide */ @SystemApi @RequiresPermission(allOf = { Manifest.permission.BLUETOOTH_ADMIN, Manifest.permission.UPDATE_DEVICE_STATS}) public void startScanFromSource(List filters, ScanSettings settings, final WorkSource workSource, final ScanCallback callback) { startScan(filters, settings, workSource, callback, null, null); } private int startScan(List filters, ScanSettings settings, final WorkSource workSource, final ScanCallback callback, final PendingIntent callbackIntent, List> resultStorages) { BluetoothLeUtils.checkAdapterStateOn(mBluetoothAdapter); if (callback == null && callbackIntent == null) { throw new IllegalArgumentException("callback is null"); } if (settings == null) { throw new IllegalArgumentException("settings is null"); } synchronized (mLeScanClients) { if (callback != null && mLeScanClients.containsKey(callback)) { return postCallbackErrorOrReturn(callback, ScanCallback.SCAN_FAILED_ALREADY_STARTED); } IBluetoothGatt gatt; try { gatt = mBluetoothManager.getBluetoothGatt(); } catch (RemoteException e) { gatt = null; } if (gatt == null) { return postCallbackErrorOrReturn(callback, ScanCallback.SCAN_FAILED_INTERNAL_ERROR); } if (!isSettingsConfigAllowedForScan(settings)) { return postCallbackErrorOrReturn(callback, ScanCallback.SCAN_FAILED_FEATURE_UNSUPPORTED); } if (!isHardwareResourcesAvailableForScan(settings)) { return postCallbackErrorOrReturn(callback, ScanCallback.SCAN_FAILED_OUT_OF_HARDWARE_RESOURCES); } if (!isSettingsAndFilterComboAllowed(settings, filters)) { return postCallbackErrorOrReturn(callback, ScanCallback.SCAN_FAILED_FEATURE_UNSUPPORTED); } if (callback != null) { BleScanCallbackWrapper wrapper = new BleScanCallbackWrapper(gatt, filters, settings, workSource, callback, resultStorages); wrapper.startRegistration(); } else { try { gatt.startScanForIntent(callbackIntent, settings, filters, ActivityThread.currentOpPackageName()); } catch (RemoteException e) { return ScanCallback.SCAN_FAILED_INTERNAL_ERROR; } } } return ScanCallback.NO_ERROR; } /** * Stops an ongoing Bluetooth LE scan. * * @param callback */ @RequiresPermission(Manifest.permission.BLUETOOTH_ADMIN) public void stopScan(ScanCallback callback) { BluetoothLeUtils.checkAdapterStateOn(mBluetoothAdapter); synchronized (mLeScanClients) { BleScanCallbackWrapper wrapper = mLeScanClients.remove(callback); if (wrapper == null) { if (DBG) Log.d(TAG, "could not find callback wrapper"); return; } wrapper.stopLeScan(); } } /** * Stops an ongoing Bluetooth LE scan started using a PendingIntent. * * @param callbackIntent The PendingIntent that was used to start the scan. * @see #startScan(List, ScanSettings, PendingIntent) */ @RequiresPermission(Manifest.permission.BLUETOOTH_ADMIN) public void stopScan(PendingIntent callbackIntent) { BluetoothLeUtils.checkAdapterStateOn(mBluetoothAdapter); IBluetoothGatt gatt; try { gatt = mBluetoothManager.getBluetoothGatt(); gatt.stopScanForIntent(callbackIntent, ActivityThread.currentOpPackageName()); } catch (RemoteException e) { } } /** * Flush pending batch scan results stored in Bluetooth controller. This will return Bluetooth * LE scan results batched on bluetooth controller. Returns immediately, batch scan results data * will be delivered through the {@code callback}. * * @param callback Callback of the Bluetooth LE Scan, it has to be the same instance as the one * used to start scan. */ public void flushPendingScanResults(ScanCallback callback) { BluetoothLeUtils.checkAdapterStateOn(mBluetoothAdapter); if (callback == null) { throw new IllegalArgumentException("callback cannot be null!"); } synchronized (mLeScanClients) { BleScanCallbackWrapper wrapper = mLeScanClients.get(callback); if (wrapper == null) { return; } wrapper.flushPendingBatchResults(); } } /** * Start truncated scan. * * @hide */ @SystemApi public void startTruncatedScan(List truncatedFilters, ScanSettings settings, final ScanCallback callback) { int filterSize = truncatedFilters.size(); List scanFilters = new ArrayList(filterSize); List> scanStorages = new ArrayList>(filterSize); for (TruncatedFilter filter : truncatedFilters) { scanFilters.add(filter.getFilter()); scanStorages.add(filter.getStorageDescriptors()); } startScan(scanFilters, settings, null, callback, null, scanStorages); } /** * Cleans up scan clients. Should be called when bluetooth is down. * * @hide */ public void cleanup() { mLeScanClients.clear(); } /** * Bluetooth GATT interface callbacks */ private class BleScanCallbackWrapper extends IScannerCallback.Stub { private static final int REGISTRATION_CALLBACK_TIMEOUT_MILLIS = 2000; private final ScanCallback mScanCallback; private final List mFilters; private final WorkSource mWorkSource; private ScanSettings mSettings; private IBluetoothGatt mBluetoothGatt; private List> mResultStorages; // mLeHandle 0: not registered // -2: registration failed because app is scanning to frequently // -1: scan stopped or registration failed // > 0: registered and scan started private int mScannerId; public BleScanCallbackWrapper(IBluetoothGatt bluetoothGatt, List filters, ScanSettings settings, WorkSource workSource, ScanCallback scanCallback, List> resultStorages) { mBluetoothGatt = bluetoothGatt; mFilters = filters; mSettings = settings; mWorkSource = workSource; mScanCallback = scanCallback; mScannerId = 0; mResultStorages = resultStorages; } public void startRegistration() { synchronized (this) { // Scan stopped. if (mScannerId == -1 || mScannerId == -2) return; try { mBluetoothGatt.registerScanner(this, mWorkSource); wait(REGISTRATION_CALLBACK_TIMEOUT_MILLIS); } catch (InterruptedException | RemoteException e) { Log.e(TAG, "application registeration exception", e); postCallbackError(mScanCallback, ScanCallback.SCAN_FAILED_INTERNAL_ERROR); } if (mScannerId > 0) { mLeScanClients.put(mScanCallback, this); } else { // Registration timed out or got exception, reset RscannerId to -1 so no // subsequent operations can proceed. if (mScannerId == 0) mScannerId = -1; // If scanning too frequently, don't report anything to the app. if (mScannerId == -2) return; postCallbackError(mScanCallback, ScanCallback.SCAN_FAILED_APPLICATION_REGISTRATION_FAILED); } } } public void stopLeScan() { synchronized (this) { if (mScannerId <= 0) { Log.e(TAG, "Error state, mLeHandle: " + mScannerId); return; } try { mBluetoothGatt.stopScan(mScannerId); mBluetoothGatt.unregisterScanner(mScannerId); } catch (RemoteException e) { Log.e(TAG, "Failed to stop scan and unregister", e); } mScannerId = -1; } } void flushPendingBatchResults() { synchronized (this) { if (mScannerId <= 0) { Log.e(TAG, "Error state, mLeHandle: " + mScannerId); return; } try { mBluetoothGatt.flushPendingBatchResults(mScannerId); } catch (RemoteException e) { Log.e(TAG, "Failed to get pending scan results", e); } } } /** * Application interface registered - app is ready to go */ @Override public void onScannerRegistered(int status, int scannerId) { Log.d(TAG, "onScannerRegistered() - status=" + status + " scannerId=" + scannerId + " mScannerId=" + mScannerId); synchronized (this) { if (status == BluetoothGatt.GATT_SUCCESS) { try { if (mScannerId == -1) { // Registration succeeds after timeout, unregister client. mBluetoothGatt.unregisterClient(scannerId); } else { mScannerId = scannerId; mBluetoothGatt.startScan(mScannerId, mSettings, mFilters, mResultStorages, ActivityThread.currentOpPackageName()); } } catch (RemoteException e) { Log.e(TAG, "fail to start le scan: " + e); mScannerId = -1; } } else if (status == ScanCallback.SCAN_FAILED_SCANNING_TOO_FREQUENTLY) { // applicaiton was scanning too frequently mScannerId = -2; } else { // registration failed mScannerId = -1; } notifyAll(); } } /** * Callback reporting an LE scan result. * * @hide */ @Override public void onScanResult(final ScanResult scanResult) { if (VDBG) Log.d(TAG, "onScanResult() - " + scanResult.toString()); // Check null in case the scan has been stopped synchronized (this) { if (mScannerId <= 0) return; } Handler handler = new Handler(Looper.getMainLooper()); handler.post(new Runnable() { @Override public void run() { mScanCallback.onScanResult(ScanSettings.CALLBACK_TYPE_ALL_MATCHES, scanResult); } }); } @Override public void onBatchScanResults(final List results) { Handler handler = new Handler(Looper.getMainLooper()); handler.post(new Runnable() { @Override public void run() { mScanCallback.onBatchScanResults(results); } }); } @Override public void onFoundOrLost(final boolean onFound, final ScanResult scanResult) { if (VDBG) { Log.d(TAG, "onFoundOrLost() - onFound = " + onFound + " " + scanResult.toString()); } // Check null in case the scan has been stopped synchronized (this) { if (mScannerId <= 0) { return; } } Handler handler = new Handler(Looper.getMainLooper()); handler.post(new Runnable() { @Override public void run() { if (onFound) { mScanCallback.onScanResult(ScanSettings.CALLBACK_TYPE_FIRST_MATCH, scanResult); } else { mScanCallback.onScanResult(ScanSettings.CALLBACK_TYPE_MATCH_LOST, scanResult); } } }); } @Override public void onScanManagerErrorCallback(final int errorCode) { if (VDBG) { Log.d(TAG, "onScanManagerErrorCallback() - errorCode = " + errorCode); } synchronized (this) { if (mScannerId <= 0) { return; } } postCallbackError(mScanCallback, errorCode); } } private int postCallbackErrorOrReturn(final ScanCallback callback, final int errorCode) { if (callback == null) { return errorCode; } else { postCallbackError(callback, errorCode); return ScanCallback.NO_ERROR; } } private void postCallbackError(final ScanCallback callback, final int errorCode) { mHandler.post(new Runnable() { @Override public void run() { callback.onScanFailed(errorCode); } }); } private boolean isSettingsConfigAllowedForScan(ScanSettings settings) { if (mBluetoothAdapter.isOffloadedFilteringSupported()) { return true; } final int callbackType = settings.getCallbackType(); // Only support regular scan if no offloaded filter support. if (callbackType == ScanSettings.CALLBACK_TYPE_ALL_MATCHES && settings.getReportDelayMillis() == 0) { return true; } return false; } private boolean isSettingsAndFilterComboAllowed(ScanSettings settings, List filterList) { final int callbackType = settings.getCallbackType(); // If onlost/onfound is requested, a non-empty filter is expected if ((callbackType & (ScanSettings.CALLBACK_TYPE_FIRST_MATCH | ScanSettings.CALLBACK_TYPE_MATCH_LOST)) != 0) { if (filterList == null) { return false; } for (ScanFilter filter : filterList) { if (filter.isAllFieldsEmpty()) { return false; } } } return true; } private boolean isHardwareResourcesAvailableForScan(ScanSettings settings) { final int callbackType = settings.getCallbackType(); if ((callbackType & ScanSettings.CALLBACK_TYPE_FIRST_MATCH) != 0 || (callbackType & ScanSettings.CALLBACK_TYPE_MATCH_LOST) != 0) { // For onlost/onfound, we required hw support be available return (mBluetoothAdapter.isOffloadedFilteringSupported() && mBluetoothAdapter.isHardwareTrackingFiltersAvailable()); } return true; } }