1 /* 2 * Copyright (C) 2022 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.fastpair; 18 19 import static com.android.server.nearby.fastpair.Constant.TAG; 20 21 import static com.google.common.primitives.Bytes.concat; 22 23 import android.accounts.Account; 24 import android.annotation.Nullable; 25 import android.content.Context; 26 import android.nearby.FastPairDevice; 27 import android.nearby.NearbyDevice; 28 import android.util.Log; 29 30 import com.android.internal.annotations.VisibleForTesting; 31 import com.android.server.nearby.common.ble.decode.FastPairDecoder; 32 import com.android.server.nearby.common.ble.util.RangingUtils; 33 import com.android.server.nearby.common.bloomfilter.BloomFilter; 34 import com.android.server.nearby.common.bloomfilter.FastPairBloomFilterHasher; 35 import com.android.server.nearby.common.locator.Locator; 36 import com.android.server.nearby.fastpair.cache.DiscoveryItem; 37 import com.android.server.nearby.fastpair.cache.FastPairCacheManager; 38 import com.android.server.nearby.fastpair.halfsheet.FastPairHalfSheetManager; 39 import com.android.server.nearby.fastpair.notification.FastPairNotificationManager; 40 import com.android.server.nearby.provider.FastPairDataProvider; 41 import com.android.server.nearby.util.ArrayUtils; 42 import com.android.server.nearby.util.DataUtils; 43 import com.android.server.nearby.util.Hex; 44 45 import java.util.List; 46 47 import service.proto.Cache; 48 import service.proto.Data; 49 import service.proto.Rpcs; 50 51 /** 52 * Handler that handle fast pair related broadcast. 53 */ 54 public class FastPairAdvHandler { 55 Context mContext; 56 String mBleAddress; 57 // TODO(b/247152236): Need to confirm the usage 58 // and deleted this after notification manager in use. 59 private boolean mIsFirst = true; 60 private FastPairDataProvider mPairDataProvider; 61 private static final double NEARBY_DISTANCE_THRESHOLD = 0.6; 62 // The byte, 0bLLLLTTTT, for battery length and type. 63 // Bit 0 - 3: type, 0b0011 (show UI indication) or 0b0100 (hide UI indication). 64 // Bit 4 - 7: length. 65 // https://developers.google.com/nearby/fast-pair/specifications/extensions/batterynotification 66 private static final byte SHOW_UI_INDICATION = 0b0011; 67 private static final byte HIDE_UI_INDICATION = 0b0100; 68 private static final int LENGTH_ADVERTISEMENT_TYPE_BIT = 4; 69 70 /** The types about how the bloomfilter is processed. */ 71 public enum ProcessBloomFilterType { 72 IGNORE, // The bloomfilter is not handled. e.g. distance is too far away. 73 CACHE, // The bloomfilter is recognized in the local cache. 74 FOOTPRINT, // Need to check the bloomfilter from the footprints. 75 ACCOUNT_KEY_HIT // The specified account key was hit the bloom filter. 76 } 77 78 /** 79 * Constructor function. 80 */ FastPairAdvHandler(Context context)81 public FastPairAdvHandler(Context context) { 82 mContext = context; 83 } 84 85 @VisibleForTesting FastPairAdvHandler(Context context, FastPairDataProvider dataProvider)86 FastPairAdvHandler(Context context, FastPairDataProvider dataProvider) { 87 mContext = context; 88 mPairDataProvider = dataProvider; 89 } 90 91 /** 92 * Handles all of the scanner result. Fast Pair will handle model id broadcast bloomfilter 93 * broadcast and battery level broadcast. 94 */ handleBroadcast(NearbyDevice device)95 public void handleBroadcast(NearbyDevice device) { 96 FastPairDevice fastPairDevice = (FastPairDevice) device; 97 mBleAddress = fastPairDevice.getBluetoothAddress(); 98 if (mPairDataProvider == null) { 99 mPairDataProvider = FastPairDataProvider.getInstance(); 100 } 101 if (mPairDataProvider == null) { 102 return; 103 } 104 105 if (FastPairDecoder.checkModelId(fastPairDevice.getData())) { 106 byte[] model = FastPairDecoder.getModelId(fastPairDevice.getData()); 107 Log.v(TAG, "On discovery model id " + Hex.bytesToStringLowercase(model)); 108 // Use api to get anti spoofing key from model id. 109 try { 110 List<Account> accountList = mPairDataProvider.loadFastPairEligibleAccounts(); 111 Rpcs.GetObservedDeviceResponse response = 112 mPairDataProvider.loadFastPairAntispoofKeyDeviceMetadata(model); 113 if (response == null) { 114 Log.e(TAG, "server does not have model id " 115 + Hex.bytesToStringLowercase(model)); 116 return; 117 } 118 // Check the distance of the device if the distance is larger than the threshold 119 // do not show half sheet. 120 if (!isNearby(fastPairDevice.getRssi(), 121 response.getDevice().getBleTxPower() == 0 ? fastPairDevice.getTxPower() 122 : response.getDevice().getBleTxPower())) { 123 return; 124 } 125 Locator.get(mContext, FastPairHalfSheetManager.class).showHalfSheet( 126 DataUtils.toScanFastPairStoreItem( 127 response, mBleAddress, Hex.bytesToStringLowercase(model), 128 accountList.isEmpty() ? null : accountList.get(0).name)); 129 } catch (IllegalStateException e) { 130 Log.e(TAG, "OEM does not construct fast pair data proxy correctly"); 131 } 132 } else { 133 // Start to process bloom filter. Yet to finish. 134 try { 135 subsequentPair(fastPairDevice); 136 } catch (IllegalStateException e) { 137 Log.e(TAG, "handleBroadcast: subsequent pair failed", e); 138 } 139 } 140 } 141 142 @Nullable 143 @VisibleForTesting getBloomFilterBytes(byte[] data)144 static byte[] getBloomFilterBytes(byte[] data) { 145 byte[] bloomFilterBytes = FastPairDecoder.getBloomFilter(data); 146 if (bloomFilterBytes == null) { 147 bloomFilterBytes = FastPairDecoder.getBloomFilterNoNotification(data); 148 } 149 if (ArrayUtils.isEmpty(bloomFilterBytes)) { 150 Log.d(TAG, "subsequentPair: bloomFilterByteArray empty"); 151 return null; 152 } 153 return bloomFilterBytes; 154 } 155 getTxPower(FastPairDevice scannedDevice, Data.FastPairDeviceWithAccountKey recognizedDevice)156 private int getTxPower(FastPairDevice scannedDevice, 157 Data.FastPairDeviceWithAccountKey recognizedDevice) { 158 return recognizedDevice.getDiscoveryItem().getTxPower() == 0 159 ? scannedDevice.getTxPower() 160 : recognizedDevice.getDiscoveryItem().getTxPower(); 161 } 162 subsequentPair(FastPairDevice scannedDevice)163 private void subsequentPair(FastPairDevice scannedDevice) { 164 byte[] data = scannedDevice.getData(); 165 166 if (ArrayUtils.isEmpty(data)) { 167 Log.d(TAG, "subsequentPair: no valid data"); 168 return; 169 } 170 171 byte[] bloomFilterBytes = getBloomFilterBytes(data); 172 if (ArrayUtils.isEmpty(bloomFilterBytes)) { 173 Log.d(TAG, "subsequentPair: no valid bloom filter"); 174 return; 175 } 176 177 byte[] salt = FastPairDecoder.getBloomFilterSalt(data); 178 if (ArrayUtils.isEmpty(salt)) { 179 Log.d(TAG, "subsequentPair: no valid salt"); 180 return; 181 } 182 byte[] saltWithData = concat(salt, generateBatteryData(data)); 183 184 List<Account> accountList = mPairDataProvider.loadFastPairEligibleAccounts(); 185 for (Account account : accountList) { 186 List<Data.FastPairDeviceWithAccountKey> devices = 187 mPairDataProvider.loadFastPairDeviceWithAccountKey(account); 188 Data.FastPairDeviceWithAccountKey recognizedDevice = 189 findRecognizedDevice(devices, 190 new BloomFilter(bloomFilterBytes, 191 new FastPairBloomFilterHasher()), saltWithData); 192 if (recognizedDevice == null) { 193 Log.v(TAG, "subsequentPair: recognizedDevice is null"); 194 continue; 195 } 196 197 // Check the distance of the device if the distance is larger than the 198 // threshold 199 if (!isNearby(scannedDevice.getRssi(), getTxPower(scannedDevice, recognizedDevice))) { 200 Log.v(TAG, 201 "subsequentPair: the distance of the device is larger than the threshold"); 202 return; 203 } 204 205 // Check if the device is already paired 206 List<Cache.StoredFastPairItem> storedFastPairItemList = 207 Locator.get(mContext, FastPairCacheManager.class) 208 .getAllSavedStoredFastPairItem(); 209 Cache.StoredFastPairItem recognizedStoredFastPairItem = 210 findRecognizedDeviceFromCachedItem(storedFastPairItemList, 211 new BloomFilter(bloomFilterBytes, 212 new FastPairBloomFilterHasher()), saltWithData); 213 if (recognizedStoredFastPairItem != null) { 214 // The bloomfilter is recognized in the cache so the device is paired 215 // before 216 Log.d(TAG, "bloom filter is recognized in the cache"); 217 continue; 218 } 219 showSubsequentNotification(account, scannedDevice, recognizedDevice); 220 } 221 } 222 showSubsequentNotification(Account account, FastPairDevice scannedDevice, Data.FastPairDeviceWithAccountKey recognizedDevice)223 private void showSubsequentNotification(Account account, FastPairDevice scannedDevice, 224 Data.FastPairDeviceWithAccountKey recognizedDevice) { 225 // Get full info from api the initial request will only return 226 // part of the info due to size limit. 227 List<Data.FastPairDeviceWithAccountKey> devicesWithAccountKeys = 228 mPairDataProvider.loadFastPairDeviceWithAccountKey(account, 229 List.of(recognizedDevice.getAccountKey().toByteArray())); 230 if (devicesWithAccountKeys == null || devicesWithAccountKeys.isEmpty()) { 231 Log.d(TAG, "No fast pair device with account key is found."); 232 return; 233 } 234 235 // Saved device from footprint does not have ble address. 236 // We need to fill ble address with current scan result. 237 Cache.StoredDiscoveryItem storedDiscoveryItem = 238 devicesWithAccountKeys.get(0).getDiscoveryItem().toBuilder() 239 .setMacAddress( 240 scannedDevice.getBluetoothAddress()) 241 .build(); 242 // Show notification 243 FastPairNotificationManager fastPairNotificationManager = 244 Locator.get(mContext, FastPairNotificationManager.class); 245 DiscoveryItem item = new DiscoveryItem(mContext, storedDiscoveryItem); 246 Locator.get(mContext, FastPairCacheManager.class).saveDiscoveryItem(item); 247 fastPairNotificationManager.showDiscoveryNotification(item, 248 devicesWithAccountKeys.get(0).getAccountKey().toByteArray()); 249 } 250 251 // Battery advertisement format: 252 // Byte 0: Battery length and type, Bit 0 - 3: type, Bit 4 - 7: length. 253 // Byte 1 - 3: Battery values. 254 // Reference: 255 // https://developers.google.com/nearby/fast-pair/specifications/extensions/batterynotification 256 @VisibleForTesting generateBatteryData(byte[] data)257 static byte[] generateBatteryData(byte[] data) { 258 byte[] batteryLevelNoNotification = FastPairDecoder.getBatteryLevelNoNotification(data); 259 boolean suppressBatteryNotification = 260 (batteryLevelNoNotification != null && batteryLevelNoNotification.length > 0); 261 byte[] batteryValues = 262 suppressBatteryNotification 263 ? batteryLevelNoNotification 264 : FastPairDecoder.getBatteryLevel(data); 265 if (ArrayUtils.isEmpty(batteryValues)) { 266 return new byte[0]; 267 } 268 return generateBatteryData(suppressBatteryNotification, batteryValues); 269 } 270 271 @VisibleForTesting generateBatteryData(boolean suppressBatteryNotification, byte[] batteryValues)272 static byte[] generateBatteryData(boolean suppressBatteryNotification, byte[] batteryValues) { 273 return concat( 274 new byte[] { 275 (byte) 276 (batteryValues.length << LENGTH_ADVERTISEMENT_TYPE_BIT 277 | (suppressBatteryNotification 278 ? HIDE_UI_INDICATION : SHOW_UI_INDICATION)) 279 }, 280 batteryValues); 281 } 282 283 /** 284 * Checks the bloom filter to see if any of the devices are recognized and should have a 285 * notification displayed for them. A device is recognized if the account key + salt combination 286 * is inside the bloom filter. 287 */ 288 @Nullable 289 @VisibleForTesting findRecognizedDevice( List<Data.FastPairDeviceWithAccountKey> devices, BloomFilter bloomFilter, byte[] salt)290 static Data.FastPairDeviceWithAccountKey findRecognizedDevice( 291 List<Data.FastPairDeviceWithAccountKey> devices, BloomFilter bloomFilter, byte[] salt) { 292 for (Data.FastPairDeviceWithAccountKey device : devices) { 293 if (device.getAccountKey().toByteArray() == null || salt == null) { 294 return null; 295 } 296 byte[] rotatedKey = concat(device.getAccountKey().toByteArray(), salt); 297 298 StringBuilder sb = new StringBuilder(); 299 for (byte b : rotatedKey) { 300 sb.append(b); 301 } 302 303 if (bloomFilter.possiblyContains(rotatedKey)) { 304 return device; 305 } 306 } 307 return null; 308 } 309 310 @Nullable findRecognizedDeviceFromCachedItem( List<Cache.StoredFastPairItem> devices, BloomFilter bloomFilter, byte[] salt)311 static Cache.StoredFastPairItem findRecognizedDeviceFromCachedItem( 312 List<Cache.StoredFastPairItem> devices, BloomFilter bloomFilter, byte[] salt) { 313 for (Cache.StoredFastPairItem device : devices) { 314 if (device.getAccountKey().toByteArray() == null || salt == null) { 315 return null; 316 } 317 byte[] rotatedKey = concat(device.getAccountKey().toByteArray(), salt); 318 if (bloomFilter.possiblyContains(rotatedKey)) { 319 return device; 320 } 321 } 322 return null; 323 } 324 325 /** 326 * Check the device distance for certain rssi value. 327 */ isNearby(int rssi, int txPower)328 boolean isNearby(int rssi, int txPower) { 329 return RangingUtils.distanceFromRssiAndTxPower(rssi, txPower) < NEARBY_DISTANCE_THRESHOLD; 330 } 331 } 332