• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2021 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 com.android.server.nearby.common.ble.decode;
18 
19 import android.bluetooth.le.ScanRecord;
20 import android.os.ParcelUuid;
21 import android.util.Log;
22 import android.util.SparseArray;
23 
24 import androidx.annotation.Nullable;
25 
26 import com.android.server.nearby.common.ble.BleFilter;
27 import com.android.server.nearby.common.ble.BleRecord;
28 
29 import java.util.Arrays;
30 
31 /**
32  * Parses Fast Pair information out of {@link BleRecord}s.
33  *
34  * <p>There are 2 different packet formats that are supported, which is used can be determined by
35  * packet length:
36  *
37  * <p>For 3-byte packets, the full packet is the model ID.
38  *
39  * <p>For all other packets, the first byte is the header, followed by the model ID, followed by
40  * zero or more extra fields. Each field has its own header byte followed by the field value. The
41  * packet header is formatted as 0bVVVLLLLR (V = version, L = model ID length, R = reserved) and
42  * each extra field header is 0bLLLLTTTT (L = field length, T = field type).
43  *
44  * @see <a href="http://go/fast-pair-2-service-data">go/fast-pair-2-service-data</a>
45  */
46 public class FastPairDecoder extends BeaconDecoder {
47 
48     private static final int FIELD_TYPE_BLOOM_FILTER = 0;
49     private static final int FIELD_TYPE_BLOOM_FILTER_SALT = 1;
50     private static final int FIELD_TYPE_BLOOM_FILTER_NO_NOTIFICATION = 2;
51     private static final int FIELD_TYPE_BATTERY = 3;
52     private static final int FIELD_TYPE_BATTERY_NO_NOTIFICATION = 4;
53     public static final int FIELD_TYPE_CONNECTION_STATE = 5;
54     private static final int FIELD_TYPE_RANDOM_RESOLVABLE_DATA = 6;
55 
56     /** FE2C is the 16-bit Service UUID. The rest is the base UUID. See BluetoothUuid (hidden). */
57     private static final ParcelUuid FAST_PAIR_SERVICE_PARCEL_UUID =
58             ParcelUuid.fromString("0000FE2C-0000-1000-8000-00805F9B34FB");
59 
60     /** The filter you use to scan for Fast Pair BLE advertisements. */
61     public static final BleFilter FILTER =
62             new BleFilter.Builder().setServiceData(FAST_PAIR_SERVICE_PARCEL_UUID,
63                     new byte[0]).build();
64 
65     // NOTE: Ensure that all bitmasks are always ints, not bytes so that bitshifting works correctly
66     // without needing worry about signing errors.
67     private static final int HEADER_VERSION_BITMASK = 0b11100000;
68     private static final int HEADER_LENGTH_BITMASK = 0b00011110;
69     private static final int HEADER_VERSION_OFFSET = 5;
70     private static final int HEADER_LENGTH_OFFSET = 1;
71 
72     private static final int EXTRA_FIELD_LENGTH_BITMASK = 0b11110000;
73     private static final int EXTRA_FIELD_TYPE_BITMASK = 0b00001111;
74     private static final int EXTRA_FIELD_LENGTH_OFFSET = 4;
75     private static final int EXTRA_FIELD_TYPE_OFFSET = 0;
76 
77     private static final int MIN_ID_LENGTH = 3;
78     private static final int MAX_ID_LENGTH = 14;
79     private static final int HEADER_INDEX = 0;
80     private static final int HEADER_LENGTH = 1;
81     private static final int FIELD_HEADER_LENGTH = 1;
82 
83     // Not using java.util.IllegalFormatException because it is unchecked.
84     private static class IllegalFormatException extends Exception {
IllegalFormatException(String message)85         private IllegalFormatException(String message) {
86             super(message);
87         }
88     }
89 
90     @Nullable
91     @Override
getCalibratedBeaconTxPower(BleRecord bleRecord)92     public Integer getCalibratedBeaconTxPower(BleRecord bleRecord) {
93         return null;
94     }
95 
96     // TODO(b/205320613) create beacon type
97     @Override
getBeaconIdType()98     public int getBeaconIdType() {
99         return 1;
100     }
101 
102     /** Returns the Model ID from our service data, if present. */
103     @Nullable
104     @Override
getBeaconIdBytes(BleRecord bleRecord)105     public byte[] getBeaconIdBytes(BleRecord bleRecord) {
106         return getModelId(bleRecord.getServiceData(FAST_PAIR_SERVICE_PARCEL_UUID));
107     }
108 
109     /** Returns the Model ID from our service data, if present. */
110     @Nullable
getModelId(@ullable byte[] serviceData)111     public static byte[] getModelId(@Nullable byte[] serviceData) {
112         if (serviceData == null) {
113             return null;
114         }
115 
116         if (serviceData.length >= MIN_ID_LENGTH) {
117             if (serviceData.length == MIN_ID_LENGTH) {
118                 // If the length == 3, all bytes are the ID. See flag docs for more about
119                 // endianness.
120                 return serviceData;
121             } else {
122                 // Otherwise, the first byte is a header which contains the length of the
123                 // big-endian model
124                 // ID that follows. The model ID will be trimmed if it contains leading zeros.
125                 int idIndex = 1;
126                 int end = idIndex + getIdLength(serviceData);
127                 while (serviceData[idIndex] == 0 && end - idIndex > MIN_ID_LENGTH) {
128                     idIndex++;
129                 }
130                 return Arrays.copyOfRange(serviceData, idIndex, end);
131             }
132         }
133         return null;
134     }
135 
136     /** Gets the FastPair service data array if available, otherwise returns null. */
137     @Nullable
getServiceDataArray(BleRecord bleRecord)138     public static byte[] getServiceDataArray(BleRecord bleRecord) {
139         return bleRecord.getServiceData(FAST_PAIR_SERVICE_PARCEL_UUID);
140     }
141 
142     /** Gets the FastPair service data array if available, otherwise returns null. */
143     @Nullable
getServiceDataArray(ScanRecord scanRecord)144     public static byte[] getServiceDataArray(ScanRecord scanRecord) {
145         return scanRecord.getServiceData(FAST_PAIR_SERVICE_PARCEL_UUID);
146     }
147 
148     /** Gets the bloom filter from the extra fields if available, otherwise returns null. */
149     @Nullable
getBloomFilter(@ullable byte[] serviceData)150     public static byte[] getBloomFilter(@Nullable byte[] serviceData) {
151         return getExtraField(serviceData, FIELD_TYPE_BLOOM_FILTER);
152     }
153 
154     /** Gets the bloom filter salt from the extra fields if available, otherwise returns null. */
155     @Nullable
getBloomFilterSalt(byte[] serviceData)156     public static byte[] getBloomFilterSalt(byte[] serviceData) {
157         return getExtraField(serviceData, FIELD_TYPE_BLOOM_FILTER_SALT);
158     }
159 
160     /**
161      * Gets the suppress notification with bloom filter from the extra fields if available,
162      * otherwise
163      * returns null.
164      */
165     @Nullable
getBloomFilterNoNotification(@ullable byte[] serviceData)166     public static byte[] getBloomFilterNoNotification(@Nullable byte[] serviceData) {
167         return getExtraField(serviceData, FIELD_TYPE_BLOOM_FILTER_NO_NOTIFICATION);
168     }
169 
170     /** Gets the battery level from extra fields if available, otherwise return null. */
171     @Nullable
getBatteryLevel(byte[] serviceData)172     public static byte[] getBatteryLevel(byte[] serviceData) {
173         return getExtraField(serviceData, FIELD_TYPE_BATTERY);
174     }
175 
176     /**
177      * Gets the suppress notification with battery level from extra fields if available, otherwise
178      * return null.
179      */
180     @Nullable
getBatteryLevelNoNotification(byte[] serviceData)181     public static byte[] getBatteryLevelNoNotification(byte[] serviceData) {
182         return getExtraField(serviceData, FIELD_TYPE_BATTERY_NO_NOTIFICATION);
183     }
184 
185     /**
186      * Gets the random resolvable data from extra fields if available, otherwise
187      * return null.
188      */
189     @Nullable
getRandomResolvableData(byte[] serviceData)190     public static byte[] getRandomResolvableData(byte[] serviceData) {
191         return getExtraField(serviceData, FIELD_TYPE_RANDOM_RESOLVABLE_DATA);
192     }
193 
194     @Nullable
getExtraField(@ullable byte[] serviceData, int fieldId)195     private static byte[] getExtraField(@Nullable byte[] serviceData, int fieldId) {
196         if (serviceData == null || serviceData.length < HEADER_INDEX + HEADER_LENGTH) {
197             return null;
198         }
199         try {
200             return getExtraFields(serviceData).get(fieldId);
201         } catch (IllegalFormatException e) {
202             Log.v("FastPairDecode", "Extra fields incorrectly formatted.");
203             return null;
204         }
205     }
206 
207     /** Gets extra field data at the end of the packet, defined by the extra field header. */
getExtraFields(byte[] serviceData)208     private static SparseArray<byte[]> getExtraFields(byte[] serviceData)
209             throws IllegalFormatException {
210         SparseArray<byte[]> extraFields = new SparseArray<>();
211         if (getVersion(serviceData) != 0) {
212             return extraFields;
213         }
214         int headerIndex = getFirstExtraFieldHeaderIndex(serviceData);
215         while (headerIndex < serviceData.length) {
216             int length = getExtraFieldLength(serviceData, headerIndex);
217             int index = headerIndex + FIELD_HEADER_LENGTH;
218             int type = getExtraFieldType(serviceData, headerIndex);
219             int end = index + length;
220             if (extraFields.get(type) == null) {
221                 if (end <= serviceData.length) {
222                     extraFields.put(type, Arrays.copyOfRange(serviceData, index, end));
223                 } else {
224                     throw new IllegalFormatException(
225                             "Invalid length, " + end + " is longer than service data size "
226                                     + serviceData.length);
227                 }
228             }
229             headerIndex = end;
230         }
231         return extraFields;
232     }
233 
234     /** Checks whether or not a valid ID is included in the service data packet. */
hasBeaconIdBytes(BleRecord bleRecord)235     public static boolean hasBeaconIdBytes(BleRecord bleRecord) {
236         byte[] serviceData = bleRecord.getServiceData(FAST_PAIR_SERVICE_PARCEL_UUID);
237         return checkModelId(serviceData);
238     }
239 
240     /** Check whether byte array is FastPair model id or not. */
checkModelId(@ullable byte[] scanResult)241     public static boolean checkModelId(@Nullable byte[] scanResult) {
242         return scanResult != null
243                 // The 3-byte format has no header byte (all bytes are the ID).
244                 && (scanResult.length == MIN_ID_LENGTH
245                 // Header byte exists. We support only format version 0. (A different version
246                 // indicates
247                 // a breaking change in the format.)
248                 || (scanResult.length > MIN_ID_LENGTH
249                 && getVersion(scanResult) == 0
250                 && isIdLengthValid(scanResult)));
251     }
252 
253     /** Checks whether or not bloom filter is included in the service data packet. */
hasBloomFilter(BleRecord bleRecord)254     public static boolean hasBloomFilter(BleRecord bleRecord) {
255         return (getBloomFilter(getServiceDataArray(bleRecord)) != null
256                 || getBloomFilterNoNotification(getServiceDataArray(bleRecord)) != null);
257     }
258 
259     /** Checks whether or not bloom filter is included in the service data packet. */
hasBloomFilter(ScanRecord scanRecord)260     public static boolean hasBloomFilter(ScanRecord scanRecord) {
261         return (getBloomFilter(getServiceDataArray(scanRecord)) != null
262                 || getBloomFilterNoNotification(getServiceDataArray(scanRecord)) != null);
263     }
264 
getVersion(byte[] serviceData)265     private static int getVersion(byte[] serviceData) {
266         return serviceData.length == MIN_ID_LENGTH
267                 ? 0
268                 : (serviceData[HEADER_INDEX] & HEADER_VERSION_BITMASK) >> HEADER_VERSION_OFFSET;
269     }
270 
getIdLength(byte[] serviceData)271     private static int getIdLength(byte[] serviceData) {
272         return serviceData.length == MIN_ID_LENGTH
273                 ? MIN_ID_LENGTH
274                 : (serviceData[HEADER_INDEX] & HEADER_LENGTH_BITMASK) >> HEADER_LENGTH_OFFSET;
275     }
276 
getFirstExtraFieldHeaderIndex(byte[] serviceData)277     private static int getFirstExtraFieldHeaderIndex(byte[] serviceData) {
278         return HEADER_INDEX + HEADER_LENGTH + getIdLength(serviceData);
279     }
280 
getExtraFieldLength(byte[] serviceData, int extraFieldIndex)281     private static int getExtraFieldLength(byte[] serviceData, int extraFieldIndex) {
282         return (serviceData[extraFieldIndex] & EXTRA_FIELD_LENGTH_BITMASK)
283                 >> EXTRA_FIELD_LENGTH_OFFSET;
284     }
285 
getExtraFieldType(byte[] serviceData, int extraFieldIndex)286     private static int getExtraFieldType(byte[] serviceData, int extraFieldIndex) {
287         return (serviceData[extraFieldIndex] & EXTRA_FIELD_TYPE_BITMASK) >> EXTRA_FIELD_TYPE_OFFSET;
288     }
289 
isIdLengthValid(byte[] serviceData)290     private static boolean isIdLengthValid(byte[] serviceData) {
291         int idLength = getIdLength(serviceData);
292         return MIN_ID_LENGTH <= idLength
293                 && idLength <= MAX_ID_LENGTH
294                 && idLength + HEADER_LENGTH <= serviceData.length;
295     }
296 }
297 
298