1 /*
2  * Copyright (C) 2017 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.companion;
18 
19 import static android.companion.BluetoothDeviceFilterUtils.getDeviceDisplayNameInternal;
20 import static android.companion.BluetoothDeviceFilterUtils.patternFromString;
21 import static android.companion.BluetoothDeviceFilterUtils.patternToString;
22 
23 import static com.android.internal.util.Preconditions.checkArgument;
24 import static com.android.internal.util.Preconditions.checkState;
25 
26 import android.annotation.NonNull;
27 import android.annotation.Nullable;
28 import android.bluetooth.BluetoothDevice;
29 import android.bluetooth.le.ScanFilter;
30 import android.bluetooth.le.ScanRecord;
31 import android.bluetooth.le.ScanResult;
32 import android.compat.annotation.UnsupportedAppUsage;
33 import android.os.Build;
34 import android.os.Parcel;
35 import android.provider.OneTimeUseBuilder;
36 import android.text.TextUtils;
37 import android.util.Log;
38 
39 import com.android.internal.util.BitUtils;
40 import com.android.internal.util.ObjectUtils;
41 
42 import libcore.util.HexEncoding;
43 
44 import java.nio.ByteOrder;
45 import java.util.Arrays;
46 import java.util.Objects;
47 import java.util.regex.Pattern;
48 
49 /**
50  * A filter for Bluetooth LE devices
51  *
52  * @see ScanFilter
53  */
54 public final class BluetoothLeDeviceFilter implements DeviceFilter<ScanResult> {
55 
56     private static final boolean DEBUG = false;
57     private static final String LOG_TAG = "CDM_BluetoothLeDeviceFilter";
58 
59     private static final int RENAME_PREFIX_LENGTH_LIMIT = 10;
60 
61     private final Pattern mNamePattern;
62     private final ScanFilter mScanFilter;
63     private final byte[] mRawDataFilter;
64     private final byte[] mRawDataFilterMask;
65     private final String mRenamePrefix;
66     private final String mRenameSuffix;
67     private final int mRenameBytesFrom;
68     private final int mRenameBytesLength;
69     private final int mRenameNameFrom;
70     private final int mRenameNameLength;
71     private final boolean mRenameBytesReverseOrder;
72 
BluetoothLeDeviceFilter(Pattern namePattern, ScanFilter scanFilter, byte[] rawDataFilter, byte[] rawDataFilterMask, String renamePrefix, String renameSuffix, int renameBytesFrom, int renameBytesLength, int renameNameFrom, int renameNameLength, boolean renameBytesReverseOrder)73     private BluetoothLeDeviceFilter(Pattern namePattern, ScanFilter scanFilter,
74             byte[] rawDataFilter, byte[] rawDataFilterMask, String renamePrefix,
75             String renameSuffix, int renameBytesFrom, int renameBytesLength,
76             int renameNameFrom, int renameNameLength, boolean renameBytesReverseOrder) {
77         mNamePattern = namePattern;
78         mScanFilter = ObjectUtils.firstNotNull(scanFilter, new ScanFilter.Builder().build());
79         mRawDataFilter = rawDataFilter;
80         mRawDataFilterMask = rawDataFilterMask;
81         mRenamePrefix = renamePrefix;
82         mRenameSuffix = renameSuffix;
83         mRenameBytesFrom = renameBytesFrom;
84         mRenameBytesLength = renameBytesLength;
85         mRenameNameFrom = renameNameFrom;
86         mRenameNameLength = renameNameLength;
87         mRenameBytesReverseOrder = renameBytesReverseOrder;
88     }
89 
90     /** @hide */
91     @Nullable
getNamePattern()92     public Pattern getNamePattern() {
93         return mNamePattern;
94     }
95 
96     /** @hide */
97     @NonNull
98     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
getScanFilter()99     public ScanFilter getScanFilter() {
100         return mScanFilter;
101     }
102 
103     /** @hide */
104     @Nullable
getRawDataFilter()105     public byte[] getRawDataFilter() {
106         return mRawDataFilter;
107     }
108 
109     /** @hide */
110     @Nullable
getRawDataFilterMask()111     public byte[] getRawDataFilterMask() {
112         return mRawDataFilterMask;
113     }
114 
115     /** @hide */
116     @Nullable
getRenamePrefix()117     public String getRenamePrefix() {
118         return mRenamePrefix;
119     }
120 
121     /** @hide */
122     @Nullable
getRenameSuffix()123     public String getRenameSuffix() {
124         return mRenameSuffix;
125     }
126 
127     /** @hide */
getRenameBytesFrom()128     public int getRenameBytesFrom() {
129         return mRenameBytesFrom;
130     }
131 
132     /** @hide */
getRenameBytesLength()133     public int getRenameBytesLength() {
134         return mRenameBytesLength;
135     }
136 
137     /** @hide */
isRenameBytesReverseOrder()138     public boolean isRenameBytesReverseOrder() {
139         return mRenameBytesReverseOrder;
140     }
141 
142     /** @hide */
143     @Override
144     @Nullable
getDeviceDisplayName(ScanResult sr)145     public String getDeviceDisplayName(ScanResult sr) {
146         if (mRenameBytesFrom < 0 && mRenameNameFrom < 0) {
147             return getDeviceDisplayNameInternal(sr.getDevice());
148         }
149         final StringBuilder sb = new StringBuilder(TextUtils.emptyIfNull(mRenamePrefix));
150         if (mRenameBytesFrom >= 0) {
151             final byte[] bytes = sr.getScanRecord().getBytes();
152             int startInclusive = mRenameBytesFrom;
153             int endInclusive = mRenameBytesFrom + mRenameBytesLength -1;
154             int initial = mRenameBytesReverseOrder ? endInclusive : startInclusive;
155             int step = mRenameBytesReverseOrder ? -1 : 1;
156             for (int i = initial; startInclusive <= i && i <= endInclusive; i += step) {
157                 sb.append(HexEncoding.encodeToString(bytes[i], true));
158             }
159         } else {
160             sb.append(
161                     getDeviceDisplayNameInternal(sr.getDevice())
162                             .substring(mRenameNameFrom, mRenameNameFrom + mRenameNameLength));
163         }
164         return sb.append(TextUtils.emptyIfNull(mRenameSuffix)).toString();
165     }
166 
167     /** @hide */
168     @Override
matches(ScanResult scanResult)169     public boolean matches(ScanResult scanResult) {
170         BluetoothDevice device = scanResult.getDevice();
171         boolean result = getScanFilter().matches(scanResult)
172                 && BluetoothDeviceFilterUtils.matchesName(getNamePattern(), device)
173                 && (mRawDataFilter == null
174                     || BitUtils.maskedEquals(scanResult.getScanRecord().getBytes(),
175                             mRawDataFilter, mRawDataFilterMask));
176         if (DEBUG) Log.i(LOG_TAG, "matches(this = " + this + ", device = " + device +
177                 ") -> " + result);
178         return result;
179     }
180 
181     /** @hide */
182     @Override
getMediumType()183     public int getMediumType() {
184         return DeviceFilter.MEDIUM_TYPE_BLUETOOTH_LE;
185     }
186 
187     @Override
equals(@ullable Object o)188     public boolean equals(@Nullable Object o) {
189         if (this == o) return true;
190         if (o == null || getClass() != o.getClass()) return false;
191         BluetoothLeDeviceFilter that = (BluetoothLeDeviceFilter) o;
192         return mRenameBytesFrom == that.mRenameBytesFrom &&
193                 mRenameBytesLength == that.mRenameBytesLength &&
194                 mRenameNameFrom == that.mRenameNameFrom &&
195                 mRenameNameLength == that.mRenameNameLength &&
196                 mRenameBytesReverseOrder == that.mRenameBytesReverseOrder &&
197                 Objects.equals(mNamePattern, that.mNamePattern) &&
198                 Objects.equals(mScanFilter, that.mScanFilter) &&
199                 Arrays.equals(mRawDataFilter, that.mRawDataFilter) &&
200                 Arrays.equals(mRawDataFilterMask, that.mRawDataFilterMask) &&
201                 Objects.equals(mRenamePrefix, that.mRenamePrefix) &&
202                 Objects.equals(mRenameSuffix, that.mRenameSuffix);
203     }
204 
205     @Override
hashCode()206     public int hashCode() {
207         return Objects.hash(mNamePattern, mScanFilter, Arrays.hashCode(mRawDataFilter),
208                 Arrays.hashCode(mRawDataFilterMask), mRenamePrefix, mRenameSuffix,
209                 mRenameBytesFrom, mRenameBytesLength, mRenameNameFrom, mRenameNameLength,
210                 mRenameBytesReverseOrder);
211     }
212 
213     @Override
writeToParcel(Parcel dest, int flags)214     public void writeToParcel(Parcel dest, int flags) {
215         dest.writeString(patternToString(getNamePattern()));
216         dest.writeParcelable(mScanFilter, flags);
217         dest.writeByteArray(mRawDataFilter);
218         dest.writeByteArray(mRawDataFilterMask);
219         dest.writeString(mRenamePrefix);
220         dest.writeString(mRenameSuffix);
221         dest.writeInt(mRenameBytesFrom);
222         dest.writeInt(mRenameBytesLength);
223         dest.writeInt(mRenameNameFrom);
224         dest.writeInt(mRenameNameLength);
225         dest.writeBoolean(mRenameBytesReverseOrder);
226     }
227 
228     @Override
describeContents()229     public int describeContents() {
230         return 0;
231     }
232 
233     @Override
toString()234     public String toString() {
235         return "BluetoothLEDeviceFilter{" +
236                 "mNamePattern=" + mNamePattern +
237                 ", mScanFilter=" + mScanFilter +
238                 ", mRawDataFilter=" + Arrays.toString(mRawDataFilter) +
239                 ", mRawDataFilterMask=" + Arrays.toString(mRawDataFilterMask) +
240                 ", mRenamePrefix='" + mRenamePrefix + '\'' +
241                 ", mRenameSuffix='" + mRenameSuffix + '\'' +
242                 ", mRenameBytesFrom=" + mRenameBytesFrom +
243                 ", mRenameBytesLength=" + mRenameBytesLength +
244                 ", mRenameNameFrom=" + mRenameNameFrom +
245                 ", mRenameNameLength=" + mRenameNameLength +
246                 ", mRenameBytesReverseOrder=" + mRenameBytesReverseOrder +
247                 '}';
248     }
249 
250     public static final @android.annotation.NonNull Creator<BluetoothLeDeviceFilter> CREATOR
251             = new Creator<BluetoothLeDeviceFilter>() {
252         @Override
253         public BluetoothLeDeviceFilter createFromParcel(Parcel in) {
254             Builder builder = new Builder()
255                     .setNamePattern(patternFromString(in.readString()))
256                     .setScanFilter(in.readParcelable(null, android.bluetooth.le.ScanFilter.class));
257             byte[] rawDataFilter = in.createByteArray();
258             byte[] rawDataFilterMask = in.createByteArray();
259             if (rawDataFilter != null) {
260                 builder.setRawDataFilter(rawDataFilter, rawDataFilterMask);
261             }
262             String renamePrefix = in.readString();
263             String suffix = in.readString();
264             int bytesFrom = in.readInt();
265             int bytesTo = in.readInt();
266             int nameFrom = in.readInt();
267             int nameTo = in.readInt();
268             boolean bytesReverseOrder = in.readBoolean();
269             if (renamePrefix != null) {
270                 if (bytesFrom >= 0) {
271                     builder.setRenameFromBytes(renamePrefix, suffix, bytesFrom, bytesTo,
272                             bytesReverseOrder ? ByteOrder.LITTLE_ENDIAN : ByteOrder.BIG_ENDIAN);
273                 } else {
274                     builder.setRenameFromName(renamePrefix, suffix, nameFrom, nameTo);
275                 }
276             }
277             return builder.build();
278         }
279 
280         @Override
281         public BluetoothLeDeviceFilter[] newArray(int size) {
282             return new BluetoothLeDeviceFilter[size];
283         }
284     };
285 
getRenamePrefixLengthLimit()286     public static int getRenamePrefixLengthLimit() {
287         return RENAME_PREFIX_LENGTH_LIMIT;
288     }
289 
290     /**
291      * Builder for {@link BluetoothLeDeviceFilter}
292      */
293     public static final class Builder extends OneTimeUseBuilder<BluetoothLeDeviceFilter> {
294         private ScanFilter mScanFilter;
295         private Pattern mNamePattern;
296         private byte[] mRawDataFilter;
297         private byte[] mRawDataFilterMask;
298         private String mRenamePrefix;
299         private String mRenameSuffix;
300         private int mRenameBytesFrom = -1;
301         private int mRenameBytesLength;
302         private int mRenameNameFrom = -1;
303         private int mRenameNameLength;
304         private boolean mRenameBytesReverseOrder = false;
305 
306         /**
307          * @param regex if set, only devices with {@link BluetoothDevice#getName name} matching the
308          *              given regular expression will be shown
309          * @return self for chaining
310          */
setNamePattern(@ullable Pattern regex)311         public Builder setNamePattern(@Nullable Pattern regex) {
312             checkNotUsed();
313             mNamePattern = regex;
314             return this;
315         }
316 
317         /**
318          * @param scanFilter a {@link ScanFilter} to filter devices by
319          *
320          * @return self for chaining
321          * @see ScanFilter for specific details on its various fields
322          */
323         @NonNull
setScanFilter(@ullable ScanFilter scanFilter)324         public Builder setScanFilter(@Nullable ScanFilter scanFilter) {
325             checkNotUsed();
326             mScanFilter = scanFilter;
327             return this;
328         }
329 
330         /**
331          * Filter devices by raw advertisement data, as obtained by {@link ScanRecord#getBytes}
332          *
333          * @param rawDataFilter bit values that have to match against advertized data
334          * @param rawDataFilterMask bits that have to be matched
335          * @return self for chaining
336          */
337         @NonNull
setRawDataFilter(@onNull byte[] rawDataFilter, @Nullable byte[] rawDataFilterMask)338         public Builder setRawDataFilter(@NonNull byte[] rawDataFilter,
339                 @Nullable byte[] rawDataFilterMask) {
340             checkNotUsed();
341             Objects.requireNonNull(rawDataFilter);
342             checkArgument(rawDataFilterMask == null ||
343                     rawDataFilter.length == rawDataFilterMask.length,
344                     "Mask and filter should be the same length");
345             mRawDataFilter = rawDataFilter;
346             mRawDataFilterMask = rawDataFilterMask;
347             return this;
348         }
349 
350         /**
351          * Rename the devices shown in the list, using specific bytes from the raw advertisement
352          * data ({@link ScanRecord#getBytes}) in hexadecimal format, as well as a custom
353          * prefix/suffix around them
354          *
355          * Note that the prefix length is limited to {@link #getRenamePrefixLengthLimit} characters
356          * to ensure that there's enough space to display the byte data
357          *
358          * The range of bytes to be displayed cannot be empty
359          *
360          * @param prefix to be displayed before the byte data
361          * @param suffix to be displayed after the byte data
362          * @param bytesFrom the start byte index to be displayed (inclusive)
363          * @param bytesLength the number of bytes to be displayed from the given index
364          * @param byteOrder whether the given range of bytes is big endian (will be displayed
365          *                   in same order) or little endian (will be flipped before displaying)
366          * @return self for chaining
367          */
368         @NonNull
setRenameFromBytes(@onNull String prefix, @NonNull String suffix, int bytesFrom, int bytesLength, ByteOrder byteOrder)369         public Builder setRenameFromBytes(@NonNull String prefix, @NonNull String suffix,
370                 int bytesFrom, int bytesLength, ByteOrder byteOrder) {
371             checkRenameNotSet();
372             checkRangeNotEmpty(bytesLength);
373             mRenameBytesFrom = bytesFrom;
374             mRenameBytesLength = bytesLength;
375             mRenameBytesReverseOrder = byteOrder == ByteOrder.LITTLE_ENDIAN;
376             return setRename(prefix, suffix);
377         }
378 
379         /**
380          * Rename the devices shown in the list, using specific characters from the advertised name,
381          * as well as a custom prefix/suffix around them
382          *
383          * Note that the prefix length is limited to {@link #getRenamePrefixLengthLimit} characters
384          * to ensure that there's enough space to display the byte data
385          *
386          * The range of name characters to be displayed cannot be empty
387          *
388          * @param prefix to be displayed before the byte data
389          * @param suffix to be displayed after the byte data
390          * @param nameFrom the start name character index to be displayed (inclusive)
391          * @param nameLength the number of characters to be displayed from the given index
392          * @return self for chaining
393          */
394         @NonNull
setRenameFromName(@onNull String prefix, @NonNull String suffix, int nameFrom, int nameLength)395         public Builder setRenameFromName(@NonNull String prefix, @NonNull String suffix,
396                 int nameFrom, int nameLength) {
397             checkRenameNotSet();
398             checkRangeNotEmpty(nameLength);
399             mRenameNameFrom = nameFrom;
400             mRenameNameLength = nameLength;
401             mRenameBytesReverseOrder = false;
402             return setRename(prefix, suffix);
403         }
404 
checkRenameNotSet()405         private void checkRenameNotSet() {
406             checkState(mRenamePrefix == null, "Renaming rule can only be set once");
407         }
408 
checkRangeNotEmpty(int length)409         private void checkRangeNotEmpty(int length) {
410             checkArgument(length > 0, "Range must be non-empty");
411         }
412 
413         @NonNull
setRename(@onNull String prefix, @NonNull String suffix)414         private Builder setRename(@NonNull String prefix, @NonNull String suffix) {
415             checkNotUsed();
416             checkArgument(TextUtils.length(prefix) <= getRenamePrefixLengthLimit(),
417                     "Prefix is too long");
418             mRenamePrefix = prefix;
419             mRenameSuffix = suffix;
420             return this;
421         }
422 
423         /** @inheritDoc */
424         @Override
425         @NonNull
build()426         public BluetoothLeDeviceFilter build() {
427             markUsed();
428             return new BluetoothLeDeviceFilter(mNamePattern, mScanFilter,
429                     mRawDataFilter, mRawDataFilterMask,
430                     mRenamePrefix, mRenameSuffix,
431                     mRenameBytesFrom, mRenameBytesLength,
432                     mRenameNameFrom, mRenameNameLength,
433                     mRenameBytesReverseOrder);
434         }
435     }
436 }
437