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 + '}';
+ }
+ }
+ }
+}