diff options
author | Wei Wang <weiwa@google.com> | 2014-04-16 18:49:18 -0700 |
---|---|---|
committer | Wei Wang <weiwa@google.com> | 2014-05-19 19:40:49 -0700 |
commit | 62d5bc745a4fd7f305a47b628b1fe902ee9d29f8 (patch) | |
tree | de267354e1e446114a8333462dbb9ac6def4d8fa /core/java | |
parent | 51800847d9a9965a5d00a7960abb50983651d4a2 (diff) | |
download | frameworks_base-62d5bc745a4fd7f305a47b628b1fe902ee9d29f8.zip frameworks_base-62d5bc745a4fd7f305a47b628b1fe902ee9d29f8.tar.gz frameworks_base-62d5bc745a4fd7f305a47b628b1fe902ee9d29f8.tar.bz2 |
APIs for BLE scan, scan filter, batch scan, onFound/onLost and multiple
advertising.
Change-Id: I1655eb9cffa890b6fe38108bf51078662e90bc03
Diffstat (limited to 'core/java')
12 files changed, 2714 insertions, 2 deletions
diff --git a/core/java/android/bluetooth/BluetoothAdapter.java b/core/java/android/bluetooth/BluetoothAdapter.java index e79deec..9e1c995 100644 --- a/core/java/android/bluetooth/BluetoothAdapter.java +++ b/core/java/android/bluetooth/BluetoothAdapter.java @@ -498,6 +498,34 @@ public final class BluetoothAdapter { } /** + * Returns a {@link BluetoothLeAdvertiser} object for Bluetooth LE Advertising operations. + */ + public BluetoothLeAdvertiser getBluetoothLeAdvertiser() { + // TODO: Return null if this feature is not supported by hardware. + try { + IBluetoothGatt iGatt = mManagerService.getBluetoothGatt(); + return new BluetoothLeAdvertiser(iGatt); + } catch (RemoteException e) { + Log.e(TAG, "failed to get BluetoothLeAdvertiser, error: " + e); + return null; + } + } + + /** + * Returns a {@link BluetoothLeScanner} object for Bluetooth LE scan operations. + */ + public BluetoothLeScanner getBluetoothLeScanner() { + // TODO: Return null if BLE scan is not supported by hardware. + try { + IBluetoothGatt iGatt = mManagerService.getBluetoothGatt(); + return new BluetoothLeScanner(iGatt); + } catch (RemoteException e) { + Log.e(TAG, "failed to get BluetoothLeScanner, error: " + e); + return null; + } + } + + /** * Interface for BLE advertising callback. * * @hide @@ -2024,6 +2052,10 @@ public final class BluetoothAdapter { } } + @Override + public void onMultiAdvertiseCallback(int status) { + // no op + } /** * Callback reporting LE ATT MTU. * @hide diff --git a/core/java/android/bluetooth/BluetoothGatt.java b/core/java/android/bluetooth/BluetoothGatt.java index 601d9ee..c9df9c0 100644 --- a/core/java/android/bluetooth/BluetoothGatt.java +++ b/core/java/android/bluetooth/BluetoothGatt.java @@ -581,7 +581,15 @@ public final class BluetoothGatt implements BluetoothProfile { public void onAdvertiseStateChange(int state, int status) { if (DBG) Log.d(TAG, "onAdvertiseStateChange() - state = " + state + " status=" + status); - } + } + + /** + * @hide + */ + @Override + public void onMultiAdvertiseCallback(int status) { + // no op. + } /** * Callback invoked when the MTU for a given connection changes diff --git a/core/java/android/bluetooth/BluetoothLeAdvertiseScanData.aidl b/core/java/android/bluetooth/BluetoothLeAdvertiseScanData.aidl new file mode 100644 index 0000000..4aa8881 --- /dev/null +++ b/core/java/android/bluetooth/BluetoothLeAdvertiseScanData.aidl @@ -0,0 +1,19 @@ +/* + * 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; + +parcelable BluetoothLeAdvertiseScanData.AdvertisementData;
\ No newline at end of file diff --git a/core/java/android/bluetooth/BluetoothLeAdvertiseScanData.java b/core/java/android/bluetooth/BluetoothLeAdvertiseScanData.java new file mode 100644 index 0000000..3d85810 --- /dev/null +++ b/core/java/android/bluetooth/BluetoothLeAdvertiseScanData.java @@ -0,0 +1,649 @@ +/* + * 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.bluetooth.BluetoothLeAdvertiseScanData.AdvertisementData; +import android.os.Parcel; +import android.os.ParcelUuid; +import android.os.Parcelable; +import android.util.Log; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Represents Bluetooth LE advertise and scan response data. This could be either the advertisement + * data to be advertised, or the scan record obtained from BLE scans. + * <p> + * The exact bluetooth advertising and scan response data fields and types are defined in Bluetooth + * 4.0 specification, Volume 3, Part C, Section 11 and 18, as well as Supplement to the Bluetooth + * Core Specification Version 4. Currently the following fields are allowed to be set: + * <li>Service UUIDs which identify the bluetooth gatt services running on the device. + * <li>Tx power level which is the transmission power level. + * <li>Service data which is the data associated with a service. + * <li>Manufacturer specific data which is the data associated with a particular manufacturer. + * + * @see BluetoothLeAdvertiser + */ +public final class BluetoothLeAdvertiseScanData { + private static final String TAG = "BluetoothLeAdvertiseScanData"; + + /** + * Bluetooth LE Advertising Data type, the data will be placed in AdvData field of advertising + * packet. + */ + public static final int ADVERTISING_DATA = 0; + /** + * Bluetooth LE scan response data, the data will be placed in ScanRspData field of advertising + * packet. + * <p> + * TODO: unhide when stack supports setting scan response data. + * + * @hide + */ + public static final int SCAN_RESPONSE_DATA = 1; + /** + * Scan record parsed from Bluetooth LE scans. The content can contain a concatenation of + * advertising data and scan response data. + */ + public static final int PARSED_SCAN_RECORD = 2; + + /** + * Base data type which contains the common fields for {@link AdvertisementData} and + * {@link ScanRecord}. + */ + public abstract static class AdvertiseBaseData { + + private final int mDataType; + + @Nullable + private final List<ParcelUuid> mServiceUuids; + + private final int mManufacturerId; + @Nullable + private final byte[] mManufacturerSpecificData; + + @Nullable + private final ParcelUuid mServiceDataUuid; + @Nullable + private final byte[] mServiceData; + + private AdvertiseBaseData(int dataType, + List<ParcelUuid> serviceUuids, + ParcelUuid serviceDataUuid, byte[] serviceData, + int manufacturerId, + byte[] manufacturerSpecificData) { + mDataType = dataType; + mServiceUuids = serviceUuids; + mManufacturerId = manufacturerId; + mManufacturerSpecificData = manufacturerSpecificData; + mServiceDataUuid = serviceDataUuid; + mServiceData = serviceData; + } + + /** + * Returns the type of data, indicating whether the data is advertising data, scan response + * data or scan record. + */ + public int getDataType() { + return mDataType; + } + + /** + * Returns a list of service uuids within the advertisement that are used to identify the + * bluetooth gatt services. + */ + public List<ParcelUuid> getServiceUuids() { + return mServiceUuids; + } + + /** + * Returns the manufacturer identifier, which is a non-negative number assigned by Bluetooth + * SIG. + */ + public int getManufacturerId() { + return mManufacturerId; + } + + /** + * Returns the manufacturer specific data which is the content of manufacturer specific data + * field. The first 2 bytes of the data contain the company id. + */ + public byte[] getManufacturerSpecificData() { + return mManufacturerSpecificData; + } + + /** + * Returns a 16 bit uuid of the service that the service data is associated with. + */ + public ParcelUuid getServiceDataUuid() { + return mServiceDataUuid; + } + + /** + * Returns service data. The first two bytes should be a 16 bit service uuid associated with + * the service data. + */ + public byte[] getServiceData() { + return mServiceData; + } + + @Override + public String toString() { + return "AdvertiseBaseData [mDataType=" + mDataType + ", mServiceUuids=" + mServiceUuids + + ", mManufacturerId=" + mManufacturerId + ", mManufacturerSpecificData=" + + Arrays.toString(mManufacturerSpecificData) + ", mServiceDataUuid=" + + mServiceDataUuid + ", mServiceData=" + Arrays.toString(mServiceData) + "]"; + } + } + + /** + * Advertisement data packet for Bluetooth LE advertising. This represents the data to be + * broadcasted in Bluetooth LE advertising. + * <p> + * Use {@link AdvertisementData.Builder} to create an instance of {@link AdvertisementData} to + * be advertised. + * + * @see BluetoothLeAdvertiser + */ + public static final class AdvertisementData extends AdvertiseBaseData implements Parcelable { + + private boolean mIncludeTxPowerLevel; + + /** + * Whether the transmission power level will be included in the advertisement packet. + */ + public boolean getIncludeTxPowerLevel() { + return mIncludeTxPowerLevel; + } + + /** + * Returns a {@link Builder} to build {@link AdvertisementData}. + */ + public static Builder newBuilder() { + return new Builder(); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(getDataType()); + List<ParcelUuid> uuids = getServiceUuids(); + if (uuids == null) { + dest.writeInt(0); + } else { + dest.writeInt(uuids.size()); + dest.writeList(uuids); + } + + dest.writeInt(getManufacturerId()); + byte[] manufacturerData = getManufacturerSpecificData(); + if (manufacturerData == null) { + dest.writeInt(0); + } else { + dest.writeInt(manufacturerData.length); + dest.writeByteArray(manufacturerData); + } + + ParcelUuid serviceDataUuid = getServiceDataUuid(); + if (serviceDataUuid == null) { + dest.writeInt(0); + } else { + dest.writeInt(1); + dest.writeParcelable(serviceDataUuid, flags); + byte[] serviceData = getServiceData(); + if (serviceData == null) { + dest.writeInt(0); + } else { + dest.writeInt(serviceData.length); + dest.writeByteArray(serviceData); + } + } + dest.writeByte((byte) (getIncludeTxPowerLevel() ? 1 : 0)); + } + + private AdvertisementData(int dataType, + List<ParcelUuid> serviceUuids, + ParcelUuid serviceDataUuid, byte[] serviceData, + int manufacturerId, + byte[] manufacturerSpecificData, boolean mIncludeTxPowerLevel) { + super(dataType, serviceUuids, serviceDataUuid, serviceData, manufacturerId, + manufacturerSpecificData); + this.mIncludeTxPowerLevel = mIncludeTxPowerLevel; + } + + public static final Parcelable.Creator<AdvertisementData> CREATOR = + new Creator<AdvertisementData>() { + @Override + public AdvertisementData[] newArray(int size) { + return new AdvertisementData[size]; + } + + @Override + public AdvertisementData createFromParcel(Parcel in) { + Builder builder = newBuilder(); + int dataType = in.readInt(); + builder.dataType(dataType); + if (in.readInt() > 0) { + List<ParcelUuid> uuids = new ArrayList<ParcelUuid>(); + in.readList(uuids, ParcelUuid.class.getClassLoader()); + builder.serviceUuids(uuids); + } + int manufacturerId = in.readInt(); + int manufacturerDataLength = in.readInt(); + if (manufacturerDataLength > 0) { + byte[] manufacturerData = new byte[manufacturerDataLength]; + in.readByteArray(manufacturerData); + builder.manufacturerData(manufacturerId, manufacturerData); + } + if (in.readInt() == 1) { + ParcelUuid serviceDataUuid = in.readParcelable( + ParcelUuid.class.getClassLoader()); + int serviceDataLength = in.readInt(); + if (serviceDataLength > 0) { + byte[] serviceData = new byte[serviceDataLength]; + in.readByteArray(serviceData); + builder.serviceData(serviceDataUuid, serviceData); + } + } + builder.includeTxPowerLevel(in.readByte() == 1); + return builder.build(); + } + }; + + /** + * Builder for {@link BluetoothLeAdvertiseScanData.AdvertisementData}. Use + * {@link AdvertisementData#newBuilder()} to get an instance of the Builder. + */ + public static final class Builder { + private static final int MAX_ADVERTISING_DATA_BYTES = 31; + // Each fields need one byte for field length and another byte for field type. + private static final int OVERHEAD_BYTES_PER_FIELD = 2; + // Flags field will be set by system. + private static final int FLAGS_FIELD_BYTES = 3; + + private int mDataType; + @Nullable + private List<ParcelUuid> mServiceUuids; + private boolean mIncludeTxPowerLevel; + private int mManufacturerId; + @Nullable + private byte[] mManufacturerSpecificData; + @Nullable + private ParcelUuid mServiceDataUuid; + @Nullable + private byte[] mServiceData; + + /** + * Set data type. + * + * @param dataType Data type, could only be + * {@link BluetoothLeAdvertiseScanData#ADVERTISING_DATA} or + * {@link BluetoothLeAdvertiseScanData#SCAN_RESPONSE_DATA} + * @throws IllegalArgumentException If the {@code dataType} is invalid. + */ + public Builder dataType(int dataType) { + if (mDataType != ADVERTISING_DATA && mDataType != SCAN_RESPONSE_DATA) { + throw new IllegalArgumentException("invalid data type - " + dataType); + } + mDataType = dataType; + return this; + } + + /** + * Set the service uuids. Note the corresponding bluetooth Gatt services need to be + * already added on the device before start BLE advertising. + * + * @param serviceUuids Service uuids to be advertised, could be 16-bit, 32-bit or + * 128-bit uuids. + * @throws IllegalArgumentException If the {@code serviceUuids} are null. + */ + public Builder serviceUuids(List<ParcelUuid> serviceUuids) { + if (serviceUuids == null) { + throw new IllegalArgumentException("serivceUuids are null"); + } + mServiceUuids = serviceUuids; + return this; + } + + /** + * Add service data to advertisement. + * + * @param serviceDataUuid A 16 bit uuid of the service data + * @param serviceData Service data - the first two bytes of the service data are the + * service data uuid. + * @throws IllegalArgumentException If the {@code serviceDataUuid} or + * {@code serviceData} is empty. + */ + public Builder serviceData(ParcelUuid serviceDataUuid, byte[] serviceData) { + if (serviceDataUuid == null || serviceData == null) { + throw new IllegalArgumentException( + "serviceDataUuid or serviceDataUuid is null"); + } + mServiceDataUuid = serviceDataUuid; + mServiceData = serviceData; + return this; + } + + /** + * Set manufacturer id and data. See <a + * href="https://www.bluetooth.org/en-us/specification/assigned-numbers/company-identifiers">assigned + * manufacturer identifies</a> for the existing company identifiers. + * + * @param manufacturerId Manufacturer id assigned by Bluetooth SIG. + * @param manufacturerSpecificData Manufacturer specific data - the first two bytes of + * the manufacturer specific data are the manufacturer id. + * @throws IllegalArgumentException If the {@code manufacturerId} is negative or + * {@code manufacturerSpecificData} is null. + */ + public Builder manufacturerData(int manufacturerId, byte[] manufacturerSpecificData) { + if (manufacturerId < 0) { + throw new IllegalArgumentException( + "invalid manufacturerId - " + manufacturerId); + } + if (manufacturerSpecificData == null) { + throw new IllegalArgumentException("manufacturerSpecificData is null"); + } + mManufacturerId = manufacturerId; + mManufacturerSpecificData = manufacturerSpecificData; + return this; + } + + /** + * Whether the transmission power level should be included in the advertising packet. + */ + public Builder includeTxPowerLevel(boolean includeTxPowerLevel) { + mIncludeTxPowerLevel = includeTxPowerLevel; + return this; + } + + /** + * Build the {@link BluetoothLeAdvertiseScanData}. + * + * @throws IllegalArgumentException If the data size is larger than 31 bytes. + */ + public AdvertisementData build() { + if (totalBytes() > MAX_ADVERTISING_DATA_BYTES) { + throw new IllegalArgumentException( + "advertisement data size is larger than 31 bytes"); + } + return new AdvertisementData(mDataType, + mServiceUuids, + mServiceDataUuid, + mServiceData, mManufacturerId, mManufacturerSpecificData, + mIncludeTxPowerLevel); + } + + // Compute the size of the advertisement data. + private int totalBytes() { + int size = FLAGS_FIELD_BYTES; // flags field is always set. + if (mServiceUuids != null) { + int num16BitUuids = 0; + int num32BitUuids = 0; + int num128BitUuids = 0; + for (ParcelUuid uuid : mServiceUuids) { + if (BluetoothUuid.is16BitUuid(uuid)) { + ++num16BitUuids; + } else if (BluetoothUuid.is32BitUuid(uuid)) { + ++num32BitUuids; + } else { + ++num128BitUuids; + } + } + // 16 bit service uuids are grouped into one field when doing advertising. + if (num16BitUuids != 0) { + size += OVERHEAD_BYTES_PER_FIELD + + num16BitUuids * BluetoothUuid.UUID_BYTES_16_BIT; + } + // 32 bit service uuids are grouped into one field when doing advertising. + if (num32BitUuids != 0) { + size += OVERHEAD_BYTES_PER_FIELD + + num32BitUuids * BluetoothUuid.UUID_BYTES_32_BIT; + } + // 128 bit service uuids are grouped into one field when doing advertising. + if (num128BitUuids != 0) { + size += OVERHEAD_BYTES_PER_FIELD + + num128BitUuids * BluetoothUuid.UUID_BYTES_128_BIT; + } + } + if (mServiceData != null) { + size += OVERHEAD_BYTES_PER_FIELD + mServiceData.length; + } + if (mManufacturerSpecificData != null) { + size += OVERHEAD_BYTES_PER_FIELD + mManufacturerSpecificData.length; + } + if (mIncludeTxPowerLevel) { + size += OVERHEAD_BYTES_PER_FIELD + 1; // tx power level value is one byte. + } + return size; + } + } + + } + + /** + * Represents a scan record from Bluetooth LE scan. + */ + public static final class ScanRecord extends AdvertiseBaseData { + // Flags of the advertising data. + private final int mAdvertiseFlags; + + // Transmission power level(in dB). + private final int mTxPowerLevel; + + // Local name of the Bluetooth LE device. + private final String mLocalName; + + /** + * Returns the advertising flags indicating the discoverable mode and capability of the + * device. Returns -1 if the flag field is not set. + */ + public int getAdvertiseFlags() { + return mAdvertiseFlags; + } + + /** + * Returns the transmission power level of the packet in dBm. Returns + * {@link Integer#MIN_VALUE} if the field is not set. This value can be used to calculate + * the path loss of a received packet using the following equation: + * <p> + * <code>pathloss = txPowerLevel - rssi</code> + */ + public int getTxPowerLevel() { + return mTxPowerLevel; + } + + /** + * Returns the local name of the BLE device. The is a UTF-8 encoded string. + */ + @Nullable + public String getLocalName() { + return mLocalName; + } + + ScanRecord(int dataType, + List<ParcelUuid> serviceUuids, + ParcelUuid serviceDataUuid, byte[] serviceData, + int manufacturerId, + byte[] manufacturerSpecificData, int advertiseFlags, int txPowerLevel, + String localName) { + super(dataType, serviceUuids, serviceDataUuid, serviceData, manufacturerId, + manufacturerSpecificData); + mLocalName = localName; + mAdvertiseFlags = advertiseFlags; + mTxPowerLevel = txPowerLevel; + } + + /** + * Get a {@link Parser} to parse the scan record byte array into {@link ScanRecord}. + */ + public static Parser getParser() { + return new Parser(); + } + + /** + * A parser class used to parse a Bluetooth LE scan record to + * {@link BluetoothLeAdvertiseScanData}. Note not all field types would be parsed. + */ + public static final class Parser { + private static final String PARSER_TAG = "BluetoothLeAdvertiseDataParser"; + + // The following data type values are assigned by Bluetooth SIG. + // For more details refer to Bluetooth 4.0 specification, Volume 3, Part C, Section 18. + private static final int DATA_TYPE_FLAGS = 0x01; + private static final int DATA_TYPE_SERVICE_UUIDS_16_BIT_PARTIAL = 0x02; + private static final int DATA_TYPE_SERVICE_UUIDS_16_BIT_COMPLETE = 0x03; + private static final int DATA_TYPE_SERVICE_UUIDS_32_BIT_PARTIAL = 0x04; + private static final int DATA_TYPE_SERVICE_UUIDS_32_BIT_COMPLETE = 0x05; + private static final int DATA_TYPE_SERVICE_UUIDS_128_BIT_PARTIAL = 0x06; + private static final int DATA_TYPE_SERVICE_UUIDS_128_BIT_COMPLETE = 0x07; + private static final int DATA_TYPE_LOCAL_NAME_SHORT = 0x08; + private static final int DATA_TYPE_LOCAL_NAME_COMPLETE = 0x09; + private static final int DATA_TYPE_TX_POWER_LEVEL = 0x0A; + private static final int DATA_TYPE_SERVICE_DATA = 0x16; + private static final int DATA_TYPE_MANUFACTURER_SPECIFIC_DATA = 0xFF; + + // Helper method to extract bytes from byte array. + private static byte[] extractBytes(byte[] scanRecord, int start, int length) { + byte[] bytes = new byte[length]; + System.arraycopy(scanRecord, start, bytes, 0, length); + return bytes; + } + + /** + * Parse scan record to {@link BluetoothLeAdvertiseScanData.ScanRecord}. + * <p> + * The format is defined in Bluetooth 4.0 specification, Volume 3, Part C, Section 11 + * and 18. + * <p> + * All numerical multi-byte entities and values shall use little-endian + * <strong>byte</strong> order. + * + * @param scanRecord The scan record of Bluetooth LE advertisement and/or scan response. + */ + public ScanRecord parseFromScanRecord(byte[] scanRecord) { + if (scanRecord == null) { + return null; + } + + int currentPos = 0; + int advertiseFlag = -1; + List<ParcelUuid> serviceUuids = new ArrayList<ParcelUuid>(); + String localName = null; + int txPowerLevel = Integer.MIN_VALUE; + ParcelUuid serviceDataUuid = null; + byte[] serviceData = null; + int manufacturerId = -1; + byte[] manufacturerSpecificData = null; + + try { + while (currentPos < scanRecord.length) { + // length is unsigned int. + int length = scanRecord[currentPos++] & 0xFF; + if (length == 0) { + break; + } + // Note the length includes the length of the field type itself. + int dataLength = length - 1; + // fieldType is unsigned int. + int fieldType = scanRecord[currentPos++] & 0xFF; + switch (fieldType) { + case DATA_TYPE_FLAGS: + advertiseFlag = scanRecord[currentPos] & 0xFF; + break; + case DATA_TYPE_SERVICE_UUIDS_16_BIT_PARTIAL: + case DATA_TYPE_SERVICE_UUIDS_16_BIT_COMPLETE: + parseServiceUuid(scanRecord, currentPos, + dataLength, BluetoothUuid.UUID_BYTES_16_BIT, serviceUuids); + break; + case DATA_TYPE_SERVICE_UUIDS_32_BIT_PARTIAL: + case DATA_TYPE_SERVICE_UUIDS_32_BIT_COMPLETE: + parseServiceUuid(scanRecord, currentPos, dataLength, + BluetoothUuid.UUID_BYTES_32_BIT, serviceUuids); + break; + case DATA_TYPE_SERVICE_UUIDS_128_BIT_PARTIAL: + case DATA_TYPE_SERVICE_UUIDS_128_BIT_COMPLETE: + parseServiceUuid(scanRecord, currentPos, dataLength, + BluetoothUuid.UUID_BYTES_128_BIT, serviceUuids); + break; + case DATA_TYPE_LOCAL_NAME_SHORT: + case DATA_TYPE_LOCAL_NAME_COMPLETE: + localName = new String( + extractBytes(scanRecord, currentPos, dataLength)); + break; + case DATA_TYPE_TX_POWER_LEVEL: + txPowerLevel = scanRecord[currentPos]; + break; + case DATA_TYPE_SERVICE_DATA: + serviceData = extractBytes(scanRecord, currentPos, dataLength); + // The first two bytes of the service data are service data uuid. + int serviceUuidLength = BluetoothUuid.UUID_BYTES_16_BIT; + byte[] serviceDataUuidBytes = extractBytes(scanRecord, currentPos, + serviceUuidLength); + serviceDataUuid = BluetoothUuid.parseUuidFrom(serviceDataUuidBytes); + break; + case DATA_TYPE_MANUFACTURER_SPECIFIC_DATA: + manufacturerSpecificData = extractBytes(scanRecord, currentPos, + dataLength); + // The first two bytes of the manufacturer specific data are + // manufacturer ids in little endian. + manufacturerId = ((manufacturerSpecificData[1] & 0xFF) << 8) + + (manufacturerSpecificData[0] & 0xFF); + break; + default: + // Just ignore, we don't handle such data type. + break; + } + currentPos += dataLength; + } + + if (serviceUuids.isEmpty()) { + serviceUuids = null; + } + return new ScanRecord(PARSED_SCAN_RECORD, + serviceUuids, serviceDataUuid, serviceData, + manufacturerId, manufacturerSpecificData, advertiseFlag, txPowerLevel, + localName); + } catch (IndexOutOfBoundsException e) { + Log.e(PARSER_TAG, + "unable to parse scan record: " + Arrays.toString(scanRecord)); + return null; + } + } + + // Parse service uuids. + private int parseServiceUuid(byte[] scanRecord, int currentPos, int dataLength, + int uuidLength, List<ParcelUuid> serviceUuids) { + while (dataLength > 0) { + byte[] uuidBytes = extractBytes(scanRecord, currentPos, + uuidLength); + serviceUuids.add(BluetoothUuid.parseUuidFrom(uuidBytes)); + dataLength -= uuidLength; + currentPos += uuidLength; + } + return currentPos; + } + } + } + +} diff --git a/core/java/android/bluetooth/BluetoothLeAdvertiser.aidl b/core/java/android/bluetooth/BluetoothLeAdvertiser.aidl new file mode 100644 index 0000000..3108610 --- /dev/null +++ b/core/java/android/bluetooth/BluetoothLeAdvertiser.aidl @@ -0,0 +1,19 @@ +/* + * 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; + +parcelable BluetoothLeAdvertiser.Settings;
\ No newline at end of file diff --git a/core/java/android/bluetooth/BluetoothLeAdvertiser.java b/core/java/android/bluetooth/BluetoothLeAdvertiser.java new file mode 100644 index 0000000..2a8aa23 --- /dev/null +++ b/core/java/android/bluetooth/BluetoothLeAdvertiser.java @@ -0,0 +1,600 @@ +/* + * 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.bluetooth.BluetoothLeAdvertiseScanData.AdvertisementData; +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.util.Log; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +/** + * This class provides a way to perform Bluetooth LE advertise operations, such as start and stop + * advertising. An advertiser can broadcast up to 31 bytes of advertisement data represented by + * {@link BluetoothLeAdvertiseScanData.AdvertisementData}. + * <p> + * To get an instance of {@link BluetoothLeAdvertiser}, call the + * {@link BluetoothAdapter#getBluetoothLeAdvertiser()} method. + * <p> + * Note most of the methods here require {@link android.Manifest.permission#BLUETOOTH_ADMIN} + * permission. + * + * @see BluetoothLeAdvertiseScanData.AdvertisementData + */ +public class BluetoothLeAdvertiser { + + private static final String TAG = "BluetoothLeAdvertiser"; + + /** + * The {@link Settings} provide a way to adjust advertising preferences for each individual + * advertisement. Use {@link Settings.Builder} to create a {@link Settings} instance. + */ + public static final class Settings implements Parcelable { + /** + * Perform Bluetooth LE advertising in low power mode. This is the default and preferred + * advertising mode as it consumes the least power. + */ + public static final int ADVERTISE_MODE_LOW_POWER = 0; + /** + * Perform Bluetooth LE advertising in balanced power mode. This is balanced between + * advertising frequency and power consumption. + */ + public static final int ADVERTISE_MODE_BALANCED = 1; + /** + * Perform Bluetooth LE advertising in low latency, high power mode. This has the highest + * power consumption and should not be used for background continuous advertising. + */ + public static final int ADVERTISE_MODE_LOW_LATENCY = 2; + + /** + * Advertise using the lowest transmission(tx) power level. An app can use low transmission + * power to restrict the visibility range of its advertising packet. + */ + public static final int ADVERTISE_TX_POWER_ULTRA_LOW = 0; + /** + * Advertise using low tx power level. + */ + public static final int ADVERTISE_TX_POWER_LOW = 1; + /** + * Advertise using medium tx power level. + */ + public static final int ADVERTISE_TX_POWER_MEDIUM = 2; + /** + * Advertise using high tx power level. This is corresponding to largest visibility range of + * the advertising packet. + */ + public static final int ADVERTISE_TX_POWER_HIGH = 3; + + /** + * Non-connectable undirected advertising event, as defined in Bluetooth Specification V4.0 + * vol6, part B, section 4.4.2 - Advertising state. + */ + public static final int ADVERTISE_TYPE_NON_CONNECTABLE = 0; + /** + * Scannable undirected advertise type, as defined in same spec mentioned above. This event + * type allows a scanner to send a scan request asking additional information about the + * advertiser. + */ + public static final int ADVERTISE_TYPE_SCANNABLE = 1; + /** + * Connectable undirected advertising type, as defined in same spec mentioned above. This + * event type allows a scanner to send scan request asking additional information about the + * advertiser. It also allows an initiator to send a connect request for connection. + */ + public static final int ADVERTISE_TYPE_CONNECTABLE = 2; + + private final int mAdvertiseMode; + private final int mAdvertiseTxPowerLevel; + private final int mAdvertiseEventType; + + private Settings(int advertiseMode, int advertiseTxPowerLevel, + int advertiseEventType) { + mAdvertiseMode = advertiseMode; + mAdvertiseTxPowerLevel = advertiseTxPowerLevel; + mAdvertiseEventType = advertiseEventType; + } + + private Settings(Parcel in) { + mAdvertiseMode = in.readInt(); + mAdvertiseTxPowerLevel = in.readInt(); + mAdvertiseEventType = in.readInt(); + } + + /** + * Creates a {@link Builder} to construct a {@link Settings} object. + */ + public static Builder newBuilder() { + return new Builder(); + } + + /** + * Returns the advertise mode. + */ + public int getMode() { + return mAdvertiseMode; + } + + /** + * Returns the tx power level for advertising. + */ + public int getTxPowerLevel() { + return mAdvertiseTxPowerLevel; + } + + /** + * Returns the advertise event type. + */ + public int getType() { + return mAdvertiseEventType; + } + + @Override + public String toString() { + return "Settings [mAdvertiseMode=" + mAdvertiseMode + ", mAdvertiseTxPowerLevel=" + + mAdvertiseTxPowerLevel + ", mAdvertiseEventType=" + mAdvertiseEventType + "]"; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mAdvertiseMode); + dest.writeInt(mAdvertiseTxPowerLevel); + dest.writeInt(mAdvertiseEventType); + } + + public static final Parcelable.Creator<Settings> CREATOR = + new Creator<BluetoothLeAdvertiser.Settings>() { + @Override + public Settings[] newArray(int size) { + return new Settings[size]; + } + + @Override + public Settings createFromParcel(Parcel in) { + return new Settings(in); + } + }; + + /** + * Builder class for {@link BluetoothLeAdvertiser.Settings}. Caller should use + * {@link Settings#newBuilder()} to get an instance of the builder. + */ + public static final class Builder { + private int mMode = ADVERTISE_MODE_LOW_POWER; + private int mTxPowerLevel = ADVERTISE_TX_POWER_MEDIUM; + private int mType = ADVERTISE_TYPE_NON_CONNECTABLE; + + // Private constructor, use Settings.newBuilder() get an instance of BUILDER. + private Builder() { + } + + /** + * Set advertise mode to control the advertising power and latency. + * + * @param advertiseMode Bluetooth LE Advertising mode, can only be one of + * {@link Settings#ADVERTISE_MODE_LOW_POWER}, + * {@link Settings#ADVERTISE_MODE_BALANCED}, or + * {@link Settings#ADVERTISE_MODE_LOW_LATENCY}. + * @throws IllegalArgumentException If the advertiseMode is invalid. + */ + public Builder advertiseMode(int advertiseMode) { + if (advertiseMode < ADVERTISE_MODE_LOW_POWER + || advertiseMode > ADVERTISE_MODE_LOW_LATENCY) { + throw new IllegalArgumentException("unknown mode " + advertiseMode); + } + mMode = advertiseMode; + return this; + } + + /** + * Set advertise tx power level to control the transmission power level for the + * advertising. + * + * @param txPowerLevel Transmission power of Bluetooth LE Advertising, can only be one + * of {@link Settings#ADVERTISE_TX_POWER_ULTRA_LOW}, + * {@link Settings#ADVERTISE_TX_POWER_LOW}, + * {@link Settings#ADVERTISE_TX_POWER_MEDIUM} or + * {@link Settings#ADVERTISE_TX_POWER_HIGH}. + * @throws IllegalArgumentException If the {@code txPowerLevel} is invalid. + */ + public Builder txPowerLevel(int txPowerLevel) { + if (txPowerLevel < ADVERTISE_TX_POWER_ULTRA_LOW + || txPowerLevel > ADVERTISE_TX_POWER_HIGH) { + throw new IllegalArgumentException("unknown tx power level " + txPowerLevel); + } + mTxPowerLevel = txPowerLevel; + return this; + } + + /** + * Set advertise type to control the event type of advertising. + * + * @param type Bluetooth LE Advertising type, can be either + * {@link Settings#ADVERTISE_TYPE_NON_CONNECTABLE}, + * {@link Settings#ADVERTISE_TYPE_SCANNABLE} or + * {@link Settings#ADVERTISE_TYPE_CONNECTABLE}. + * @throws IllegalArgumentException If the {@code type} is invalid. + */ + public Builder type(int type) { + if (type < ADVERTISE_TYPE_NON_CONNECTABLE + || type > ADVERTISE_TYPE_CONNECTABLE) { + throw new IllegalArgumentException("unknown advertise type " + type); + } + mType = type; + return this; + } + + /** + * Build the {@link Settings} object. + */ + public Settings build() { + return new Settings(mMode, mTxPowerLevel, mType); + } + } + } + + /** + * Callback of Bluetooth LE advertising, which is used to deliver operation status for start and + * stop advertising. + */ + public interface AdvertiseCallback { + + /** + * The operation is success. + * + * @hide + */ + public static final int SUCCESS = 0; + /** + * Fails to start advertising as the advertisement data contains services that are not added + * to the local bluetooth Gatt server. + */ + public static final int ADVERTISING_SERVICE_UNKNOWN = 1; + /** + * Fails to start advertising as system runs out of quota for advertisers. + */ + public static final int TOO_MANY_ADVERTISERS = 2; + + /** + * Fails to start advertising as the advertising is already started. + */ + public static final int ADVERTISING_ALREADY_STARTED = 3; + /** + * Fails to stop advertising as the advertising is not started. + */ + public static final int ADVERISING_NOT_STARTED = 4; + + /** + * Operation fails due to bluetooth controller failure. + */ + public static final int CONTROLLER_FAILURE = 5; + + /** + * Callback when advertising operation succeeds. + * + * @param settingsInEffect The actual settings used for advertising, which may be different + * from what the app asks. + */ + public void onSuccess(Settings settingsInEffect); + + /** + * Callback when advertising operation fails. + * + * @param errorCode Error code for failures. + */ + public void onFailure(int errorCode); + } + + private final IBluetoothGatt mBluetoothGatt; + private final Handler mHandler; + private final Map<Settings, AdvertiseCallbackWrapper> + mLeAdvertisers = new HashMap<Settings, AdvertiseCallbackWrapper>(); + + // Package private constructor, use BluetoothAdapter.getLeAdvertiser() instead. + BluetoothLeAdvertiser(IBluetoothGatt bluetoothGatt) { + mBluetoothGatt = bluetoothGatt; + mHandler = new Handler(Looper.getMainLooper()); + } + + /** + * Bluetooth GATT interface callbacks for advertising. + */ + private static class AdvertiseCallbackWrapper extends IBluetoothGattCallback.Stub { + private static final int LE_CALLBACK_TIMEOUT_MILLIS = 2000; + private final AdvertiseCallback mAdvertiseCallback; + private final AdvertisementData mAdvertisement; + private final Settings mSettings; + private final IBluetoothGatt mBluetoothGatt; + + // mLeHandle 0: not registered + // -1: scan stopped + // >0: registered and scan started + private int mLeHandle; + private boolean isAdvertising = false; + + public AdvertiseCallbackWrapper(AdvertiseCallback advertiseCallback, + AdvertisementData advertiseData, Settings settings, + IBluetoothGatt bluetoothGatt) { + mAdvertiseCallback = advertiseCallback; + mAdvertisement = advertiseData; + mSettings = settings; + mBluetoothGatt = bluetoothGatt; + mLeHandle = 0; + } + + public boolean advertiseStarted() { + boolean started = false; + synchronized (this) { + if (mLeHandle == -1) { + return false; + } + try { + wait(LE_CALLBACK_TIMEOUT_MILLIS); + } catch (InterruptedException e) { + Log.e(TAG, "Callback reg wait interrupted: " + e); + } + started = (mLeHandle > 0 && isAdvertising); + } + return started; + } + + public boolean advertiseStopped() { + synchronized (this) { + try { + wait(LE_CALLBACK_TIMEOUT_MILLIS); + } catch (InterruptedException e) { + Log.e(TAG, "Callback reg wait interrupted: " + e); + } + return !isAdvertising; + } + } + + /** + * 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 (status == BluetoothGatt.GATT_SUCCESS) { + mLeHandle = clientIf; + try { + mBluetoothGatt.startMultiAdvertising(mLeHandle, mAdvertisement, mSettings); + } catch (RemoteException e) { + Log.e(TAG, "fail to start le advertise: " + e); + mLeHandle = -1; + notifyAll(); + } catch (Exception e) { + Log.e(TAG, "fail to start advertise: " + e.getStackTrace()); + } + } else { + // registration failed + mLeHandle = -1; + notifyAll(); + } + } + } + + @Override + public void onClientConnectionState(int status, int clientIf, + boolean connected, String address) { + // no op + } + + @Override + public void onScanResult(String address, int rssi, byte[] advData) { + // no op + } + + @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) { + synchronized (this) { + if (status == 0) { + isAdvertising = !isAdvertising; + if (!isAdvertising) { + try { + mBluetoothGatt.unregisterClient(mLeHandle); + mLeHandle = -1; + } catch (RemoteException e) { + Log.e(TAG, "remote exception when unregistering", e); + } + } + mAdvertiseCallback.onSuccess(null); + } else { + mAdvertiseCallback.onFailure(status); + } + notifyAll(); + } + + } + + /** + * Callback reporting LE ATT MTU. + * + * @hide + */ + public void onConfigureMTU(String address, int mtu, int status) { + // no op + } + } + + /** + * Start Bluetooth LE Advertising. + * + * @param settings {@link Settings} for Bluetooth LE advertising. + * @param advertiseData {@link AdvertisementData} to be advertised. + * @param callback {@link AdvertiseCallback} for advertising status. + */ + public void startAdvertising(Settings settings, + AdvertisementData advertiseData, final AdvertiseCallback callback) { + if (callback == null) { + throw new IllegalArgumentException("callback cannot be null"); + } + if (mLeAdvertisers.containsKey(settings)) { + postCallbackFailure(callback, AdvertiseCallback.ADVERTISING_ALREADY_STARTED); + return; + } + AdvertiseCallbackWrapper wrapper = new AdvertiseCallbackWrapper(callback, advertiseData, + settings, + mBluetoothGatt); + UUID uuid = UUID.randomUUID(); + try { + mBluetoothGatt.registerClient(new ParcelUuid(uuid), wrapper); + if (wrapper.advertiseStarted()) { + mLeAdvertisers.put(settings, wrapper); + } + } catch (RemoteException e) { + Log.e(TAG, "failed to stop advertising", e); + } + } + + /** + * Stop Bluetooth LE advertising. Returns immediately, the operation status will be delivered + * through the {@code callback}. + * <p> + * Requires {@link android.Manifest.permission#BLUETOOTH_ADMIN} + * + * @param settings {@link Settings} used to start Bluetooth LE advertising. + * @param callback {@link AdvertiseCallback} for delivering stopping advertising status. + */ + public void stopAdvertising(final Settings settings, final AdvertiseCallback callback) { + if (callback == null) { + throw new IllegalArgumentException("callback cannot be null"); + } + AdvertiseCallbackWrapper wrapper = mLeAdvertisers.get(settings); + if (wrapper == null) { + postCallbackFailure(callback, AdvertiseCallback.ADVERISING_NOT_STARTED); + return; + } + try { + mBluetoothGatt.stopMultiAdvertising(wrapper.mLeHandle); + if (wrapper.advertiseStopped()) { + mLeAdvertisers.remove(settings); + } + } catch (RemoteException e) { + Log.e(TAG, "failed to stop advertising", e); + } + } + + private void postCallbackFailure(final AdvertiseCallback callback, final int error) { + mHandler.post(new Runnable() { + @Override + public void run() { + callback.onFailure(error); + } + }); + } +} diff --git a/core/java/android/bluetooth/BluetoothLeScanFilter.aidl b/core/java/android/bluetooth/BluetoothLeScanFilter.aidl new file mode 100644 index 0000000..86ee06d --- /dev/null +++ b/core/java/android/bluetooth/BluetoothLeScanFilter.aidl @@ -0,0 +1,19 @@ +/* + * 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; + +parcelable BluetoothLeScanFilter; diff --git a/core/java/android/bluetooth/BluetoothLeScanFilter.java b/core/java/android/bluetooth/BluetoothLeScanFilter.java new file mode 100644 index 0000000..2ed85ba --- /dev/null +++ b/core/java/android/bluetooth/BluetoothLeScanFilter.java @@ -0,0 +1,577 @@ +/* + * 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.bluetooth.BluetoothLeAdvertiseScanData.ScanRecord; +import android.bluetooth.BluetoothLeScanner.ScanResult; +import android.os.Parcel; +import android.os.ParcelUuid; +import android.os.Parcelable; + +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.UUID; + +/** + * {@link BluetoothLeScanFilter} abstracts different scan filters across Bluetooth Advertisement + * packet fields. + * <p> + * Current filtering on the following fields are supported: + * <li>Service UUIDs which identify the bluetooth gatt services running on the device. + * <li>Name of remote Bluetooth LE device. + * <li>Mac address of the remote device. + * <li>Rssi which indicates the received power level. + * <li>Service data which is the data associated with a service. + * <li>Manufacturer specific data which is the data associated with a particular manufacturer. + * + * @see BluetoothLeAdvertiseScanData.ScanRecord + * @see BluetoothLeScanner + */ +public final class BluetoothLeScanFilter implements Parcelable { + + @Nullable + private final String mLocalName; + + @Nullable + private final String mMacAddress; + + @Nullable + private final ParcelUuid mServiceUuid; + @Nullable + private final ParcelUuid mServiceUuidMask; + + @Nullable + private final byte[] mServiceData; + @Nullable + private final byte[] mServiceDataMask; + + private final int mManufacturerId; + @Nullable + private final byte[] mManufacturerData; + @Nullable + private final byte[] mManufacturerDataMask; + + private final int mMinRssi; + private final int mMaxRssi; + + private BluetoothLeScanFilter(String name, String macAddress, ParcelUuid uuid, + ParcelUuid uuidMask, byte[] serviceData, byte[] serviceDataMask, + int manufacturerId, byte[] manufacturerData, byte[] manufacturerDataMask, + int minRssi, int maxRssi) { + mLocalName = name; + mServiceUuid = uuid; + mServiceUuidMask = uuidMask; + mMacAddress = macAddress; + mServiceData = serviceData; + mServiceDataMask = serviceDataMask; + mManufacturerId = manufacturerId; + mManufacturerData = manufacturerData; + mManufacturerDataMask = manufacturerDataMask; + mMinRssi = minRssi; + mMaxRssi = maxRssi; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mLocalName == null ? 0 : 1); + if (mLocalName != null) { + dest.writeString(mLocalName); + } + dest.writeInt(mMacAddress == null ? 0 : 1); + if (mMacAddress != null) { + dest.writeString(mMacAddress); + } + dest.writeInt(mServiceUuid == null ? 0 : 1); + if (mServiceUuid != null) { + dest.writeParcelable(mServiceUuid, flags); + } + dest.writeInt(mServiceUuidMask == null ? 0 : 1); + if (mServiceUuidMask != null) { + dest.writeParcelable(mServiceUuidMask, flags); + } + dest.writeInt(mServiceData == null ? 0 : mServiceData.length); + if (mServiceData != null) { + dest.writeByteArray(mServiceData); + } + dest.writeInt(mServiceDataMask == null ? 0 : mServiceDataMask.length); + if (mServiceDataMask != null) { + dest.writeByteArray(mServiceDataMask); + } + dest.writeInt(mManufacturerId); + dest.writeInt(mManufacturerData == null ? 0 : mManufacturerData.length); + if (mManufacturerData != null) { + dest.writeByteArray(mManufacturerData); + } + dest.writeInt(mManufacturerDataMask == null ? 0 : mManufacturerDataMask.length); + if (mManufacturerDataMask != null) { + dest.writeByteArray(mManufacturerDataMask); + } + dest.writeInt(mMinRssi); + dest.writeInt(mMaxRssi); + } + + /** + * A {@link android.os.Parcelable.Creator} to create {@link BluetoothLeScanFilter} form parcel. + */ + public static final Creator<BluetoothLeScanFilter> + CREATOR = new Creator<BluetoothLeScanFilter>() { + + @Override + public BluetoothLeScanFilter[] newArray(int size) { + return new BluetoothLeScanFilter[size]; + } + + @Override + public BluetoothLeScanFilter createFromParcel(Parcel in) { + Builder builder = newBuilder(); + if (in.readInt() == 1) { + builder.name(in.readString()); + } + if (in.readInt() == 1) { + builder.macAddress(in.readString()); + } + if (in.readInt() == 1) { + ParcelUuid uuid = in.readParcelable(ParcelUuid.class.getClassLoader()); + builder.serviceUuid(uuid); + } + if (in.readInt() == 1) { + ParcelUuid uuidMask = in.readParcelable(ParcelUuid.class.getClassLoader()); + builder.serviceUuidMask(uuidMask); + } + int serviceDataLength = in.readInt(); + if (serviceDataLength > 0) { + byte[] serviceData = new byte[serviceDataLength]; + in.readByteArray(serviceData); + builder.serviceData(serviceData); + } + int serviceDataMaskLength = in.readInt(); + if (serviceDataMaskLength > 0) { + byte[] serviceDataMask = new byte[serviceDataMaskLength]; + in.readByteArray(serviceDataMask); + builder.serviceDataMask(serviceDataMask); + } + int manufacturerId = in.readInt(); + int manufacturerDataLength = in.readInt(); + if (manufacturerDataLength > 0) { + byte[] manufacturerData = new byte[manufacturerDataLength]; + in.readByteArray(manufacturerData); + builder.manufacturerData(manufacturerId, manufacturerData); + } + int manufacturerDataMaskLength = in.readInt(); + if (manufacturerDataMaskLength > 0) { + byte[] manufacturerDataMask = new byte[manufacturerDataMaskLength]; + in.readByteArray(manufacturerDataMask); + builder.manufacturerDataMask(manufacturerDataMask); + } + int minRssi = in.readInt(); + int maxRssi = in.readInt(); + builder.rssiRange(minRssi, maxRssi); + return builder.build(); + } + }; + + /** + * Returns the filter set the local name field of Bluetooth advertisement data. + */ + @Nullable + public String getLocalName() { + return mLocalName; + } + + @Nullable /** + * Returns the filter set on the service uuid. + */ + public ParcelUuid getServiceUuid() { + return mServiceUuid; + } + + @Nullable + public ParcelUuid getServiceUuidMask() { + return mServiceUuidMask; + } + + @Nullable + public String getDeviceAddress() { + return mMacAddress; + } + + @Nullable + public byte[] getServiceData() { + return mServiceData; + } + + @Nullable + public byte[] getServiceDataMask() { + return mServiceDataMask; + } + + /** + * Returns the manufacturer id. -1 if the manufacturer filter is not set. + */ + public int getManufacturerId() { + return mManufacturerId; + } + + @Nullable + public byte[] getManufacturerData() { + return mManufacturerData; + } + + @Nullable + public byte[] getManufacturerDataMask() { + return mManufacturerDataMask; + } + + /** + * Returns minimum value of rssi for the scan filter. {@link Integer#MIN_VALUE} if not set. + */ + public int getMinRssi() { + return mMinRssi; + } + + /** + * Returns maximum value of the rssi for the scan filter. {@link Integer#MAX_VALUE} if not set. + */ + public int getMaxRssi() { + return mMaxRssi; + } + + /** + * Check if the scan filter matches a {@code scanResult}. A scan result is considered as a match + * if it matches all the field filters. + */ + public boolean matches(ScanResult scanResult) { + if (scanResult == null) { + return false; + } + BluetoothDevice device = scanResult.getDevice(); + // Device match. + if (mMacAddress != null && (device == null || !mMacAddress.equals(device.getAddress()))) { + return false; + } + + int rssi = scanResult.getRssi(); + if (rssi < mMinRssi || rssi > mMaxRssi) { + return false; + } + + byte[] scanRecordBytes = scanResult.getScanRecord(); + ScanRecord scanRecord = ScanRecord.getParser().parseFromScanRecord(scanRecordBytes); + + // Scan record is null but there exist filters on it. + if (scanRecord == null + && (mLocalName != null || mServiceUuid != null || mManufacturerData != null + || mServiceData != null)) { + return false; + } + + // Local name match. + if (mLocalName != null && !mLocalName.equals(scanRecord.getLocalName())) { + return false; + } + + // UUID match. + if (mServiceUuid != null && !matchesServiceUuids(mServiceUuid, mServiceUuidMask, + scanRecord.getServiceUuids())) { + return false; + } + + // Service data match + if (mServiceData != null && + !matchesPartialData(mServiceData, mServiceDataMask, scanRecord.getServiceData())) { + return false; + } + + // Manufacturer data match. + if (mManufacturerData != null && !matchesPartialData(mManufacturerData, + mManufacturerDataMask, scanRecord.getManufacturerSpecificData())) { + return false; + } + // All filters match. + return true; + } + + // Check if the uuid pattern is contained in a list of parcel uuids. + private boolean matchesServiceUuids(ParcelUuid uuid, ParcelUuid parcelUuidMask, + List<ParcelUuid> uuids) { + if (uuid == null) { + return true; + } + if (uuids == null) { + return false; + } + + for (ParcelUuid parcelUuid : uuids) { + UUID uuidMask = parcelUuidMask == null ? null : parcelUuidMask.getUuid(); + if (matchesServiceUuid(uuid.getUuid(), uuidMask, parcelUuid.getUuid())) { + return true; + } + } + return false; + } + + // Check if the uuid pattern matches the particular service uuid. + private boolean matchesServiceUuid(UUID uuid, UUID mask, UUID data) { + if (mask == null) { + return uuid.equals(data); + } + if ((uuid.getLeastSignificantBits() & mask.getLeastSignificantBits()) != + (data.getLeastSignificantBits() & mask.getLeastSignificantBits())) { + return false; + } + return ((uuid.getMostSignificantBits() & mask.getMostSignificantBits()) == + (data.getMostSignificantBits() & mask.getMostSignificantBits())); + } + + // Check whether the data pattern matches the parsed data. + private boolean matchesPartialData(byte[] data, byte[] dataMask, byte[] parsedData) { + if (dataMask == null) { + return Arrays.equals(data, parsedData); + } + if (parsedData == null) { + return false; + } + for (int i = 0; i < data.length; ++i) { + if ((dataMask[i] & parsedData[i]) != (dataMask[i] & data[i])) { + return false; + } + } + return true; + } + + @Override + public String toString() { + return "BluetoothLeScanFilter [mLocalName=" + mLocalName + ", mMacAddress=" + mMacAddress + + ", mUuid=" + mServiceUuid + ", mUuidMask=" + mServiceUuidMask + ", mServiceData=" + + Arrays.toString(mServiceData) + ", mServiceDataMask=" + + Arrays.toString(mServiceDataMask) + ", mManufacturerId=" + mManufacturerId + + ", mManufacturerData=" + Arrays.toString(mManufacturerData) + + ", mManufacturerDataMask=" + Arrays.toString(mManufacturerDataMask) + + ", mMinRssi=" + mMinRssi + ", mMaxRssi=" + mMaxRssi + "]"; + } + + @Override + public int hashCode() { + return Objects.hash(mLocalName, mMacAddress, mManufacturerId, mManufacturerData, + mManufacturerDataMask, mMaxRssi, mMinRssi, mServiceData, mServiceDataMask, + mServiceUuid, mServiceUuidMask); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + BluetoothLeScanFilter other = (BluetoothLeScanFilter) obj; + return Objects.equals(mLocalName, other.mLocalName) && + Objects.equals(mMacAddress, other.mMacAddress) && + mManufacturerId == other.mManufacturerId && + Objects.deepEquals(mManufacturerData, other.mManufacturerData) && + Objects.deepEquals(mManufacturerDataMask, other.mManufacturerDataMask) && + mMinRssi == other.mMinRssi && mMaxRssi == other.mMaxRssi && + Objects.deepEquals(mServiceData, other.mServiceData) && + Objects.deepEquals(mServiceDataMask, other.mServiceDataMask) && + Objects.equals(mServiceUuid, other.mServiceUuid) && + Objects.equals(mServiceUuidMask, other.mServiceUuidMask); + } + + /** + * Returns the {@link Builder} for {@link BluetoothLeScanFilter}. + */ + public static Builder newBuilder() { + return new Builder(); + } + + /** + * Builder class for {@link BluetoothLeScanFilter}. Use + * {@link BluetoothLeScanFilter#newBuilder()} to get an instance of the {@link Builder}. + */ + public static class Builder { + + private String mLocalName; + private String mMacAddress; + + private ParcelUuid mServiceUuid; + private ParcelUuid mUuidMask; + + private byte[] mServiceData; + private byte[] mServiceDataMask; + + private int mManufacturerId = -1; + private byte[] mManufacturerData; + private byte[] mManufacturerDataMask; + + private int mMinRssi = Integer.MIN_VALUE; + private int mMaxRssi = Integer.MAX_VALUE; + + // Private constructor, use BluetoothLeScanFilter.newBuilder instead. + private Builder() { + } + + /** + * Set filtering on local name. + */ + public Builder name(String localName) { + mLocalName = localName; + return this; + } + + /** + * Set filtering on device mac address. + * + * @param macAddress The device mac address for the filter. It needs to be in the format of + * "01:02:03:AB:CD:EF". The mac address can be validated using + * {@link BluetoothAdapter#checkBluetoothAddress}. + * @throws IllegalArgumentException If the {@code macAddress} is invalid. + */ + public Builder macAddress(String macAddress) { + if (macAddress != null && !BluetoothAdapter.checkBluetoothAddress(macAddress)) { + throw new IllegalArgumentException("invalid mac address " + macAddress); + } + mMacAddress = macAddress; + return this; + } + + /** + * Set filtering on service uuid. + */ + public Builder serviceUuid(ParcelUuid serviceUuid) { + mServiceUuid = serviceUuid; + return this; + } + + /** + * Set partial uuid filter. The {@code uuidMask} is the bit mask for the {@code uuid} set + * through {@link #serviceUuid(ParcelUuid)} method. Set any bit in the mask to 1 to indicate + * a match is needed for the bit in {@code serviceUuid}, and 0 to ignore that bit. + * <p> + * The length of {@code uuidMask} must be the same as {@code serviceUuid}. + */ + public Builder serviceUuidMask(ParcelUuid uuidMask) { + mUuidMask = uuidMask; + return this; + } + + /** + * Set service data filter. + */ + public Builder serviceData(byte[] serviceData) { + mServiceData = serviceData; + return this; + } + + /** + * Set partial service data filter bit mask. For any bit in the mask, set it to 1 if it + * needs to match the one in service data, otherwise set it to 0 to ignore that bit. + * <p> + * The {@code serviceDataMask} must have the same length of the {@code serviceData} set + * through {@link #serviceData(byte[])}. + */ + public Builder serviceDataMask(byte[] serviceDataMask) { + mServiceDataMask = serviceDataMask; + return this; + } + + /** + * Set manufacturerId and manufacturerData. A negative manufacturerId is considered as + * invalid id. + * <p> + * Note the first two bytes of the {@code manufacturerData} is the manufacturerId. + */ + public Builder manufacturerData(int manufacturerId, byte[] manufacturerData) { + if (manufacturerData != null && manufacturerId < 0) { + throw new IllegalArgumentException("invalid manufacture id"); + } + mManufacturerId = manufacturerId; + mManufacturerData = manufacturerData; + return this; + } + + /** + * Set partial manufacture data filter bit mask. For any bit in the mask, set it the 1 if it + * needs to match the one in manufacturer data, otherwise set it to 0. + * <p> + * The {@code manufacturerDataMask} must have the same length of {@code manufacturerData} + * set through {@link #manufacturerData(int, byte[])}. + */ + public Builder manufacturerDataMask(byte[] manufacturerDataMask) { + mManufacturerDataMask = manufacturerDataMask; + return this; + } + + /** + * Set the desired rssi range for the filter. A scan result with rssi in the range of + * [minRssi, maxRssi] will be consider as a match. + */ + public Builder rssiRange(int minRssi, int maxRssi) { + mMinRssi = minRssi; + mMaxRssi = maxRssi; + return this; + } + + /** + * Build {@link BluetoothLeScanFilter}. + * + * @throws IllegalArgumentException If the filter cannot be built. + */ + public BluetoothLeScanFilter build() { + if (mUuidMask != null && mServiceUuid == null) { + throw new IllegalArgumentException("uuid is null while uuidMask is not null!"); + } + + if (mServiceDataMask != null) { + if (mServiceData == null) { + throw new IllegalArgumentException( + "serviceData is null while serviceDataMask is not null"); + } + // Since the mServiceDataMask is a bit mask for mServiceData, the lengths of the two + // byte array need to be the same. + if (mServiceData.length != mServiceDataMask.length) { + throw new IllegalArgumentException( + "size mismatch for service data and service data mask"); + } + } + + if (mManufacturerDataMask != null) { + if (mManufacturerData == null) { + throw new IllegalArgumentException( + "manufacturerData is null while manufacturerDataMask is not null"); + } + // Since the mManufacturerDataMask is a bit mask for mManufacturerData, the lengths + // of the two byte array need to be the same. + if (mManufacturerData.length != mManufacturerDataMask.length) { + throw new IllegalArgumentException( + "size mismatch for manufacturerData and manufacturerDataMask"); + } + } + return new BluetoothLeScanFilter(mLocalName, mMacAddress, + mServiceUuid, mUuidMask, + mServiceData, mServiceDataMask, + mManufacturerId, mManufacturerData, mManufacturerDataMask, mMinRssi, mMaxRssi); + } + } +} diff --git a/core/java/android/bluetooth/BluetoothLeScanner.aidl b/core/java/android/bluetooth/BluetoothLeScanner.aidl new file mode 100644 index 0000000..8cecdd7 --- /dev/null +++ b/core/java/android/bluetooth/BluetoothLeScanner.aidl @@ -0,0 +1,20 @@ +/* + * 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; + +parcelable BluetoothLeScanner.ScanResult; +parcelable BluetoothLeScanner.Settings; diff --git a/core/java/android/bluetooth/BluetoothLeScanner.java b/core/java/android/bluetooth/BluetoothLeScanner.java new file mode 100644 index 0000000..ed3188b --- /dev/null +++ b/core/java/android/bluetooth/BluetoothLeScanner.java @@ -0,0 +1,759 @@ +/* + * 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. + * <p> + * Use {@link BluetoothAdapter#getBluetoothLeScanner()} to get an instance of + * {@link BluetoothLeScanner}. + * <p> + * 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<Settings> CREATOR = new Creator<Settings>() { + @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<ScanResult> CREATOR = new Creator<ScanResult>() { + @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<ScanResult> 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<Settings, BleScanCallbackWrapper> mLeScanClients; + + BluetoothLeScanner(IBluetoothGatt bluetoothGatt) { + mBluetoothGatt = bluetoothGatt; + mHandler = new Handler(Looper.getMainLooper()); + mLeScanClients = new HashMap<Settings, BleScanCallbackWrapper>(); + } + + /** + * 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<BluetoothLeScanFilter> 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<BluetoothLeScanFilter> 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<BluetoothLeScanFilter> 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<ScanResult> getBatchScanResults(ScanCallback callback, boolean flush) { + throw new UnsupportedOperationException("not impelemented"); + } + +} diff --git a/core/java/android/bluetooth/IBluetoothGatt.aidl b/core/java/android/bluetooth/IBluetoothGatt.aidl index 3dd7094..091b806 100644 --- a/core/java/android/bluetooth/IBluetoothGatt.aidl +++ b/core/java/android/bluetooth/IBluetoothGatt.aidl @@ -17,6 +17,10 @@ package android.bluetooth; import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothLeAdvertiseScanData; +import android.bluetooth.BluetoothLeAdvertiser; +import android.bluetooth.BluetoothLeScanFilter; +import android.bluetooth.BluetoothLeScanner; import android.os.ParcelUuid; import android.bluetooth.IBluetoothGattCallback; @@ -33,8 +37,13 @@ interface IBluetoothGatt { void startScanWithUuids(in int appIf, in boolean isServer, in ParcelUuid[] ids); void startScanWithUuidsScanParam(in int appIf, in boolean isServer, in ParcelUuid[] ids, int scanWindow, int scanInterval); + void startScanWithFilters(in int appIf, in boolean isServer, + in BluetoothLeScanner.Settings settings, + in List<BluetoothLeScanFilter> filters); void stopScan(in int appIf, in boolean isServer); - + void startMultiAdvertising(in int appIf, in BluetoothLeAdvertiseScanData.AdvertisementData data, + in BluetoothLeAdvertiser.Settings settings); + void stopMultiAdvertising(in int appIf); void registerClient(in ParcelUuid appId, in IBluetoothGattCallback callback); void unregisterClient(in int clientIf); void clientConnect(in int clientIf, in String address, in boolean isDirect, in int transport); diff --git a/core/java/android/bluetooth/IBluetoothGattCallback.aidl b/core/java/android/bluetooth/IBluetoothGattCallback.aidl index a78c29b..bf9e0a7 100644 --- a/core/java/android/bluetooth/IBluetoothGattCallback.aidl +++ b/core/java/android/bluetooth/IBluetoothGattCallback.aidl @@ -64,5 +64,6 @@ interface IBluetoothGattCallback { in byte[] value); void onReadRemoteRssi(in String address, in int rssi, in int status); oneway void onAdvertiseStateChange(in int advertiseState, in int status); + oneway void onMultiAdvertiseCallback(in int status); void onConfigureMTU(in String address, in int mtu, in int status); } |