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