Add Unit Test for BluetoothAudioPairer.

Bug: 204780849
Test: unit test
Change-Id: I9b60eac80c5f43cfd0aa45df19bef86e1067c538
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/BluetoothAudioPairer.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/BluetoothAudioPairer.java
index 9f12e84..07306c1 100644
--- a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/BluetoothAudioPairer.java
+++ b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/BluetoothAudioPairer.java
@@ -104,6 +104,12 @@
     private final PasskeyConfirmationHandler mPasskeyConfirmationHandler;
     private final TimingLogger mTimingLogger;
 
+    private static boolean sTestMode = false;
+
+    static void enableTestMode() {
+        sTestMode = true;
+    }
+
     static class KeyBasedPairingInfo {
 
         private final byte[] mSecret;
@@ -151,22 +157,24 @@
         //
         // If that OS bug doesn't get fixed, we can flip these flags to force-reject the
         // permissions.
-        if (preferences.getRejectPhonebookAccess()
-                && !device.setPhonebookAccessPermission(BluetoothDevice.ACCESS_REJECTED)) {
+        if (preferences.getRejectPhonebookAccess() && (sTestMode ? false :
+                !device.setPhonebookAccessPermission(BluetoothDevice.ACCESS_REJECTED))) {
             throw new PairingException("Failed to deny contacts (phonebook) access.");
         }
         if (preferences.getRejectMessageAccess()
-                && !device.setMessageAccessPermission(BluetoothDevice.ACCESS_REJECTED)) {
+                && (sTestMode ? false :
+                !device.setMessageAccessPermission(BluetoothDevice.ACCESS_REJECTED))) {
             throw new PairingException("Failed to deny message access.");
         }
         if (preferences.getRejectSimAccess()
-                && !device.setSimAccessPermission(BluetoothDevice.ACCESS_REJECTED)) {
+                && (sTestMode ? false :
+                !device.setSimAccessPermission(BluetoothDevice.ACCESS_REJECTED))) {
             throw new PairingException("Failed to deny SIM access.");
         }
     }
 
     boolean isPaired() {
-        return mDevice.getBondState() == BOND_BONDED;
+        return (sTestMode ? false : mDevice.getBondState() == BOND_BONDED);
     }
 
     /**
@@ -175,7 +183,7 @@
     @WorkerThread
     void unpair()
             throws InterruptedException, ExecutionException, TimeoutException, PairingException {
-        int bondState =  mDevice.getBondState();
+        int bondState =  sTestMode ? BOND_NONE : mDevice.getBondState();
         try (UnbondedReceiver unbondedReceiver = new UnbondedReceiver();
                 ScopedTiming scopedTiming = new ScopedTiming(mTimingLogger,
                         "Unpair for state: " + bondState)) {
@@ -223,15 +231,16 @@
                 ScopedTiming scopedTiming = new ScopedTiming(mTimingLogger, "Create bond")) {
             // If the provider's initiating the bond, we do nothing but wait for broadcasts.
             if (mKeyBasedPairingInfo == null || !mKeyBasedPairingInfo.mProviderInitiatesBonding) {
-                Log.i(TAG, "createBond with " + maskBluetoothAddress(mDevice) + ", type="
+                if (!sTestMode) {
+                    Log.i(TAG, "createBond with " + maskBluetoothAddress(mDevice) + ", type="
                         + mDevice.getType());
-                if (mPreferences.getSpecifyCreateBondTransportType()) {
-                    mDevice.createBond(mPreferences.getCreateBondTransportType());
-                } else {
-                    mDevice.createBond();
+                    if (mPreferences.getSpecifyCreateBondTransportType()) {
+                        mDevice.createBond(mPreferences.getCreateBondTransportType());
+                    } else {
+                        mDevice.createBond();
+                    }
                 }
             }
-
             try {
                 bondedReceiver.await(mPreferences.getCreateBondTimeoutSeconds(), TimeUnit.SECONDS);
             } catch (TimeoutException e) {
@@ -286,19 +295,23 @@
             // Try to connect via reflection
             Log.v(TAG, "Connect to proxy=" + proxy);
 
-            if (!(Boolean) Reflect.on(proxy).withMethod("connect", BluetoothDevice.class)
-                    .get(mDevice)) {
-                // If we're already connecting, connect() may return false. :/
-                Log.w(TAG, "connect returned false, expected if connecting, state="
-                        + proxy.getConnectionState(mDevice));
+            if (!sTestMode) {
+                if (!(Boolean) Reflect.on(proxy).withMethod("connect", BluetoothDevice.class)
+                        .get(mDevice)) {
+                    // If we're already connecting, connect() may return false. :/
+                    Log.w(TAG, "connect returned false, expected if connecting, state="
+                            + proxy.getConnectionState(mDevice));
+                }
             }
 
             // If we're already connected, the OS may not send the connection state broadcast, so
             // return immediately for that case.
-            if (proxy.getConnectionState(mDevice) == BluetoothProfile.STATE_CONNECTED) {
-                Log.v(TAG, "connectByProfileProxy: already connected to device="
-                        + maskBluetoothAddress(mDevice));
-                return;
+            if (!sTestMode) {
+                if (proxy.getConnectionState(mDevice) == BluetoothProfile.STATE_CONNECTED) {
+                    Log.v(TAG, "connectByProfileProxy: already connected to device="
+                            + maskBluetoothAddress(mDevice));
+                    return;
+                }
             }
 
             try (ScopedTiming scopedTiming = new ScopedTiming(mTimingLogger, "Wait connection")) {
@@ -332,7 +345,9 @@
         public void close() {
             try (ScopedTiming scopedTiming =
                     new ScopedTiming(mTimingLogger, "Close profile: " + mProfile)) {
-                mBluetoothAdapter.closeProfileProxy(mProfile.type, mProxy);
+                if (!sTestMode) {
+                    mBluetoothAdapter.closeProfileProxy(mProfile.type, mProxy);
+                }
             }
         }
 
@@ -388,7 +403,7 @@
     /**
      * Receiver that closes after bonding has completed.
      */
-    private class BondedReceiver extends DeviceIntentReceiver {
+    class BondedReceiver extends DeviceIntentReceiver {
 
         private boolean mReceivedUuids = false;
         private boolean mReceivedPasskey = false;
@@ -470,13 +485,17 @@
                     // https://source.android.com/security/bulletin/2019-12-01#system
                     // Since we've certified the Fast Pair 1.0 devices, and user taps to pair it
                     // (with the device's image), we could help user to accept the consent.
-                    mDevice.setPairingConfirmation(true);
+                    if (!sTestMode) {
+                        mDevice.setPairingConfirmation(true);
+                    }
                     if (mPreferences.getMoreEventLogForQuality()) {
                         mEventLogger.logCurrentEventSucceeded();
                     }
                     return;
                 } else if (variant != BluetoothDevice.PAIRING_VARIANT_PASSKEY_CONFIRMATION) {
-                    mDevice.setPairingConfirmation(false);
+                    if (!sTestMode) {
+                        mDevice.setPairingConfirmation(false);
+                    }
                     if (mPreferences.getMoreEventLogForQuality()) {
                         mEventLogger.logCurrentEventFailed(
                                 new CreateBondException(
@@ -492,10 +511,14 @@
                         // Must be the simulator using FP 1.0 (no Key-based Pairing). Real
                         // headphones using FP 1.0 use Just Works instead (and maybe we should
                         // disable this flag for them).
-                        mDevice.setPairingConfirmation(true);
+                        if (!sTestMode) {
+                            mDevice.setPairingConfirmation(true);
+                        }
                     }
                     if (mPreferences.getMoreEventLogForQuality()) {
-                        mEventLogger.logCurrentEventSucceeded();
+                        if (!sTestMode) {
+                            mEventLogger.logCurrentEventSucceeded();
+                        }
                     }
                     return;
                 }
@@ -559,8 +582,8 @@
                                                         encryptedRemotePasskey);
                                     }
 
-                                    // We log success if we made it through with no exceptions. If
-                                    // the passkey was wrong, pairing will fail and we'll log
+                                    // We log success if we made it through with no exceptions.
+                                    // If the passkey was wrong, pairing will fail and we'll log
                                     // BOND_BROKEN with reason = AUTH_FAILED.
                                     mEventLogger.logCurrentEventSucceeded();
 
@@ -572,8 +595,8 @@
                                                 + ", remote=" + remotePasskey);
                                     }
 
