blob: 8a9614ed71c38ee13638fa56272dc933c7b21463 [file] [log] [blame]
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.nearby.fastpair;
import static com.android.nearby.halfsheet.constants.Constant.ACTION_FAST_PAIR;
import static com.android.nearby.halfsheet.constants.Constant.ACTION_FAST_PAIR_HALF_SHEET_BAN_STATE_RESET;
import static com.android.nearby.halfsheet.constants.Constant.ACTION_FAST_PAIR_HALF_SHEET_CANCEL;
import static com.android.nearby.halfsheet.constants.Constant.ACTION_HALF_SHEET_FOREGROUND_STATE;
import static com.android.nearby.halfsheet.constants.Constant.EXTRA_HALF_SHEET_FOREGROUND;
import static com.android.nearby.halfsheet.constants.FastPairConstants.EXTRA_MODEL_ID;
import static com.android.server.nearby.fastpair.Constant.TAG;
import static com.google.common.io.BaseEncoding.base16;
import android.annotation.Nullable;
import android.annotation.WorkerThread;
import android.app.KeyguardManager;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.database.ContentObserver;
import android.nearby.FastPairDevice;
import android.nearby.NearbyDevice;
import android.nearby.NearbyManager;
import android.nearby.ScanCallback;
import android.nearby.ScanRequest;
import android.util.Log;
import androidx.annotation.NonNull;
import com.android.server.nearby.common.ble.decode.FastPairDecoder;
import com.android.server.nearby.common.bluetooth.BluetoothException;
import com.android.server.nearby.common.bluetooth.fastpair.FastPairConnection;
import com.android.server.nearby.common.bluetooth.fastpair.FastPairDualConnection;
import com.android.server.nearby.common.bluetooth.fastpair.PairingException;
import com.android.server.nearby.common.bluetooth.fastpair.Preferences;
import com.android.server.nearby.common.bluetooth.fastpair.ReflectionException;
import com.android.server.nearby.common.bluetooth.fastpair.SimpleBroadcastReceiver;
import com.android.server.nearby.common.eventloop.Annotations;
import com.android.server.nearby.common.eventloop.EventLoop;
import com.android.server.nearby.common.eventloop.NamedRunnable;
import com.android.server.nearby.common.locator.Locator;
import com.android.server.nearby.common.locator.LocatorContextWrapper;
import com.android.server.nearby.fastpair.cache.DiscoveryItem;
import com.android.server.nearby.fastpair.cache.FastPairCacheManager;
import com.android.server.nearby.fastpair.footprint.FootprintsDeviceManager;
import com.android.server.nearby.fastpair.halfsheet.FastPairHalfSheetManager;
import com.android.server.nearby.fastpair.pairinghandler.PairingProgressHandlerBase;
import com.android.server.nearby.util.ForegroundThread;
import com.android.server.nearby.util.Hex;
import com.google.common.collect.ImmutableList;
import com.google.protobuf.ByteString;
import java.security.GeneralSecurityException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import service.proto.Cache;
import service.proto.Rpcs;
/**
* FastPairManager is the class initiated in nearby service to handle Fast Pair related
* work.
*/
public class FastPairManager {
private static final String ACTION_PREFIX = UserActionHandler.PREFIX;
private static final int WAIT_FOR_UNLOCK_MILLIS = 5000;
/** A notification ID which should be dismissed */
public static final String EXTRA_NOTIFICATION_ID = ACTION_PREFIX + "EXTRA_NOTIFICATION_ID";
private static Executor sFastPairExecutor;
private ContentObserver mFastPairScanChangeContentObserver = null;
final LocatorContextWrapper mLocatorContextWrapper;
final IntentFilter mIntentFilter;
final Locator mLocator;
private boolean mScanEnabled = false;
private final FastPairCacheManager mFastPairCacheManager;
private final BroadcastReceiver mScreenBroadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
switch (action) {
case Intent.ACTION_SCREEN_ON:
Log.d(TAG, "onReceive: ACTION_SCREEN_ON");
invalidateScan();
break;
case Intent.ACTION_BOOT_COMPLETED:
Log.d(TAG, "onReceive: ACTION_BOOT_COMPLETED.");
invalidateScan();
break;
case BluetoothDevice.ACTION_BOND_STATE_CHANGED:
Log.d(TAG, "onReceive: ACTION_BOND_STATE_CHANGED");
processBluetoothConnectionEvent(intent);
break;
case ACTION_HALF_SHEET_FOREGROUND_STATE:
boolean state = intent.getBooleanExtra(EXTRA_HALF_SHEET_FOREGROUND, false);
Log.d(TAG, "halfsheet report foreground state: " + state);
Locator.get(mLocatorContextWrapper, FastPairHalfSheetManager.class)
.setHalfSheetForeground(state);
break;
case ACTION_FAST_PAIR_HALF_SHEET_BAN_STATE_RESET:
Log.d(TAG, "onReceive: ACTION_FAST_PAIR_HALF_SHEET_BAN_STATE_RESET");
String deviceModelId = intent.getStringExtra(EXTRA_MODEL_ID);
if (deviceModelId == null) {
Log.d(TAG, "HalfSheetManager reset device ban state skipped, "
+ "deviceModelId not found");
break;
}
Locator.get(mLocatorContextWrapper, FastPairHalfSheetManager.class)
.resetBanState(deviceModelId);
break;
case ACTION_FAST_PAIR_HALF_SHEET_CANCEL:
Log.d(TAG, "onReceive: ACTION_FAST_PAIR_HALF_SHEET_CANCEL");
String modelId = intent.getStringExtra(EXTRA_MODEL_ID);
if (modelId == null) {
Log.d(TAG, "skip half sheet cancel action, model id not found");
break;
}
Locator.get(mLocatorContextWrapper, FastPairHalfSheetManager.class)
.dismiss(modelId);
break;
case ACTION_FAST_PAIR:
Log.d(TAG, "onReceive: ACTION_FAST_PAIR");
String itemId = intent.getStringExtra(UserActionHandler.EXTRA_ITEM_ID);
String accountKeyString = intent
.getStringExtra(UserActionHandler.EXTRA_FAST_PAIR_SECRET);
if (itemId == null || accountKeyString == null) {
Log.d(TAG, "skip pair action, item id "
+ "or fast pair account key not found");
break;
}
try {
FastPairController controller =
Locator.getFromContextWrapper(mLocatorContextWrapper,
FastPairController.class);
if (mFastPairCacheManager != null) {
controller.pair(mFastPairCacheManager.getDiscoveryItem(itemId),
base16().decode(accountKeyString), /* companionApp= */ null);
}
} catch (IllegalStateException e) {
Log.e(TAG, "Cannot find FastPairController class", e);
}
}
}
};
public FastPairManager(LocatorContextWrapper contextWrapper) {
mLocatorContextWrapper = contextWrapper;
mIntentFilter = new IntentFilter();
mLocator = mLocatorContextWrapper.getLocator();
mLocator.bind(new FastPairModule());
Rpcs.GetObservedDeviceResponse getObservedDeviceResponse =
Rpcs.GetObservedDeviceResponse.newBuilder().build();
mFastPairCacheManager =
Locator.getFromContextWrapper(mLocatorContextWrapper, FastPairCacheManager.class);
}
final ScanCallback mScanCallback = new ScanCallback() {
@Override
public void onDiscovered(@NonNull NearbyDevice device) {
Locator.get(mLocatorContextWrapper, FastPairAdvHandler.class).handleBroadcast(device);
}
@Override
public void onUpdated(@NonNull NearbyDevice device) {
FastPairDevice fastPairDevice = (FastPairDevice) device;
byte[] modelArray = FastPairDecoder.getModelId(fastPairDevice.getData());
Log.d(TAG, "update model id" + Hex.bytesToStringLowercase(modelArray));
}
@Override
public void onLost(@NonNull NearbyDevice device) {
FastPairDevice fastPairDevice = (FastPairDevice) device;
byte[] modelArray = FastPairDecoder.getModelId(fastPairDevice.getData());
Log.d(TAG, "lost model id" + Hex.bytesToStringLowercase(modelArray));
}
@Override
public void onError(int errorCode) {
Log.w(TAG, "[FastPairManager] Scan error is " + errorCode);
}
};
/**
* Function called when nearby service start.
*/
public void initiate() {
mIntentFilter.addAction(Intent.ACTION_SCREEN_ON);
mIntentFilter.addAction(Intent.ACTION_SCREEN_OFF);
mIntentFilter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
mIntentFilter.addAction(Intent.ACTION_BOOT_COMPLETED);
mIntentFilter.addAction(ACTION_FAST_PAIR_HALF_SHEET_CANCEL);
mIntentFilter.addAction(ACTION_FAST_PAIR_HALF_SHEET_BAN_STATE_RESET);
mIntentFilter.addAction(ACTION_HALF_SHEET_FOREGROUND_STATE);
mIntentFilter.addAction(ACTION_FAST_PAIR);
mLocatorContextWrapper.getContext().registerReceiver(mScreenBroadcastReceiver,
mIntentFilter, Context.RECEIVER_EXPORTED);
}
/**
* Function to free up fast pair resource.
*/
public void cleanUp() {
mLocatorContextWrapper.getContext().unregisterReceiver(mScreenBroadcastReceiver);
if (mFastPairScanChangeContentObserver != null) {
mLocatorContextWrapper.getContentResolver().unregisterContentObserver(
mFastPairScanChangeContentObserver);
}
}
/**
* Starts fast pair process.
*/
@Annotations.EventThread
public static Future<Void> pair(
ExecutorService executor,
Context context,
DiscoveryItem item,
@Nullable byte[] accountKey,
@Nullable String companionApp,
FootprintsDeviceManager footprints,
PairingProgressHandlerBase pairingProgressHandlerBase) {
return executor.submit(
() -> pairInternal(context, item, companionApp, accountKey, footprints,
pairingProgressHandlerBase), /* result= */ null);
}
/**
* Starts fast pair
*/
@WorkerThread
public static void pairInternal(
Context context,
DiscoveryItem item,
@Nullable String companionApp,
@Nullable byte[] accountKey,
FootprintsDeviceManager footprints,
PairingProgressHandlerBase pairingProgressHandlerBase) {
FastPairHalfSheetManager fastPairHalfSheetManager =
Locator.get(context, FastPairHalfSheetManager.class);
try {
pairingProgressHandlerBase.onPairingStarted();
if (pairingProgressHandlerBase.skipWaitingScreenUnlock()) {
// Do nothing due to we are not showing the status notification in some pairing
// types, e.g. the retroactive pairing.
} else {
// If the screen is locked when the user taps to pair, the screen will unlock. We
// must wait for the unlock to complete before showing the status notification, or
// it won't be heads-up.
pairingProgressHandlerBase.onWaitForScreenUnlock();
waitUntilScreenIsUnlocked(context);
pairingProgressHandlerBase.onScreenUnlocked();
}
BluetoothAdapter bluetoothAdapter = getBluetoothAdapter(context);
boolean isBluetoothEnabled = bluetoothAdapter != null && bluetoothAdapter.isEnabled();
if (!isBluetoothEnabled) {
if (bluetoothAdapter == null || !bluetoothAdapter.enable()) {
Log.d(TAG, "FastPair: Failed to enable bluetooth");
return;
}
Log.v(TAG, "FastPair: Enabling bluetooth for fast pair");
Locator.get(context, EventLoop.class)
.postRunnable(
new NamedRunnable("enableBluetoothToast") {
@Override
public void run() {
Log.d(TAG, "Enable bluetooth toast test");
}
});
// Set up call back to call this function again once bluetooth has been
// enabled; this does not seem to be a problem as the device connects without a
// problem, but in theory the timeout also includes turning on bluetooth now.
}
pairingProgressHandlerBase.onReadyToPair();
String modelId = item.getTriggerId();
Preferences.Builder prefsBuilder =
Preferences.builder()
.setEnableBrEdrHandover(false)
.setIgnoreDiscoveryError(true);
pairingProgressHandlerBase.onSetupPreferencesBuilder(prefsBuilder);
if (item.getFastPairInformation() != null) {
prefsBuilder.setSkipConnectingProfiles(
item.getFastPairInformation().getDataOnlyConnection());
}
// When add watch and auto device needs to change the config
prefsBuilder.setRejectMessageAccess(true);
prefsBuilder.setRejectPhonebookAccess(true);
prefsBuilder.setHandlePasskeyConfirmationByUi(false);
FastPairConnection connection = new FastPairDualConnection(
context, item.getMacAddress(),
prefsBuilder.build(),
null);
connection.setOnPairedCallback(
address -> {
Log.v(TAG, "connection on paired callback;");
// TODO(b/259150992) add fill Bluetooth metadata values logic
pairingProgressHandlerBase.onPairedCallbackCalled(
connection, accountKey, footprints, address);
});
pairingProgressHandlerBase.onPairingSetupCompleted();
FastPairConnection.SharedSecret sharedSecret;
if ((accountKey != null || item.getAuthenticationPublicKeySecp256R1() != null)) {
sharedSecret =
connection.pair(
accountKey != null ? accountKey
: item.getAuthenticationPublicKeySecp256R1());
if (accountKey == null) {
// Account key is null so it is initial pairing
if (sharedSecret != null) {
Locator.get(context, FastPairController.class).addDeviceToFootprint(
connection.getPublicAddress(), sharedSecret.getKey(), item);
cacheFastPairDevice(context, connection.getPublicAddress(),
sharedSecret.getKey(), item);
}
}
} else {
// Fast Pair one
connection.pair();
}
// TODO(b/213373051): Merge logic with pairingProgressHandlerBase or delete the
// pairingProgressHandlerBase class.
fastPairHalfSheetManager.showPairingSuccessHalfSheet(connection.getPublicAddress());
pairingProgressHandlerBase.onPairingSuccess(connection.getPublicAddress());
} catch (BluetoothException
| InterruptedException
| ReflectionException
| TimeoutException
| ExecutionException
| PairingException
| GeneralSecurityException e) {
Log.e(TAG, "Failed to pair.", e);
// TODO(b/213373051): Merge logic with pairingProgressHandlerBase or delete the
// pairingProgressHandlerBase class.
fastPairHalfSheetManager.showPairingFailed();
pairingProgressHandlerBase.onPairingFailed(e);
}
}
private static void cacheFastPairDevice(Context context, String publicAddress, byte[] key,
DiscoveryItem item) {
try {
Locator.get(context, EventLoop.class).postAndWait(
new NamedRunnable("FastPairCacheDevice") {
@Override
public void run() {
Cache.StoredFastPairItem storedFastPairItem =
Cache.StoredFastPairItem.newBuilder()
.setMacAddress(publicAddress)
.setAccountKey(ByteString.copyFrom(key))
.setModelId(item.getTriggerId())
.addAllFeatures(item.getFastPairInformation() == null
? ImmutableList.of() :
item.getFastPairInformation().getFeaturesList())
.setDiscoveryItem(item.getCopyOfStoredItem())
.build();
Locator.get(context, FastPairCacheManager.class)
.putStoredFastPairItem(storedFastPairItem);
}
}
);
} catch (InterruptedException e) {
Log.e(TAG, "Fail to insert paired device into cache");
}
}
/** Checks if the pairing is initial pairing with fast pair 2.0 design. */
public static boolean isThroughFastPair2InitialPairing(
DiscoveryItem item, @Nullable byte[] accountKey) {
return accountKey == null && item.getAuthenticationPublicKeySecp256R1() != null;
}
private static void waitUntilScreenIsUnlocked(Context context)
throws InterruptedException, ExecutionException, TimeoutException {
KeyguardManager keyguardManager = context.getSystemService(KeyguardManager.class);
// KeyguardManager's isScreenLocked() counterintuitively returns false when the lock screen
// is showing if the user has set "swipe to unlock" (i.e. no required password, PIN, or
// pattern) So we use this method instead, which returns true when on the lock screen
// regardless.
if (keyguardManager.isKeyguardLocked()) {
Log.v(TAG, "FastPair: Screen is locked, waiting until unlocked "
+ "to show status notifications.");
try (SimpleBroadcastReceiver isUnlockedReceiver =
SimpleBroadcastReceiver.oneShotReceiver(
context, FlagUtils.getPreferencesBuilder().build(),
Intent.ACTION_USER_PRESENT)) {
isUnlockedReceiver.await(WAIT_FOR_UNLOCK_MILLIS, TimeUnit.MILLISECONDS);
}
}
}
/**
* Processed task in a background thread
*/
@Annotations.EventThread
public static void processBackgroundTask(Runnable runnable) {
getExecutor().execute(runnable);
}
/**
* This function should only be called on main thread since there is no lock
*/
private static Executor getExecutor() {
if (sFastPairExecutor != null) {
return sFastPairExecutor;
}
sFastPairExecutor = Executors.newSingleThreadExecutor();
return sFastPairExecutor;
}
/**
* Null when the Nearby Service is not available.
*/
@Nullable
private NearbyManager getNearbyManager() {
return (NearbyManager) mLocatorContextWrapper
.getApplicationContext().getSystemService(Context.NEARBY_SERVICE);
}
/**
* Starts or stops scanning according to mAllowScan value.
*/
private void invalidateScan() {
NearbyManager nearbyManager = getNearbyManager();
if (nearbyManager == null) {
Log.w(TAG, "invalidateScan: "
+ "failed to start or stop scanning because NearbyManager is null.");
return;
}
if (mScanEnabled) {
Log.v(TAG, "invalidateScan: scan is enabled");
nearbyManager.startScan(new ScanRequest.Builder()
.setScanType(ScanRequest.SCAN_TYPE_FAST_PAIR).build(),
ForegroundThread.getExecutor(),
mScanCallback);
} else {
Log.v(TAG, "invalidateScan: scan is disabled");
nearbyManager.stopScan(mScanCallback);
}
}
/**
* When certain device is forgotten we need to remove the info from database because the info
* is no longer useful.
*/
private void processBluetoothConnectionEvent(Intent intent) {
int bondState = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE,
BluetoothDevice.ERROR);
if (bondState == BluetoothDevice.BOND_NONE) {
BluetoothDevice device =
intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
if (device != null) {
Log.d("FastPairService", "Forget device detect");
processBackgroundTask(new Runnable() {
@Override
public void run() {
mFastPairCacheManager.removeStoredFastPairItem(device.getAddress());
}
});
}
}
}
/**
* Helper function to get bluetooth adapter.
*/
@Nullable
public static BluetoothAdapter getBluetoothAdapter(Context context) {
BluetoothManager manager = context.getSystemService(BluetoothManager.class);
return manager == null ? null : manager.getAdapter();
}
}