• 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.fastpair;
18 
19 import static com.android.nearby.halfsheet.constants.Constant.ACTION_FAST_PAIR;
20 import static com.android.nearby.halfsheet.constants.Constant.ACTION_FAST_PAIR_HALF_SHEET_BAN_STATE_RESET;
21 import static com.android.nearby.halfsheet.constants.Constant.ACTION_FAST_PAIR_HALF_SHEET_CANCEL;
22 import static com.android.nearby.halfsheet.constants.Constant.ACTION_HALF_SHEET_FOREGROUND_STATE;
23 import static com.android.nearby.halfsheet.constants.Constant.EXTRA_HALF_SHEET_FOREGROUND;
24 import static com.android.nearby.halfsheet.constants.FastPairConstants.EXTRA_MODEL_ID;
25 import static com.android.server.nearby.fastpair.Constant.TAG;
26 
27 import static com.google.common.io.BaseEncoding.base16;
28 
29 import android.annotation.Nullable;
30 import android.annotation.WorkerThread;
31 import android.app.KeyguardManager;
32 import android.bluetooth.BluetoothAdapter;
33 import android.bluetooth.BluetoothDevice;
34 import android.bluetooth.BluetoothManager;
35 import android.content.BroadcastReceiver;
36 import android.content.Context;
37 import android.content.Intent;
38 import android.content.IntentFilter;
39 import android.database.ContentObserver;
40 import android.nearby.FastPairDevice;
41 import android.nearby.NearbyDevice;
42 import android.nearby.NearbyManager;
43 import android.nearby.ScanCallback;
44 import android.nearby.ScanRequest;
45 import android.util.Log;
46 
47 import androidx.annotation.NonNull;
48 
49 import com.android.server.nearby.common.ble.decode.FastPairDecoder;
50 import com.android.server.nearby.common.bluetooth.BluetoothException;
51 import com.android.server.nearby.common.bluetooth.fastpair.FastPairConnection;
52 import com.android.server.nearby.common.bluetooth.fastpair.FastPairDualConnection;
53 import com.android.server.nearby.common.bluetooth.fastpair.PairingException;
54 import com.android.server.nearby.common.bluetooth.fastpair.Preferences;
55 import com.android.server.nearby.common.bluetooth.fastpair.ReflectionException;
56 import com.android.server.nearby.common.bluetooth.fastpair.SimpleBroadcastReceiver;
57 import com.android.server.nearby.common.eventloop.Annotations;
58 import com.android.server.nearby.common.eventloop.EventLoop;
59 import com.android.server.nearby.common.eventloop.NamedRunnable;
60 import com.android.server.nearby.common.locator.Locator;
61 import com.android.server.nearby.common.locator.LocatorContextWrapper;
62 import com.android.server.nearby.fastpair.cache.DiscoveryItem;
63 import com.android.server.nearby.fastpair.cache.FastPairCacheManager;
64 import com.android.server.nearby.fastpair.footprint.FootprintsDeviceManager;
65 import com.android.server.nearby.fastpair.halfsheet.FastPairHalfSheetManager;
66 import com.android.server.nearby.fastpair.pairinghandler.PairingProgressHandlerBase;
67 import com.android.server.nearby.util.ForegroundThread;
68 import com.android.server.nearby.util.Hex;
69 
70 import com.google.common.collect.ImmutableList;
71 import com.google.protobuf.ByteString;
72 
73 import java.security.GeneralSecurityException;
74 import java.util.concurrent.ExecutionException;
75 import java.util.concurrent.Executor;
76 import java.util.concurrent.ExecutorService;
77 import java.util.concurrent.Executors;
78 import java.util.concurrent.Future;
79 import java.util.concurrent.TimeUnit;
80 import java.util.concurrent.TimeoutException;
81 
82 import service.proto.Cache;
83 import service.proto.Rpcs;
84 
85 /**
86  * FastPairManager is the class initiated in nearby service to handle Fast Pair related
87  * work.
88  */
89 
90 public class FastPairManager {
91 
92     private static final String ACTION_PREFIX = UserActionHandler.PREFIX;
93     private static final int WAIT_FOR_UNLOCK_MILLIS = 5000;
94 
95     /** A notification ID which should be dismissed */
96     public static final String EXTRA_NOTIFICATION_ID = ACTION_PREFIX + "EXTRA_NOTIFICATION_ID";
97 
98     private static Executor sFastPairExecutor;
99 
100     private ContentObserver mFastPairScanChangeContentObserver = null;
101 
102     final LocatorContextWrapper mLocatorContextWrapper;
103     final IntentFilter mIntentFilter;
104     final Locator mLocator;
105     private boolean mScanEnabled = false;
106     private final FastPairCacheManager mFastPairCacheManager;
107 
108     private final BroadcastReceiver mScreenBroadcastReceiver = new BroadcastReceiver() {
109         @Override
110         public void onReceive(Context context, Intent intent) {
111             String action = intent.getAction();
112             switch (action) {
113                 case Intent.ACTION_SCREEN_ON:
114                     Log.d(TAG, "onReceive: ACTION_SCREEN_ON");
115                     invalidateScan();
116                     break;
117                 case Intent.ACTION_BOOT_COMPLETED:
118                     Log.d(TAG, "onReceive: ACTION_BOOT_COMPLETED.");
119                     invalidateScan();
120                     break;
121                 case BluetoothDevice.ACTION_BOND_STATE_CHANGED:
122                     Log.d(TAG, "onReceive: ACTION_BOND_STATE_CHANGED");
123                     processBluetoothConnectionEvent(intent);
124                     break;
125                 case ACTION_HALF_SHEET_FOREGROUND_STATE:
126                     boolean state = intent.getBooleanExtra(EXTRA_HALF_SHEET_FOREGROUND, false);
127                     Log.d(TAG, "halfsheet report foreground state:  " + state);
128                     Locator.get(mLocatorContextWrapper, FastPairHalfSheetManager.class)
129                             .setHalfSheetForeground(state);
130                     break;
131                 case ACTION_FAST_PAIR_HALF_SHEET_BAN_STATE_RESET:
132                     Log.d(TAG, "onReceive: ACTION_FAST_PAIR_HALF_SHEET_BAN_STATE_RESET");
133                     String deviceModelId = intent.getStringExtra(EXTRA_MODEL_ID);
134                     if (deviceModelId == null) {
135                         Log.d(TAG, "HalfSheetManager reset device ban state skipped, "
136                                 + "deviceModelId not found");
137                         break;
138                     }
139                     Locator.get(mLocatorContextWrapper, FastPairHalfSheetManager.class)
140                             .resetBanState(deviceModelId);
141                     break;
142                 case ACTION_FAST_PAIR_HALF_SHEET_CANCEL:
143                     Log.d(TAG, "onReceive: ACTION_FAST_PAIR_HALF_SHEET_CANCEL");
144                     String modelId = intent.getStringExtra(EXTRA_MODEL_ID);
145                     if (modelId == null) {
146                         Log.d(TAG, "skip half sheet cancel action, model id not found");
147                         break;
148                     }
149                     Locator.get(mLocatorContextWrapper, FastPairHalfSheetManager.class)
150                             .dismiss(modelId);
151                     break;
152                 case ACTION_FAST_PAIR:
153                     Log.d(TAG, "onReceive: ACTION_FAST_PAIR");
154                     String itemId = intent.getStringExtra(UserActionHandler.EXTRA_ITEM_ID);
155                     String accountKeyString = intent
156                             .getStringExtra(UserActionHandler.EXTRA_FAST_PAIR_SECRET);
157                     if (itemId == null || accountKeyString == null) {
158                         Log.d(TAG, "skip pair action, item id "
159                                 + "or fast pair account key not found");
160                         break;
161                     }
162                     try {
163                         FastPairController controller =
164                                 Locator.getFromContextWrapper(mLocatorContextWrapper,
165                                         FastPairController.class);
166                         if (mFastPairCacheManager != null) {
167                             controller.pair(mFastPairCacheManager.getDiscoveryItem(itemId),
168                                     base16().decode(accountKeyString), /* companionApp= */ null);
169                         }
170                     } catch (IllegalStateException e) {
171                         Log.e(TAG, "Cannot find FastPairController class", e);
172                     }
173             }
174         }
175     };
176 
FastPairManager(LocatorContextWrapper contextWrapper)177     public FastPairManager(LocatorContextWrapper contextWrapper) {
178         mLocatorContextWrapper = contextWrapper;
179         mIntentFilter = new IntentFilter();
180         mLocator = mLocatorContextWrapper.getLocator();
181         mLocator.bind(new FastPairModule());
182         Rpcs.GetObservedDeviceResponse getObservedDeviceResponse =
183                 Rpcs.GetObservedDeviceResponse.newBuilder().build();
184         mFastPairCacheManager =
185                 Locator.getFromContextWrapper(mLocatorContextWrapper, FastPairCacheManager.class);
186     }
187 
188     final ScanCallback mScanCallback = new ScanCallback() {
189         @Override
190         public void onDiscovered(@NonNull NearbyDevice device) {
191             Locator.get(mLocatorContextWrapper, FastPairAdvHandler.class).handleBroadcast(device);
192         }
193 
194         @Override
195         public void onUpdated(@NonNull NearbyDevice device) {
196             FastPairDevice fastPairDevice = (FastPairDevice) device;
197             byte[] modelArray = FastPairDecoder.getModelId(fastPairDevice.getData());
198             Log.d(TAG, "update model id" + Hex.bytesToStringLowercase(modelArray));
199         }
200 
201         @Override
202         public void onLost(@NonNull NearbyDevice device) {
203             FastPairDevice fastPairDevice = (FastPairDevice) device;
204             byte[] modelArray = FastPairDecoder.getModelId(fastPairDevice.getData());
205             Log.d(TAG, "lost model id" + Hex.bytesToStringLowercase(modelArray));
206         }
207 
208         @Override
209         public void onError(int errorCode) {
210             Log.w(TAG, "[FastPairManager] Scan error is " + errorCode);
211         }
212     };
213 
214     /**
215      * Function called when nearby service start.
216      */
initiate()217     public void initiate() {
218         mIntentFilter.addAction(Intent.ACTION_SCREEN_ON);
219         mIntentFilter.addAction(Intent.ACTION_SCREEN_OFF);
220         mIntentFilter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
221         mIntentFilter.addAction(Intent.ACTION_BOOT_COMPLETED);
222         mIntentFilter.addAction(ACTION_FAST_PAIR_HALF_SHEET_CANCEL);
223         mIntentFilter.addAction(ACTION_FAST_PAIR_HALF_SHEET_BAN_STATE_RESET);
224         mIntentFilter.addAction(ACTION_HALF_SHEET_FOREGROUND_STATE);
225         mIntentFilter.addAction(ACTION_FAST_PAIR);
226 
227         mLocatorContextWrapper.getContext().registerReceiver(mScreenBroadcastReceiver,
228                 mIntentFilter, Context.RECEIVER_EXPORTED);
229     }
230 
231     /**
232      * Function to free up fast pair resource.
233      */
cleanUp()234     public void cleanUp() {
235         mLocatorContextWrapper.getContext().unregisterReceiver(mScreenBroadcastReceiver);
236         if (mFastPairScanChangeContentObserver != null) {
237             mLocatorContextWrapper.getContentResolver().unregisterContentObserver(
238                     mFastPairScanChangeContentObserver);
239         }
240     }
241 
242     /**
243      * Starts fast pair process.
244      */
245     @Annotations.EventThread
pair( ExecutorService executor, Context context, DiscoveryItem item, @Nullable byte[] accountKey, @Nullable String companionApp, FootprintsDeviceManager footprints, PairingProgressHandlerBase pairingProgressHandlerBase)246     public static Future<Void> pair(
247             ExecutorService executor,
248             Context context,
249             DiscoveryItem item,
250             @Nullable byte[] accountKey,
251             @Nullable String companionApp,
252             FootprintsDeviceManager footprints,
253             PairingProgressHandlerBase pairingProgressHandlerBase) {
254         return executor.submit(
255                 () -> pairInternal(context, item, companionApp, accountKey, footprints,
256                         pairingProgressHandlerBase), /* result= */ null);
257     }
258 
259     /**
260      * Starts fast pair
261      */
262     @WorkerThread
pairInternal( Context context, DiscoveryItem item, @Nullable String companionApp, @Nullable byte[] accountKey, FootprintsDeviceManager footprints, PairingProgressHandlerBase pairingProgressHandlerBase)263     public static void pairInternal(
264             Context context,
265             DiscoveryItem item,
266             @Nullable String companionApp,
267             @Nullable byte[] accountKey,
268             FootprintsDeviceManager footprints,
269             PairingProgressHandlerBase pairingProgressHandlerBase) {
270         FastPairHalfSheetManager fastPairHalfSheetManager =
271                 Locator.get(context, FastPairHalfSheetManager.class);
272         try {
273             pairingProgressHandlerBase.onPairingStarted();
274             if (pairingProgressHandlerBase.skipWaitingScreenUnlock()) {
275                 // Do nothing due to we are not showing the status notification in some pairing
276                 // types, e.g. the retroactive pairing.
277             } else {
278                 // If the screen is locked when the user taps to pair, the screen will unlock. We
279                 // must wait for the unlock to complete before showing the status notification, or
280                 // it won't be heads-up.
281                 pairingProgressHandlerBase.onWaitForScreenUnlock();
282                 waitUntilScreenIsUnlocked(context);
283                 pairingProgressHandlerBase.onScreenUnlocked();
284             }
285             BluetoothAdapter bluetoothAdapter = getBluetoothAdapter(context);
286 
287             boolean isBluetoothEnabled = bluetoothAdapter != null && bluetoothAdapter.isEnabled();
288             if (!isBluetoothEnabled) {
289                 if (bluetoothAdapter == null || !bluetoothAdapter.enable()) {
290                     Log.d(TAG, "FastPair: Failed to enable bluetooth");
291                     return;
292                 }
293                 Log.v(TAG, "FastPair: Enabling bluetooth for fast pair");
294 
295                 Locator.get(context, EventLoop.class)
296                         .postRunnable(
297                                 new NamedRunnable("enableBluetoothToast") {
298                                     @Override
299                                     public void run() {
300                                         Log.d(TAG, "Enable bluetooth toast test");
301                                     }
302                                 });
303                 // Set up call back to call this function again once bluetooth has been
304                 // enabled; this does not seem to be a problem as the device connects without a
305                 // problem, but in theory the timeout also includes turning on bluetooth now.
306             }
307 
308             pairingProgressHandlerBase.onReadyToPair();
309 
310             String modelId = item.getTriggerId();
311             Preferences.Builder prefsBuilder =
312                     Preferences.builder()
313                             .setEnableBrEdrHandover(false)
314                             .setIgnoreDiscoveryError(true);
315             pairingProgressHandlerBase.onSetupPreferencesBuilder(prefsBuilder);
316             if (item.getFastPairInformation() != null) {
317                 prefsBuilder.setSkipConnectingProfiles(
318                         item.getFastPairInformation().getDataOnlyConnection());
319             }
320             // When add watch and auto device needs to change the config
321             prefsBuilder.setRejectMessageAccess(true);
322             prefsBuilder.setRejectPhonebookAccess(true);
323             prefsBuilder.setHandlePasskeyConfirmationByUi(false);
324 
325             FastPairConnection connection = new FastPairDualConnection(
326                     context, item.getMacAddress(),
327                     prefsBuilder.build(),
328                     null);
329             connection.setOnPairedCallback(
330                     address -> {
331                         Log.v(TAG, "connection on paired callback;");
332                         // TODO(b/259150992) add fill Bluetooth metadata values logic
333                         pairingProgressHandlerBase.onPairedCallbackCalled(
334                                 connection, accountKey, footprints, address);
335                     });
336             pairingProgressHandlerBase.onPairingSetupCompleted();
337 
338             FastPairConnection.SharedSecret sharedSecret;
339             if ((accountKey != null || item.getAuthenticationPublicKeySecp256R1() != null)) {
340                 sharedSecret =
341                         connection.pair(
342                                 accountKey != null ? accountKey
343                                         : item.getAuthenticationPublicKeySecp256R1());
344                 if (accountKey == null) {
345                     // Account key is null so it is initial pairing
346                     if (sharedSecret != null) {
347                         Locator.get(context, FastPairController.class).addDeviceToFootprint(
348                                 connection.getPublicAddress(), sharedSecret.getKey(), item);
349                         cacheFastPairDevice(context, connection.getPublicAddress(),
350                                 sharedSecret.getKey(), item);
351                     }
352                 }
353             } else {
354                 // Fast Pair one
355                 connection.pair();
356             }
357             // TODO(b/213373051): Merge logic with pairingProgressHandlerBase or delete the
358             // pairingProgressHandlerBase class.
359             fastPairHalfSheetManager.showPairingSuccessHalfSheet(connection.getPublicAddress());
360             pairingProgressHandlerBase.onPairingSuccess(connection.getPublicAddress());
361         } catch (BluetoothException
362                 | InterruptedException
363                 | ReflectionException
364                 | TimeoutException
365                 | ExecutionException
366                 | PairingException
367                 | GeneralSecurityException e) {
368             Log.e(TAG, "Failed to pair.", e);
369 
370             // TODO(b/213373051): Merge logic with pairingProgressHandlerBase or delete the
371             // pairingProgressHandlerBase class.
372             fastPairHalfSheetManager.showPairingFailed();
373             pairingProgressHandlerBase.onPairingFailed(e);
374         }
375     }
376 
cacheFastPairDevice(Context context, String publicAddress, byte[] key, DiscoveryItem item)377     private static void cacheFastPairDevice(Context context, String publicAddress, byte[] key,
378             DiscoveryItem item) {
379         try {
380             Locator.get(context, EventLoop.class).postAndWait(
381                     new NamedRunnable("FastPairCacheDevice") {
382                         @Override
383                         public void run() {
384                             Cache.StoredFastPairItem storedFastPairItem =
385                                     Cache.StoredFastPairItem.newBuilder()
386                                             .setMacAddress(publicAddress)
387                                             .setAccountKey(ByteString.copyFrom(key))
388                                             .setModelId(item.getTriggerId())
389                                             .addAllFeatures(item.getFastPairInformation() == null
390                                                     ? ImmutableList.of() :
391                                                     item.getFastPairInformation().getFeaturesList())
392                                             .setDiscoveryItem(item.getCopyOfStoredItem())
393                                             .build();
394                             Locator.get(context, FastPairCacheManager.class)
395                                     .putStoredFastPairItem(storedFastPairItem);
396                         }
397                     }
398             );
399         } catch (InterruptedException e) {
400             Log.e(TAG, "Fail to insert paired device into cache");
401         }
402     }
403 
404     /** Checks if the pairing is initial pairing with fast pair 2.0 design. */
isThroughFastPair2InitialPairing( DiscoveryItem item, @Nullable byte[] accountKey)405     public static boolean isThroughFastPair2InitialPairing(
406             DiscoveryItem item, @Nullable byte[] accountKey) {
407         return accountKey == null && item.getAuthenticationPublicKeySecp256R1() != null;
408     }
409 
waitUntilScreenIsUnlocked(Context context)410     private static void waitUntilScreenIsUnlocked(Context context)
411             throws InterruptedException, ExecutionException, TimeoutException {
412         KeyguardManager keyguardManager = context.getSystemService(KeyguardManager.class);
413 
414         // KeyguardManager's isScreenLocked() counterintuitively returns false when the lock screen
415         // is showing if the user has set "swipe to unlock" (i.e. no required password, PIN, or
416         // pattern) So we use this method instead, which returns true when on the lock screen
417         // regardless.
418         if (keyguardManager.isKeyguardLocked()) {
419             Log.v(TAG, "FastPair: Screen is locked, waiting until unlocked "
420                     + "to show status notifications.");
421             try (SimpleBroadcastReceiver isUnlockedReceiver =
422                          SimpleBroadcastReceiver.oneShotReceiver(
423                                  context, FlagUtils.getPreferencesBuilder().build(),
424                                  Intent.ACTION_USER_PRESENT)) {
425                 isUnlockedReceiver.await(WAIT_FOR_UNLOCK_MILLIS, TimeUnit.MILLISECONDS);
426             }
427         }
428     }
429 
430     /**
431      * Processed task in a background thread
432      */
433     @Annotations.EventThread
processBackgroundTask(Runnable runnable)434     public static void processBackgroundTask(Runnable runnable) {
435         getExecutor().execute(runnable);
436     }
437 
438     /**
439      * This function should only be called on main thread since there is no lock
440      */
getExecutor()441     private static Executor getExecutor() {
442         if (sFastPairExecutor != null) {
443             return sFastPairExecutor;
444         }
445         sFastPairExecutor = Executors.newSingleThreadExecutor();
446         return sFastPairExecutor;
447     }
448 
449     /**
450      * Null when the Nearby Service is not available.
451      */
452     @Nullable
getNearbyManager()453     private NearbyManager getNearbyManager() {
454         return (NearbyManager) mLocatorContextWrapper
455                 .getApplicationContext().getSystemService(Context.NEARBY_SERVICE);
456     }
457 
458     /**
459      * Starts or stops scanning according to mAllowScan value.
460      */
invalidateScan()461     private void invalidateScan() {
462         NearbyManager nearbyManager = getNearbyManager();
463         if (nearbyManager == null) {
464             Log.w(TAG, "invalidateScan: "
465                     + "failed to start or stop scanning because NearbyManager is null.");
466             return;
467         }
468         if (mScanEnabled) {
469             Log.v(TAG, "invalidateScan: scan is enabled");
470             nearbyManager.startScan(new ScanRequest.Builder()
471                             .setScanType(ScanRequest.SCAN_TYPE_FAST_PAIR).build(),
472                     ForegroundThread.getExecutor(),
473                     mScanCallback);
474         } else {
475             Log.v(TAG, "invalidateScan: scan is disabled");
476             nearbyManager.stopScan(mScanCallback);
477         }
478     }
479 
480     /**
481      * When certain device is forgotten we need to remove the info from database because the info
482      * is no longer useful.
483      */
processBluetoothConnectionEvent(Intent intent)484     private void processBluetoothConnectionEvent(Intent intent) {
485         int bondState = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE,
486                 BluetoothDevice.ERROR);
487         if (bondState == BluetoothDevice.BOND_NONE) {
488             BluetoothDevice device =
489                     intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
490             if (device != null) {
491                 Log.d("FastPairService", "Forget device detect");
492                 processBackgroundTask(new Runnable() {
493                     @Override
494                     public void run() {
495                         mFastPairCacheManager.removeStoredFastPairItem(device.getAddress());
496                     }
497                 });
498             }
499 
500         }
501     }
502 
503     /**
504      * Helper function to get bluetooth adapter.
505      */
506     @Nullable
getBluetoothAdapter(Context context)507     public static BluetoothAdapter getBluetoothAdapter(Context context) {
508         BluetoothManager manager = context.getSystemService(BluetoothManager.class);
509         return manager == null ? null : manager.getAdapter();
510     }
511 }
512