Add GattConnectionManager.

Test: unit test will be added later per b/202524672.

Bug: 200231384
Change-Id: I4d97f5f2e393c7e0a8c89fd286a70a89c9b95065
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/FastPairDualConnection.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/FastPairDualConnection.java
new file mode 100644
index 0000000..d5e2256
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/FastPairDualConnection.java
@@ -0,0 +1,136 @@
+/*
+ * 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 android.text.TextUtils;
+
+import androidx.annotation.Nullable;
+
+import com.android.server.nearby.proto.NearbyEventCodes.NearbyEvent.EventCode;
+
+import com.google.common.base.Ascii;
+
+/**
+ * Supports Fast Pair pairing with certain Bluetooth headphones, Auto, etc.
+ *
+ * <p>Based on go/fast-pair-2-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>
+ */
+public abstract class FastPairDualConnection extends FastPairConnection {
+
+    static void logRetrySuccessEvent(
+            EventCode eventCode,
+            @Nullable Exception recoverFromException,
+            EventLoggerWrapper eventLogger) {
+        if (recoverFromException == null) {
+            return;
+        }
+        eventLogger.setCurrentEvent(eventCode);
+        eventLogger.logCurrentEventFailed(recoverFromException);
+    }
+
+    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);
+        }
+    }
+}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/GattConnectionManager.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/GattConnectionManager.java
new file mode 100644
index 0000000..88936c0
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/GattConnectionManager.java
@@ -0,0 +1,270 @@
+/*
+ * 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 com.android.server.nearby.common.bluetooth.fastpair.AesEcbSingleBlockEncryption.AES_BLOCK_LENGTH;
+import static com.android.server.nearby.common.bluetooth.fastpair.BluetoothAddress.maskBluetoothAddress;
+import static com.android.server.nearby.common.bluetooth.fastpair.FastPairDualConnection.logRetrySuccessEvent;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import android.content.Context;
+import android.os.SystemClock;
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import androidx.core.util.Consumer;
+
+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.TimingLogger.ScopedTiming;
+import com.android.server.nearby.common.bluetooth.gatt.BluetoothGattConnection;
+import com.android.server.nearby.common.bluetooth.gatt.BluetoothGattHelper;
+import com.android.server.nearby.common.bluetooth.gatt.BluetoothGattHelper.ConnectionOptions;
+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.proto.FastPairEnums.FastPairEvent.ErrorCode;
+import com.android.server.nearby.proto.NearbyEventCodes.NearbyEvent.EventCode;
+
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * Manager for working with Gatt connections.
+ *
+ * <p>This helper class allows for opening and closing GATT connections to a provided address.
+ * Optionally, it can also support automatically reopening a connection in the case that it has been
+ * closed when it's next needed through {@link Preferences#getAutomaticallyReconnectGattWhenNeeded}.
+ */
+// TODO(b/202524672): Add class unit test.
+final class GattConnectionManager {
+
+    private static final String TAG = GattConnectionManager.class.getSimpleName();
+
+    private final Context mContext;
+    private final Preferences mPreferences;
+    private final EventLoggerWrapper mEventLogger;
+    private final BluetoothAdapter mBluetoothAdapter;
+    private final ToggleBluetoothTask mToggleBluetooth;
+    private final String mAddress;
+    private final TimingLogger mTimingLogger;
+    private final boolean mSetMtu;
+    @Nullable
+    private final FastPairConnection.FastPairSignalChecker mFastPairSignalChecker;
+    @Nullable
+    private BluetoothGattConnection mGattConnection;
+
+    GattConnectionManager(
+            Context context,
+            Preferences preferences,
+            EventLoggerWrapper eventLogger,
+            BluetoothAdapter bluetoothAdapter,
+            ToggleBluetoothTask toggleBluetooth,
+            String address,
+            TimingLogger timingLogger,
+            @Nullable FastPairConnection.FastPairSignalChecker fastPairSignalChecker,
+            boolean setMtu) {
+        this.mContext = context;
+        this.mPreferences = preferences;
+        this.mEventLogger = eventLogger;
+        this.mBluetoothAdapter = bluetoothAdapter;
+        this.mToggleBluetooth = toggleBluetooth;
+        this.mAddress = address;
+        this.mTimingLogger = timingLogger;
+        this.mFastPairSignalChecker = fastPairSignalChecker;
+        this.mSetMtu = setMtu;
+    }
+
+    /**
+     * Gets a gatt connection to address. If this connection does not exist, it creates one.
+     */
+    BluetoothGattConnection getConnection()
+            throws InterruptedException, ExecutionException, TimeoutException, BluetoothException {
+        if (mGattConnection == null) {
+            try {
+                mGattConnection =
+                        connect(mAddress, /* checkSignalWhenFail= */ false,
+                                /* rescueFromError= */ null);
+            } catch (SignalLostException | SignalRotatedException e) {
+                // Impossible to happen here because we didn't do signal check.
+                throw new ExecutionException("getConnection throws SignalLostException", e);
+            }
+        }
+        return mGattConnection;
+    }
+
+    BluetoothGattConnection getConnectionWithSignalLostCheck(
+            @Nullable Consumer<ErrorCode> rescueFromError)
+            throws InterruptedException, ExecutionException, TimeoutException, BluetoothException,
+            SignalLostException, SignalRotatedException {
+        if (mGattConnection == null) {
+            mGattConnection = connect(mAddress, /* checkSignalWhenFail= */ true,
+                    rescueFromError);
+        }
+        return mGattConnection;
+    }
+
+    /**
+     * Closes the gatt connection when it is open.
+     */
+    void closeConnection() throws BluetoothException {
+        if (mGattConnection != null) {
+            try (ScopedTiming scopedTiming = new ScopedTiming(mTimingLogger, "Close GATT")) {
+                mGattConnection.close();
+                mGattConnection = null;
+            }
+        }
+    }
+
+    private BluetoothGattConnection connect(
+            String address, boolean checkSignalWhenFail,
+            @Nullable Consumer<ErrorCode> rescueFromError)
+            throws InterruptedException, ExecutionException, TimeoutException, BluetoothException,
+            SignalLostException, SignalRotatedException {
+        int i = 1;
+        boolean isRecoverable = true;
+        long startElapsedRealtime = SystemClock.elapsedRealtime();
+        BluetoothException lastException = null;
+        mEventLogger.setCurrentEvent(EventCode.GATT_CONNECT);
+        while (isRecoverable) {
+            try (ScopedTiming scopedTiming =
+                    new ScopedTiming(mTimingLogger, "Connect GATT #" + i)) {
+                Log.i(TAG, "Connecting to GATT server at " + maskBluetoothAddress(address));
+                BluetoothGattConnection connection =
+                        new BluetoothGattHelper(mContext, mBluetoothAdapter)
+                                .connect(
+                                        mBluetoothAdapter.getRemoteDevice(address),
+                                        getConnectionOptions(startElapsedRealtime));
+                connection.setOperationTimeout(
+                        TimeUnit.SECONDS.toMillis(mPreferences.getGattOperationTimeoutSeconds()));
+                if (mPreferences.getAutomaticallyReconnectGattWhenNeeded()) {
+                    connection.addCloseListener(
+                            () -> {
+                                Log.i(TAG, "Gatt connection with " + maskBluetoothAddress(address)
+                                        + " closed.");
+                                mGattConnection = null;
+                            });
+                }
+                mEventLogger.logCurrentEventSucceeded();
+                if (lastException != null) {
+                    logRetrySuccessEvent(EventCode.RECOVER_BY_RETRY_GATT, lastException,
+                            mEventLogger);
+                }
+                return connection;
+            } catch (BluetoothException e) {
+                lastException = e;
+
+                boolean ableToRetry;
+                if (mPreferences.getGattConnectRetryTimeoutMillis() > 0) {
+                    ableToRetry =
+                            (SystemClock.elapsedRealtime() - startElapsedRealtime)
+                                    < mPreferences.getGattConnectRetryTimeoutMillis();
+                    Log.i(TAG, "Retry connecting GATT by timeout: " + ableToRetry);
+                } else {
+                    ableToRetry = i < mPreferences.getNumAttempts();
+                }
+
+                if (mPreferences.getRetryGattConnectionAndSecretHandshake()) {
+                    if (isNoRetryError(mPreferences, e)) {
+                        ableToRetry = false;
+                    }
+
+                    if (ableToRetry) {
+                        if (rescueFromError != null) {
+                            rescueFromError.accept(
+                                    e instanceof BluetoothOperationTimeoutException
+                                            ? ErrorCode.SUCCESS_RETRY_GATT_TIMEOUT
+                                            : ErrorCode.SUCCESS_RETRY_GATT_ERROR);
+                        }
+                        if (mFastPairSignalChecker != null && checkSignalWhenFail) {
+                            FastPairDualConnection
+                                    .checkFastPairSignal(mFastPairSignalChecker, address, e);
+                        }
+                    }
+                    isRecoverable = ableToRetry;
+                    if (ableToRetry && mPreferences.getPairingRetryDelayMs() > 0) {
+                        SystemClock.sleep(mPreferences.getPairingRetryDelayMs());
+                    }
+                } else {
+                    isRecoverable =
+                            ableToRetry
+                                    && (e instanceof BluetoothOperationTimeoutException
+                                    || e instanceof BluetoothTimeoutException
+                                    || (e instanceof BluetoothGattException
+                                    && ((BluetoothGattException) e).getGattErrorCode() == 133));
+                }
+                Log.w(TAG, "GATT connect attempt " + i + "of " + mPreferences.getNumAttempts()
+                        + " failed, " + (isRecoverable ? "recovering" : "permanently"), e);
+                if (isRecoverable) {
+                    // If we're going to retry, log failure here. If we throw, an upper level will
+                    // log it.
+                    mToggleBluetooth.toggleBluetooth();
+                    i++;
+                    mEventLogger.logCurrentEventFailed(e);
+                    mEventLogger.setCurrentEvent(EventCode.GATT_CONNECT);
+                }
+            }
+        }
+        throw checkNotNull(lastException);
+    }
+
+    static boolean isNoRetryError(Preferences preferences, BluetoothException e) {
+        return e instanceof BluetoothGattException
+                && preferences
+                .getGattConnectionAndSecretHandshakeNoRetryGattError()
+                .contains(((BluetoothGattException) e).getGattErrorCode());
+    }
+
+    @VisibleForTesting
+    long getTimeoutMs(long spentTime) {
+        long timeoutInMs;
+        if (mPreferences.getRetryGattConnectionAndSecretHandshake()) {
+            timeoutInMs =
+                    spentTime < mPreferences.getGattConnectShortTimeoutRetryMaxSpentTimeMs()
+                            ? mPreferences.getGattConnectShortTimeoutMs()
+                            : mPreferences.getGattConnectLongTimeoutMs();
+        } else {
+            timeoutInMs = TimeUnit.SECONDS.toMillis(mPreferences.getGattConnectionTimeoutSeconds());
+        }
+        return timeoutInMs;
+    }
+
+    private ConnectionOptions getConnectionOptions(long startElapsedRealtime) {
+        return createConnectionOptions(
+                mSetMtu,
+                getTimeoutMs(SystemClock.elapsedRealtime() - startElapsedRealtime));
+    }
+
+    public static ConnectionOptions createConnectionOptions(boolean setMtu, long timeoutInMs) {
+        ConnectionOptions.Builder builder = ConnectionOptions.builder();
+        if (setMtu) {
+            // There are 3 overhead bytes added to BLE packets.
+            builder.setMtu(
+                    AES_BLOCK_LENGTH + EllipticCurveDiffieHellmanExchange.PUBLIC_KEY_LENGTH + 3);
+        }
+        builder.setConnectionTimeoutMillis(timeoutInMs);
+        return builder.build();
+    }
+
+    @VisibleForTesting
+    void setGattConnection(BluetoothGattConnection gattConnection) {
+        this.mGattConnection = gattConnection;
+    }
+}