/* * 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; import android.annotation.Nullable; import android.os.Handler; import android.os.Looper; import android.os.Parcel; import android.os.ParcelUuid; import android.os.Parcelable; import android.os.RemoteException; import android.os.SystemClock; import android.util.Log; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.UUID; import java.util.concurrent.TimeUnit; /** * This class provides methods to perform scan related operations for Bluetooth LE devices. An * application can scan for a particular type of BLE devices using {@link BluetoothLeScanFilter}. 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 BluetoothLeScanFilter */ public class BluetoothLeScanner { private static final String TAG = "BluetoothLeScanner"; private static final boolean DBG = true; /** * Settings for Bluetooth LE scan. */ public static final class Settings implements Parcelable { /** * Perform Bluetooth LE scan in low power mode. This is the default scan mode as it consumes * the least power. */ public static final int SCAN_MODE_LOW_POWER = 0; /** * Perform Bluetooth LE scan in balanced power mode. */ public static final int SCAN_MODE_BALANCED = 1; /** * Scan using highest duty cycle. It's recommended only using this mode when the application * is running in foreground. */ public static final int SCAN_MODE_LOW_LATENCY = 2; /** * Callback each time when a bluetooth advertisement is found. */ public static final int CALLBACK_TYPE_ON_UPDATE = 0; /** * Callback when a bluetooth advertisement is found for the first time. */ public static final int CALLBACK_TYPE_ON_FOUND = 1; /** * Callback when a bluetooth advertisement is found for the first time, then lost. */ public static final int CALLBACK_TYPE_ON_LOST = 2; /** * Full scan result which contains device mac address, rssi, advertising and scan response * and scan timestamp. */ public static final int SCAN_RESULT_TYPE_FULL = 0; /** * Truncated scan result which contains device mac address, rssi and scan timestamp. Note * it's possible for an app to get more scan results that it asks if there are multiple apps * using this type. TODO: decide whether we could unhide this setting. * * @hide */ public static final int SCAN_RESULT_TYPE_TRUNCATED = 1; // Bluetooth LE scan mode. private int mScanMode; // Bluetooth LE scan callback type private int mCallbackType; // Bluetooth LE scan result type private int mScanResultType; // Time of delay for reporting the scan result private long mReportDelayMicros; public int getScanMode() { return mScanMode; } public int getCallbackType() { return mCallbackType; } public int getScanResultType() { return mScanResultType; } /** * Returns report delay timestamp based on the device clock. */ public long getReportDelayMicros() { return mReportDelayMicros; } /** * Creates a new {@link Builder} to build {@link Settings} object. */ public static Builder newBuilder() { return new Builder(); } private Settings(int scanMode, int callbackType, int scanResultType, long reportDelayMicros) { mScanMode = scanMode; mCallbackType = callbackType; mScanResultType = scanResultType; mReportDelayMicros = reportDelayMicros; } private Settings(Parcel in) { mScanMode = in.readInt(); mCallbackType = in.readInt(); mScanResultType = in.readInt(); mReportDelayMicros = in.readLong(); } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeInt(mScanMode); dest.writeInt(mCallbackType); dest.writeInt(mScanResultType); dest.writeLong(mReportDelayMicros); } @Override public int describeContents() { return 0; } public static final Parcelable.Creator CREATOR = new Creator() { @Override public Settings[] newArray(int size) { return new Settings[size]; } @Override public Settings createFromParcel(Parcel in) { return new Settings(in); } }; /** * Builder for {@link BluetoothLeScanner.Settings}. */ public static class Builder { private int mScanMode = SCAN_MODE_LOW_POWER; private int mCallbackType = CALLBACK_TYPE_ON_UPDATE; private int mScanResultType = SCAN_RESULT_TYPE_FULL; private long mReportDelayMicros = 0; // Hidden constructor. private Builder() { } /** * Set scan mode for Bluetooth LE scan. * * @param scanMode The scan mode can be one of {@link Settings#SCAN_MODE_LOW_POWER}, * {@link Settings#SCAN_MODE_BALANCED} or * {@link Settings#SCAN_MODE_LOW_LATENCY}. * @throws IllegalArgumentException If the {@code scanMode} is invalid. */ public Builder scanMode(int scanMode) { if (scanMode < SCAN_MODE_LOW_POWER || scanMode > SCAN_MODE_LOW_LATENCY) { throw new IllegalArgumentException("invalid scan mode " + scanMode); } mScanMode = scanMode; return this; } /** * Set callback type for Bluetooth LE scan. * * @param callbackType The callback type for the scan. Can be either one of * {@link Settings#CALLBACK_TYPE_ON_UPDATE}, * {@link Settings#CALLBACK_TYPE_ON_FOUND} or * {@link Settings#CALLBACK_TYPE_ON_LOST}. * @throws IllegalArgumentException If the {@code callbackType} is invalid. */ public Builder callbackType(int callbackType) { if (callbackType < CALLBACK_TYPE_ON_UPDATE || callbackType > CALLBACK_TYPE_ON_LOST) { throw new IllegalArgumentException("invalid callback type - " + callbackType); } mCallbackType = callbackType; return this; } /** * Set scan result type for Bluetooth LE scan. * * @param scanResultType Type for scan result, could be either * {@link Settings#SCAN_RESULT_TYPE_FULL} or * {@link Settings#SCAN_RESULT_TYPE_TRUNCATED}. * @throws IllegalArgumentException If the {@code scanResultType} is invalid. * @hide */ public Builder scanResultType(int scanResultType) { if (scanResultType < SCAN_RESULT_TYPE_FULL || scanResultType > SCAN_RESULT_TYPE_TRUNCATED) { throw new IllegalArgumentException( "invalid scanResultType - " + scanResultType); } mScanResultType = scanResultType; return this; } /** * Set report delay timestamp for Bluetooth LE scan. */ public Builder reportDelayMicros(long reportDelayMicros) { mReportDelayMicros = reportDelayMicros; return this; } /** * Build {@link Settings}. */ public Settings build() { return new Settings(mScanMode, mCallbackType, mScanResultType, mReportDelayMicros); } } } /** * ScanResult for Bluetooth LE scan. */ public static final class ScanResult implements Parcelable { // Remote bluetooth device. private BluetoothDevice mDevice; // Scan record, including advertising data and scan response data. private byte[] mScanRecord; // Received signal strength. private int mRssi; // Device timestamp when the result was last seen. private long mTimestampMicros; // Constructor of scan result. public ScanResult(BluetoothDevice device, byte[] scanRecord, int rssi, long timestampMicros) { mDevice = device; mScanRecord = scanRecord; mRssi = rssi; mTimestampMicros = timestampMicros; } private ScanResult(Parcel in) { readFromParcel(in); } @Override public void writeToParcel(Parcel dest, int flags) { if (mDevice != null) { dest.writeInt(1); mDevice.writeToParcel(dest, flags); } else { dest.writeInt(0); } if (mScanRecord != null) { dest.writeInt(1); dest.writeByteArray(mScanRecord); } else { dest.writeInt(0); } dest.writeInt(mRssi); dest.writeLong(mTimestampMicros); } private void readFromParcel(Parcel in) { if (in.readInt() == 1) { mDevice = BluetoothDevice.CREATOR.createFromParcel(in); } if (in.readInt() == 1) { mScanRecord = in.createByteArray(); } mRssi = in.readInt(); mTimestampMicros = in.readLong(); } @Override public int describeContents() { return 0; } /** * Returns the remote bluetooth device identified by the bluetooth device address. */ @Nullable public BluetoothDevice getDevice() { return mDevice; } @Nullable /** * Returns the scan record, which can be a combination of advertisement and scan response. */ public byte[] getScanRecord() { return mScanRecord; } /** * Returns the received signal strength in dBm. The valid range is [-127, 127]. */ public int getRssi() { return mRssi; } /** * Returns timestamp since boot when the scan record was observed. */ public long getTimestampMicros() { return mTimestampMicros; } @Override public int hashCode() { return Objects.hash(mDevice, mRssi, mScanRecord, mTimestampMicros); } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null || getClass() != obj.getClass()) { return false; } ScanResult other = (ScanResult) obj; return Objects.equals(mDevice, other.mDevice) && (mRssi == other.mRssi) && Objects.deepEquals(mScanRecord, other.mScanRecord) && (mTimestampMicros == other.mTimestampMicros); } @Override public String toString() { return "ScanResult{" + "mDevice=" + mDevice + ", mScanRecord=" + Arrays.toString(mScanRecord) + ", mRssi=" + mRssi + ", mTimestampMicros=" + mTimestampMicros + '}'; } public static final Parcelable.Creator CREATOR = new Creator() { @Override public ScanResult createFromParcel(Parcel source) { return new ScanResult(source); } @Override public ScanResult[] newArray(int size) { return new ScanResult[size]; } }; } /** * Callback of Bluetooth LE scans. The results of the scans will be delivered through the * callbacks. */ public interface ScanCallback { /** * Callback when any BLE beacon is found. * * @param result A Bluetooth LE scan result. */ public void onDeviceUpdate(ScanResult result); /** * Callback when the BLE beacon is found for the first time. * * @param result The Bluetooth LE scan result when the onFound event is triggered. */ public void onDeviceFound(ScanResult result); /** * Callback when the BLE device was lost. Note a device has to be "found" before it's lost. * * @param device The Bluetooth device that is lost. */ public void onDeviceLost(BluetoothDevice device); /** * Callback when batch results are delivered. * * @param results List of scan results that are previously scanned. */ public void onBatchScanResults(List results); /** * Fails to start scan as BLE scan with the same settings is already started by the app. */ public static final int SCAN_ALREADY_STARTED = 1; /** * Fails to start scan as app cannot be registered. */ public static final int APPLICATION_REGISTRATION_FAILED = 2; /** * Fails to start scan due to gatt service failure. */ public static final int GATT_SERVICE_FAILURE = 3; /** * Fails to start scan due to controller failure. */ public static final int CONTROLLER_FAILURE = 4; /** * Callback when scan failed. */ public void onScanFailed(int errorCode); } private final IBluetoothGatt mBluetoothGatt; private final Handler mHandler; private final Map mLeScanClients; BluetoothLeScanner(IBluetoothGatt bluetoothGatt) { mBluetoothGatt = bluetoothGatt; mHandler = new Handler(Looper.getMainLooper()); mLeScanClients = new HashMap(); } /** * Bluetooth GATT interface callbacks */ private static class BleScanCallbackWrapper extends IBluetoothGattCallback.Stub { private static final int REGISTRATION_CALLBACK_TIMEOUT_SECONDS = 5; private final ScanCallback mScanCallback; private final List mFilters; private Settings mSettings; private IBluetoothGatt mBluetoothGatt; // mLeHandle 0: not registered // -1: scan stopped // > 0: registered and scan started private int mLeHandle; public BleScanCallbackWrapper(IBluetoothGatt bluetoothGatt, List filters, Settings settings, ScanCallback scanCallback) { mBluetoothGatt = bluetoothGatt; mFilters = filters; mSettings = settings; mScanCallback = scanCallback; mLeHandle = 0; } public boolean scanStarted() { synchronized (this) { if (mLeHandle == -1) { return false; } try { wait(REGISTRATION_CALLBACK_TIMEOUT_SECONDS); } catch (InterruptedException e) { Log.e(TAG, "Callback reg wait interrupted: " + e); } } return mLeHandle > 0; } public void stopLeScan() { synchronized (this) { if (mLeHandle <= 0) { Log.e(TAG, "Error state, mLeHandle: " + mLeHandle); return; } try { mBluetoothGatt.stopScan(mLeHandle, false); mBluetoothGatt.unregisterClient(mLeHandle); } catch (RemoteException e) { Log.e(TAG, "Failed to stop scan and unregister" + e); } mLeHandle = -1; notifyAll(); } } /** * Application interface registered - app is ready to go */ @Override public void onClientRegistered(int status, int clientIf) { Log.d(TAG, "onClientRegistered() - status=" + status + " clientIf=" + clientIf); synchronized (this) { if (mLeHandle == -1) { if (DBG) Log.d(TAG, "onClientRegistered LE scan canceled"); } if (status == BluetoothGatt.GATT_SUCCESS) { mLeHandle = clientIf; try { mBluetoothGatt.startScanWithFilters(mLeHandle, false, mSettings, mFilters); } catch (RemoteException e) { Log.e(TAG, "fail to start le scan: " + e); mLeHandle = -1; } } else { // registration failed mLeHandle = -1; } notifyAll(); } } @Override public void onClientConnectionState(int status, int clientIf, boolean connected, String address) { // no op } /** * Callback reporting an LE scan result. * * @hide */ @Override public void onScanResult(String address, int rssi, byte[] advData) { if (DBG) Log.d(TAG, "onScanResult() - Device=" + address + " RSSI=" + rssi); // Check null in case the scan has been stopped synchronized (this) { if (mLeHandle <= 0) return; } BluetoothDevice device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice( address); long scanMicros = TimeUnit.NANOSECONDS.toMicros(SystemClock.elapsedRealtimeNanos()); ScanResult result = new ScanResult(device, advData, rssi, scanMicros); mScanCallback.onDeviceUpdate(result); } @Override public void onGetService(String address, int srvcType, int srvcInstId, ParcelUuid srvcUuid) { // no op } @Override public void onGetIncludedService(String address, int srvcType, int srvcInstId, ParcelUuid srvcUuid, int inclSrvcType, int inclSrvcInstId, ParcelUuid inclSrvcUuid) { // no op } @Override public void onGetCharacteristic(String address, int srvcType, int srvcInstId, ParcelUuid srvcUuid, int charInstId, ParcelUuid charUuid, int charProps) { // no op } @Override public void onGetDescriptor(String address, int srvcType, int srvcInstId, ParcelUuid srvcUuid, int charInstId, ParcelUuid charUuid, int descInstId, ParcelUuid descUuid) { // no op } @Override public void onSearchComplete(String address, int status) { // no op } @Override public void onCharacteristicRead(String address, int status, int srvcType, int srvcInstId, ParcelUuid srvcUuid, int charInstId, ParcelUuid charUuid, byte[] value) { // no op } @Override public void onCharacteristicWrite(String address, int status, int srvcType, int srvcInstId, ParcelUuid srvcUuid, int charInstId, ParcelUuid charUuid) { // no op } @Override public void onNotify(String address, int srvcType, int srvcInstId, ParcelUuid srvcUuid, int charInstId, ParcelUuid charUuid, byte[] value) { // no op } @Override public void onDescriptorRead(String address, int status, int srvcType, int srvcInstId, ParcelUuid srvcUuid, int charInstId, ParcelUuid charUuid, int descInstId, ParcelUuid descrUuid, byte[] value) { // no op } @Override public void onDescriptorWrite(String address, int status, int srvcType, int srvcInstId, ParcelUuid srvcUuid, int charInstId, ParcelUuid charUuid, int descInstId, ParcelUuid descrUuid) { // no op } @Override public void onExecuteWrite(String address, int status) { // no op } @Override public void onReadRemoteRssi(String address, int rssi, int status) { // no op } @Override public void onAdvertiseStateChange(int advertiseState, int status) { // no op } @Override public void onMultiAdvertiseCallback(int status) { // no op } @Override public void onConfigureMTU(String address, int mtu, int status) { // no op } } /** * Scan Bluetooth LE scan. The scan results will be delivered through {@code callback}. * * @param filters {@link BluetoothLeScanFilter}s for finding exact BLE devices. * @param settings Settings for ble scan. * @param callback Callback when scan results are delivered. * @throws IllegalArgumentException If {@code settings} or {@code callback} is null. */ public void startScan(List filters, Settings settings, final ScanCallback callback) { if (settings == null || callback == null) { throw new IllegalArgumentException("settings or callback is null"); } synchronized (mLeScanClients) { if (mLeScanClients.get(settings) != null) { postCallbackError(callback, ScanCallback.SCAN_ALREADY_STARTED); return; } BleScanCallbackWrapper wrapper = new BleScanCallbackWrapper(mBluetoothGatt, filters, settings, callback); try { UUID uuid = UUID.randomUUID(); mBluetoothGatt.registerClient(new ParcelUuid(uuid), wrapper); if (wrapper.scanStarted()) { mLeScanClients.put(settings, wrapper); } else { postCallbackError(callback, ScanCallback.APPLICATION_REGISTRATION_FAILED); return; } } catch (RemoteException e) { Log.e(TAG, "GATT service exception when starting scan", e); postCallbackError(callback, ScanCallback.GATT_SERVICE_FAILURE); } } } private void postCallbackError(final ScanCallback callback, final int errorCode) { mHandler.post(new Runnable() { @Override public void run() { callback.onScanFailed(errorCode); } }); } /** * Stop Bluetooth LE scan. * * @param settings The same settings as used in {@link #startScan}, which is used to identify * the BLE scan. */ public void stopScan(Settings settings) { synchronized (mLeScanClients) { BleScanCallbackWrapper wrapper = mLeScanClients.remove(settings); if (wrapper == null) { return; } wrapper.stopLeScan(); } } /** * Returns available storage size for batch scan results. It's recommended not to use batch scan * if available storage size is small (less than 1k bytes, for instance). * * @hide TODO: unhide when batching is supported in stack. */ public int getAvailableBatchStorageSizeBytes() { throw new UnsupportedOperationException("not impelemented"); } /** * Poll scan results from bluetooth controller. This will return Bluetooth LE scan results * batched on bluetooth controller. * * @param callback Callback of the Bluetooth LE Scan, it has to be the same instance as the one * used to start scan. * @param flush Whether to flush the batch scan buffer. Note the other batch scan clients will * get batch scan callback if the batch scan buffer is flushed. * @return Batch Scan results. * @hide TODO: unhide when batching is supported in stack. */ public List getBatchScanResults(ScanCallback callback, boolean flush) { throw new UnsupportedOperationException("not impelemented"); } }