1 /*
2  * Copyright 2023 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.NonNull;
20 import android.annotation.Nullable;
21 import android.annotation.SystemApi;
22 import android.bluetooth.BluetoothAssignedNumbers.OrganizationId;
23 import android.os.Parcel;
24 import android.os.Parcelable;
25 
26 import java.util.Arrays;
27 import java.util.Objects;
28 
29 /**
30  * Wrapper for filter input for Transport Discovery Data Transport Blocks. This class represents the
31  * filter for a Transport Block from a Transport Discovery Data advertisement data.
32  *
33  * @see ScanFilter
34  * @hide
35  */
36 @SystemApi
37 public final class TransportBlockFilter implements Parcelable {
38 
39     private final int mOrgId;
40     private final int mTdsFlags;
41     private final int mTdsFlagsMask;
42     private final byte[] mTransportData;
43     private final byte[] mTransportDataMask;
44     private final byte[] mWifiNanHash;
45 
46     /**
47      * Length of a Wi-FI NAN hash in bytes/
48      *
49      * @hide
50      */
51     @SystemApi public static final int WIFI_NAN_HASH_LENGTH_BYTES = 8;
52 
TransportBlockFilter( int orgId, int tdsFlags, int tdsFlagsMask, @Nullable byte[] transportData, @Nullable byte[] transportDataMask, @Nullable byte[] wifiNanHash)53     private TransportBlockFilter(
54             int orgId,
55             int tdsFlags,
56             int tdsFlagsMask,
57             @Nullable byte[] transportData,
58             @Nullable byte[] transportDataMask,
59             @Nullable byte[] wifiNanHash) {
60         if (orgId < 1) {
61             throw new IllegalArgumentException("invalid organization id " + orgId);
62         }
63         if (tdsFlags == -1) {
64             throw new IllegalArgumentException("tdsFlag is invalid");
65         }
66         if (tdsFlagsMask == -1) {
67             throw new IllegalArgumentException("tdsFlagsMask is invalid");
68         }
69         if (orgId == OrganizationId.WIFI_ALLIANCE_NEIGHBOR_AWARENESS_NETWORKING) {
70             if (transportData != null || transportDataMask != null) {
71                 throw new IllegalArgumentException(
72                         "wifiNanHash should be used instead of transportData and/or "
73                                 + "transportDataMask when orgId is "
74                                 + "WIFI_ALLIANCE_NEIGHBOR_AWARENESS_NETWORKING");
75             }
76             if (wifiNanHash != null && wifiNanHash.length != WIFI_NAN_HASH_LENGTH_BYTES) {
77                 throw new IllegalArgumentException(
78                         "wifiNanHash should be WIFI_NAN_HASH_LENGTH_BYTES long, but the input is "
79                                 + wifiNanHash.length
80                                 + " bytes");
81             }
82         } else {
83             if (wifiNanHash != null) {
84                 throw new IllegalArgumentException(
85                         "wifiNanHash should not be used when orgId is "
86                                 + "not WIFI_ALLIANCE_NEIGHBOR_AWARENESS_NETWORKING");
87             }
88         }
89         mOrgId = orgId;
90         mTdsFlags = tdsFlags;
91         mTdsFlagsMask = tdsFlagsMask;
92         mTransportData = transportData;
93         mTransportDataMask = transportDataMask;
94         mWifiNanHash = wifiNanHash;
95     }
96 
97     /**
98      * Get Organization ID assigned by Bluetooth SIG. For more details refer to Transport Discovery
99      * Service Organization IDs in <a
100      * href="https://www.bluetooth.com/specifications/assigned-numbers/">Bluetooth Assigned
101      * Numbers</a>
102      *
103      * @hide
104      */
105     @SystemApi
getOrgId()106     public int getOrgId() {
107         return mOrgId;
108     }
109 
110     /**
111      * Get Transport Discovery Service (TDS) flags to filter Transport Discovery Blocks
112      *
113      * @hide
114      */
115     @SystemApi
getTdsFlags()116     public int getTdsFlags() {
117         return mTdsFlags;
118     }
119 
120     /**
121      * Get masks for filtering Transport Discovery Service (TDS) flags in Transport Discovery Blocks
122      *
123      * @return a bitmask to select which bits in {@code tdsFlag} to match. 0 means no bit in
124      *     tdsFlags will be used for matching
125      * @hide
126      */
127     @SystemApi
getTdsFlagsMask()128     public int getTdsFlagsMask() {
129         return mTdsFlagsMask;
130     }
131 
132     /**
133      * Get data to filter Transport Discovery Blocks.
134      *
135      * <p>Cannot be used when {@code orgId} is {@link OrganizationId
136      * #WIFI_ALLIANCE_NEIGHBOR_AWARENESS_NETWORKING}
137      *
138      * @return Data to filter Transport Discovery Blocks, null if not used
139      * @hide
140      */
141     @SystemApi
142     @Nullable
getTransportData()143     public byte[] getTransportData() {
144         return mTransportData;
145     }
146 
147     /**
148      * Get masks for filtering data in Transport Discovery Blocks.
149      *
150      * <p>Cannot be used when {@code orgId} is {@link
151      * OrganizationId#WIFI_ALLIANCE_NEIGHBOR_AWARENESS_NETWORKING}
152      *
153      * @return a byte array with matching length to {@code transportData} to select which bit to use
154      *     in filter, null is not used
155      * @hide
156      */
157     @SystemApi
158     @Nullable
getTransportDataMask()159     public byte[] getTransportDataMask() {
160         return mTransportDataMask;
161     }
162 
163     /**
164      * Get hashed bloom filter value to filter {@link
165      * OrganizationId#WIFI_ALLIANCE_NEIGHBOR_AWARENESS_NETWORKING} services in Transport Discovery
166      * Blocks.
167      *
168      * <p>Can only be used when {@code orgId} is {@link
169      * OrganizationId#WIFI_ALLIANCE_NEIGHBOR_AWARENESS_NETWORKING}.
170      *
171      * @return 8 octets Wi-Fi NAN defined bloom filter hash, null if not used
172      * @hide
173      */
174     @SystemApi
175     @Nullable
getWifiNanHash()176     public byte[] getWifiNanHash() {
177         return mWifiNanHash;
178     }
179 
180     /**
181      * Check if a scan result matches this transport block filter.
182      *
183      * @param scanResult scan result to match
184      * @return true if matches
185      * @hide
186      */
matches(ScanResult scanResult)187     boolean matches(ScanResult scanResult) {
188         ScanRecord scanRecord = scanResult.getScanRecord();
189         // Transport Discovery data match
190         TransportDiscoveryData transportDiscoveryData = scanRecord.getTransportDiscoveryData();
191 
192         if ((transportDiscoveryData != null)) {
193             for (TransportBlock transportBlock : transportDiscoveryData.getTransportBlocks()) {
194                 int orgId = transportBlock.getOrgId();
195                 int tdsFlags = transportBlock.getTdsFlags();
196                 int transportDataLength = transportBlock.getTransportDataLength();
197                 byte[] transportData = transportBlock.getTransportData();
198 
199                 if (mOrgId != orgId) {
200                     continue;
201                 }
202                 if ((mTdsFlags & mTdsFlagsMask) != (tdsFlags & mTdsFlagsMask)) {
203                     continue;
204                 }
205                 if ((mOrgId != OrganizationId.WIFI_ALLIANCE_NEIGHBOR_AWARENESS_NETWORKING)
206                         && (mTransportData != null)
207                         && (mTransportDataMask != null)) {
208                     if (transportDataLength != 0) {
209                         if (!ScanFilter.matchesPartialData(
210                                 mTransportData, mTransportDataMask, transportData)) {
211                             continue;
212                         }
213                     } else {
214                         continue;
215                     }
216                 }
217                 return true;
218             }
219         }
220 
221         return false;
222     }
223 
224     /** @hide */
225     @Override
describeContents()226     public int describeContents() {
227         return 0;
228     }
229 
230     /**
231      * {@inheritDoc}
232      *
233      * @hide
234      */
235     @SystemApi
236     @Override
writeToParcel(@onNull Parcel dest, int flags)237     public void writeToParcel(@NonNull Parcel dest, int flags) {
238         dest.writeInt(mOrgId);
239         dest.writeInt(mTdsFlags);
240         dest.writeInt(mTdsFlagsMask);
241         dest.writeInt(mTransportData == null ? 0 : 1);
242         if (mTransportData != null) {
243             dest.writeInt(mTransportData.length);
244             dest.writeByteArray(mTransportData);
245             dest.writeInt(mTransportDataMask == null ? 0 : 1);
246             if (mTransportDataMask != null) {
247                 dest.writeInt(mTransportDataMask.length);
248                 dest.writeByteArray(mTransportDataMask);
249             }
250         }
251         dest.writeInt(mWifiNanHash == null ? 0 : 1);
252         if (mWifiNanHash != null) {
253             dest.writeInt(mWifiNanHash.length);
254             dest.writeByteArray(mWifiNanHash);
255         }
256     }
257 
258     /** Get a human-readable string for this object. */
259     @Override
toString()260     public String toString() {
261         return "TransportBlockFilter [mOrgId="
262                 + mOrgId
263                 + ", mTdsFlags="
264                 + mTdsFlags
265                 + ", mTdsFlagsMask="
266                 + mTdsFlagsMask
267                 + ", mTransportData="
268                 + Arrays.toString(mTransportData)
269                 + ", mTransportDataMask="
270                 + Arrays.toString(mTransportDataMask)
271                 + ", mWifiNanHash="
272                 + Arrays.toString(mWifiNanHash)
273                 + "]";
274     }
275 
276     @Override
hashCode()277     public int hashCode() {
278         return Objects.hash(
279                 mOrgId,
280                 mTdsFlags,
281                 mTdsFlagsMask,
282                 Arrays.hashCode(mTransportData),
283                 Arrays.hashCode(mTransportDataMask),
284                 Arrays.hashCode(mWifiNanHash));
285     }
286 
287     @Override
equals(@ullable Object obj)288     public boolean equals(@Nullable Object obj) {
289         if (!(obj instanceof TransportBlockFilter)) {
290             return false;
291         }
292         if (this == obj) {
293             return true;
294         }
295         final TransportBlockFilter other = (TransportBlockFilter) obj;
296         return mOrgId == other.getOrgId()
297                 && mTdsFlags == other.getTdsFlags()
298                 && mTdsFlagsMask == other.getTdsFlagsMask()
299                 && Arrays.equals(mTransportData, other.getTransportData())
300                 && Arrays.equals(mTransportDataMask, other.getTransportDataMask())
301                 && Arrays.equals(mWifiNanHash, other.getWifiNanHash());
302     }
303 
304     /**
305      * Creator for {@link TransportBlockFilter} so that we can create it from {@link Parcel}.
306      *
307      * @hide
308      */
309     @SystemApi @NonNull
310     public static final Creator<TransportBlockFilter> CREATOR =
311             new Creator<>() {
312                 @Override
313                 public TransportBlockFilter createFromParcel(Parcel source) {
314                     final int orgId = source.readInt();
315                     Builder builder = new Builder(orgId);
316                     builder.setTdsFlags(source.readInt(), source.readInt());
317                     if (source.readInt() == 1) {
318                         int transportDataLength = source.readInt();
319                         byte[] transportData = new byte[transportDataLength];
320                         source.readByteArray(transportData);
321                         byte[] transportDataMask = null;
322                         if (source.readInt() == 1) {
323                             int transportDataMaskLength = source.readInt();
324                             transportDataMask = new byte[transportDataMaskLength];
325                             source.readByteArray(transportDataMask);
326                         }
327                         builder.setTransportData(transportData, transportDataMask);
328                     }
329                     if (source.readInt() == 1) {
330                         int wifiNanHashLength = source.readInt();
331                         byte[] wifiNanHash = new byte[wifiNanHashLength];
332                         source.readByteArray(wifiNanHash);
333                         builder.setWifiNanHash(wifiNanHash);
334                     }
335                     return builder.build();
336                 }
337 
338                 @Override
339                 public TransportBlockFilter[] newArray(int size) {
340                     return new TransportBlockFilter[0];
341                 }
342             };
343 
344     /**
345      * Builder class for {@link TransportBlockFilter}.
346      *
347      * @hide
348      */
349     @SystemApi
350     public static final class Builder {
351 
352         private final int mOrgId;
353         private int mTdsFlags = 0;
354         private int mTdsFlagsMask = 0;
355         private byte[] mTransportData = null;
356         private byte[] mTransportDataMask = null;
357         private byte[] mWifiNanHash = null;
358 
359         /**
360          * Builder for {@link TransportBlockFilter}.
361          *
362          * @param orgId Organization ID assigned by Bluetooth SIG. For more details refer to
363          *     Transport Discovery Service Organization IDs in <a
364          *     href="https://www.bluetooth.com/specifications/assigned-numbers/">Bluetooth Assigned
365          *     Numbers</a>.
366          * @throws IllegalArgumentException If the {@code orgId} is invalid
367          * @see OrganizationId
368          * @hide
369          */
370         @SystemApi
Builder(int orgId)371         public Builder(int orgId) {
372             if (orgId < 1) {
373                 throw new IllegalArgumentException("invalid organization id " + orgId);
374             }
375             mOrgId = orgId;
376         }
377 
378         /**
379          * Set Transport Discovery Service (TDS) flags to filter Transport Discovery Blocks.
380          *
381          * @param tdsFlags 1 octet value that represents the role of the device and information
382          *     about its state and supported features. Negative values are invalid for this
383          *     argument. Default to 0. See Transport Discovery Service specification for more
384          *     details.
385          * @param tdsFlagsMask a bitmask to select which bits in {@code tdsFlags} to match. Default
386          *     to 0, meaning no flag match required. Negative values are invalid for this argument.
387          * @throws IllegalArgumentException if either {@code tdsFlags} or {@code tdsFlagsMask} is
388          *     invalid.
389          * @return this builder
390          * @hide
391          */
392         @SystemApi
393         @NonNull
setTdsFlags(int tdsFlags, int tdsFlagsMask)394         public Builder setTdsFlags(int tdsFlags, int tdsFlagsMask) {
395             if (tdsFlags < 0) {
396                 throw new IllegalArgumentException("tdsFlag is invalid");
397             }
398             if (tdsFlagsMask < 0) {
399                 throw new IllegalArgumentException("tdsFlagsMask is invalid");
400             }
401             mTdsFlags = tdsFlags;
402             mTdsFlagsMask = tdsFlagsMask;
403             return this;
404         }
405 
406         /**
407          * Set data to filter Transport Discovery Blocks.
408          *
409          * <p>Cannot be used when {@code orgId} is {@link
410          * OrganizationId#WIFI_ALLIANCE_NEIGHBOR_AWARENESS_NETWORKING}
411          *
412          * @param transportData must be valid value for the particular {@code orgId}. See Transport
413          *     Discovery Service specification for more details.
414          * @param transportDataMask a byte array with matching length to {@code transportData} to
415          *     select which bit to use in filter.
416          * @throws IllegalArgumentException when {@code orgId} is {@link
417          *     OrganizationId#WIFI_ALLIANCE_NEIGHBOR_AWARENESS_NETWORKING}
418          * @throws NullPointerException if {@code transportData} or {@code transportDataMask} is
419          *     {@code null}
420          * @throws IllegalArgumentException if {@code transportData} or {@code transportDataMask} is
421          *     empty
422          * @throws IllegalArgumentException if length of {@code transportData} and {@code
423          *     transportDataMask} do not match
424          * @return this builder
425          * @hide
426          */
427         @SystemApi
428         @NonNull
setTransportData( @onNull byte[] transportData, @NonNull byte[] transportDataMask)429         public Builder setTransportData(
430                 @NonNull byte[] transportData, @NonNull byte[] transportDataMask) {
431             if (mOrgId == OrganizationId.WIFI_ALLIANCE_NEIGHBOR_AWARENESS_NETWORKING) {
432                 throw new IllegalArgumentException(
433                         "setWifiNanHash() should be used instead of setTransportData() when orgId "
434                                 + "is WIFI_ALLIANCE_NEIGHBOR_AWARENESS_NETWORKING");
435             }
436             Objects.requireNonNull(transportData);
437             Objects.requireNonNull(transportDataMask);
438             if (transportData.length == 0) {
439                 throw new IllegalArgumentException("transportData is empty");
440             }
441             if (transportDataMask.length == 0) {
442                 throw new IllegalArgumentException("transportDataMask is empty");
443             }
444             if (transportData.length != transportDataMask.length) {
445                 throw new IllegalArgumentException(
446                         "Length of transportData and transportDataMask do not match");
447             }
448             mTransportData = transportData;
449             mTransportDataMask = transportDataMask;
450             return this;
451         }
452 
453         /**
454          * Set hashed bloom filter value to filter {@link OrganizationId
455          * #WIFI_ALLIANCE_NEIGHBOR_AWARENESS_NETWORKING} services in Transport Discovery Blocks.
456          *
457          * <p>Can only be used when {@code orgId} is {@link OrganizationId
458          * #WIFI_ALLIANCE_NEIGHBOR_AWARENESS_NETWORKING}.
459          *
460          * <p>Cannot be used together with {@link #setTransportData(byte[], byte[])}
461          *
462          * @param wifiNanHash 8 octets Wi-Fi NAN defined bloom filter hash
463          * @throws IllegalArgumentException when {@code orgId} is not {@link
464          *     OrganizationId#WIFI_ALLIANCE_NEIGHBOR_AWARENESS_NETWORKING}
465          * @throws IllegalArgumentException when {@code wifiNanHash} is not {@link
466          *     TransportBlockFilter#WIFI_NAN_HASH_LENGTH_BYTES} long
467          * @throws NullPointerException when {@code wifiNanHash} is null
468          * @return this builder
469          * @hide
470          */
471         @SystemApi
472         @NonNull
setWifiNanHash(@onNull byte[] wifiNanHash)473         public Builder setWifiNanHash(@NonNull byte[] wifiNanHash) {
474             if (mOrgId != OrganizationId.WIFI_ALLIANCE_NEIGHBOR_AWARENESS_NETWORKING) {
475                 throw new IllegalArgumentException(
476                         "setWifiNanHash() can only be used when orgId is"
477                                 + " WIFI_ALLIANCE_NEIGHBOR_AWARENESS_NETWORKING");
478             }
479             Objects.requireNonNull(wifiNanHash);
480             if (wifiNanHash.length != WIFI_NAN_HASH_LENGTH_BYTES) {
481                 throw new IllegalArgumentException("Wi-Fi NAN hash must be 8 octets long");
482             }
483             mWifiNanHash = wifiNanHash;
484             return this;
485         }
486 
487         /**
488          * Build {@link TransportBlockFilter}.
489          *
490          * @return {@link TransportBlockFilter}
491          * @throws IllegalStateException if the filter cannot be built
492          * @hide
493          */
494         @SystemApi
495         @NonNull
build()496         public TransportBlockFilter build() {
497             return new TransportBlockFilter(
498                     mOrgId,
499                     mTdsFlags,
500                     mTdsFlagsMask,
501                     mTransportData,
502                     mTransportDataMask,
503                     mWifiNanHash);
504         }
505     }
506 }
507