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