Added target preparer to enable bluetooth pairing

Cherry Picked from aosp/1468215

Test: Tested locally
Bug: 169893695
Merged-In: I717512770ff7c128962a093a75e979876f76ec1a
Change-Id: Ide6726016bde298ff366447d8bc72e77cc071ad2
diff --git a/src/com/android/tradefed/targetprep/multi/PairingMultiTargetPreparer.java b/src/com/android/tradefed/targetprep/multi/PairingMultiTargetPreparer.java
new file mode 100644
index 0000000..aa2836a
--- /dev/null
+++ b/src/com/android/tradefed/targetprep/multi/PairingMultiTargetPreparer.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2020 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.tradefed.targetprep.multi;
+
+import com.android.loganalysis.util.config.OptionClass;
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.config.Option;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.invoker.IInvocationContext;
+import com.android.tradefed.targetprep.BuildError;
+import com.android.tradefed.targetprep.TargetSetupError;
+import com.android.tradefed.util.Sl4aBluetoothUtil;
+import com.android.tradefed.util.Sl4aBluetoothUtil.BluetoothAccessLevel;
+import com.android.tradefed.util.Sl4aBluetoothUtil.BluetoothPriorityLevel;
+import com.android.tradefed.util.Sl4aBluetoothUtil.BluetoothProfile;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/** A multi-target preparer helps make Bluetooth pairing (and connection) between two devices. */
+@OptionClass(alias = "bluetooth-multi-target-pairing")
+public class PairingMultiTargetPreparer extends BaseMultiTargetPreparer {
+
+    @Option(
+            name = "bt-connection-primary-device",
+            description = "The target name of the primary device during BT pairing.",
+            mandatory = true)
+    private String mPrimaryDeviceName;
+
+    @Option(
+            name = "with-connection",
+            description =
+                    "Connect the profiles once the devices are paired."
+                            + " If true, given bluetooth profiles will be connected."
+                            + " If false, the connection status is non-deterministic."
+                            + " Devices will be paired but connection will not be done explicitly."
+                            + " The connection status depends on the type of device,"
+                            + " i.e. some devices will automatically connect, but some won't.")
+    private boolean mConnectDevices = true;
+
+    @Option(
+            name = "bt-profile",
+            description =
+                    "A set of Bluetooth profiles that will be connected if connection is needed."
+                            + " They should be specified as Bluetooth profile name defined in"
+                            + " android.bluetooth.BluetoothProfile")
+    private Set<BluetoothProfile> mProfiles = new HashSet<>();
+
+    @VisibleForTesting
+    void setBluetoothUtil(Sl4aBluetoothUtil util) {
+        mUtil = util;
+    }
+
+    private ITestDevice mPrimaryDevice;
+    private ITestDevice mCompanionDevice;
+    private Sl4aBluetoothUtil mUtil = new Sl4aBluetoothUtil();
+
+    @Override
+    public void setUp(IInvocationContext context)
+            throws TargetSetupError, BuildError, DeviceNotAvailableException {
+        setDeviceInfos(context.getDeviceBuildMap());
+        try {
+            if (!mUtil.enable(mPrimaryDevice)) {
+                throw new TargetSetupError(
+                        "Failed to enable Bluetooth", mPrimaryDevice.getDeviceDescriptor());
+            }
+            if (!mUtil.enable(mCompanionDevice)) {
+                throw new TargetSetupError(
+                        "Failed to enable Bluetooth", mCompanionDevice.getDeviceDescriptor());
+            }
+            if (!mUtil.pair(mPrimaryDevice, mCompanionDevice)) {
+                throw new TargetSetupError(
+                        "Bluetooth pairing failed.", mPrimaryDevice.getDeviceDescriptor());
+            }
+            // Always enable PBAP between primary and companion devices in case it's not enabled
+            // For now, assume PBAP client profile is always on primary device, and enable PBAP on
+            // companion device.
+            if (!mUtil.changeProfileAccessPermission(
+                    mCompanionDevice,
+                    mPrimaryDevice,
+                    BluetoothProfile.PBAP,
+                    BluetoothAccessLevel.ACCESS_ALLOWED)) {
+                throw new TargetSetupError(
+                        "Failed to allow PBAP access", mCompanionDevice.getDeviceDescriptor());
+            }
+            if (!mUtil.setProfilePriority(
+                    mPrimaryDevice,
+                    mCompanionDevice,
+                    Collections.singleton(BluetoothProfile.PBAP_CLIENT),
+                    BluetoothPriorityLevel.PRIORITY_ON)) {
+                throw new TargetSetupError(
+                        "Failed to turn on PBAP client priority",
+                        mPrimaryDevice.getDeviceDescriptor());
+            }
+            if (mConnectDevices && mProfiles.size() > 0) {
+                if (!mUtil.connect(mPrimaryDevice, mCompanionDevice, mProfiles)) {
+                    throw new TargetSetupError(
+                            "Failed to connect bluetooth profiles",
+                            mPrimaryDevice.getDeviceDescriptor());
+                }
+            }
+        } finally {
+            mUtil.stopSl4a();
+        }
+    }
+
+    private void setDeviceInfos(Map<ITestDevice, IBuildInfo> deviceInfos) throws TargetSetupError {
+        List<ITestDevice> devices = new ArrayList<>(deviceInfos.keySet());
+        if (devices.size() != 2) {
+            throw new TargetSetupError(
+                    "The preparer assumes 2 devices only", devices.get(0).getDeviceDescriptor());
+        }
+        try {
+            int primaryIdx = mPrimaryDeviceName.equals(devices.get(0).getProductType()) ? 0 : 1;
+            mPrimaryDevice = devices.get(primaryIdx);
+            mCompanionDevice = devices.get(1 - primaryIdx);
+        } catch (DeviceNotAvailableException e) {
+            throw new TargetSetupError(
+                    "Device is not available", e, devices.get(0).getDeviceDescriptor());
+        }
+    }
+}
diff --git a/tests/src/com/android/tradefed/UnitTests.java b/tests/src/com/android/tradefed/UnitTests.java
index 832cc95..6df25d7 100644
--- a/tests/src/com/android/tradefed/UnitTests.java
+++ b/tests/src/com/android/tradefed/UnitTests.java
@@ -256,6 +256,7 @@
 import com.android.tradefed.targetprep.app.NoApkTestSkipperTest;
 import com.android.tradefed.targetprep.multi.MergeMultiBuildTargetPreparerTest;
 import com.android.tradefed.targetprep.multi.MixImageZipPreparerTest;
