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