| /* |
| * Copyright 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.common.bluetooth.fastpair; |
| |
| import static android.bluetooth.BluetoothDevice.BOND_BONDED; |
| import static android.bluetooth.BluetoothDevice.BOND_BONDING; |
| import static android.bluetooth.BluetoothDevice.BOND_NONE; |
| |
| import static com.android.server.nearby.common.bluetooth.fastpair.BluetoothAddress.maskBluetoothAddress; |
| import static com.android.server.nearby.common.bluetooth.fastpair.BluetoothUuids.get16BitUuid; |
| import static com.android.server.nearby.common.bluetooth.fastpair.BluetoothUuids.to128BitUuid; |
| import static com.android.server.nearby.common.bluetooth.fastpair.Bytes.toBytes; |
| import static com.android.server.nearby.common.bluetooth.fastpair.Bytes.toShorts; |
| |
| import static com.google.common.base.Preconditions.checkNotNull; |
| import static com.google.common.base.Verify.verifyNotNull; |
| import static com.google.common.io.BaseEncoding.base16; |
| import static com.google.common.primitives.Bytes.concat; |
| |
| import android.bluetooth.BluetoothDevice; |
| import android.bluetooth.BluetoothGattCharacteristic; |
| import android.content.BroadcastReceiver; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.IntentFilter; |
| import android.os.ParcelUuid; |
| import android.os.SystemClock; |
| import android.text.TextUtils; |
| import android.util.Log; |
| |
| import androidx.annotation.GuardedBy; |
| import androidx.annotation.IntDef; |
| import androidx.annotation.Nullable; |
| import androidx.annotation.VisibleForTesting; |
| import androidx.annotation.WorkerThread; |
| |
| import com.android.server.nearby.common.bluetooth.BluetoothException; |
| import com.android.server.nearby.common.bluetooth.BluetoothGattException; |
| import com.android.server.nearby.common.bluetooth.BluetoothTimeoutException; |
| import com.android.server.nearby.common.bluetooth.fastpair.BluetoothAudioPairer.KeyBasedPairingInfo; |
| import com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService; |
| import com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.AccountKeyCharacteristic; |
| import com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.AdditionalDataCharacteristic.AdditionalDataType; |
| import com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.FirmwareVersionCharacteristic; |
| import com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic.KeyBasedPairingRequestFlag; |
| import com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.NameCharacteristic; |
| import com.android.server.nearby.common.bluetooth.fastpair.Constants.TransportDiscoveryService; |
| import com.android.server.nearby.common.bluetooth.fastpair.HandshakeHandler.ActionOverBle; |
| import com.android.server.nearby.common.bluetooth.fastpair.HandshakeHandler.HandshakeException; |
| import com.android.server.nearby.common.bluetooth.fastpair.HandshakeHandler.HandshakeMessage; |
| import com.android.server.nearby.common.bluetooth.fastpair.HandshakeHandler.KeyBasedPairingRequest; |
| import com.android.server.nearby.common.bluetooth.fastpair.Ltv.ParseException; |
| import com.android.server.nearby.common.bluetooth.fastpair.TimingLogger.ScopedTiming; |
| import com.android.server.nearby.common.bluetooth.gatt.BluetoothGattConnection; |
| import com.android.server.nearby.common.bluetooth.gatt.BluetoothGattConnection.ChangeObserver; |
| import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.BluetoothAdapter; |
| import com.android.server.nearby.common.bluetooth.util.BluetoothOperationExecutor.BluetoothOperationTimeoutException; |
| import com.android.server.nearby.common.locator.Locator; |
| import com.android.server.nearby.fastpair.FastPairController; |
| import com.android.server.nearby.intdefs.FastPairEventIntDefs.BrEdrHandoverErrorCode; |
| import com.android.server.nearby.intdefs.FastPairEventIntDefs.ConnectErrorCode; |
| import com.android.server.nearby.intdefs.FastPairEventIntDefs.CreateBondErrorCode; |
| import com.android.server.nearby.intdefs.FastPairEventIntDefs.ErrorCode; |
| import com.android.server.nearby.intdefs.NearbyEventIntDefs.EventCode; |
| |
| import com.google.common.base.Ascii; |
| import com.google.common.base.Preconditions; |
| import com.google.common.primitives.Shorts; |
| |
| import java.lang.annotation.Retention; |
| import java.lang.annotation.RetentionPolicy; |
| import java.nio.ByteOrder; |
| import java.security.GeneralSecurityException; |
| import java.security.NoSuchAlgorithmException; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Locale; |
| import java.util.Set; |
| import java.util.UUID; |
| import java.util.concurrent.CountDownLatch; |
| import java.util.concurrent.ExecutionException; |
| import java.util.concurrent.TimeUnit; |
| import java.util.concurrent.TimeoutException; |
| import java.util.concurrent.atomic.AtomicBoolean; |
| |
| /** |
| * Supports Fast Pair pairing with certain Bluetooth headphones, Auto, etc. |
| * |
| * <p>Based on https://developers.google.com/nearby/fast-pair/spec, the pairing is constructed by |
| * both BLE and BREDR connections. Example state transitions for Fast Pair 2, ie a pairing key is |
| * included in the request (note: timeouts and retries are governed by flags, may change): |
| * |
| * <pre> |
| * {@code |
| * Connect GATT |
| * A) Success -> Handshake |
| * B) Failure (3s timeout) -> Retry 2x -> end |
| * |
| * Handshake |
| * A) Generate a shared secret with the headset (either using anti-spoofing key or account key) |
| * 1) Account key is used directly as the key |
| * 2) Anti-spoofing key is used by combining out private key with the headset's public and |
| * sending our public to the headset to combine with their private to generate a shared |
| * key. Sending our public key to headset takes ~3s. |
| * B) Write an encrypted packet to the headset containing their BLE address for verification |
| * that both sides have the same key (headset decodes this packet and checks it against their |
| * own address) (~250ms). |
| * C) Receive a response from the headset containing their public address (~250ms). |
| * |
| * Discovery (for devices < Oreo) |
| * A) Success -> Create Bond |
| * B) Failure (10s timeout) -> Sleep 1s, Retry 3x -> end |
| * |
| * Connect to device |
| * A) If already bonded |
| * 1) Attempt directly connecting to supported profiles (A2DP, etc) |
| * a) Success -> Write Account Key |
| * b) Failure (15s timeout, usually fails within a ~2s) -> Remove bond (~1s) -> Create bond |
| * B) If not already bonded |
| * 1) Create bond |
| * a) Success -> Connect profile |
| * b) Failure (15s timeout) -> Retry 2x -> end |
| * 2) Connect profile |
| * a) Success -> Write account key |
| * b) Failure -> Retry -> end |
| * |
| * Write account key |
| * A) Callback that pairing succeeded |
| * B) Disconnect GATT |
| * C) Reconnect GATT for secure connection |
| * D) Write account key (~3s) |
| * } |
| * </pre> |
| * |
| * The performance profiling result by {@link TimingLogger}: |
| * |
| * <pre> |
| * FastPairDualConnection [Exclusive time] / [Total time] ([Timestamp]) |
| * Connect GATT #1 3054ms (0) |
| * Handshake 32ms / 740ms (3054) |
| * Generate key via ECDH 10ms (3054) |
| * Add salt 1ms (3067) |
| * Encrypt request 3ms (3068) |
| * Write data to GATT 692ms (3097) |
| * Wait response from GATT 0ms (3789) |
| * Decrypt response 2ms (3789) |
| * Get BR/EDR handover information via SDP 1ms (3795) |
| * Pair device #1 6ms / 4887ms (3805) |
| * Create bond 3965ms / 4881ms (3809) |
| * Exchange passkey 587ms / 915ms (7124) |
| * Encrypt passkey 6ms (7694) |
| * Send passkey to remote 290ms (7700) |
| * Wait for remote passkey 0ms (7993) |
| * Decrypt passkey 18ms (7994) |
| * Confirm the pairing: true 14ms (8025) |
| * Close BondedReceiver 1ms (8688) |
| * Connect: A2DP 19ms / 370ms (8701) |
| * Wait connection 348ms / 349ms (8720) |
| * Close ConnectedReceiver 1ms (9068) |
| * Close profile: A2DP 2ms (9069) |
| * Write account key 2ms / 789ms (9163) |
| * Encrypt key 0ms (9164) |
| * Write key via GATT #1 777ms / 783ms (9164) |
| * Close GATT 6ms (9941) |
| * Start CloudSyncing 2ms (9947) |
| * Broadcast Validator 2ms (9949) |
| * FastPairDualConnection end, 9952ms |
| * </pre> |
| */ |
| // TODO(b/203441105): break down FastPairDualConnection into smaller classes. |
| public class FastPairDualConnection extends FastPairConnection { |
| |
| private static final String TAG = FastPairDualConnection.class.getSimpleName(); |
| |
| @VisibleForTesting |
| static final int GATT_ERROR_CODE_FAST_PAIR_SIGNAL_LOST = 10000; |
| @VisibleForTesting |
| static final int GATT_ERROR_CODE_FAST_PAIR_ADDRESS_ROTATED = 20000; |
| @VisibleForTesting |
| static final int GATT_ERROR_CODE_USER_RETRY = 30000; |
| @VisibleForTesting |
| static final int GATT_ERROR_CODE_PAIR_WITH_SAME_MODEL_ID_COUNT = 40000; |
| @VisibleForTesting |
| static final int GATT_ERROR_CODE_TIMEOUT = 1000; |
| |
| @Nullable |
| private static String sInitialConnectionFirmwareVersion; |
| private static final byte[] REQUESTED_SERVICES_LTV = |
| new Ltv( |
| TransportDiscoveryService.SERVICE_UUIDS_16_BIT_LIST_TYPE, |
| toBytes( |
| ByteOrder.LITTLE_ENDIAN, |
| Constants.A2DP_SINK_SERVICE_UUID, |
| Constants.HANDS_FREE_SERVICE_UUID, |
| Constants.HEADSET_SERVICE_UUID)) |
| .getBytes(); |
| private static final byte[] TDS_CONTROL_POINT_REQUEST = |
| concat( |
| new byte[]{ |
| TransportDiscoveryService.ControlPointCharacteristic |
| .ACTIVATE_TRANSPORT_OP_CODE, |
| TransportDiscoveryService.BLUETOOTH_SIG_ORGANIZATION_ID |
| }, |
| REQUESTED_SERVICES_LTV); |
| |
| private static boolean sTestMode = false; |
| |
| static void enableTestMode() { |
| sTestMode = true; |
| } |
| |
| /** |
| * Operation Result Code. |
| */ |
| @Retention(RetentionPolicy.SOURCE) |
| @IntDef( |
| value = { |
| ResultCode.UNKNOWN, |
| ResultCode.SUCCESS, |
| ResultCode.OP_CODE_NOT_SUPPORTED, |
| ResultCode.INVALID_PARAMETER, |
| ResultCode.UNSUPPORTED_ORGANIZATION_ID, |
| ResultCode.OPERATION_FAILED, |
| }) |
| |
| public @interface ResultCode { |
| |
| int UNKNOWN = (byte) 0xFF; |
| int SUCCESS = (byte) 0x00; |
| int OP_CODE_NOT_SUPPORTED = (byte) 0x01; |
| int INVALID_PARAMETER = (byte) 0x02; |
| int UNSUPPORTED_ORGANIZATION_ID = (byte) 0x03; |
| int OPERATION_FAILED = (byte) 0x04; |
| } |
| |
| |
| private static @ResultCode int fromTdsControlPointIndication(byte[] response) { |
| return response == null || response.length < 2 ? ResultCode.UNKNOWN : from(response[1]); |
| } |
| |
| private static @ResultCode int from(byte byteValue) { |
| switch (byteValue) { |
| case ResultCode.UNKNOWN: |
| case ResultCode.SUCCESS: |
| case ResultCode.OP_CODE_NOT_SUPPORTED: |
| case ResultCode.INVALID_PARAMETER: |
| case ResultCode.UNSUPPORTED_ORGANIZATION_ID: |
| case ResultCode.OPERATION_FAILED: |
| return byteValue; |
| default: |
| return ResultCode.UNKNOWN; |
| } |
| } |
| |
| private static class BrEdrHandoverInformation { |
| |
| private final byte[] mBluetoothAddress; |
| private final short[] mProfiles; |
| |
| private BrEdrHandoverInformation(byte[] bluetoothAddress, short[] profiles) { |
| this.mBluetoothAddress = bluetoothAddress; |
| |
| // For now, since we only connect to one profile, prefer A2DP Sink over headset/HFP. |
| // TODO(b/37167120): Connect to more than one profile. |
| Set<Short> profileSet = new HashSet<>(Shorts.asList(profiles)); |
| if (profileSet.contains(Constants.A2DP_SINK_SERVICE_UUID)) { |
| profileSet.remove(Constants.HEADSET_SERVICE_UUID); |
| profileSet.remove(Constants.HANDS_FREE_SERVICE_UUID); |
| } |
| this.mProfiles = Shorts.toArray(profileSet); |
| } |
| |
| @Override |
| public String toString() { |
| return "BrEdrHandoverInformation{" |
| + maskBluetoothAddress(BluetoothAddress.encode(mBluetoothAddress)) |
| + ", profiles=" |
| + (mProfiles.length > 0 ? Shorts.join(",", mProfiles) : "(none)") |
| + "}"; |
| } |
| } |
| |
| private final Context mContext; |
| private final Preferences mPreferences; |
| private final EventLoggerWrapper mEventLogger; |
| private final BluetoothAdapter mBluetoothAdapter = |
| checkNotNull(BluetoothAdapter.getDefaultAdapter()); |
| private String mBleAddress; |
| |
| private final TimingLogger mTimingLogger; |
| private GattConnectionManager mGattConnectionManager; |
| private boolean mProviderInitiatesBonding; |
| private @Nullable |
| byte[] mPairingSecret; |
| private @Nullable |
| byte[] mPairingKey; |
| @Nullable |
| private String mPublicAddress; |
| @VisibleForTesting |
| @Nullable |
| FastPairHistoryFinder mPairedHistoryFinder; |
| @Nullable |
| private String mProviderDeviceName = null; |
| private boolean mNeedUpdateProviderName = false; |
| @Nullable |
| DeviceNameReceiver mDeviceNameReceiver; |
| @Nullable |
| private HandshakeHandler mHandshakeHandlerForTest; |
| @Nullable |
| private Runnable mBeforeDirectlyConnectProfileFromCacheForTest; |
| |
| public FastPairDualConnection( |
| Context context, |
| String bleAddress, |
| Preferences preferences, |
| @Nullable EventLogger eventLogger) { |
| this(context, bleAddress, preferences, eventLogger, |
| new TimingLogger("FastPairDualConnection", preferences)); |
| } |
| |
| @VisibleForTesting |
| FastPairDualConnection( |
| Context context, |
| String bleAddress, |
| Preferences preferences, |
| @Nullable EventLogger eventLogger, |
| TimingLogger timingLogger) { |
| this.mContext = context; |
| this.mPreferences = preferences; |
| this.mEventLogger = new EventLoggerWrapper(eventLogger); |
| this.mBleAddress = bleAddress; |
| this.mTimingLogger = timingLogger; |
| } |
| |
| /** |
| * Unpairs with headphones. Synchronous: Blocks until unpaired. Throws on any error. |
| */ |
| @WorkerThread |
| public void unpair(BluetoothDevice device) |
| throws ReflectionException, InterruptedException, ExecutionException, TimeoutException, |
| PairingException { |
| if (mPreferences.getExtraLoggingInformation() != null) { |
| mEventLogger |
| .bind(mContext, device.getAddress(), mPreferences.getExtraLoggingInformation()); |
| } |
| new BluetoothAudioPairer( |
| mContext, |
| device, |
| mPreferences, |
| mEventLogger, |
| /* keyBasedPairingInfo= */ null, |
| /* passkeyConfirmationHandler= */ null, |
| mTimingLogger) |
| .unpair(); |
| if (mEventLogger.isBound()) { |
| mEventLogger.unbind(mContext); |
| } |
| } |
| |
| /** |
| * Sets the fast pair history for identifying the provider which has paired (without being |
| * forgotten) with the primary account on the device, i.e. the history is not limited on this |
| * phone, can be on other phones with the same account. If they have already paired, Fast Pair |
| * should not generate new account key and default personalized name for it after initial pair. |
| */ |
| @WorkerThread |
| public void setFastPairHistory(List<FastPairHistoryItem> fastPairHistoryItem) { |
| Log.i(TAG, "Paired history has been set."); |
| this.mPairedHistoryFinder = new FastPairHistoryFinder(fastPairHistoryItem); |
| } |
| |
| /** |
| * Update the provider device name when we take provider default name and account based name |
| * into consideration. |
| */ |
| public void setProviderDeviceName(String deviceName) { |
| Log.i(TAG, "Update provider device name = " + deviceName); |
| mProviderDeviceName = deviceName; |
| mNeedUpdateProviderName = true; |
| } |
| |
| /** |
| * Gets the device name from the Provider (via GATT notify). |
| */ |
| @Nullable |
| public String getProviderDeviceName() { |
| if (mDeviceNameReceiver == null) { |
| Log.i(TAG, "getProviderDeviceName failed, deviceNameReceiver == null."); |
| return null; |
| } |
| if (mPairingSecret == null) { |
| Log.i(TAG, "getProviderDeviceName failed, pairingSecret == null."); |
| return null; |
| } |
| String deviceName = mDeviceNameReceiver.getParsedResult(mPairingSecret); |
| Log.i(TAG, "getProviderDeviceName = " + deviceName); |
| |
| return deviceName; |
| } |
| |
| /** |
| * Get the existing account key of the provider, this API can be called after handshake. |
| * |
| * @return the existing account key if the provider has paired with the account before. |
| * Otherwise, return null, i.e. it is a real initial pairing. |
| */ |
| @WorkerThread |
| @Nullable |
| public byte[] getExistingAccountKey() { |
| return mPairedHistoryFinder == null ? null : mPairedHistoryFinder.getExistingAccountKey(); |
| } |
| |
| /** |
| * Pairs with headphones. Synchronous: Blocks until paired and connected. Throws on any error. |
| * |
| * @return the secret key for the user's account, if written. |
| */ |
| @WorkerThread |
| @Nullable |
| public SharedSecret pair() |
| throws BluetoothException, InterruptedException, ReflectionException, TimeoutException, |
| ExecutionException, PairingException { |
| try { |
| return pair(/*key=*/ null); |
| } catch (GeneralSecurityException e) { |
| throw new RuntimeException("Should never happen, no security key!", e); |
| } |
| } |
| |
| /** |
| * Pairs with headphones. Synchronous: Blocks until paired and connected. Throws on any error. |
| * |
| * @param key can be in two different formats. If it is 16 bytes long, then it is an AES account |
| * key. Otherwise, it's a public key generated by {@link EllipticCurveDiffieHellmanExchange}. |
| * See go/fast-pair-2-spec for how each of these keys are used. |
| * @return the secret key for the user's account, if written |
| */ |
| @WorkerThread |
| @Nullable |
| public SharedSecret pair(@Nullable byte[] key) |
| throws BluetoothException, InterruptedException, ReflectionException, TimeoutException, |
| ExecutionException, PairingException, GeneralSecurityException { |
| mPairingKey = key; |
| if (key != null) { |
| Log.i(TAG, "Starting to pair " + maskBluetoothAddress(mBleAddress) + ": key[" |
| + key.length + "], " + mPreferences); |
| } else { |
| Log.i(TAG, "Pairing " + maskBluetoothAddress(mBleAddress) + ": " + mPreferences); |
| } |
| if (mPreferences.getExtraLoggingInformation() != null) { |
| this.mEventLogger.bind( |
| mContext, mBleAddress, mPreferences.getExtraLoggingInformation()); |
| } |
| // Provider never initiates if key is null (Fast Pair 1.0). |
| if (key != null && mPreferences.getProviderInitiatesBondingIfSupported()) { |
| // Provider can't initiate if we can't get our own public address, so check. |
| this.mEventLogger.setCurrentEvent(EventCode.GET_LOCAL_PUBLIC_ADDRESS); |
| if (BluetoothAddress.getPublicAddress(mContext) != null) { |
| this.mEventLogger.logCurrentEventSucceeded(); |
| mProviderInitiatesBonding = true; |
| } else { |
| this.mEventLogger |
| .logCurrentEventFailed(new IllegalStateException("null bluetooth_address")); |
| Log.e(TAG, |
| "Want provider to initiate bonding, but cannot access Bluetooth public " |
| + "address. Falling back to initiating bonding ourselves."); |
| } |
| } |
| |
| // User might be pairing with a bonded device. In this case, we just connect profile |
| // directly and finish pairing. |
| if (directConnectProfileWithCachedAddress()) { |
| callbackOnPaired(); |
| mTimingLogger.dump(); |
| if (mEventLogger.isBound()) { |
| mEventLogger.unbind(mContext); |
| } |
| return null; |
| } |
| |
| // Lazily initialize a new connection manager for each pairing request. |
| initGattConnectionManager(); |
| boolean isSecretHandshakeCompleted = true; |
| |
| try { |
| if (key != null && key.length > 0) { |
| // GATT_CONNECTION_AND_SECRET_HANDSHAKE start. |
| mEventLogger.setCurrentEvent(EventCode.GATT_CONNECTION_AND_SECRET_HANDSHAKE); |
| isSecretHandshakeCompleted = false; |
| Exception lastException = null; |
| boolean lastExceptionFromHandshake = false; |
| long startTime = SystemClock.elapsedRealtime(); |
| // We communicate over this connection twice for Key-based Pairing: once before |
| // bonding begins, and once during (to transfer the passkey). Empirically, keeping |
| // it alive throughout is far more reliable than disconnecting and reconnecting for |
| // each step. The while loop is for retry of GATT connection and handshake only. |
| do { |
| boolean isHandshaking = false; |
| try (BluetoothGattConnection connection = |
| mGattConnectionManager |
| .getConnectionWithSignalLostCheck(mRescueFromError)) { |
| mEventLogger.setCurrentEvent(EventCode.SECRET_HANDSHAKE); |
| if (lastException != null && !lastExceptionFromHandshake) { |
| logRetrySuccessEvent(EventCode.RECOVER_BY_RETRY_GATT, lastException, |
| mEventLogger); |
| lastException = null; |
| } |
| try (ScopedTiming scopedTiming = new ScopedTiming(mTimingLogger, |
| "Handshake")) { |
| isHandshaking = true; |
| handshakeForKeyBasedPairing(key); |
| // After handshake, Fast Pair has the public address of the provider, so |
| // we can check if it has paired with the account. |
| if (mPublicAddress != null && mPairedHistoryFinder != null) { |
| if (mPairedHistoryFinder.isInPairedHistory(mPublicAddress)) { |
| Log.i(TAG, "The provider is found in paired history."); |
| } else { |
| Log.i(TAG, "The provider is not found in paired history."); |
| } |
| } |
| } |
| isHandshaking = false; |
| // SECRET_HANDSHAKE end. |
| mEventLogger.logCurrentEventSucceeded(); |
| isSecretHandshakeCompleted = true; |
| if (mPrepareCreateBondCallback != null) { |
| mPrepareCreateBondCallback.run(); |
| } |
| if (lastException != null && lastExceptionFromHandshake) { |
| logRetrySuccessEvent(EventCode.RECOVER_BY_RETRY_HANDSHAKE_RECONNECT, |
| lastException, mEventLogger); |
| } |
| logManualRetryCounts(/* success= */ true); |
| // GATT_CONNECTION_AND_SECRET_HANDSHAKE end. |
| mEventLogger.logCurrentEventSucceeded(); |
| return pair(mPreferences.getEnableBrEdrHandover()); |
| } catch (SignalLostException e) { |
| long spentTime = SystemClock.elapsedRealtime() - startTime; |
| if (spentTime > mPreferences.getAddressRotateRetryMaxSpentTimeMs()) { |
| Log.w(TAG, "Signal lost but already spend too much time " + spentTime |
| + "ms"); |
| throw e; |
| } |
| |
| logCurrentEventFailedBySignalLost(e); |
| lastException = (Exception) e.getCause(); |
| lastExceptionFromHandshake = isHandshaking; |
| if (mRescueFromError != null && isHandshaking) { |
| mRescueFromError.accept(ErrorCode.SUCCESS_SECRET_HANDSHAKE_RECONNECT); |
| } |
| Log.i(TAG, "Signal lost, retry"); |
| // In case we meet some GATT error which is not recoverable and fail very |
| // quick. |
| SystemClock.sleep(mPreferences.getPairingRetryDelayMs()); |
| } catch (SignalRotatedException e) { |
| long spentTime = SystemClock.elapsedRealtime() - startTime; |
| if (spentTime > mPreferences.getAddressRotateRetryMaxSpentTimeMs()) { |
| Log.w(TAG, "Address rotated but already spend too much time " |
| + spentTime + "ms"); |
| throw e; |
| } |
| |
| logCurrentEventFailedBySignalRotated(e); |
| setBleAddress(e.getNewAddress()); |
| lastException = (Exception) e.getCause(); |
| lastExceptionFromHandshake = isHandshaking; |
| if (mRescueFromError != null) { |
| mRescueFromError.accept(ErrorCode.SUCCESS_ADDRESS_ROTATE); |
| } |
| Log.i(TAG, "Address rotated, retry"); |
| } catch (HandshakeException e) { |
| long spentTime = SystemClock.elapsedRealtime() - startTime; |
| if (spentTime > mPreferences |
| .getSecretHandshakeRetryGattConnectionMaxSpentTimeMs()) { |
| Log.w(TAG, "Secret handshake failed but already spend too much time " |
| + spentTime + "ms"); |
| throw e.getOriginalException(); |
| } |
| if (mEventLogger.isCurrentEvent()) { |
| mEventLogger.logCurrentEventFailed(e.getOriginalException()); |
| } |
| initGattConnectionManager(); |
| lastException = e.getOriginalException(); |
| lastExceptionFromHandshake = true; |
| if (mRescueFromError != null) { |
| mRescueFromError.accept(ErrorCode.SUCCESS_SECRET_HANDSHAKE_RECONNECT); |
| } |
| Log.i(TAG, "Handshake failed, retry GATT connection"); |
| } |
| } while (mPreferences.getRetryGattConnectionAndSecretHandshake()); |
| } |
| if (mPrepareCreateBondCallback != null) { |
| mPrepareCreateBondCallback.run(); |
| } |
| return pair(mPreferences.getEnableBrEdrHandover()); |
| } catch (SignalLostException e) { |
| logCurrentEventFailedBySignalLost(e); |
| // GATT_CONNECTION_AND_SECRET_HANDSHAKE end. |
| if (!isSecretHandshakeCompleted) { |
| logManualRetryCounts(/* success= */ false); |
| logCurrentEventFailedBySignalLost(e); |
| } |
| throw e; |
| } catch (SignalRotatedException e) { |
| logCurrentEventFailedBySignalRotated(e); |
| // GATT_CONNECTION_AND_SECRET_HANDSHAKE end. |
| if (!isSecretHandshakeCompleted) { |
| logManualRetryCounts(/* success= */ false); |
| logCurrentEventFailedBySignalRotated(e); |
| } |
| throw e; |
| } catch (BluetoothException |
| | InterruptedException |
| | ReflectionException |
| | TimeoutException |
| | ExecutionException |
| | PairingException |
| | GeneralSecurityException e) { |
| if (mEventLogger.isCurrentEvent()) { |
| mEventLogger.logCurrentEventFailed(e); |
| } |
| // GATT_CONNECTION_AND_SECRET_HANDSHAKE end. |
| if (!isSecretHandshakeCompleted) { |
| logManualRetryCounts(/* success= */ false); |
| if (mEventLogger.isCurrentEvent()) { |
| mEventLogger.logCurrentEventFailed(e); |
| } |
| } |
| throw e; |
| } finally { |
| mTimingLogger.dump(); |
| if (mEventLogger.isBound()) { |
| mEventLogger.unbind(mContext); |
| } |
| } |
| } |
| |
| private boolean directConnectProfileWithCachedAddress() throws ReflectionException { |
| if (TextUtils.isEmpty(mPreferences.getCachedDeviceAddress()) |
| || !mPreferences.getDirectConnectProfileIfModelIdInCache() |
| || mPreferences.getSkipConnectingProfiles()) { |
| return false; |
| } |
| Log.i(TAG, "Try to direct connect profile with cached address " |
| + maskBluetoothAddress(mPreferences.getCachedDeviceAddress())); |
| mEventLogger.setCurrentEvent(EventCode.DIRECTLY_CONNECT_PROFILE_WITH_CACHED_ADDRESS); |
| BluetoothDevice device = |
| mBluetoothAdapter.getRemoteDevice(mPreferences.getCachedDeviceAddress()).unwrap(); |
| AtomicBoolean interruptConnection = new AtomicBoolean(false); |
| BroadcastReceiver receiver = |
| new BroadcastReceiver() { |
| @Override |
| public void onReceive(Context context, Intent intent) { |
| if (intent == null |
| || !BluetoothDevice.ACTION_PAIRING_REQUEST |
| .equals(intent.getAction())) { |
| return; |
| } |
| BluetoothDevice pairingDevice = intent |
| .getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); |
| if (pairingDevice == null || !device.getAddress() |
| .equals(pairingDevice.getAddress())) { |
| return; |
| } |
| abortBroadcast(); |
| // Should be the clear link key case, make it fail directly to go back to |
| // initial pairing process. |
| pairingDevice.setPairingConfirmation(/* confirm= */ false); |
| Log.w(TAG, "Get pairing request broadcast for device " |
| + maskBluetoothAddress(device.getAddress()) |
| + " while try to direct connect profile with cached address, reject" |
| + " and to go back to initial pairing process"); |
| interruptConnection.set(true); |
| } |
| }; |
| mContext.registerReceiver(receiver, |
| new IntentFilter(BluetoothDevice.ACTION_PAIRING_REQUEST)); |
| try (ScopedTiming scopedTiming = |
| new ScopedTiming(mTimingLogger, |
| "Connect to profile with cached address directly")) { |
| if (mBeforeDirectlyConnectProfileFromCacheForTest != null) { |
| mBeforeDirectlyConnectProfileFromCacheForTest.run(); |
| } |
| attemptConnectProfiles( |
| new BluetoothAudioPairer( |
| mContext, |
| device, |
| mPreferences, |
| mEventLogger, |
| /* keyBasedPairingInfo= */ null, |
| /* passkeyConfirmationHandler= */ null, |
| mTimingLogger), |
| maskBluetoothAddress(device), |
| getSupportedProfiles(device), |
| /* numConnectionAttempts= */ 1, |
| /* enablePairingBehavior= */ false, |
| interruptConnection); |
| Log.i(TAG, |
| "Directly connected to " + maskBluetoothAddress(device) |
| + "with cached address."); |
| mEventLogger.logCurrentEventSucceeded(); |
| mEventLogger.setDevice(device); |
| logPairWithPossibleCachedAddress(device.getAddress()); |
| return true; |
| } catch (PairingException e) { |
| if (interruptConnection.get()) { |
| Log.w(TAG, "Fail to connected to " + maskBluetoothAddress(device) |
| + " with cached address due to link key is cleared.", e); |
| mEventLogger.logCurrentEventFailed( |
| new ConnectException(ConnectErrorCode.LINK_KEY_CLEARED, |
| "Link key is cleared")); |
| } else { |
| Log.w(TAG, "Fail to connected to " + maskBluetoothAddress(device) |
| + " with cached address.", e); |
| mEventLogger.logCurrentEventFailed(e); |
| } |
| return false; |
| } finally { |
| mContext.unregisterReceiver(receiver); |
| } |
| } |
| |
| /** |
| * Logs for user retry, check go/fastpairquality21q3 for more details. |
| */ |
| private void logManualRetryCounts(boolean success) { |
| if (!mPreferences.getLogUserManualRetry()) { |
| return; |
| } |
| |
| // We don't want to be the final event on analytics. |
| if (!mEventLogger.isCurrentEvent()) { |
| return; |
| } |
| |
| mEventLogger.setCurrentEvent(EventCode.GATT_HANDSHAKE_MANUAL_RETRY_ATTEMPTS); |
| if (mPreferences.getPairFailureCounts() <= 0 && success) { |
| mEventLogger.logCurrentEventSucceeded(); |
| } else { |
| int errorCode = mPreferences.getPairFailureCounts(); |
| if (errorCode > 99) { |
| errorCode = 99; |
| } |
| errorCode += success ? 0 : 100; |
| // To not conflict with current error codes. |
| errorCode += GATT_ERROR_CODE_USER_RETRY; |
| mEventLogger.logCurrentEventFailed( |
| new BluetoothGattException("Error for manual retry", errorCode)); |
| } |
| } |
| |
| static void logRetrySuccessEvent( |
| @EventCode int eventCode, |
| @Nullable Exception recoverFromException, |
| EventLoggerWrapper eventLogger) { |
| if (recoverFromException == null) { |
| return; |
| } |
| eventLogger.setCurrentEvent(eventCode); |
| eventLogger.logCurrentEventFailed(recoverFromException); |
| } |
| |
| private void initGattConnectionManager() { |
| mGattConnectionManager = |
| new GattConnectionManager( |
| mContext, |
| mPreferences, |
| mEventLogger, |
| mBluetoothAdapter, |
| this::toggleBluetooth, |
| mBleAddress, |
| mTimingLogger, |
| mFastPairSignalChecker, |
| isPairingWithAntiSpoofingPublicKey()); |
| } |
| |
| private void logCurrentEventFailedBySignalRotated(SignalRotatedException e) { |
| if (!mEventLogger.isCurrentEvent()) { |
| return; |
| } |
| |
| Log.w(TAG, "BLE Address for pairing device might rotated!"); |
| mEventLogger.logCurrentEventFailed( |
| new BluetoothGattException( |
| "BLE Address for pairing device might rotated", |
| appendMoreErrorCode(GATT_ERROR_CODE_FAST_PAIR_ADDRESS_ROTATED, |
| e.getCause()), |
| e)); |
| } |
| |
| private void logCurrentEventFailedBySignalLost(SignalLostException e) { |
| if (!mEventLogger.isCurrentEvent()) { |
| return; |
| } |
| |
| Log.w(TAG, "BLE signal for pairing device might lost!"); |
| mEventLogger.logCurrentEventFailed( |
| new BluetoothGattException( |
| "BLE signal for pairing device might lost", |
| appendMoreErrorCode(GATT_ERROR_CODE_FAST_PAIR_SIGNAL_LOST, e.getCause()), |
| e)); |
| } |
| |
| @VisibleForTesting |
| static int appendMoreErrorCode(int masterErrorCode, @Nullable Throwable cause) { |
| if (cause instanceof BluetoothGattException) { |
| return masterErrorCode + ((BluetoothGattException) cause).getGattErrorCode(); |
| } else if (cause instanceof TimeoutException |
| || cause instanceof BluetoothTimeoutException |
| || cause instanceof BluetoothOperationTimeoutException) { |
| return masterErrorCode + GATT_ERROR_CODE_TIMEOUT; |
| } else { |
| return masterErrorCode; |
| } |
| } |
| |
| private void setBleAddress(String newAddress) { |
| if (TextUtils.isEmpty(newAddress) || Ascii.equalsIgnoreCase(newAddress, mBleAddress)) { |
| return; |
| } |
| |
| mBleAddress = newAddress; |
| |
| // Recreates a GattConnectionManager with the new address for establishing a new GATT |
| // connection later. |
| initGattConnectionManager(); |
| |
| mEventLogger.setDevice(mBluetoothAdapter.getRemoteDevice(mBleAddress).unwrap()); |
| } |
| |
| /** |
| * Gets the public address of the headset used in the connection. Before the handshake, this |
| * could be null. |
| */ |
| @Nullable |
| public String getPublicAddress() { |
| return mPublicAddress; |
| } |
| |
| /** |
| * Pairs with a Bluetooth device. In general, this process goes through the following steps: |
| * |
| * <ol> |
| * <li>Get BrEdr handover information if requested |
| * <li>Discover the device (on Android N and lower to work around a bug) |
| * <li>Connect to the device |
| * <ul> |
| * <li>Attempt a direct connection to a supported profile if we're already bonded |
| * <li>Create a new bond with the not bonded device and then connect to a supported |
| * profile |
| * </ul> |
| * <li>Write the account secret |
| * </ol> |
| * |
| * <p>Blocks until paired. May take 10+ seconds, so run on a background thread. |
| */ |
| @Nullable |
| private SharedSecret pair(boolean enableBrEdrHandover) |
| throws BluetoothException, InterruptedException, ReflectionException, TimeoutException, |
| ExecutionException, PairingException, GeneralSecurityException { |
| BrEdrHandoverInformation brEdrHandoverInformation = null; |
| if (enableBrEdrHandover) { |
| try (ScopedTiming scopedTiming = |
| new ScopedTiming(mTimingLogger, "Get BR/EDR handover information via GATT")) { |
| brEdrHandoverInformation = |
| getBrEdrHandoverInformation(mGattConnectionManager.getConnection()); |
| } catch (BluetoothException | TdsException e) { |
| Log.w(TAG, |
| "Couldn't get BR/EDR Handover info via TDS. Trying direct connect.", e); |
| mEventLogger.logCurrentEventFailed(e); |
| } |
| } |
| |
| if (brEdrHandoverInformation == null) { |
| // Pair directly to the BLE address. Works if the BLE and Bluetooth Classic addresses |
| // are the same, or if we can do BLE cross-key transport. |
| brEdrHandoverInformation = |
| new BrEdrHandoverInformation( |
| BluetoothAddress |
| .decode(mPublicAddress != null ? mPublicAddress : mBleAddress), |
| attemptGetBluetoothClassicProfiles( |
| mBluetoothAdapter.getRemoteDevice(mBleAddress).unwrap(), |
| mPreferences.getNumSdpAttempts())); |
| } |
| |
| BluetoothDevice device = |
| mBluetoothAdapter.getRemoteDevice(brEdrHandoverInformation.mBluetoothAddress) |
| .unwrap(); |
| callbackOnGetAddress(device.getAddress()); |
| mEventLogger.setDevice(device); |
| |
| Log.i(TAG, "Pairing with " + brEdrHandoverInformation); |
| KeyBasedPairingInfo keyBasedPairingInfo = |
| mPairingSecret == null |
| ? null |
| : new KeyBasedPairingInfo( |
| mPairingSecret, mGattConnectionManager, mProviderInitiatesBonding); |
| |
| BluetoothAudioPairer pairer = |
| new BluetoothAudioPairer( |
| mContext, |
| device, |
| mPreferences, |
| mEventLogger, |
| keyBasedPairingInfo, |
| mPasskeyConfirmationHandler, |
| mTimingLogger); |
| |
| logPairWithPossibleCachedAddress(device.getAddress()); |
| logPairWithModelIdInCacheAndDiscoveryFailForCachedAddress(device); |
| |
| // In the case where we are already bonded, we should first just try connecting to supported |
| // profiles. If successful, then this will be much faster than recreating the bond like we |
| // normally do and we can finish early. It is also more reliable than tearing down the bond |
| // and recreating it. |
| try { |
| if (!sTestMode) { |
| attemptDirectConnectionIfBonded(device, pairer); |
| } |
| callbackOnPaired(); |
| return maybeWriteAccountKey(device); |
| } catch (PairingException e) { |
| Log.i(TAG, "Failed to directly connect to supported profiles: " + e.getMessage()); |
| // Catches exception when we fail to connect support profile. And makes the flow to go |
| // through step to write account key when device is bonded. |
| if (mPreferences.getEnablePairFlowShowUiWithoutProfileConnection() |
| && device.getBondState() == BluetoothDevice.BOND_BONDED) { |
| if (mPreferences.getSkipConnectingProfiles() |
| && !mPreferences.getCheckBondStateWhenSkipConnectingProfiles()) { |
| Log.i(TAG, "For notCheckBondStateWhenSkipConnectingProfiles case should do " |
| + "re-bond"); |
| } else { |
| Log.i(TAG, "Fail to connect profile when device is bonded, still call back on" |
| + "pair callback to show ui"); |
| callbackOnPaired(); |
| return maybeWriteAccountKey(device); |
| } |
| } |
| } |
| |
| if (mPreferences.getMoreEventLogForQuality()) { |
| switch (device.getBondState()) { |
| case BOND_BONDED: |
| mEventLogger.setCurrentEvent(EventCode.BEFORE_CREATE_BOND_BONDED); |
| break; |
| case BOND_BONDING: |
| mEventLogger.setCurrentEvent(EventCode.BEFORE_CREATE_BOND_BONDING); |
| break; |
| case BOND_NONE: |
| default: |
| mEventLogger.setCurrentEvent(EventCode.BEFORE_CREATE_BOND); |
| } |
| } |
| |
| for (int i = 1; i <= mPreferences.getNumCreateBondAttempts(); i++) { |
| try (ScopedTiming scopedTiming = new ScopedTiming(mTimingLogger, "Pair device #" + i)) { |
| pairer.pair(); |
| if (mPreferences.getMoreEventLogForQuality()) { |
| // For EventCode.BEFORE_CREATE_BOND |
| mEventLogger.logCurrentEventSucceeded(); |
| } |
| break; |
| } catch (Exception e) { |
| mEventLogger.logCurrentEventFailed(e); |
| if (mPasskeyIsGotten) { |
| Log.w(TAG, |
| "createBond() failed because of " + e.getMessage() |
| + " after getting the passkey. Skip retry."); |
| if (mPreferences.getMoreEventLogForQuality()) { |
| // For EventCode.BEFORE_CREATE_BOND |
| mEventLogger.logCurrentEventFailed( |
| new CreateBondException( |
| CreateBondErrorCode.FAILED_BUT_ALREADY_RECEIVE_PASS_KEY, |
| 0, |
| "Already get the passkey")); |
| } |
| break; |
| } |
| Log.e(TAG, |
| "removeBond() or createBond() failed, attempt " + i + " of " + mPreferences |
| .getNumCreateBondAttempts() + ". Bond state " |
| + device.getBondState(), e); |
| if (i < mPreferences.getNumCreateBondAttempts()) { |
| toggleBluetooth(); |
| |
| // We've seen 3 createBond() failures within 100ms (!). And then success again |
| // later (even without turning on/off bluetooth). So create some minimum break |
| // time. |
| Log.i(TAG, "Sleeping 1 sec after createBond() failure."); |
| SystemClock.sleep(1000); |
| } else if (mPreferences.getMoreEventLogForQuality()) { |
| // For EventCode.BEFORE_CREATE_BOND |
| mEventLogger.logCurrentEventFailed(e); |
| } |
| } |
| } |
| boolean deviceCreateBondFailWithNullSecret = false; |
| if (!pairer.isPaired()) { |
| if (mPairingSecret != null) { |
| // Bonding could fail for a few different reasons here. It could be an error, an |
| // attacker may have tried to bond, or the device may not be up to spec. |
| throw new PairingException("createBond() failed, exiting connection process."); |
| } else if (mPreferences.getSkipConnectingProfiles()) { |
| throw new PairingException( |
| "createBond() failed and skipping connecting to a profile."); |
| } else { |
| // When bond creation has failed, connecting a profile will still work most of the |
| // time for Fast Pair 1.0 devices (ie, pairing secret is null), so continue on with |
| // the spec anyways and attempt to connect supported profiles. |
| Log.w(TAG, "createBond() failed, will try connecting profiles anyway."); |
| deviceCreateBondFailWithNullSecret = true; |
| } |
| } else if (mPreferences.getEnablePairFlowShowUiWithoutProfileConnection()) { |
| Log.i(TAG, "new flow to call on paired callback for ui when pairing step is finished"); |
| callbackOnPaired(); |
| } |
| |
| if (!mPreferences.getSkipConnectingProfiles()) { |
| if (mPreferences.getWaitForUuidsAfterBonding() |
| && brEdrHandoverInformation.mProfiles.length == 0) { |
| short[] supportedProfiles = getCachedUuids(device); |
| if (supportedProfiles.length == 0 |
| && mPreferences.getNumSdpAttemptsAfterBonded() > 0) { |
| Log.i(TAG, "Found no supported profiles in UUID cache, manually trigger SDP."); |
| attemptGetBluetoothClassicProfiles(device, |
| mPreferences.getNumSdpAttemptsAfterBonded()); |
| } |
| brEdrHandoverInformation = |
| new BrEdrHandoverInformation( |
| brEdrHandoverInformation.mBluetoothAddress, supportedProfiles); |
| } |
| short[] profiles = brEdrHandoverInformation.mProfiles; |
| if (profiles.length == 0) { |
| profiles = Constants.getSupportedProfiles(); |
| Log.w(TAG, |
| "Attempting to connect constants profiles, " + Arrays.toString(profiles)); |
| } else { |
| Log.i(TAG, "Attempting to connect device profiles, " + Arrays.toString(profiles)); |
| } |
| |
| try { |
| attemptConnectProfiles( |
| pairer, |
| maskBluetoothAddress(device), |
| profiles, |
| mPreferences.getNumConnectAttempts(), |
| /* enablePairingBehavior= */ false); |
| } catch (PairingException e) { |
| // For new pair flow to show ui, we already show success ui when finishing the |
| // createBond step. So we should catch the exception from connecting profile to |
| // avoid showing fail ui for user. |
| if (mPreferences.getEnablePairFlowShowUiWithoutProfileConnection() |
| && !deviceCreateBondFailWithNullSecret) { |
| Log.i(TAG, "Fail to connect profile when device is bonded"); |
| } else { |
| throw e; |
| } |
| } |
| } |
| if (!mPreferences.getEnablePairFlowShowUiWithoutProfileConnection()) { |
| Log.i(TAG, "original flow to call on paired callback for ui"); |
| callbackOnPaired(); |
| } else if (deviceCreateBondFailWithNullSecret) { |
| // This paired callback is called for device which create bond fail with null secret |
| // such as FastPair 1.0 device when directly connecting to any supported profile. |
| Log.i(TAG, "call on paired callback for ui for device with null secret without bonded " |
| + "state"); |
| callbackOnPaired(); |
| } |
| if (mPreferences.getEnableFirmwareVersionCharacteristic() |
| && validateBluetoothGattCharacteristic( |
| mGattConnectionManager.getConnection(), FirmwareVersionCharacteristic.ID)) { |
| try { |
| sInitialConnectionFirmwareVersion = readFirmwareVersion(); |
| } catch (BluetoothException e) { |
| Log.i(TAG, "Fast Pair: head phone does not support firmware read", e); |
| } |
| } |
| |
| // Catch exception when writing account key or name fail to avoid showing pairing failure |
| // notice for user. Because device is already paired successfully based on paring step. |
| SharedSecret secret = null; |
| try { |
| secret = maybeWriteAccountKey(device); |
| } catch (InterruptedException |
| | ExecutionException |
| | TimeoutException |
| | NoSuchAlgorithmException |
| | BluetoothException e) { |
| Log.w(TAG, "Fast Pair: Got exception when writing account key or name to provider", e); |
| } |
| |
| return secret; |
| } |
| |
| private void logPairWithPossibleCachedAddress(String brEdrAddressForBonding) { |
| if (TextUtils.isEmpty(mPreferences.getPossibleCachedDeviceAddress()) |
| || !mPreferences.getLogPairWithCachedModelId()) { |
| return; |
| } |
| mEventLogger.setCurrentEvent(EventCode.PAIR_WITH_CACHED_MODEL_ID); |
| if (Ascii.equalsIgnoreCase( |
| mPreferences.getPossibleCachedDeviceAddress(), brEdrAddressForBonding)) { |
| mEventLogger.logCurrentEventSucceeded(); |
| Log.i(TAG, "Repair with possible cached device " |
| + maskBluetoothAddress(mPreferences.getPossibleCachedDeviceAddress())); |
| } else { |
| mEventLogger.logCurrentEventFailed( |
| new PairingException("Pairing with 2nd device with same model ID")); |
| Log.i(TAG, "Pair with a new device " + maskBluetoothAddress(brEdrAddressForBonding) |
| + " with model ID in cache " |
| + maskBluetoothAddress(mPreferences.getPossibleCachedDeviceAddress())); |
| } |
| } |
| |
| /** |
| * Logs two type of events. First, why cachedAddress mechanism doesn't work if it's repair with |
| * bonded device case. Second, if it's not the case, log how many devices with the same model Id |
| * is already paired. |
| */ |
| private void logPairWithModelIdInCacheAndDiscoveryFailForCachedAddress(BluetoothDevice device) { |
| if (!mPreferences.getLogPairWithCachedModelId()) { |
| return; |
| } |
| |
| if (device.getBondState() == BOND_BONDED) { |
| if (mPreferences.getSameModelIdPairedDeviceCount() <= 0) { |
| Log.i(TAG, "Device is bonded but we don't have this model Id in cache."); |
| } else if (TextUtils.isEmpty(mPreferences.getCachedDeviceAddress()) |
| && mPreferences.getDirectConnectProfileIfModelIdInCache() |
| && !mPreferences.getSkipConnectingProfiles()) { |
| // Pair with bonded device case. Log why the cached address is not found. |
| mEventLogger.setCurrentEvent( |
| EventCode.DIRECTLY_CONNECT_PROFILE_WITH_CACHED_ADDRESS); |
| mEventLogger.logCurrentEventFailed( |
| mPreferences.getIsDeviceFinishCheckAddressFromCache() |
| ? new ConnectException(ConnectErrorCode.FAIL_TO_DISCOVERY, |
| "Failed to discovery") |
| : new ConnectException( |
| ConnectErrorCode.DISCOVERY_NOT_FINISHED, |
| "Discovery not finished")); |
| Log.i(TAG, "Failed to get cached address due to " |
| + (mPreferences.getIsDeviceFinishCheckAddressFromCache() |
| ? "Failed to discovery" |
| : "Discovery not finished")); |
| } |
| } else if (device.getBondState() == BOND_NONE) { |
| // Pair with new device case, log how many devices with the same model id is in FastPair |
| // cache already. |
| mEventLogger.setCurrentEvent(EventCode.PAIR_WITH_NEW_MODEL); |
| if (mPreferences.getSameModelIdPairedDeviceCount() <= 0) { |
| mEventLogger.logCurrentEventSucceeded(); |
| } else { |
| mEventLogger.logCurrentEventFailed( |
| new BluetoothGattException( |
| "Already have this model ID in cache", |
| GATT_ERROR_CODE_PAIR_WITH_SAME_MODEL_ID_COUNT |
| + mPreferences.getSameModelIdPairedDeviceCount())); |
| } |
| Log.i(TAG, "This device already has " + mPreferences.getSameModelIdPairedDeviceCount() |
| + " peripheral with the same model Id"); |
| } |
| } |
| |
| /** |
| * Attempts to directly connect to any supported profile if we're already bonded, this will save |
| * time over tearing down the bond and recreating it. |
| */ |
| private void attemptDirectConnectionIfBonded(BluetoothDevice device, |
| BluetoothAudioPairer pairer) |
| throws PairingException { |
| if (mPreferences.getSkipConnectingProfiles()) { |
| if (mPreferences.getCheckBondStateWhenSkipConnectingProfiles() |
| && device.getBondState() == BluetoothDevice.BOND_BONDED) { |
| Log.i(TAG, "Skipping connecting to profiles by preferences."); |
| return; |
| } |
| throw new PairingException( |
| "Skipping connecting to profiles, no direct connection possible."); |
| } else if (!mPreferences.getAttemptDirectConnectionWhenPreviouslyBonded() |
| || device.getBondState() != BluetoothDevice.BOND_BONDED) { |
| throw new PairingException( |
| "Not previously bonded skipping direct connection, %s", device.getBondState()); |
| } |
| short[] supportedProfiles = getSupportedProfiles(device); |
| mEventLogger.setCurrentEvent(EventCode.DIRECTLY_CONNECTED_TO_PROFILE); |
| try (ScopedTiming scopedTiming = |
| new ScopedTiming(mTimingLogger, "Connect to profile directly")) { |
| attemptConnectProfiles( |
| pairer, |
| maskBluetoothAddress(device), |
| supportedProfiles, |
| mPreferences.getEnablePairFlowShowUiWithoutProfileConnection() |
| ? mPreferences.getNumConnectAttempts() |
| : 1, |
| mPreferences.getEnablePairingWhileDirectlyConnecting()); |
| Log.i(TAG, "Directly connected to " + maskBluetoothAddress(device)); |
| mEventLogger.logCurrentEventSucceeded(); |
| } catch (PairingException e) { |
| mEventLogger.logCurrentEventFailed(e); |
| // Rethrow e so that the exception bubbles up and we continue the normal pairing |
| // process. |
| throw e; |
| } |
| } |
| |
| @VisibleForTesting |
| void attemptConnectProfiles( |
| BluetoothAudioPairer pairer, |
| String deviceMaskedBluetoothAddress, |
| short[] profiles, |
| int numConnectionAttempts, |
| boolean enablePairingBehavior) |
| throws PairingException { |
| attemptConnectProfiles( |
| pairer, |
| deviceMaskedBluetoothAddress, |
| profiles, |
| numConnectionAttempts, |
| enablePairingBehavior, |
| new AtomicBoolean(false)); |
| } |
| |
| private void attemptConnectProfiles( |
| BluetoothAudioPairer pairer, |
| String deviceMaskedBluetoothAddress, |
| short[] profiles, |
| int numConnectionAttempts, |
| boolean enablePairingBehavior, |
| AtomicBoolean interruptConnection) |
| throws PairingException { |
| if (mPreferences.getMoreEventLogForQuality()) { |
| mEventLogger.setCurrentEvent(EventCode.BEFORE_CONNECT_PROFILE); |
| } |
| Exception lastException = null; |
| for (short profile : profiles) { |
| if (interruptConnection.get()) { |
| Log.w(TAG, "attemptConnectProfiles interrupted"); |
| break; |
| } |
| if (!mPreferences.isSupportedProfile(profile)) { |
| Log.w(TAG, "Ignoring unsupported profile=" + profile); |
| continue; |
| } |
| for (int i = 1; i <= numConnectionAttempts; i++) { |
| if (interruptConnection.get()) { |
| Log.w(TAG, "attemptConnectProfiles interrupted"); |
| break; |
| } |
| mEventLogger.setCurrentEvent(EventCode.CONNECT_PROFILE); |
| mEventLogger.setCurrentProfile(profile); |
| try { |
| pairer.connect(profile, enablePairingBehavior); |
| mEventLogger.logCurrentEventSucceeded(); |
| if (mPreferences.getMoreEventLogForQuality()) { |
| // For EventCode.BEFORE_CONNECT_PROFILE |
| mEventLogger.logCurrentEventSucceeded(); |
| } |
| // If successful, we're done. |
| // TODO(b/37167120): Connect to more than one profile. |
| return; |
| } catch (InterruptedException |
| | ReflectionException |
| | TimeoutException |
| | ExecutionException |
| | ConnectException e) { |
| Log.w(TAG, |
| "Error connecting to profile=" + profile |
| + " for device=" + deviceMaskedBluetoothAddress |
| + " (attempt " + i + " of " + mPreferences |
| .getNumConnectAttempts(), e); |
| mEventLogger.logCurrentEventFailed(e); |
| lastException = e; |
| } |
| } |
| } |
| if (mPreferences.getMoreEventLogForQuality()) { |
| // For EventCode.BEFORE_CONNECT_PROFILE |
| if (lastException != null) { |
| mEventLogger.logCurrentEventFailed(lastException); |
| } else { |
| mEventLogger.logCurrentEventSucceeded(); |
| } |
| } |
| throw new PairingException( |
| "Unable to connect to any profiles in: %s", Arrays.toString(profiles)); |
| } |
| |
| /** |
| * Checks whether or not an account key should be written to the device and writes it if so. |
| * This is called after handle notifying the pairedCallback that we've finished pairing, because |
| * at this point the headset is ready to use. |
| */ |
| @Nullable |
| private SharedSecret maybeWriteAccountKey(BluetoothDevice device) |
| throws InterruptedException, ExecutionException, TimeoutException, |
| NoSuchAlgorithmException, |
| BluetoothException { |
| if (!sTestMode) { |
| Locator.get(mContext, FastPairController.class).setShouldUpload(false); |
| } |
| if (!shouldWriteAccountKey()) { |
| // For FastPair 2.0, here should be a subsequent pairing case. |
| return null; |
| } |
| |
| // Check if it should be a subsequent pairing but go through initial pairing. If there is an |
| // existed paired history found, use the same account key instead of creating a new one. |
| byte[] accountKey = |
| mPairedHistoryFinder == null ? null : mPairedHistoryFinder.getExistingAccountKey(); |
| if (accountKey == null) { |
| // It is a real initial pairing, generate a new account key for the headset. |
| try (ScopedTiming scopedTiming1 = |
| new ScopedTiming(mTimingLogger, "Write account key")) { |
| accountKey = doWriteAccountKey(createAccountKey(), device.getAddress()); |
| if (accountKey == null) { |
| // Without writing account key back to provider, close the connection. |
| mGattConnectionManager.closeConnection(); |
| return null; |
| } |
| if (!mPreferences.getIsRetroactivePairing()) { |
| try (ScopedTiming scopedTiming2 = new ScopedTiming(mTimingLogger, |
| "Start CloudSyncing")) { |
| // Start to sync to the footprint |
| Locator.get(mContext, FastPairController.class).setShouldUpload(true); |
| //mContext.startService(createCloudSyncingIntent(accountKey)); |
| } catch (SecurityException e) { |
| Log.w(TAG, "Error adding device.", e); |
| } |
| } |
| } |
| } else if (shouldWriteAccountKeyForExistingCase(accountKey)) { |
| // There is an existing account key, but go through initial pairing, and still write the |
| // existing account key. |
| doWriteAccountKey(accountKey, device.getAddress()); |
| } |
| |
| // When finish writing account key in initial pairing, write new device name back to |
| // provider. |
| UUID characteristicUuid = NameCharacteristic.getId(mGattConnectionManager.getConnection()); |
| if (mPreferences.getEnableNamingCharacteristic() |
| && mNeedUpdateProviderName |
| && validateBluetoothGattCharacteristic( |
| mGattConnectionManager.getConnection(), characteristicUuid)) { |
| try (ScopedTiming scopedTiming = new ScopedTiming(mTimingLogger, |
| "WriteNameToProvider")) { |
| writeNameToProvider(this.mProviderDeviceName, device.getAddress()); |
| } |
| } |
| |
| // When finish writing account key and name back to provider, close the connection. |
| mGattConnectionManager.closeConnection(); |
| return SharedSecret.create(accountKey, device.getAddress()); |
| } |
| |
| private boolean shouldWriteAccountKey() { |
| return isWritingAccountKeyEnabled() && isPairingWithAntiSpoofingPublicKey(); |
| } |
| |
| private boolean isWritingAccountKeyEnabled() { |
| return mPreferences.getNumWriteAccountKeyAttempts() > 0; |
| } |
| |
| private boolean isPairingWithAntiSpoofingPublicKey() { |
| return isPairingWithAntiSpoofingPublicKey(mPairingKey); |
| } |
| |
| private boolean isPairingWithAntiSpoofingPublicKey(@Nullable byte[] key) { |
| return key != null && key.length == EllipticCurveDiffieHellmanExchange.PUBLIC_KEY_LENGTH; |
| } |
| |
| /** |
| * Creates and writes an account key to the provided mac address. |
| */ |
| @Nullable |
| private byte[] doWriteAccountKey(byte[] accountKey, String macAddress) |
| throws InterruptedException, ExecutionException, TimeoutException, BluetoothException { |
| byte[] localPairingSecret = mPairingSecret; |
| if (localPairingSecret == null) { |
| Log.w(TAG, "Pairing secret was null, account key couldn't be encrypted or written."); |
| return null; |
| } |
| if (!mPreferences.getSkipDisconnectingGattBeforeWritingAccountKey()) { |
| try (ScopedTiming scopedTiming = new ScopedTiming(mTimingLogger, |
| "Close GATT and sleep")) { |
| // Make a new connection instead of reusing gattConnection, because this is |
| // post-pairing and we need an encrypted connection. |
| mGattConnectionManager.closeConnection(); |
| // Sleep before re-connecting to gatt, for writing account key, could increase |
| // stability. |
| Thread.sleep(mPreferences.getWriteAccountKeySleepMillis()); |
| } |
| } |
| |
| byte[] encryptedKey; |
| try (ScopedTiming scopedTiming = new ScopedTiming(mTimingLogger, "Encrypt key")) { |
| encryptedKey = AesEcbSingleBlockEncryption.encrypt(localPairingSecret, accountKey); |
| } catch (GeneralSecurityException e) { |
| Log.w("Failed to encrypt key.", e); |
| return null; |
| } |
| |
| for (int i = 1; i <= mPreferences.getNumWriteAccountKeyAttempts(); i++) { |
| mEventLogger.setCurrentEvent(EventCode.WRITE_ACCOUNT_KEY); |
| try (ScopedTiming scopedTiming = new ScopedTiming(mTimingLogger, |
| "Write key via GATT #" + i)) { |
| writeAccountKey(encryptedKey, macAddress); |
| mEventLogger.logCurrentEventSucceeded(); |
| return accountKey; |
| } catch (BluetoothException e) { |
| Log.w("Error writing account key attempt " + i + " of " + mPreferences |
| .getNumWriteAccountKeyAttempts(), e); |
| mEventLogger.logCurrentEventFailed(e); |
| // Retry with a while for stability. |
| Thread.sleep(mPreferences.getWriteAccountKeySleepMillis()); |
| } |
| } |
| return null; |
| } |
| |
| private byte[] createAccountKey() throws NoSuchAlgorithmException { |
| return AccountKeyGenerator.createAccountKey(); |
| } |
| |
| @VisibleForTesting |
| boolean shouldWriteAccountKeyForExistingCase(byte[] existingAccountKey) { |
| if (!mPreferences.getKeepSameAccountKeyWrite()) { |
| Log.i(TAG, |
| "The provider has already paired with the account, skip writing account key."); |
| return false; |
| } |
| if (existingAccountKey[0] != AccountKeyCharacteristic.TYPE) { |
| Log.i(TAG, |
| "The provider has already paired with the account, but accountKey[0] != 0x04." |
| + " Forget the device from the account and re-try"); |
| |
| return false; |
| } |
| Log.i(TAG, "The provider has already paired with the account, still write the same account " |
| + "key."); |
| return true; |
| } |
| |
| /** |
| * Performs a key-based pairing request handshake to authenticate and get the remote device's |
| * public address. |
| * |
| * @param key is described in {@link #pair(byte[])} |
| */ |
| @VisibleForTesting |
| SharedSecret handshakeForKeyBasedPairing(byte[] key) |
| throws InterruptedException, ExecutionException, TimeoutException, BluetoothException, |
| GeneralSecurityException, PairingException { |
| // We may also initialize gattConnectionManager of prepareForHandshake() that will be used |
| // in registerNotificationForNamePacket(), so we need to call it here. |
| HandshakeHandler handshakeHandler = prepareForHandshake(); |
| KeyBasedPairingRequest.Builder keyBasedPairingRequestBuilder = |
| new KeyBasedPairingRequest.Builder() |
| .setVerificationData(BluetoothAddress.decode(mBleAddress)); |
| if (mProviderInitiatesBonding) { |
| keyBasedPairingRequestBuilder |
| .addFlag(KeyBasedPairingRequestFlag.PROVIDER_INITIATES_BONDING); |
| } |
| // Seeker only request provider device name in initial pairing. |
| if (mPreferences.getEnableNamingCharacteristic() && isPairingWithAntiSpoofingPublicKey( |
| key)) { |
| keyBasedPairingRequestBuilder.addFlag(KeyBasedPairingRequestFlag.REQUEST_DEVICE_NAME); |
| // Register listener to receive name characteristic response from provider. |
| registerNotificationForNamePacket(); |
| } |
| if (mPreferences.getIsRetroactivePairing()) { |
| keyBasedPairingRequestBuilder |
| .addFlag(KeyBasedPairingRequestFlag.REQUEST_RETROACTIVE_PAIR); |
| keyBasedPairingRequestBuilder.setSeekerPublicAddress( |
| Preconditions.checkNotNull(BluetoothAddress.getPublicAddress(mContext))); |
| } |
| |
| return performHandshakeWithRetryAndSignalLostCheck( |
| handshakeHandler, key, keyBasedPairingRequestBuilder.build(), /* withRetry= */ |
| true); |
| } |
| |
| /** |
| * Performs an action-over-BLE request handshake for authentication, i.e. to identify the shared |
| * secret. The given key should be the account key. |
| */ |
| private SharedSecret handshakeForActionOverBle(byte[] key, |
| @AdditionalDataType int additionalDataType) |
| throws InterruptedException, ExecutionException, TimeoutException, BluetoothException, |
| GeneralSecurityException, PairingException { |
| HandshakeHandler handshakeHandler = prepareForHandshake(); |
| return performHandshakeWithRetryAndSignalLostCheck( |
| handshakeHandler, |
| key, |
| new ActionOverBle.Builder() |
| .setVerificationData(BluetoothAddress.decode(mBleAddress)) |
| .setAdditionalDataType(additionalDataType) |
| .build(), |
| /* withRetry= */ false); |
| } |
| |
| private HandshakeHandler prepareForHandshake() { |
| if (mGattConnectionManager == null) { |
| mGattConnectionManager = |
| new GattConnectionManager( |
| mContext, |
| mPreferences, |
| mEventLogger, |
| mBluetoothAdapter, |
| this::toggleBluetooth, |
| mBleAddress, |
| mTimingLogger, |
| mFastPairSignalChecker, |
| isPairingWithAntiSpoofingPublicKey()); |
| } |
| if (mHandshakeHandlerForTest != null) { |
| Log.w(TAG, "Use handshakeHandlerForTest!"); |
| return verifyNotNull(mHandshakeHandlerForTest); |
| } |
| return new HandshakeHandler( |
| mGattConnectionManager, mBleAddress, mPreferences, mEventLogger, |
| mFastPairSignalChecker); |
| } |
| |
| @VisibleForTesting |
| void setHandshakeHandlerForTest(@Nullable HandshakeHandler handshakeHandlerForTest) { |
| this.mHandshakeHandlerForTest = handshakeHandlerForTest; |
| } |
| |
| private SharedSecret performHandshakeWithRetryAndSignalLostCheck( |
| HandshakeHandler handshakeHandler, |
| byte[] key, |
| HandshakeMessage handshakeMessage, |
| boolean withRetry) |
| throws GeneralSecurityException, ExecutionException, BluetoothException, |
| InterruptedException, TimeoutException, PairingException { |
| SharedSecret handshakeResult = |
| withRetry |
| ? handshakeHandler.doHandshakeWithRetryAndSignalLostCheck( |
| key, handshakeMessage, mRescueFromError) |
| : handshakeHandler.doHandshake(key, handshakeMessage); |
| // TODO: Try to remove these two global variables, publicAddress and pairingSecret. |
| mPublicAddress = handshakeResult.getAddress(); |
| mPairingSecret = handshakeResult.getKey(); |
| return handshakeResult; |
| } |
| |
| private void toggleBluetooth() |
| throws InterruptedException, ExecutionException, TimeoutException { |
| if (!mPreferences.getToggleBluetoothOnFailure()) { |
| return; |
| } |
| |
| Log.i(TAG, "Turning Bluetooth off."); |
| mEventLogger.setCurrentEvent(EventCode.DISABLE_BLUETOOTH); |
| mBluetoothAdapter.unwrap().disable(); |
| disableBle(mBluetoothAdapter.unwrap()); |
| try { |
| waitForBluetoothState(android.bluetooth.BluetoothAdapter.STATE_OFF); |
| mEventLogger.logCurrentEventSucceeded(); |
| } catch (TimeoutException e) { |
| mEventLogger.logCurrentEventFailed(e); |
| // Soldier on despite failing to turn off Bluetooth. We can't control whether other |
| // clients (even inside GCore) kept it enabled in BLE-only mode. |
| Log.w(TAG, "Bluetooth still on. BluetoothAdapter state=" |
| + getBleState(mBluetoothAdapter.unwrap()), e); |
| } |
| |
| // Note: Intentionally don't re-enable BLE-only mode, because we don't know which app |
| // enabled it. The client app should listen to Bluetooth events and enable as necessary |
| // (because the user can toggle at any time; e.g. via Airplane mode). |
| Log.i(TAG, "Turning Bluetooth on."); |
| mEventLogger.setCurrentEvent(EventCode.ENABLE_BLUETOOTH); |
| mBluetoothAdapter.unwrap().enable(); |
| waitForBluetoothState(android.bluetooth.BluetoothAdapter.STATE_ON); |
| mEventLogger.logCurrentEventSucceeded(); |
| } |
| |
| private void waitForBluetoothState(int state) |
| throws TimeoutException, ExecutionException, InterruptedException { |
| waitForBluetoothStateUsingPolling(state); |
| } |
| |
| private void waitForBluetoothStateUsingPolling(int state) throws TimeoutException { |
| // There's a bug where we (pretty often!) never get the broadcast for STATE_ON or STATE_OFF. |
| // So poll instead. |
| long start = SystemClock.elapsedRealtime(); |
| long timeoutMillis = mPreferences.getBluetoothToggleTimeoutSeconds() * 1000L; |
| while (SystemClock.elapsedRealtime() - start < timeoutMillis) { |
| if (state == getBleState(mBluetoothAdapter.unwrap())) { |
| break; |
| } |
| SystemClock.sleep(mPreferences.getBluetoothStatePollingMillis()); |
| } |
| |
| if (state != getBleState(mBluetoothAdapter.unwrap())) { |
| throw new TimeoutException( |
| String.format( |
| Locale.getDefault(), |
| "Timed out waiting for state %d, current state is %d", |
| state, |
| getBleState(mBluetoothAdapter.unwrap()))); |
| } |
| } |
| |
| private BrEdrHandoverInformation getBrEdrHandoverInformation(BluetoothGattConnection connection) |
| throws BluetoothException, TdsException, InterruptedException, ExecutionException, |
| TimeoutException { |
| Log.i(TAG, "Connecting GATT server to BLE address=" + maskBluetoothAddress(mBleAddress)); |
| Log.i(TAG, "Telling device to become discoverable"); |
| mEventLogger.setCurrentEvent(EventCode.BR_EDR_HANDOVER_WRITE_CONTROL_POINT_REQUEST); |
| ChangeObserver changeObserver = |
| connection.enableNotification( |
| TransportDiscoveryService.ID, |
| TransportDiscoveryService.ControlPointCharacteristic.ID); |
| connection.writeCharacteristic( |
| TransportDiscoveryService.ID, |
| TransportDiscoveryService.ControlPointCharacteristic.ID, |
| TDS_CONTROL_POINT_REQUEST); |
| |
| byte[] response = |
| changeObserver.waitForUpdate( |
| TimeUnit.SECONDS.toMillis(mPreferences.getGattOperationTimeoutSeconds())); |
| @ResultCode int resultCode = fromTdsControlPointIndication(response); |
| if (resultCode != ResultCode.SUCCESS) { |
| throw new TdsException( |
| BrEdrHandoverErrorCode.CONTROL_POINT_RESULT_CODE_NOT_SUCCESS, |
| "TDS Control Point result code (%s) was not success in response %s", |
| resultCode, |
| base16().lowerCase().encode(response)); |
| } |
| mEventLogger.logCurrentEventSucceeded(); |
| return new BrEdrHandoverInformation( |
| getAddressFromBrEdrConnection(connection), |
| getProfilesFromBrEdrConnection(connection)); |
| } |
| |
| private byte[] getAddressFromBrEdrConnection(BluetoothGattConnection connection) |
| throws BluetoothException, TdsException { |
| Log.i(TAG, "Getting Bluetooth MAC"); |
| mEventLogger.setCurrentEvent(EventCode.BR_EDR_HANDOVER_READ_BLUETOOTH_MAC); |
| byte[] brHandoverData = |
| connection.readCharacteristic( |
| TransportDiscoveryService.ID, |
| to128BitUuid(mPreferences.getBrHandoverDataCharacteristicId())); |
| if (brHandoverData == null || brHandoverData.length < 7) { |
| throw new TdsException( |
| BrEdrHandoverErrorCode.BLUETOOTH_MAC_INVALID, |
| "Bluetooth MAC not contained in BR handover data: %s", |
| brHandoverData != null ? base16().lowerCase().encode(brHandoverData) |
| : "(none)"); |
| } |
| byte[] bluetoothAddress = |
| new Bytes.Value(Arrays.copyOfRange(brHandoverData, 1, 7), ByteOrder.LITTLE_ENDIAN) |
| .getBytes(ByteOrder.BIG_ENDIAN); |
| mEventLogger.logCurrentEventSucceeded(); |
| return bluetoothAddress; |
| } |
| |
| private short[] getProfilesFromBrEdrConnection(BluetoothGattConnection connection) { |
| mEventLogger.setCurrentEvent(EventCode.BR_EDR_HANDOVER_READ_TRANSPORT_BLOCK); |
| try { |
| byte[] transportBlock = |
| connection.readDescriptor( |
| TransportDiscoveryService.ID, |
| to128BitUuid(mPreferences.getBluetoothSigDataCharacteristicId()), |
| to128BitUuid(mPreferences.getBrTransportBlockDataDescriptorId())); |
| Log.i(TAG, "Got transport block: " + base16().lowerCase().encode(transportBlock)); |
| short[] profiles = getSupportedProfiles(transportBlock); |
| mEventLogger.logCurrentEventSucceeded(); |
| return profiles; |
| } catch (BluetoothException | TdsException | ParseException e) { |
| Log.w(TAG, "Failed to get supported profiles from transport block.", e); |
| mEventLogger.logCurrentEventFailed(e); |
| } |
| return new short[0]; |
| } |
| |
| @VisibleForTesting |
| boolean writeNameToProvider(@Nullable String deviceName, @Nullable String address) |
| throws InterruptedException, TimeoutException, ExecutionException { |
| if (deviceName == null || address == null) { |
| Log.i(TAG, "writeNameToProvider fail because provider name or address is null."); |
| return false; |
| } |
| if (mPairingSecret == null) { |
| Log.i(TAG, "writeNameToProvider fail because no pairingSecret."); |
| return false; |
| } |
| byte[] encryptedDeviceNamePacket; |
| try (ScopedTiming scopedTiming = new ScopedTiming(mTimingLogger, "Encode device name")) { |
| encryptedDeviceNamePacket = |
| NamingEncoder.encodeNamingPacket(mPairingSecret, deviceName); |
| } catch (GeneralSecurityException e) { |
| Log.w(TAG, "Failed to encrypt device name.", e); |
| return false; |
| } |
| |
| for (int i = 1; i <= mPreferences.getNumWriteAccountKeyAttempts(); i++) { |
| mEventLogger.setCurrentEvent(EventCode.WRITE_DEVICE_NAME); |
| try { |
| writeDeviceName(encryptedDeviceNamePacket, address); |
| mEventLogger.logCurrentEventSucceeded(); |
| return true; |
| } catch (BluetoothException e) { |
| Log.w(TAG, "Error writing name attempt " + i + " of " |
| + mPreferences.getNumWriteAccountKeyAttempts()); |
| mEventLogger.logCurrentEventFailed(e); |
| // Reuses the existing preference because the same usage. |
| Thread.sleep(mPreferences.getWriteAccountKeySleepMillis()); |
| } |
| } |
| return false; |
| } |
| |
| private void writeAccountKey(byte[] encryptedAccountKey, String address) |
| throws BluetoothException, InterruptedException, ExecutionException, TimeoutException { |
| Log.i(TAG, "Writing account key to address=" + maskBluetoothAddress(address)); |
| BluetoothGattConnection connection = mGattConnectionManager.getConnection(); |
| connection.setOperationTimeout( |
| TimeUnit.SECONDS.toMillis(mPreferences.getGattOperationTimeoutSeconds())); |
| UUID characteristicUuid = AccountKeyCharacteristic.getId(connection); |
| connection.writeCharacteristic(FastPairService.ID, characteristicUuid, encryptedAccountKey); |
| Log.i(TAG, |
| "Finished writing encrypted account key=" + base16().encode(encryptedAccountKey)); |
| } |
| |
| private void writeDeviceName(byte[] naming, String address) |
| throws BluetoothException, InterruptedException, ExecutionException, TimeoutException { |
| Log.i(TAG, "Writing new device name to address=" + maskBluetoothAddress(address)); |
| BluetoothGattConnection connection = mGattConnectionManager.getConnection(); |
| connection.setOperationTimeout( |
| TimeUnit.SECONDS.toMillis(mPreferences.getGattOperationTimeoutSeconds())); |
| UUID characteristicUuid = NameCharacteristic.getId(connection); |
| connection.writeCharacteristic(FastPairService.ID, characteristicUuid, naming); |
| Log.i(TAG, "Finished writing new device name=" + base16().encode(naming)); |
| } |
| |
| /** |
| * Reads firmware version after write account key to provider since simulator is more stable to |
| * read firmware version in initial gatt connection. This function will also read firmware when |
| * detect bloomfilter. Need to verify this after real device come out. TODO(b/130592473) |
| */ |
| @Nullable |
| public String readFirmwareVersion() |
| throws BluetoothException, InterruptedException, ExecutionException, TimeoutException { |
| if (!TextUtils.isEmpty(sInitialConnectionFirmwareVersion)) { |
| String result = sInitialConnectionFirmwareVersion; |
| sInitialConnectionFirmwareVersion = null; |
| return result; |
| } |
| if (mGattConnectionManager == null) { |
| mGattConnectionManager = |
| new GattConnectionManager( |
| mContext, |
| mPreferences, |
| mEventLogger, |
| mBluetoothAdapter, |
| this::toggleBluetooth, |
| mBleAddress, |
| mTimingLogger, |
| mFastPairSignalChecker, |
| /* setMtu= */ true); |
| mGattConnectionManager.closeConnection(); |
| } |
| if (sTestMode) { |
| return null; |
| } |
| BluetoothGattConnection connection = mGattConnectionManager.getConnection(); |
| connection.setOperationTimeout( |
| TimeUnit.SECONDS.toMillis(mPreferences.getGattOperationTimeoutSeconds())); |
| |
| try { |
| String firmwareVersion = |
| new String( |
| connection.readCharacteristic( |
| FastPairService.ID, |
| to128BitUuid( |
| mPreferences.getFirmwareVersionCharacteristicId()))); |
| Log.i(TAG, "FastPair: Got the firmware info version number = " + firmwareVersion); |
| mGattConnectionManager.closeConnection(); |
| return firmwareVersion; |
| } catch (BluetoothException e) { |
| Log.i(TAG, "FastPair: can't read firmware characteristic.", e); |
| mGattConnectionManager.closeConnection(); |
| return null; |
| } |
| } |
| |
| @VisibleForTesting |
| @Nullable |
| String getInitialConnectionFirmware() { |
| return sInitialConnectionFirmwareVersion; |
| } |
| |
| private void registerNotificationForNamePacket() |
| throws BluetoothException, InterruptedException, ExecutionException, TimeoutException { |
| Log.i(TAG, |
| "register for the device name response from address=" + maskBluetoothAddress( |
| mBleAddress)); |
| |
| BluetoothGattConnection gattConnection = mGattConnectionManager.getConnection(); |
| gattConnection.setOperationTimeout( |
| TimeUnit.SECONDS.toMillis(mPreferences.getGattOperationTimeoutSeconds())); |
| try { |
| mDeviceNameReceiver = new DeviceNameReceiver(gattConnection); |
| } catch (BluetoothException e) { |
| Log.i(TAG, "Can't register for device name response, no naming characteristic."); |
| return; |
| } |
| } |
| |
| private short[] getSupportedProfiles(BluetoothDevice device) { |
| short[] supportedProfiles = getCachedUuids(device); |
| if (supportedProfiles.length == 0 && mPreferences.getNumSdpAttemptsAfterBonded() > 0) { |
| supportedProfiles = |
| attemptGetBluetoothClassicProfiles(device, |
| mPreferences.getNumSdpAttemptsAfterBonded()); |
| } |
| if (supportedProfiles.length == 0) { |
| supportedProfiles = Constants.getSupportedProfiles(); |
| Log.w(TAG, "Attempting to connect constants profiles, " |
| + Arrays.toString(supportedProfiles)); |
| } else { |
| Log.i(TAG, |
| "Attempting to connect device profiles, " + Arrays.toString(supportedProfiles)); |
| } |
| return supportedProfiles; |
| } |
| |
| private static short[] getSupportedProfiles(byte[] transportBlock) |
| throws TdsException, ParseException { |
| if (transportBlock == null || transportBlock.length < 4) { |
| throw new TdsException( |
| BrEdrHandoverErrorCode.TRANSPORT_BLOCK_INVALID, |
| "Transport Block null or too short: %s", |
| base16().lowerCase().encode(transportBlock)); |
| } |
| int transportDataLength = transportBlock[2]; |
| if (transportBlock.length < 3 + transportDataLength) { |
| throw new TdsException( |
| BrEdrHandoverErrorCode.TRANSPORT_BLOCK_INVALID, |
| "Transport Block has wrong length byte: %s", |
| base16().lowerCase().encode(transportBlock)); |
| } |
| byte[] transportData = Arrays.copyOfRange(transportBlock, 3, 3 + transportDataLength); |
| for (Ltv ltv : Ltv.parse(transportData)) { |
| int uuidLength = uuidLength(ltv.mType); |
| // We currently only support a single list of 2-byte UUIDs. |
| // TODO(b/37539535): Support multiple lists, and longer (32-bit, 128-bit) IDs? |
| if (uuidLength == 2) { |
| return toShorts(ByteOrder.LITTLE_ENDIAN, ltv.mValue); |
| } |
| } |
| return new short[0]; |
| } |
| |
| /** |
| * Returns 0 if the type is not one of the UUID list types; otherwise returns length in bytes. |
| */ |
| private static int uuidLength(byte dataType) { |
| switch (dataType) { |
| case TransportDiscoveryService.SERVICE_UUIDS_16_BIT_LIST_TYPE: |
| return 2; |
| case TransportDiscoveryService.SERVICE_UUIDS_32_BIT_LIST_TYPE: |
| return 4; |
| case TransportDiscoveryService.SERVICE_UUIDS_128_BIT_LIST_TYPE: |
| return 16; |
| default: |
| return 0; |
| } |
| } |
| |
| private short[] attemptGetBluetoothClassicProfiles(BluetoothDevice device, int numSdpAttempts) { |
| // The docs say that if fetchUuidsWithSdp() has an error or "takes a long time", we get an |
| // intent containing only the stuff in the cache (i.e. nothing). Retry a few times. |
| short[] supportedProfiles = null; |
| for (int i = 1; i <= numSdpAttempts; i++) { |
| mEventLogger.setCurrentEvent(EventCode.GET_PROFILES_VIA_SDP); |
| try (ScopedTiming scopedTiming = |
| new ScopedTiming(mTimingLogger, |
| "Get BR/EDR handover information via SDP #" + i)) { |
| supportedProfiles = getSupportedProfilesViaBluetoothClassic(device); |
| } catch (ExecutionException | InterruptedException | TimeoutException e) { |
| // Ignores and retries if needed. |
| } |
| if (supportedProfiles != null && supportedProfiles.length != 0) { |
| mEventLogger.logCurrentEventSucceeded(); |
| break; |
| } else { |
| mEventLogger.logCurrentEventFailed(new TimeoutException()); |
| Log.w(TAG, "SDP returned no UUIDs from " + maskBluetoothAddress(device.getAddress()) |
| + ", assuming timeout (attempt " + i + " of " + numSdpAttempts + ")."); |
| } |
| } |
| return (supportedProfiles == null) ? new short[0] : supportedProfiles; |
| } |
| |
| private short[] getSupportedProfilesViaBluetoothClassic(BluetoothDevice device) |
| throws ExecutionException, InterruptedException, TimeoutException { |
| Log.i(TAG, "Getting supported profiles via SDP (Bluetooth Classic) for " |
| + maskBluetoothAddress(device.getAddress())); |
| try (DeviceIntentReceiver supportedProfilesReceiver = |
| DeviceIntentReceiver.oneShotReceiver( |
| mContext, mPreferences, device, BluetoothDevice.ACTION_UUID)) { |
| device.fetchUuidsWithSdp(); |
| supportedProfilesReceiver.await(mPreferences.getSdpTimeoutSeconds(), TimeUnit.SECONDS); |
| } |
| return getCachedUuids(device); |
| } |
| |
| private static short[] getCachedUuids(BluetoothDevice device) { |
| ParcelUuid[] parcelUuids = device.getUuids(); |
| Log.i(TAG, "Got supported UUIDs: " + Arrays.toString(parcelUuids)); |
| if (parcelUuids == null) { |
| // The OS can return null. |
| parcelUuids = new ParcelUuid[0]; |
| } |
| |
| List<Short> shortUuids = new ArrayList<>(parcelUuids.length); |
| for (ParcelUuid parcelUuid : parcelUuids) { |
| UUID uuid = parcelUuid.getUuid(); |
| if (BluetoothUuids.is16BitUuid(uuid)) { |
| shortUuids.add(get16BitUuid(uuid)); |
| } |
| } |
| return Shorts.toArray(shortUuids); |
| } |
| |
| private void callbackOnPaired() { |
| if (mPairedCallback != null) { |
| mPairedCallback.onPaired(mPublicAddress != null ? mPublicAddress : mBleAddress); |
| } |
| } |
| |
| private void callbackOnGetAddress(String address) { |
| if (mOnGetBluetoothAddressCallback != null) { |
| mOnGetBluetoothAddressCallback.onGetBluetoothAddress(address); |
| } |
| } |
| |
| private boolean validateBluetoothGattCharacteristic( |
| BluetoothGattConnection connection, UUID characteristicUUID) { |
| try (ScopedTiming scopedTiming = |
| new ScopedTiming(mTimingLogger, "Get service characteristic list")) { |
| List<BluetoothGattCharacteristic> serviceCharacteristicList = |
| connection.getService(FastPairService.ID).getCharacteristics(); |
| for (BluetoothGattCharacteristic characteristic : serviceCharacteristicList) { |
| if (characteristicUUID.equals(characteristic.getUuid())) { |
| Log.i(TAG, "characteristic is exists, uuid = " + characteristicUUID); |
| return true; |
| } |
| } |
| } catch (BluetoothException e) { |
| Log.w(TAG, "Can't get service characteristic list.", e); |
| } |
| Log.i(TAG, "can't find characteristic, uuid = " + characteristicUUID); |
| return false; |
| } |
| |
| // This method is only for testing to make test method block until get name response or time |
| // out. |
| /** |
| * Set name response countdown latch. |
| */ |
| public void setNameResponseCountDownLatch(CountDownLatch countDownLatch) { |
| if (mDeviceNameReceiver != null) { |
| mDeviceNameReceiver.setCountDown(countDownLatch); |
| Log.v(TAG, "set up nameResponseCountDown"); |
| } |
| } |
| |
| private static int getBleState(android.bluetooth.BluetoothAdapter bluetoothAdapter) { |
| // Can't use the public isLeEnabled() API, because it returns false for |
| // STATE_BLE_TURNING_(ON|OFF). So if we assume false == STATE_OFF, that can be |
| // very wrong. |
| return getLeState(bluetoothAdapter); |
| } |
| |
| @VisibleForTesting |
| static int getLeState(android.bluetooth.BluetoothAdapter adapter) { |
| try { |
| return (Integer) Reflect.on(adapter).withMethod("getLeState").get(); |
| } catch (ReflectionException e) { |
| Log.i(TAG, "Can't call getLeState", e); |
| } |
| return adapter.getState(); |
| } |
| |
| private static void disableBle(android.bluetooth.BluetoothAdapter adapter) { |
| adapter.disableBLE(); |
| } |
| |
| /** |
| * Handle the searching of Fast Pair history. Since there is only one public address using |
| * during Fast Pair connection, {@link #isInPairedHistory(String)} only needs to be called once, |
| * then the result is kept, and call {@link #getExistingAccountKey()} to get the result. |
| */ |
| @VisibleForTesting |
| static final class FastPairHistoryFinder { |
| |
| private @Nullable |
| byte[] mExistingAccountKey; |
| @Nullable |
| private final List<FastPairHistoryItem> mHistoryItems; |
| |
| FastPairHistoryFinder(List<FastPairHistoryItem> historyItems) { |
| this.mHistoryItems = historyItems; |
| } |
| |
| @WorkerThread |
| @VisibleForTesting |
| boolean isInPairedHistory(String publicAddress) { |
| if (mHistoryItems == null || mHistoryItems.isEmpty()) { |
| return false; |
| } |
| for (FastPairHistoryItem item : mHistoryItems) { |
| if (item.isMatched(BluetoothAddress.decode(publicAddress))) { |
| mExistingAccountKey = item.accountKey().toByteArray(); |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| // This function should be called after isInPairedHistory(). Or it will just return null. |
| @WorkerThread |
| @VisibleForTesting |
| @Nullable |
| byte[] getExistingAccountKey() { |
| return mExistingAccountKey; |
| } |
| } |
| |
| private static final class DeviceNameReceiver { |
| |
| @GuardedBy("this") |
| private @Nullable |
| byte[] mEncryptedResponse; |
| |
| @GuardedBy("this") |
| @Nullable |
| private String mDecryptedDeviceName; |
| |
| @Nullable |
| private CountDownLatch mResponseCountDown; |
| |
| DeviceNameReceiver(BluetoothGattConnection gattConnection) throws BluetoothException { |
| UUID characteristicUuid = NameCharacteristic.getId(gattConnection); |
| ChangeObserver observer = |
| gattConnection.enableNotification(FastPairService.ID, characteristicUuid); |
| observer.setListener( |
| (byte[] value) -> { |
| synchronized (DeviceNameReceiver.this) { |
| Log.i(TAG, "DeviceNameReceiver: device name response size = " |
| + value.length); |
| // We don't decrypt it here because we may not finish handshaking and |
| // the pairing |
| // secret is not available. |
| mEncryptedResponse = value; |
| } |
| // For testing to know we get the device name from provider. |
| if (mResponseCountDown != null) { |
| mResponseCountDown.countDown(); |
| Log.v(TAG, "Finish nameResponseCountDown."); |
| } |
| }); |
| } |
| |
| void setCountDown(CountDownLatch countDownLatch) { |
| this.mResponseCountDown = countDownLatch; |
| } |
| |
| synchronized @Nullable String getParsedResult(byte[] secret) { |
| if (mDecryptedDeviceName != null) { |
| return mDecryptedDeviceName; |
| } |
| if (mEncryptedResponse == null) { |
| Log.i(TAG, "DeviceNameReceiver: no device name sent from the Provider."); |
| return null; |
| } |
| try { |
| mDecryptedDeviceName = NamingEncoder.decodeNamingPacket(secret, mEncryptedResponse); |
| Log.i(TAG, "DeviceNameReceiver: decrypted provider's name from naming response, " |
| + "name = " + mDecryptedDeviceName); |
| } catch (GeneralSecurityException e) { |
| Log.w(TAG, "DeviceNameReceiver: fail to parse the NameCharacteristic from provider" |
| + ".", e); |
| return null; |
| } |
| return mDecryptedDeviceName; |
| } |
| } |
| |
| static void checkFastPairSignal( |
| FastPairSignalChecker fastPairSignalChecker, |
| String currentAddress, |
| Exception originalException) |
| throws SignalLostException, SignalRotatedException { |
| String newAddress = fastPairSignalChecker.getValidAddressForModelId(currentAddress); |
| if (TextUtils.isEmpty(newAddress)) { |
| throw new SignalLostException("Signal lost", originalException); |
| } else if (!Ascii.equalsIgnoreCase(currentAddress, newAddress)) { |
| throw new SignalRotatedException("Address rotated", newAddress, originalException); |
| } |
| } |
| |
| @VisibleForTesting |
| public Preferences getPreferences() { |
| return mPreferences; |
| } |
| } |