• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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