-                                    // Don't estimate the {@code ScopedTiming} because the passkey
-                                    // confirmation is done by UI.
+                                    // Don't estimate the {@code ScopedTiming} because the
+                                    // passkey confirmation is done by UI.
                                     if (isPasskeyCorrect
                                             && mPreferences.getHandlePasskeyConfirmationByUi()
                                             && mPasskeyConfirmationHandler != null) {
@@ -639,8 +662,10 @@
 
             Log.i(TAG, "triggerDiscoverStateChange call startDiscovery.");
             // Uses startDiscovery to trigger Settings show pairing dialog instead of notification.
-            bluetoothAdapter.startDiscovery();
-            bluetoothAdapter.cancelDiscovery();
+            if (!sTestMode) {
+                bluetoothAdapter.startDiscovery();
+                bluetoothAdapter.cancelDiscovery();
+            }
             try {
                 receiver.await(DISCOVERY_STATE_CHANGE_TIMEOUT_MS, TimeUnit.MILLISECONDS);
             } catch (InterruptedException | ExecutionException | TimeoutException e) {
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/BluetoothAudioPairerTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/BluetoothAudioPairerTest.java
new file mode 100644
index 0000000..0a56f2f
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/BluetoothAudioPairerTest.java
@@ -0,0 +1,198 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.bluetooth.fastpair;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.MockitoAnnotations.initMocks;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.content.Context;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.filters.SdkSuppress;
+import androidx.test.filters.SmallTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.google.common.collect.Iterables;
+
+import junit.framework.TestCase;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeoutException;
+import java.util.stream.Collectors;
+
+/** Unit tests for {@link BluetoothAudioPairer}. */
+@Presubmit
+@SmallTest
+public class BluetoothAudioPairerTest extends TestCase {
+
+    private static final byte[] SECRET = new byte[]{3, 0};
+    private static final boolean PRIVATE_INITIAL_PAIRING = false;
+    private static final String EVENT_NAME = "EVENT_NAME";
+    private static final BluetoothDevice BLUETOOTH_DEVICE = BluetoothAdapter.getDefaultAdapter()
+            .getRemoteDevice("11:22:33:44:55:66");
+    private static final int BOND_TIMEOUT_SECONDS = 1;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+
+        initMocks(this);
+        BluetoothAudioPairer.enableTestMode();
+    }
+
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testKeyBasedPairingInfoConstructor() {
+        assertThat(new BluetoothAudioPairer.KeyBasedPairingInfo(
+                SECRET,
+                null /* GattConnectionManager */,
+                PRIVATE_INITIAL_PAIRING)).isNotNull();
+    }
+
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testBluetoothAudioPairerConstructor() {
+        Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
+        try {
+            assertThat(new BluetoothAudioPairer(
+                    context,
+                    BLUETOOTH_DEVICE,
+                    Preferences.builder().build(),
+                    new EventLoggerWrapper(new TestEventLogger()),
+                    null /* KeyBasePairingInfo */,
+                    null /*PasskeyConfirmationHandler */,
+                    new TimingLogger(EVENT_NAME, Preferences.builder().build()))).isNotNull();
+        } catch (PairingException e) {
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testBluetoothAudioPairerUnpairNoCrash() {
+        Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
+        try {
+            new BluetoothAudioPairer(
+                    context,
+                    BLUETOOTH_DEVICE,
+                    Preferences.builder().build(),
+                    new EventLoggerWrapper(new TestEventLogger()),
+                    null /* KeyBasePairingInfo */,
+                    null /*PasskeyConfirmationHandler */,
+                    new TimingLogger(EVENT_NAME, Preferences.builder().build())).unpair();
+        } catch (PairingException | InterruptedException | ExecutionException
+                | TimeoutException e) {
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testBluetoothAudioPairerPairNoCrash() {
+        Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
+        try {
+            new BluetoothAudioPairer(
+                    context,
+                    BLUETOOTH_DEVICE,
+                    Preferences.builder().setCreateBondTimeoutSeconds(BOND_TIMEOUT_SECONDS).build(),
+                    new EventLoggerWrapper(new TestEventLogger()),
+                    null /* KeyBasePairingInfo */,
+                    null /*PasskeyConfirmationHandler */,
+                    new TimingLogger(EVENT_NAME, Preferences.builder().build())).pair();
+        } catch (PairingException | InterruptedException | ExecutionException
+                | TimeoutException e) {
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testBluetoothAudioPairerConnectNoCrash() {
+        Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
+        try {
+            new BluetoothAudioPairer(
+                    context,
+                    BLUETOOTH_DEVICE,
+                    Preferences.builder().setCreateBondTimeoutSeconds(BOND_TIMEOUT_SECONDS).build(),
+                    new EventLoggerWrapper(new TestEventLogger()),
+                    null /* KeyBasePairingInfo */,
+                    null /*PasskeyConfirmationHandler */,
+                    new TimingLogger(EVENT_NAME, Preferences.builder().build()))
+                    .connect(Constants.A2DP_SINK_SERVICE_UUID, true /* enable pairing behavior */);
+        } catch (PairingException | InterruptedException | ExecutionException
+                | TimeoutException | ReflectionException e) {
+        }
+    }
+
+    static class TestEventLogger implements EventLogger {
+
+        private List<Item> mLogs = new ArrayList<>();
+
+        @Override
+        public void logEventSucceeded(Event event) {
+            mLogs.add(new Item(event));
+        }
+
+        @Override
+        public void logEventFailed(Event event, Exception e) {
+            mLogs.add(new ItemFailed(event, e));
+        }
+
+        List<Item> getErrorLogs() {
+            return mLogs.stream().filter(item -> item instanceof ItemFailed)
+                    .collect(Collectors.toList());
+        }
+
+        List<Item> getLogs() {
+            return mLogs;
+        }
+
+        List<Item> getLast() {
+            return mLogs.subList(mLogs.size() - 1, mLogs.size());
+        }
+
+        BluetoothDevice getDevice() {
+            return Iterables.getLast(mLogs).mEvent.getBluetoothDevice();
+        }
+
+        public static class Item {
+
+            final Event mEvent;
+
+            Item(Event event) {
+                this.mEvent = event;
+            }
+
+            @Override
+            public String toString() {
+                return "Item{" + "event=" + mEvent + '}';
+            }
+        }
+
+        public static class ItemFailed extends Item {
+
+            final Exception mException;
+
+            ItemFailed(Event event, Exception e) {
+                super(event);
+                this.mException = e;
+            }
+
+            @Override
+            public String toString() {
+                return "ItemFailed{" + "event=" + mEvent + ", exception=" + mException + '}';
+            }
+        }
+    }
+}