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