/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.bluetooth.le;
import android.annotation.Nullable;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.os.Parcel;
import android.os.ParcelUuid;
import android.os.Parcelable;
import com.android.internal.util.BitUtils;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
Criteria for filtering result from Bluetooth LE scans. A ScanFilter
allows clients to restrict scan results to only those that are of interest to them.
Current filtering on the following fields are supported:
Service UUIDs which identify the bluetooth gatt services running on the device.
Name of remote Bluetooth LE device.
Mac address of the remote device.
Service data which is the data associated with a service.
Manufacturer specific data which is the data associated with a particular manufacturer.
See Also:
/**
* Criteria for filtering result from Bluetooth LE scans. A {@link ScanFilter} allows clients to
* restrict scan results to only those that are of interest to them.
* <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>Service data which is the data associated with a service.
* <li>Manufacturer specific data which is the data associated with a particular manufacturer.
*
* @see ScanResult
* @see BluetoothLeScanner
*/
public final class ScanFilter implements Parcelable {
@Nullable
private final String mDeviceName;
@Nullable
private final String mDeviceAddress;
@Nullable
private final ParcelUuid mServiceUuid;
@Nullable
private final ParcelUuid mServiceUuidMask;
@Nullable
private final ParcelUuid mServiceDataUuid;
@Nullable
private final byte[] mServiceData;
@Nullable
private final byte[] mServiceDataMask;
private final int mManufacturerId;
@Nullable
private final byte[] mManufacturerData;
@Nullable
private final byte[] mManufacturerDataMask;
@hide
/** @hide */
public static final ScanFilter EMPTY = new ScanFilter.Builder().build();
private ScanFilter(String name, String deviceAddress, ParcelUuid uuid,
ParcelUuid uuidMask, ParcelUuid serviceDataUuid,
byte[] serviceData, byte[] serviceDataMask,
int manufacturerId, byte[] manufacturerData, byte[] manufacturerDataMask) {
mDeviceName = name;
mServiceUuid = uuid;
mServiceUuidMask = uuidMask;
mDeviceAddress = deviceAddress;
mServiceDataUuid = serviceDataUuid;
mServiceData = serviceData;
mServiceDataMask = serviceDataMask;
mManufacturerId = manufacturerId;
mManufacturerData = manufacturerData;
mManufacturerDataMask = manufacturerDataMask;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(mDeviceName == null ? 0 : 1);
if (mDeviceName != null) {
dest.writeString(mDeviceName);
}
dest.writeInt(mDeviceAddress == null ? 0 : 1);
if (mDeviceAddress != null) {
dest.writeString(mDeviceAddress);
}
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(mServiceDataUuid == null ? 0 : 1);
if (mServiceDataUuid != null) {
dest.writeParcelable(mServiceDataUuid, flags);
dest.writeInt(mServiceData == null ? 0 : 1);
if (mServiceData != null) {
dest.writeInt(mServiceData.length);
dest.writeByteArray(mServiceData);
dest.writeInt(mServiceDataMask == null ? 0 : 1);
if (mServiceDataMask != null) {
dest.writeInt(mServiceDataMask.length);
dest.writeByteArray(mServiceDataMask);
}
}
}
dest.writeInt(mManufacturerId);
dest.writeInt(mManufacturerData == null ? 0 : 1);
if (mManufacturerData != null) {
dest.writeInt(mManufacturerData.length);
dest.writeByteArray(mManufacturerData);
dest.writeInt(mManufacturerDataMask == null ? 0 : 1);
if (mManufacturerDataMask != null) {
dest.writeInt(mManufacturerDataMask.length);
dest.writeByteArray(mManufacturerDataMask);
}
}
}
A Creator
to create ScanFilter
from parcel. /**
* A {@link android.os.Parcelable.Creator} to create {@link ScanFilter} from parcel.
*/
public static final Creator<ScanFilter> CREATOR =
new Creator<ScanFilter>() {
@Override
public ScanFilter[] newArray(int size) {
return new ScanFilter[size];
}
@Override
public ScanFilter createFromParcel(Parcel in) {
Builder builder = new Builder();
if (in.readInt() == 1) {
builder.setDeviceName(in.readString());
}
if (in.readInt() == 1) {
builder.setDeviceAddress(in.readString());
}
if (in.readInt() == 1) {
ParcelUuid uuid = in.readParcelable(ParcelUuid.class.getClassLoader());
builder.setServiceUuid(uuid);
if (in.readInt() == 1) {
ParcelUuid uuidMask = in.readParcelable(
ParcelUuid.class.getClassLoader());
builder.setServiceUuid(uuid, uuidMask);
}
}
if (in.readInt() == 1) {
ParcelUuid servcieDataUuid =
in.readParcelable(ParcelUuid.class.getClassLoader());
if (in.readInt() == 1) {
int serviceDataLength = in.readInt();
byte[] serviceData = new byte[serviceDataLength];
in.readByteArray(serviceData);
if (in.readInt() == 0) {
builder.setServiceData(servcieDataUuid, serviceData);
} else {
int serviceDataMaskLength = in.readInt();
byte[] serviceDataMask = new byte[serviceDataMaskLength];
in.readByteArray(serviceDataMask);
builder.setServiceData(
servcieDataUuid, serviceData, serviceDataMask);
}
}
}
int manufacturerId = in.readInt();
if (in.readInt() == 1) {
int manufacturerDataLength = in.readInt();
byte[] manufacturerData = new byte[manufacturerDataLength];
in.readByteArray(manufacturerData);
if (in.readInt() == 0) {
builder.setManufacturerData(manufacturerId, manufacturerData);
} else {
int manufacturerDataMaskLength = in.readInt();
byte[] manufacturerDataMask = new byte[manufacturerDataMaskLength];
in.readByteArray(manufacturerDataMask);
builder.setManufacturerData(manufacturerId, manufacturerData,
manufacturerDataMask);
}
}
return builder.build();
}
};
Returns the filter set the device name field of Bluetooth advertisement data.
/**
* Returns the filter set the device name field of Bluetooth advertisement data.
*/
@Nullable
public String getDeviceName() {
return mDeviceName;
}
Returns the filter set on the service uuid.
/**
* Returns the filter set on the service uuid.
*/
@Nullable
public ParcelUuid getServiceUuid() {
return mServiceUuid;
}
@Nullable
public ParcelUuid getServiceUuidMask() {
return mServiceUuidMask;
}
@Nullable
public String getDeviceAddress() {
return mDeviceAddress;
}
@Nullable
public byte[] getServiceData() {
return mServiceData;
}
@Nullable
public byte[] getServiceDataMask() {
return mServiceDataMask;
}
@Nullable
public ParcelUuid getServiceDataUuid() {
return mServiceDataUuid;
}
Returns the manufacturer id. -1 if the manufacturer filter is not set.
/**
* 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;
}
Check if the scan filter matches a scanResult
. A scan result is considered as a match if it matches all the field filters. /**
* 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 (mDeviceAddress != null
&& (device == null || !mDeviceAddress.equals(device.getAddress()))) {
return false;
}
ScanRecord scanRecord = scanResult.getScanRecord();
// Scan record is null but there exist filters on it.
if (scanRecord == null
&& (mDeviceName != null || mServiceUuid != null || mManufacturerData != null
|| mServiceData != null)) {
return false;
}
// Local name match.
if (mDeviceName != null && !mDeviceName.equals(scanRecord.getDeviceName())) {
return false;
}
// UUID match.
if (mServiceUuid != null && !matchesServiceUuids(mServiceUuid, mServiceUuidMask,
scanRecord.getServiceUuids())) {
return false;
}
// Service data match
if (mServiceDataUuid != null) {
if (!matchesPartialData(mServiceData, mServiceDataMask,
scanRecord.getServiceData(mServiceDataUuid))) {
return false;
}
}
// Manufacturer data match.
if (mManufacturerId >= 0) {
if (!matchesPartialData(mManufacturerData, mManufacturerDataMask,
scanRecord.getManufacturerSpecificData(mManufacturerId))) {
return false;
}
}
// All filters match.
return true;
}
Check if the uuid pattern is contained in a list of parcel uuids.
@hide
/**
* Check if the uuid pattern is contained in a list of parcel uuids.
*
* @hide
*/
public static 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 static boolean matchesServiceUuid(UUID uuid, UUID mask, UUID data) {
return BitUtils.maskedEquals(data, uuid, mask);
}
// Check whether the data pattern matches the parsed data.
private boolean matchesPartialData(byte[] data, byte[] dataMask, byte[] parsedData) {
if (parsedData == null || parsedData.length < data.length) {
return false;
}
if (dataMask == null) {
for (int i = 0; i < data.length; ++i) {
if (parsedData[i] != data[i]) {
return false;
}
}
return true;
}
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 [mDeviceName=" + mDeviceName + ", mDeviceAddress="
+ mDeviceAddress
+ ", mUuid=" + mServiceUuid + ", mUuidMask=" + mServiceUuidMask
+ ", mServiceDataUuid=" + Objects.toString(mServiceDataUuid) + ", mServiceData="
+ Arrays.toString(mServiceData) + ", mServiceDataMask="
+ Arrays.toString(mServiceDataMask) + ", mManufacturerId=" + mManufacturerId
+ ", mManufacturerData=" + Arrays.toString(mManufacturerData)
+ ", mManufacturerDataMask=" + Arrays.toString(mManufacturerDataMask) + "]";
}
@Override
public int hashCode() {
return Objects.hash(mDeviceName, mDeviceAddress, mManufacturerId,
Arrays.hashCode(mManufacturerData),
Arrays.hashCode(mManufacturerDataMask),
mServiceDataUuid,
Arrays.hashCode(mServiceData),
Arrays.hashCode(mServiceDataMask),
mServiceUuid, mServiceUuidMask);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
ScanFilter other = (ScanFilter) obj;
return Objects.equals(mDeviceName, other.mDeviceName)
&& Objects.equals(mDeviceAddress, other.mDeviceAddress)
&& mManufacturerId == other.mManufacturerId
&& Objects.deepEquals(mManufacturerData, other.mManufacturerData)
&& Objects.deepEquals(mManufacturerDataMask, other.mManufacturerDataMask)
&& Objects.equals(mServiceDataUuid, other.mServiceDataUuid)
&& Objects.deepEquals(mServiceData, other.mServiceData)
&& Objects.deepEquals(mServiceDataMask, other.mServiceDataMask)
&& Objects.equals(mServiceUuid, other.mServiceUuid)
&& Objects.equals(mServiceUuidMask, other.mServiceUuidMask);
}
Checks if the scanfilter is empty
@hide
/**
* Checks if the scanfilter is empty
*
* @hide
*/
public boolean isAllFieldsEmpty() {
return EMPTY.equals(this);
}
Builder class for ScanFilter
. /**
* Builder class for {@link ScanFilter}.
*/
public static final class Builder {
private String mDeviceName;
private String mDeviceAddress;
private ParcelUuid mServiceUuid;
private ParcelUuid mUuidMask;
private ParcelUuid mServiceDataUuid;
private byte[] mServiceData;
private byte[] mServiceDataMask;
private int mManufacturerId = -1;
private byte[] mManufacturerData;
private byte[] mManufacturerDataMask;
Set filter on device name.
/**
* Set filter on device name.
*/
public Builder setDeviceName(String deviceName) {
mDeviceName = deviceName;
return this;
}
Set filter on device address.
Params: - deviceAddress – The device Bluetooth address for the filter. It needs to be in the format of "01:02:03:AB:CD:EF". The device address can be validated using
BluetoothAdapter.checkBluetoothAddress
.
Throws: - IllegalArgumentException – If the
deviceAddress
is invalid.
/**
* Set filter on device address.
*
* @param deviceAddress The device Bluetooth address for the filter. It needs to be in the
* format of "01:02:03:AB:CD:EF". The device address can be validated using {@link
* BluetoothAdapter#checkBluetoothAddress}.
* @throws IllegalArgumentException If the {@code deviceAddress} is invalid.
*/
public Builder setDeviceAddress(String deviceAddress) {
if (deviceAddress != null && !BluetoothAdapter.checkBluetoothAddress(deviceAddress)) {
throw new IllegalArgumentException("invalid device address " + deviceAddress);
}
mDeviceAddress = deviceAddress;
return this;
}
Set filter on service uuid.
/**
* Set filter on service uuid.
*/
public Builder setServiceUuid(ParcelUuid serviceUuid) {
mServiceUuid = serviceUuid;
mUuidMask = null; // clear uuid mask
return this;
}
Set filter on partial service uuid. The uuidMask
is the bit mask for the serviceUuid
. Set any bit in the mask to 1 to indicate a match is needed for the bit in serviceUuid
, and 0 to ignore that bit. Throws: - IllegalArgumentException – If
serviceUuid
is null
but
uuidMask
is not null
.
/**
* Set filter on partial service uuid. The {@code uuidMask} is the bit mask for the
* {@code serviceUuid}. 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.
*
* @throws IllegalArgumentException If {@code serviceUuid} is {@code null} but {@code
* uuidMask} is not {@code null}.
*/
public Builder setServiceUuid(ParcelUuid serviceUuid, ParcelUuid uuidMask) {
if (mUuidMask != null && mServiceUuid == null) {
throw new IllegalArgumentException("uuid is null while uuidMask is not null!");
}
mServiceUuid = serviceUuid;
mUuidMask = uuidMask;
return this;
}
Set filtering on service data.
Throws: - IllegalArgumentException – If
serviceDataUuid
is null.
/**
* Set filtering on service data.
*
* @throws IllegalArgumentException If {@code serviceDataUuid} is null.
*/
public Builder setServiceData(ParcelUuid serviceDataUuid, byte[] serviceData) {
if (serviceDataUuid == null) {
throw new IllegalArgumentException("serviceDataUuid is null");
}
mServiceDataUuid = serviceDataUuid;
mServiceData = serviceData;
mServiceDataMask = null; // clear service data mask
return this;
}
Set partial filter on service data. 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.
The serviceDataMask
must have the same length of the serviceData
.
Throws: - IllegalArgumentException – If
serviceDataUuid
is null or
serviceDataMask
is null
while serviceData
is not or
serviceDataMask
and serviceData
has different length.
/**
* Set partial filter on service data. 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}.
*
* @throws IllegalArgumentException If {@code serviceDataUuid} is null or {@code
* serviceDataMask} is {@code null} while {@code serviceData} is not or {@code
* serviceDataMask} and {@code serviceData} has different length.
*/
public Builder setServiceData(ParcelUuid serviceDataUuid,
byte[] serviceData, byte[] serviceDataMask) {
if (serviceDataUuid == null) {
throw new IllegalArgumentException("serviceDataUuid is 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");
}
}
mServiceDataUuid = serviceDataUuid;
mServiceData = serviceData;
mServiceDataMask = serviceDataMask;
return this;
}
Set filter on on manufacturerData. A negative manufacturerId is considered as invalid id.
Note the first two bytes of the manufacturerData
is the manufacturerId.
Throws: - IllegalArgumentException – If the
manufacturerId
is invalid.
/**
* Set filter on on manufacturerData. A negative manufacturerId is considered as invalid id.
* <p>
* Note the first two bytes of the {@code manufacturerData} is the manufacturerId.
*
* @throws IllegalArgumentException If the {@code manufacturerId} is invalid.
*/
public Builder setManufacturerData(int manufacturerId, byte[] manufacturerData) {
if (manufacturerData != null && manufacturerId < 0) {
throw new IllegalArgumentException("invalid manufacture id");
}
mManufacturerId = manufacturerId;
mManufacturerData = manufacturerData;
mManufacturerDataMask = null; // clear manufacturer data mask
return this;
}
Set filter on partial manufacture data. 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.
The manufacturerDataMask
must have the same length of manufacturerData
.
Throws: - IllegalArgumentException – If the
manufacturerId
is invalid, or
manufacturerData
is null while manufacturerDataMask
is not, or
manufacturerData
and manufacturerDataMask
have different length.
/**
* Set filter on partial manufacture data. 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}.
*
* @throws IllegalArgumentException If the {@code manufacturerId} is invalid, or {@code
* manufacturerData} is null while {@code manufacturerDataMask} is not, or {@code
* manufacturerData} and {@code manufacturerDataMask} have different length.
*/
public Builder setManufacturerData(int manufacturerId, byte[] manufacturerData,
byte[] manufacturerDataMask) {
if (manufacturerData != null && manufacturerId < 0) {
throw new IllegalArgumentException("invalid manufacture id");
}
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");
}
}
mManufacturerId = manufacturerId;
mManufacturerData = manufacturerData;
mManufacturerDataMask = manufacturerDataMask;
return this;
}
Build ScanFilter
. Throws: - IllegalArgumentException – If the filter cannot be built.
/**
* Build {@link ScanFilter}.
*
* @throws IllegalArgumentException If the filter cannot be built.
*/
public ScanFilter build() {
return new ScanFilter(mDeviceName, mDeviceAddress,
mServiceUuid, mUuidMask,
mServiceDataUuid, mServiceData, mServiceDataMask,
mManufacturerId, mManufacturerData, mManufacturerDataMask);
}
}
}