blob: 038994fb55357d3eb2c999dd609392e679df139f [file] [log] [blame]
/*
* 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.NonNull;
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 {@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 mServiceSolicitationUuid;
@Nullable
private final ParcelUuid mServiceSolicitationUuidMask;
@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 */
public static final ScanFilter EMPTY = new ScanFilter.Builder().build();
private ScanFilter(String name, String deviceAddress, ParcelUuid uuid,
ParcelUuid uuidMask, ParcelUuid solicitationUuid,
ParcelUuid solicitationUuidMask, ParcelUuid serviceDataUuid,
byte[] serviceData, byte[] serviceDataMask,
int manufacturerId, byte[] manufacturerData, byte[] manufacturerDataMask) {
mDeviceName = name;
mServiceUuid = uuid;
mServiceUuidMask = uuidMask;
mServiceSolicitationUuid = solicitationUuid;
mServiceSolicitationUuidMask = solicitationUuidMask;
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(mServiceSolicitationUuid == null ? 0 : 1);
if (mServiceSolicitationUuid != null) {
dest.writeParcelable(mServiceSolicitationUuid, flags);
dest.writeInt(mServiceSolicitationUuidMask == null ? 0 : 1);
if (mServiceSolicitationUuidMask != null) {
dest.writeParcelable(mServiceSolicitationUuidMask, 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 {@link android.os.Parcelable.Creator} to create {@link ScanFilter} from parcel.
*/
public static final @android.annotation.NonNull 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 solicitationUuid = in.readParcelable(
ParcelUuid.class.getClassLoader());
builder.setServiceSolicitationUuid(solicitationUuid);
if (in.readInt() == 1) {
ParcelUuid solicitationUuidMask = in.readParcelable(
ParcelUuid.class.getClassLoader());
builder.setServiceSolicitationUuid(solicitationUuid,
solicitationUuidMask);
}
}
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.
*/
@Nullable
public String getDeviceName() {
return mDeviceName;
}
/**
* Returns the filter set on the service uuid.
*/
@Nullable
public ParcelUuid getServiceUuid() {
return mServiceUuid;
}
@Nullable
public ParcelUuid getServiceUuidMask() {
return mServiceUuidMask;
}
/**
* Returns the filter set on the service Solicitation uuid.
*/
@Nullable
public ParcelUuid getServiceSolicitationUuid() {
return mServiceSolicitationUuid;
}
/**
* Returns the filter set on the service Solicitation uuid mask.
*/
@Nullable
public ParcelUuid getServiceSolicitationUuidMask() {
return mServiceSolicitationUuidMask;
}
@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.
*/
public int getManufacturerId() {
return mManufacturerId;
}
@Nullable
public byte[] getManufacturerData() {
return mManufacturerData;
}
@Nullable
public byte[] getManufacturerDataMask() {
return mManufacturerDataMask;
}
/**
* 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 || mServiceSolicitationUuid != 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;
}
// solicitation UUID match.
if (mServiceSolicitationUuid != null && !matchesServiceSolicitationUuids(
mServiceSolicitationUuid, mServiceSolicitationUuidMask,
scanRecord.getServiceSolicitationUuids())) {
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
*/
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 if the solicitation uuid pattern is contained in a list of parcel uuids.
*
*/
private static boolean matchesServiceSolicitationUuids(ParcelUuid solicitationUuid,
ParcelUuid parcelSolicitationUuidMask, List<ParcelUuid> solicitationUuids) {
if (solicitationUuid == null) {
return true;
}
if (solicitationUuids == null) {
return false;
}
for (ParcelUuid parcelSolicitationUuid : solicitationUuids) {
UUID solicitationUuidMask = parcelSolicitationUuidMask == null
? null : parcelSolicitationUuidMask.getUuid();
if (matchesServiceUuid(solicitationUuid.getUuid(), solicitationUuidMask,
parcelSolicitationUuid.getUuid())) {
return true;
}
}
return false;
}
// Check if the solicitation uuid pattern matches the particular service solicitation uuid.
private static boolean matchesServiceSolicitationUuid(UUID solicitationUuid,
UUID solicitationUuidMask, UUID data) {
return BitUtils.maskedEquals(data, solicitationUuid, solicitationUuidMask);
}
// 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
+ ", mServiceSolicitationUuid=" + mServiceSolicitationUuid
+ ", mServiceSolicitationUuidMask=" + mServiceSolicitationUuidMask
+ ", 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,
mServiceSolicitationUuid, mServiceSolicitationUuidMask);
}
@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)
&& Objects.equals(mServiceSolicitationUuid, other.mServiceSolicitationUuid)
&& Objects.equals(mServiceSolicitationUuidMask,
other.mServiceSolicitationUuidMask);
}
/**
* Checks if the scanfilter is empty
*
* @hide
*/
public boolean isAllFieldsEmpty() {
return EMPTY.equals(this);
}
/**
* Builder class for {@link ScanFilter}.
*/
public static final class Builder {
private String mDeviceName;
private String mDeviceAddress;
private ParcelUuid mServiceUuid;
private ParcelUuid mUuidMask;
private ParcelUuid mServiceSolicitationUuid;
private ParcelUuid mServiceSolicitationUuidMask;
private ParcelUuid mServiceDataUuid;
private byte[] mServiceData;
private byte[] mServiceDataMask;
private int mManufacturerId = -1;
private byte[] mManufacturerData;
private byte[] mManufacturerDataMask;
/**
* Set filter on device name.
*/
public Builder setDeviceName(String deviceName) {
mDeviceName = deviceName;
return this;
}
/**
* 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.
*/
public Builder setServiceUuid(ParcelUuid serviceUuid) {
mServiceUuid = serviceUuid;
mUuidMask = null; // clear uuid mask
return this;
}
/**
* 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 filter on service solicitation uuid.
*/
public @NonNull Builder setServiceSolicitationUuid(
@Nullable ParcelUuid serviceSolicitationUuid) {
mServiceSolicitationUuid = serviceSolicitationUuid;
if (serviceSolicitationUuid == null) {
mServiceSolicitationUuidMask = null;
}
return this;
}
/**
* Set filter on partial service Solicitation uuid. The {@code SolicitationUuidMask} is the
* bit mask for the {@code serviceSolicitationUuid}. Set any bit in the mask to 1 to
* indicate a match is needed for the bit in {@code serviceSolicitationUuid}, and 0 to
* ignore that bit.
*
* @param serviceSolicitationUuid can only be null if solicitationUuidMask is null.
* @param solicitationUuidMask can be null or a mask with no restriction.
*
* @throws IllegalArgumentException If {@code serviceSolicitationUuid} is {@code null} but
* {@code serviceSolicitationUuidMask} is not {@code null}.
*/
public @NonNull Builder setServiceSolicitationUuid(
@Nullable ParcelUuid serviceSolicitationUuid,
@Nullable ParcelUuid solicitationUuidMask) {
if (solicitationUuidMask != null && serviceSolicitationUuid == null) {
throw new IllegalArgumentException(
"SolicitationUuid is null while SolicitationUuidMask is not null!");
}
mServiceSolicitationUuid = serviceSolicitationUuid;
mServiceSolicitationUuidMask = solicitationUuidMask;
return this;
}
/**
* 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.
* <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.
* <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.
* <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 {@link ScanFilter}.
*
* @throws IllegalArgumentException If the filter cannot be built.
*/
public ScanFilter build() {
return new ScanFilter(mDeviceName, mDeviceAddress,
mServiceUuid, mUuidMask, mServiceSolicitationUuid,
mServiceSolicitationUuidMask,
mServiceDataUuid, mServiceData, mServiceDataMask,
mManufacturerId, mManufacturerData, mManufacturerDataMask);
}
}
}