Home | History | Annotate | Download | only in le
      1 /*
      2  * Copyright (C) 2014 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package android.bluetooth.le;
     18 
     19 import android.annotation.Nullable;
     20 import android.bluetooth.BluetoothAdapter;
     21 import android.bluetooth.BluetoothDevice;
     22 import android.os.Parcel;
     23 import android.os.ParcelUuid;
     24 import android.os.Parcelable;
     25 
     26 import java.util.Arrays;
     27 import java.util.List;
     28 import java.util.Objects;
     29 import java.util.UUID;
     30 
     31 /**
     32  * Criteria for filtering result from Bluetooth LE scans. A {@link ScanFilter} allows clients to
     33  * restrict scan results to only those that are of interest to them.
     34  * <p>
     35  * Current filtering on the following fields are supported:
     36  * <li>Service UUIDs which identify the bluetooth gatt services running on the device.
     37  * <li>Name of remote Bluetooth LE device.
     38  * <li>Mac address of the remote device.
     39  * <li>Service data which is the data associated with a service.
     40  * <li>Manufacturer specific data which is the data associated with a particular manufacturer.
     41  *
     42  * @see ScanResult
     43  * @see BluetoothLeScanner
     44  */
     45 public final class ScanFilter implements Parcelable {
     46 
     47     @Nullable
     48     private final String mDeviceName;
     49 
     50     @Nullable
     51     private final String mDeviceAddress;
     52 
     53     @Nullable
     54     private final ParcelUuid mServiceUuid;
     55     @Nullable
     56     private final ParcelUuid mServiceUuidMask;
     57 
     58     @Nullable
     59     private final ParcelUuid mServiceDataUuid;
     60     @Nullable
     61     private final byte[] mServiceData;
     62     @Nullable
     63     private final byte[] mServiceDataMask;
     64 
     65     private final int mManufacturerId;
     66     @Nullable
     67     private final byte[] mManufacturerData;
     68     @Nullable
     69     private final byte[] mManufacturerDataMask;
     70 
     71     private ScanFilter(String name, String deviceAddress, ParcelUuid uuid,
     72             ParcelUuid uuidMask, ParcelUuid serviceDataUuid,
     73             byte[] serviceData, byte[] serviceDataMask,
     74             int manufacturerId, byte[] manufacturerData, byte[] manufacturerDataMask) {
     75         mDeviceName = name;
     76         mServiceUuid = uuid;
     77         mServiceUuidMask = uuidMask;
     78         mDeviceAddress = deviceAddress;
     79         mServiceDataUuid = serviceDataUuid;
     80         mServiceData = serviceData;
     81         mServiceDataMask = serviceDataMask;
     82         mManufacturerId = manufacturerId;
     83         mManufacturerData = manufacturerData;
     84         mManufacturerDataMask = manufacturerDataMask;
     85     }
     86 
     87     @Override
     88     public int describeContents() {
     89         return 0;
     90     }
     91 
     92     @Override
     93     public void writeToParcel(Parcel dest, int flags) {
     94         dest.writeInt(mDeviceName == null ? 0 : 1);
     95         if (mDeviceName != null) {
     96             dest.writeString(mDeviceName);
     97         }
     98         dest.writeInt(mDeviceAddress == null ? 0 : 1);
     99         if (mDeviceAddress != null) {
    100             dest.writeString(mDeviceAddress);
    101         }
    102         dest.writeInt(mServiceUuid == null ? 0 : 1);
    103         if (mServiceUuid != null) {
    104             dest.writeParcelable(mServiceUuid, flags);
    105             dest.writeInt(mServiceUuidMask == null ? 0 : 1);
    106             if (mServiceUuidMask != null) {
    107                 dest.writeParcelable(mServiceUuidMask, flags);
    108             }
    109         }
    110         dest.writeInt(mServiceDataUuid == null ? 0 : 1);
    111         if (mServiceDataUuid != null) {
    112             dest.writeParcelable(mServiceDataUuid, flags);
    113             dest.writeInt(mServiceData == null ? 0 : 1);
    114             if (mServiceData != null) {
    115                 dest.writeInt(mServiceData.length);
    116                 dest.writeByteArray(mServiceData);
    117 
    118                 dest.writeInt(mServiceDataMask == null ? 0 : 1);
    119                 if (mServiceDataMask != null) {
    120                     dest.writeInt(mServiceDataMask.length);
    121                     dest.writeByteArray(mServiceDataMask);
    122                 }
    123             }
    124         }
    125         dest.writeInt(mManufacturerId);
    126         dest.writeInt(mManufacturerData == null ? 0 : 1);
    127         if (mManufacturerData != null) {
    128             dest.writeInt(mManufacturerData.length);
    129             dest.writeByteArray(mManufacturerData);
    130 
    131             dest.writeInt(mManufacturerDataMask == null ? 0 : 1);
    132             if (mManufacturerDataMask != null) {
    133                 dest.writeInt(mManufacturerDataMask.length);
    134                 dest.writeByteArray(mManufacturerDataMask);
    135             }
    136         }
    137     }
    138 
    139     /**
    140      * A {@link android.os.Parcelable.Creator} to create {@link ScanFilter} from parcel.
    141      */
    142     public static final Creator<ScanFilter>
    143             CREATOR = new Creator<ScanFilter>() {
    144 
    145                     @Override
    146                 public ScanFilter[] newArray(int size) {
    147                     return new ScanFilter[size];
    148                 }
    149 
    150                     @Override
    151                 public ScanFilter createFromParcel(Parcel in) {
    152                     Builder builder = new Builder();
    153                     if (in.readInt() == 1) {
    154                         builder.setDeviceName(in.readString());
    155                     }
    156                     if (in.readInt() == 1) {
    157                         builder.setDeviceAddress(in.readString());
    158                     }
    159                     if (in.readInt() == 1) {
    160                         ParcelUuid uuid = in.readParcelable(ParcelUuid.class.getClassLoader());
    161                         builder.setServiceUuid(uuid);
    162                         if (in.readInt() == 1) {
    163                             ParcelUuid uuidMask = in.readParcelable(
    164                                     ParcelUuid.class.getClassLoader());
    165                             builder.setServiceUuid(uuid, uuidMask);
    166                         }
    167                     }
    168                     if (in.readInt() == 1) {
    169                         ParcelUuid servcieDataUuid =
    170                                 in.readParcelable(ParcelUuid.class.getClassLoader());
    171                         if (in.readInt() == 1) {
    172                             int serviceDataLength = in.readInt();
    173                             byte[] serviceData = new byte[serviceDataLength];
    174                             in.readByteArray(serviceData);
    175                             if (in.readInt() == 0) {
    176                                 builder.setServiceData(servcieDataUuid, serviceData);
    177                             } else {
    178                                 int serviceDataMaskLength = in.readInt();
    179                                 byte[] serviceDataMask = new byte[serviceDataMaskLength];
    180                                 in.readByteArray(serviceDataMask);
    181                                 builder.setServiceData(
    182                                         servcieDataUuid, serviceData, serviceDataMask);
    183                             }
    184                         }
    185                     }
    186 
    187                     int manufacturerId = in.readInt();
    188                     if (in.readInt() == 1) {
    189                         int manufacturerDataLength = in.readInt();
    190                         byte[] manufacturerData = new byte[manufacturerDataLength];
    191                         in.readByteArray(manufacturerData);
    192                         if (in.readInt() == 0) {
    193                             builder.setManufacturerData(manufacturerId, manufacturerData);
    194                         } else {
    195                             int manufacturerDataMaskLength = in.readInt();
    196                             byte[] manufacturerDataMask = new byte[manufacturerDataMaskLength];
    197                             in.readByteArray(manufacturerDataMask);
    198                             builder.setManufacturerData(manufacturerId, manufacturerData,
    199                                     manufacturerDataMask);
    200                         }
    201                     }
    202 
    203                     return builder.build();
    204                 }
    205             };
    206 
    207     /**
    208      * Returns the filter set the device name field of Bluetooth advertisement data.
    209      */
    210     @Nullable
    211     public String getDeviceName() {
    212         return mDeviceName;
    213     }
    214 
    215     /**
    216      * Returns the filter set on the service uuid.
    217      */
    218     @Nullable
    219     public ParcelUuid getServiceUuid() {
    220         return mServiceUuid;
    221     }
    222 
    223     @Nullable
    224     public ParcelUuid getServiceUuidMask() {
    225         return mServiceUuidMask;
    226     }
    227 
    228     @Nullable
    229     public String getDeviceAddress() {
    230         return mDeviceAddress;
    231     }
    232 
    233     @Nullable
    234     public byte[] getServiceData() {
    235         return mServiceData;
    236     }
    237 
    238     @Nullable
    239     public byte[] getServiceDataMask() {
    240         return mServiceDataMask;
    241     }
    242 
    243     @Nullable
    244     public ParcelUuid getServiceDataUuid() {
    245         return mServiceDataUuid;
    246     }
    247 
    248     /**
    249      * Returns the manufacturer id. -1 if the manufacturer filter is not set.
    250      */
    251     public int getManufacturerId() {
    252         return mManufacturerId;
    253     }
    254 
    255     @Nullable
    256     public byte[] getManufacturerData() {
    257         return mManufacturerData;
    258     }
    259 
    260     @Nullable
    261     public byte[] getManufacturerDataMask() {
    262         return mManufacturerDataMask;
    263     }
    264 
    265     /**
    266      * Check if the scan filter matches a {@code scanResult}. A scan result is considered as a match
    267      * if it matches all the field filters.
    268      */
    269     public boolean matches(ScanResult scanResult) {
    270         if (scanResult == null) {
    271             return false;
    272         }
    273         BluetoothDevice device = scanResult.getDevice();
    274         // Device match.
    275         if (mDeviceAddress != null
    276                 && (device == null || !mDeviceAddress.equals(device.getAddress()))) {
    277             return false;
    278         }
    279 
    280         ScanRecord scanRecord = scanResult.getScanRecord();
    281 
    282         // Scan record is null but there exist filters on it.
    283         if (scanRecord == null
    284                 && (mDeviceName != null || mServiceUuid != null || mManufacturerData != null
    285                         || mServiceData != null)) {
    286             return false;
    287         }
    288 
    289         // Local name match.
    290         if (mDeviceName != null && !mDeviceName.equals(scanRecord.getDeviceName())) {
    291             return false;
    292         }
    293 
    294         // UUID match.
    295         if (mServiceUuid != null && !matchesServiceUuids(mServiceUuid, mServiceUuidMask,
    296                 scanRecord.getServiceUuids())) {
    297             return false;
    298         }
    299 
    300         // Service data match
    301         if (mServiceDataUuid != null) {
    302             if (!matchesPartialData(mServiceData, mServiceDataMask,
    303                     scanRecord.getServiceData(mServiceDataUuid))) {
    304                 return false;
    305             }
    306         }
    307 
    308         // Manufacturer data match.
    309         if (mManufacturerId >= 0) {
    310             if (!matchesPartialData(mManufacturerData, mManufacturerDataMask,
    311                     scanRecord.getManufacturerSpecificData(mManufacturerId))) {
    312                 return false;
    313             }
    314         }
    315         // All filters match.
    316         return true;
    317     }
    318 
    319     // Check if the uuid pattern is contained in a list of parcel uuids.
    320     private boolean matchesServiceUuids(ParcelUuid uuid, ParcelUuid parcelUuidMask,
    321             List<ParcelUuid> uuids) {
    322         if (uuid == null) {
    323             return true;
    324         }
    325         if (uuids == null) {
    326             return false;
    327         }
    328 
    329         for (ParcelUuid parcelUuid : uuids) {
    330             UUID uuidMask = parcelUuidMask == null ? null : parcelUuidMask.getUuid();
    331             if (matchesServiceUuid(uuid.getUuid(), uuidMask, parcelUuid.getUuid())) {
    332                 return true;
    333             }
    334         }
    335         return false;
    336     }
    337 
    338     // Check if the uuid pattern matches the particular service uuid.
    339     private boolean matchesServiceUuid(UUID uuid, UUID mask, UUID data) {
    340         if (mask == null) {
    341             return uuid.equals(data);
    342         }
    343         if ((uuid.getLeastSignificantBits() & mask.getLeastSignificantBits()) !=
    344                 (data.getLeastSignificantBits() & mask.getLeastSignificantBits())) {
    345             return false;
    346         }
    347         return ((uuid.getMostSignificantBits() & mask.getMostSignificantBits()) ==
    348                 (data.getMostSignificantBits() & mask.getMostSignificantBits()));
    349     }
    350 
    351     // Check whether the data pattern matches the parsed data.
    352     private boolean matchesPartialData(byte[] data, byte[] dataMask, byte[] parsedData) {
    353         if (parsedData == null || parsedData.length < data.length) {
    354             return false;
    355         }
    356         if (dataMask == null) {
    357             for (int i = 0; i < data.length; ++i) {
    358                 if (parsedData[i] != data[i]) {
    359                     return false;
    360                 }
    361             }
    362             return true;
    363         }
    364         for (int i = 0; i < data.length; ++i) {
    365             if ((dataMask[i] & parsedData[i]) != (dataMask[i] & data[i])) {
    366                 return false;
    367             }
    368         }
    369         return true;
    370     }
    371 
    372     @Override
    373     public String toString() {
    374         return "BluetoothLeScanFilter [mDeviceName=" + mDeviceName + ", mDeviceAddress="
    375                 + mDeviceAddress
    376                 + ", mUuid=" + mServiceUuid + ", mUuidMask=" + mServiceUuidMask
    377                 + ", mServiceDataUuid=" + Objects.toString(mServiceDataUuid) + ", mServiceData="
    378                 + Arrays.toString(mServiceData) + ", mServiceDataMask="
    379                 + Arrays.toString(mServiceDataMask) + ", mManufacturerId=" + mManufacturerId
    380                 + ", mManufacturerData=" + Arrays.toString(mManufacturerData)
    381                 + ", mManufacturerDataMask=" + Arrays.toString(mManufacturerDataMask) + "]";
    382     }
    383 
    384     @Override
    385     public int hashCode() {
    386         return Objects.hash(mDeviceName, mDeviceAddress, mManufacturerId, mManufacturerData,
    387                 mManufacturerDataMask, mServiceDataUuid, mServiceData, mServiceDataMask,
    388                 mServiceUuid, mServiceUuidMask);
    389     }
    390 
    391     @Override
    392     public boolean equals(Object obj) {
    393         if (this == obj) {
    394             return true;
    395         }
    396         if (obj == null || getClass() != obj.getClass()) {
    397             return false;
    398         }
    399         ScanFilter other = (ScanFilter) obj;
    400         return Objects.equals(mDeviceName, other.mDeviceName) &&
    401                 Objects.equals(mDeviceAddress, other.mDeviceAddress) &&
    402                         mManufacturerId == other.mManufacturerId &&
    403                 Objects.deepEquals(mManufacturerData, other.mManufacturerData) &&
    404                 Objects.deepEquals(mManufacturerDataMask, other.mManufacturerDataMask) &&
    405                 Objects.deepEquals(mServiceDataUuid, other.mServiceDataUuid) &&
    406                 Objects.deepEquals(mServiceData, other.mServiceData) &&
    407                 Objects.deepEquals(mServiceDataMask, other.mServiceDataMask) &&
    408                 Objects.equals(mServiceUuid, other.mServiceUuid) &&
    409                 Objects.equals(mServiceUuidMask, other.mServiceUuidMask);
    410     }
    411 
    412     /**
    413      * Builder class for {@link ScanFilter}.
    414      */
    415     public static final class Builder {
    416 
    417         private String mDeviceName;
    418         private String mDeviceAddress;
    419 
    420         private ParcelUuid mServiceUuid;
    421         private ParcelUuid mUuidMask;
    422 
    423         private ParcelUuid mServiceDataUuid;
    424         private byte[] mServiceData;
    425         private byte[] mServiceDataMask;
    426 
    427         private int mManufacturerId = -1;
    428         private byte[] mManufacturerData;
    429         private byte[] mManufacturerDataMask;
    430 
    431         /**
    432          * Set filter on device name.
    433          */
    434         public Builder setDeviceName(String deviceName) {
    435             mDeviceName = deviceName;
    436             return this;
    437         }
    438 
    439         /**
    440          * Set filter on device address.
    441          *
    442          * @param deviceAddress The device Bluetooth address for the filter. It needs to be in the
    443          *            format of "01:02:03:AB:CD:EF". The device address can be validated using
    444          *            {@link BluetoothAdapter#checkBluetoothAddress}.
    445          * @throws IllegalArgumentException If the {@code deviceAddress} is invalid.
    446          */
    447         public Builder setDeviceAddress(String deviceAddress) {
    448             if (deviceAddress != null && !BluetoothAdapter.checkBluetoothAddress(deviceAddress)) {
    449                 throw new IllegalArgumentException("invalid device address " + deviceAddress);
    450             }
    451             mDeviceAddress = deviceAddress;
    452             return this;
    453         }
    454 
    455         /**
    456          * Set filter on service uuid.
    457          */
    458         public Builder setServiceUuid(ParcelUuid serviceUuid) {
    459             mServiceUuid = serviceUuid;
    460             mUuidMask = null; // clear uuid mask
    461             return this;
    462         }
    463 
    464         /**
    465          * Set filter on partial service uuid. The {@code uuidMask} is the bit mask for the
    466          * {@code serviceUuid}. Set any bit in the mask to 1 to indicate a match is needed for the
    467          * bit in {@code serviceUuid}, and 0 to ignore that bit.
    468          *
    469          * @throws IllegalArgumentException If {@code serviceUuid} is {@code null} but
    470          *             {@code uuidMask} is not {@code null}.
    471          */
    472         public Builder setServiceUuid(ParcelUuid serviceUuid, ParcelUuid uuidMask) {
    473             if (mUuidMask != null && mServiceUuid == null) {
    474                 throw new IllegalArgumentException("uuid is null while uuidMask is not null!");
    475             }
    476             mServiceUuid = serviceUuid;
    477             mUuidMask = uuidMask;
    478             return this;
    479         }
    480 
    481         /**
    482          * Set filtering on service data.
    483          *
    484          * @throws IllegalArgumentException If {@code serviceDataUuid} is null.
    485          */
    486         public Builder setServiceData(ParcelUuid serviceDataUuid, byte[] serviceData) {
    487             if (serviceDataUuid == null) {
    488                 throw new IllegalArgumentException("serviceDataUuid is null");
    489             }
    490             mServiceDataUuid = serviceDataUuid;
    491             mServiceData = serviceData;
    492             mServiceDataMask = null; // clear service data mask
    493             return this;
    494         }
    495 
    496         /**
    497          * Set partial filter on service data. For any bit in the mask, set it to 1 if it needs to
    498          * match the one in service data, otherwise set it to 0 to ignore that bit.
    499          * <p>
    500          * The {@code serviceDataMask} must have the same length of the {@code serviceData}.
    501          *
    502          * @throws IllegalArgumentException If {@code serviceDataUuid} is null or
    503          *             {@code serviceDataMask} is {@code null} while {@code serviceData} is not or
    504          *             {@code serviceDataMask} and {@code serviceData} has different length.
    505          */
    506         public Builder setServiceData(ParcelUuid serviceDataUuid,
    507                 byte[] serviceData, byte[] serviceDataMask) {
    508             if (serviceDataUuid == null) {
    509                 throw new IllegalArgumentException("serviceDataUuid is null");
    510             }
    511             if (mServiceDataMask != null) {
    512                 if (mServiceData == null) {
    513                     throw new IllegalArgumentException(
    514                             "serviceData is null while serviceDataMask is not null");
    515                 }
    516                 // Since the mServiceDataMask is a bit mask for mServiceData, the lengths of the two
    517                 // byte array need to be the same.
    518                 if (mServiceData.length != mServiceDataMask.length) {
    519                     throw new IllegalArgumentException(
    520                             "size mismatch for service data and service data mask");
    521                 }
    522             }
    523             mServiceDataUuid = serviceDataUuid;
    524             mServiceData = serviceData;
    525             mServiceDataMask = serviceDataMask;
    526             return this;
    527         }
    528 
    529         /**
    530          * Set filter on on manufacturerData. A negative manufacturerId is considered as invalid id.
    531          * <p>
    532          * Note the first two bytes of the {@code manufacturerData} is the manufacturerId.
    533          *
    534          * @throws IllegalArgumentException If the {@code manufacturerId} is invalid.
    535          */
    536         public Builder setManufacturerData(int manufacturerId, byte[] manufacturerData) {
    537             if (manufacturerData != null && manufacturerId < 0) {
    538                 throw new IllegalArgumentException("invalid manufacture id");
    539             }
    540             mManufacturerId = manufacturerId;
    541             mManufacturerData = manufacturerData;
    542             mManufacturerDataMask = null; // clear manufacturer data mask
    543             return this;
    544         }
    545 
    546         /**
    547          * Set filter on partial manufacture data. For any bit in the mask, set it the 1 if it needs
    548          * to match the one in manufacturer data, otherwise set it to 0.
    549          * <p>
    550          * The {@code manufacturerDataMask} must have the same length of {@code manufacturerData}.
    551          *
    552          * @throws IllegalArgumentException If the {@code manufacturerId} is invalid, or
    553          *             {@code manufacturerData} is null while {@code manufacturerDataMask} is not,
    554          *             or {@code manufacturerData} and {@code manufacturerDataMask} have different
    555          *             length.
    556          */
    557         public Builder setManufacturerData(int manufacturerId, byte[] manufacturerData,
    558                 byte[] manufacturerDataMask) {
    559             if (manufacturerData != null && manufacturerId < 0) {
    560                 throw new IllegalArgumentException("invalid manufacture id");
    561             }
    562             if (mManufacturerDataMask != null) {
    563                 if (mManufacturerData == null) {
    564                     throw new IllegalArgumentException(
    565                             "manufacturerData is null while manufacturerDataMask is not null");
    566                 }
    567                 // Since the mManufacturerDataMask is a bit mask for mManufacturerData, the lengths
    568                 // of the two byte array need to be the same.
    569                 if (mManufacturerData.length != mManufacturerDataMask.length) {
    570                     throw new IllegalArgumentException(
    571                             "size mismatch for manufacturerData and manufacturerDataMask");
    572                 }
    573             }
    574             mManufacturerId = manufacturerId;
    575             mManufacturerData = manufacturerData;
    576             mManufacturerDataMask = manufacturerDataMask;
    577             return this;
    578         }
    579 
    580         /**
    581          * Build {@link ScanFilter}.
    582          *
    583          * @throws IllegalArgumentException If the filter cannot be built.
    584          */
    585         public ScanFilter build() {
    586             return new ScanFilter(mDeviceName, mDeviceAddress,
    587                     mServiceUuid, mUuidMask,
    588                     mServiceDataUuid, mServiceData, mServiceDataMask,
    589                     mManufacturerId, mManufacturerData, mManufacturerDataMask);
    590         }
    591     }
    592 }
    593