/* * 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.RequiresPermission; import android.annotation.SystemApi; import android.app.ActivityThread; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothGatt; import android.bluetooth.BluetoothGattCallbackWrapper; import android.bluetooth.IBluetoothGatt; import android.bluetooth.IBluetoothManager; import android.os.Handler; import android.os.Looper; import android.os.ParcelUuid; 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; import java.util.UUID; /** * 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;
private final IBluetoothManager mBluetoothManager;
private final Handler mHandler;
private BluetoothAdapter mBluetoothAdapter;
private final Map
* Requires {@link android.Manifest.permission#BLUETOOTH_ADMIN} permission.
* 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}.
*
* Requires {@link android.Manifest.permission#BLUETOOTH_ADMIN} permission.
* 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
* Requires {@link android.Manifest.permission#BLUETOOTH_ADMIN} permission.
*
* @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();
}
}
/**
* 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> resultStorages) {
BluetoothLeUtils.checkAdapterStateOn(mBluetoothAdapter);
if (callback == null) {
throw new IllegalArgumentException("callback is null");
}
if (settings == null) {
throw new IllegalArgumentException("settings is null");
}
synchronized (mLeScanClients) {
if (mLeScanClients.containsKey(callback)) {
postCallbackError(callback, ScanCallback.SCAN_FAILED_ALREADY_STARTED);
return;
}
IBluetoothGatt gatt;
try {
gatt = mBluetoothManager.getBluetoothGatt();
} catch (RemoteException e) {
gatt = null;
}
if (gatt == null) {
postCallbackError(callback, ScanCallback.SCAN_FAILED_INTERNAL_ERROR);
return;
}
if (!isSettingsConfigAllowedForScan(settings)) {
postCallbackError(callback,
ScanCallback.SCAN_FAILED_FEATURE_UNSUPPORTED);
return;
}
if (!isHardwareResourcesAvailableForScan(settings)) {
postCallbackError(callback,
ScanCallback.SCAN_FAILED_OUT_OF_HARDWARE_RESOURCES);
return;
}
if (!isSettingsAndFilterComboAllowed(settings, filters)) {
postCallbackError(callback,
ScanCallback.SCAN_FAILED_FEATURE_UNSUPPORTED);
return;
}
BleScanCallbackWrapper wrapper = new BleScanCallbackWrapper(gatt, filters,
settings, workSource, callback, resultStorages);
wrapper.startRegisteration();
}
}
/**
* Stops an ongoing Bluetooth LE scan.
*
> scanStorages =
new ArrayList
>(filterSize);
for (TruncatedFilter filter : truncatedFilters) {
scanFilters.add(filter.getFilter());
scanStorages.add(filter.getStorageDescriptors());
}
startScan(scanFilters, settings, null, callback, 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 BluetoothGattCallbackWrapper {
private static final int REGISTRATION_CALLBACK_TIMEOUT_MILLIS = 2000;
private final ScanCallback mScanCallback;
private final List
> mResultStorages;
// mLeHandle 0: not registered
// -1: scan stopped or registration failed
// > 0: registered and scan started
private int mClientIf;
public BleScanCallbackWrapper(IBluetoothGatt bluetoothGatt,
List
> resultStorages) {
mBluetoothGatt = bluetoothGatt;
mFilters = filters;
mSettings = settings;
mWorkSource = workSource;
mScanCallback = scanCallback;
mClientIf = 0;
mResultStorages = resultStorages;
}
public void startRegisteration() {
synchronized (this) {
// Scan stopped.
if (mClientIf == -1) return;
try {
UUID uuid = UUID.randomUUID();
mBluetoothGatt.registerClient(new ParcelUuid(uuid), this);
wait(REGISTRATION_CALLBACK_TIMEOUT_MILLIS);
} catch (InterruptedException | RemoteException e) {
Log.e(TAG, "application registeration exception", e);
postCallbackError(mScanCallback, ScanCallback.SCAN_FAILED_INTERNAL_ERROR);
}
if (mClientIf > 0) {
mLeScanClients.put(mScanCallback, this);
} else {
// Registration timed out or got exception, reset clientIf to -1 so no
// subsequent operations can proceed.
if (mClientIf == 0) mClientIf = -1;
postCallbackError(mScanCallback,
ScanCallback.SCAN_FAILED_APPLICATION_REGISTRATION_FAILED);
}
}
}
public void stopLeScan() {
synchronized (this) {
if (mClientIf <= 0) {
Log.e(TAG, "Error state, mLeHandle: " + mClientIf);
return;
}
try {
mBluetoothGatt.stopScan(mClientIf, false);
mBluetoothGatt.unregisterClient(mClientIf);
} catch (RemoteException e) {
Log.e(TAG, "Failed to stop scan and unregister", e);
}
mClientIf = -1;
}
}
void flushPendingBatchResults() {
synchronized (this) {
if (mClientIf <= 0) {
Log.e(TAG, "Error state, mLeHandle: " + mClientIf);
return;
}
try {
mBluetoothGatt.flushPendingBatchResults(mClientIf, false);
} catch (RemoteException e) {
Log.e(TAG, "Failed to get pending scan results", e);
}
}
}
/**
* Application interface registered - app is ready to go
*/
@Override
public void onClientRegistered(int status, int clientIf) {
Log.d(TAG, "onClientRegistered() - status=" + status +
" clientIf=" + clientIf + " mClientIf=" + mClientIf);
synchronized (this) {
if (status == BluetoothGatt.GATT_SUCCESS) {
try {
if (mClientIf == -1) {
// Registration succeeds after timeout, unregister client.
mBluetoothGatt.unregisterClient(clientIf);
} else {
mClientIf = clientIf;
mBluetoothGatt.startScan(mClientIf, false, mSettings, mFilters,
mWorkSource, mResultStorages,
ActivityThread.currentOpPackageName());
}
} catch (RemoteException e) {
Log.e(TAG, "fail to start le scan: " + e);
mClientIf = -1;
}
} else {
// registration failed
mClientIf = -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 (mClientIf <= 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