+import com.android.tradefed.targetprep.multi.PairingMultiTargetPreparerTest;
 import com.android.tradefed.targetprep.suite.SuiteApkInstallerTest;
 import com.android.tradefed.testtype.AndroidJUnitTestTest;
 import com.android.tradefed.testtype.ArtRunTestTest;
@@ -725,6 +726,7 @@
     // targetprep.multi
     MergeMultiBuildTargetPreparerTest.class,
     MixImageZipPreparerTest.class,
+    PairingMultiTargetPreparerTest.class,
 
     // targetprep.suite
     SuiteApkInstallerTest.class,
diff --git a/tests/src/com/android/tradefed/targetprep/multi/PairingMultiTargetPreparerTest.java b/tests/src/com/android/tradefed/targetprep/multi/PairingMultiTargetPreparerTest.java
new file mode 100644
index 0000000..9878ffe
--- /dev/null
+++ b/tests/src/com/android/tradefed/targetprep/multi/PairingMultiTargetPreparerTest.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2020 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.tradefed.targetprep.multi;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anySet;
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.mockito.MockitoAnnotations.initMocks;
+
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.config.ConfigurationException;
+import com.android.tradefed.config.OptionSetter;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.invoker.IInvocationContext;
+import com.android.tradefed.util.Sl4aBluetoothUtil;
+import com.android.tradefed.util.Sl4aBluetoothUtil.BluetoothProfile;
+import com.android.tradefed.util.Sl4aBluetoothUtil.BluetoothAccessLevel;
+import com.android.tradefed.util.Sl4aBluetoothUtil.BluetoothPriorityLevel;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+/** Unit tests for {@link PairingMultiTargetPreparer}. */
+@RunWith(JUnit4.class)
+public class PairingMultiTargetPreparerTest {
+
+    private static final String PRIMARY_DEVICE_NAME = "primary";
+    private static final String SECONDARY_DEVICE_NAME = "secondary";
+
+    @Mock private IInvocationContext mContext;
+    @Mock private ITestDevice mPrimary;
+    @Mock private IBuildInfo mPrimaryBuild;
+    @Mock private ITestDevice mSecondary;
+    @Mock private IBuildInfo mSecondaryBuild;
+    @Mock private Sl4aBluetoothUtil mUtil;
+    private PairingMultiTargetPreparer mPreparer;
+
+    @Before
+    public void setUp() throws Exception {
+        initMocks(this);
+        when(mPrimary.getProductType()).thenReturn(PRIMARY_DEVICE_NAME);
+        when(mSecondary.getProductType()).thenReturn(SECONDARY_DEVICE_NAME);
+        Map<ITestDevice, IBuildInfo> deviceBuildMap = new HashMap<>();
+        deviceBuildMap.put(mPrimary, mPrimaryBuild);
+        deviceBuildMap.put(mSecondary, mSecondaryBuild);
+        when(mContext.getDeviceBuildMap()).thenReturn(deviceBuildMap);
+
+        when(mUtil.enable(any(ITestDevice.class))).thenReturn(true);
+        when(mUtil.pair(any(ITestDevice.class), any(ITestDevice.class))).thenReturn(true);
+        when(mUtil.changeProfileAccessPermission(
+                        any(ITestDevice.class),
+                        any(ITestDevice.class),
+                        any(BluetoothProfile.class),
+                        any(BluetoothAccessLevel.class)))
+                .thenReturn(true);
+        when(mUtil.setProfilePriority(
+                        any(ITestDevice.class),
+                        any(ITestDevice.class),
+                        anySet(),
+                        any(BluetoothPriorityLevel.class)))
+                .thenReturn(true);
+
+        mPreparer = new PairingMultiTargetPreparer();
+        mPreparer.setBluetoothUtil(mUtil);
+
+        OptionSetter setter = new OptionSetter(mPreparer);
+        setter.setOptionValue("bt-connection-primary-device", PRIMARY_DEVICE_NAME);
+    }
+
+    @Test
+    public void testPairWithConnection() throws Exception {
+        setBluetoothProfiles();
+        when(mUtil.connect(any(ITestDevice.class), any(ITestDevice.class), anySet()))
+                .thenReturn(true);
+        mPreparer.setUp(mContext);
+        verify(mUtil).enable(mPrimary);
+        verify(mUtil).enable(mSecondary);
+        verify(mUtil).pair(mPrimary, mSecondary);
+        verify(mUtil)
+                .connect(
+                        eq(mPrimary),
+                        eq(mSecondary),
+                        argThat((Set<BluetoothProfile> profiles) -> profiles.size() == 3));
+        verify(mUtil).stopSl4a();
+    }
+
+    @Test
+    public void testPairWithoutConnection() throws Exception {
+        OptionSetter setter = new OptionSetter(mPreparer);
+        setter.setOptionValue("with-connection", "false");
+        setBluetoothProfiles();
+        mPreparer.setUp(mContext);
+        verify(mUtil).enable(mPrimary);
+        verify(mUtil).enable(mSecondary);
+        verify(mUtil).pair(mPrimary, mSecondary);
+        verify(mUtil, never()).connect(any(ITestDevice.class), any(ITestDevice.class), anySet());
+        verify(mUtil).stopSl4a();
+    }
+
+    @Test
+    public void testPairWithEmptyProfiles() throws Exception {
+        mPreparer.setUp(mContext);
+        verify(mUtil).enable(mPrimary);
+        verify(mUtil).enable(mSecondary);
+        verify(mUtil).pair(mPrimary, mSecondary);
+        verify(mUtil, never()).connect(any(ITestDevice.class), any(ITestDevice.class), anySet());
+        verify(mUtil).stopSl4a();
+    }
+
+    private void setBluetoothProfiles() throws ConfigurationException {
+        OptionSetter setter = new OptionSetter(mPreparer);
+        setter.setOptionValue("bt-connection-primary-device", PRIMARY_DEVICE_NAME);
+        setter.setOptionValue("bt-profile", BluetoothProfile.A2DP.name());
+        setter.setOptionValue("bt-profile", BluetoothProfile.HEADSET.name());
+        setter.setOptionValue("bt-profile", BluetoothProfile.MAP.name());
+    }
+}