Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit e52e3998 authored by Jack He's avatar Jack He
Browse files

HidHostDualModeTest: Stablize the test

* Added EnableBluetoothRule to toggle Bluetooth at each test, adding
  about 2 second delay to each test, but it makes the test much more
  stable
* Added device discovery as part of the test to make sure inquiry and
  LE advertisement are received before pairing
* Some intents are ordered, but some are not, make sure they are matched
  correctly

Bug: 361800494
Test: atest -v HidHostDualModeTest
Flag: EXEMPT TEST_ONLY
Change-Id: I519c4eb99bfa311f23c597ee67db2899288cdbe7
parent ecbd9379
Loading
Loading
Loading
Loading
+184 −76
Original line number Diff line number Diff line
@@ -16,11 +16,21 @@

package android.bluetooth.hid;

import static android.bluetooth.BluetoothDevice.TRANSPORT_BREDR;
import static android.bluetooth.BluetoothDevice.TRANSPORT_LE;
import static android.bluetooth.BluetoothProfile.STATE_CONNECTED;
import static android.bluetooth.BluetoothProfile.STATE_CONNECTING;
import static android.bluetooth.BluetoothProfile.STATE_DISCONNECTED;
import static android.bluetooth.BluetoothProfile.STATE_DISCONNECTING;

import static androidx.test.espresso.intent.matcher.IntentMatchers.hasAction;
import static androidx.test.espresso.intent.matcher.IntentMatchers.hasExtra;

import static com.google.common.truth.Truth.assertThat;

import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.oneOf;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
@@ -39,11 +49,13 @@ import android.bluetooth.BluetoothManager;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothUuid;
import android.bluetooth.PandoraDevice;
import android.bluetooth.test_utils.EnableBluetoothRule;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.ParcelUuid;
import android.os.Parcelable;
import android.platform.test.annotations.RequiresFlagsEnabled;
import android.platform.test.flag.junit.CheckFlagsRule;
import android.platform.test.flag.junit.DeviceFlagsValueProvider;
@@ -55,6 +67,7 @@ import androidx.test.platform.app.InstrumentationRegistry;
import com.android.bluetooth.flags.Flags;
import com.android.compatibility.common.util.AdoptShellPermissionsRule;

import org.hamcrest.CustomTypeSafeMatcher;
import org.hamcrest.Matcher;
import org.hamcrest.Matchers;
import org.hamcrest.core.AllOf;
@@ -77,9 +90,11 @@ import java.time.Duration;
import java.util.Arrays;

/** Test cases for {@link BluetoothHidHost}. */
@SuppressLint("MissingPermission")
@RunWith(AndroidJUnit4.class)
public class HidHostDualModeTest {
    private static final String TAG = "HidHostDualModeTest";
    private static final String TAG = HidHostDualModeTest.class.getSimpleName();
    private static final String BUMBLE_DEVICE_NAME = "Bumble";
    private static final Duration INTENT_TIMEOUT = Duration.ofSeconds(10);
    private static final int KEYBD_RPT_ID = 1;
    private static final int KEYBD_RPT_SIZE = 9;
@@ -101,19 +116,23 @@ public class HidHostDualModeTest {
    @Rule(order = 2)
    public final PandoraDevice mBumble = new PandoraDevice();

    @Rule(order = 3)
    public final EnableBluetoothRule enableBluetoothRule = new EnableBluetoothRule(false, true);

    @Mock private BroadcastReceiver mReceiver;
    private InOrder mInOrder = null;
    private byte[] mReportData = {};

    @Mock private BluetoothProfile.ServiceListener mProfileServiceListener;

    @SuppressLint("MissingPermission")
    private final Answer<Void> mIntentHandler =
            inv -> {
                Log.i(TAG, "onReceive(): intent=" + Arrays.toString(inv.getArguments()));
                Intent intent = inv.getArgument(1);
                String action = intent.getAction();
                if (BluetoothHidHost.ACTION_CONNECTION_STATE_CHANGED.equals(action)) {
                if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED.equals(action)) {
                    Log.d(TAG, "onReceive(): discovery finished");
                } else if (BluetoothHidHost.ACTION_CONNECTION_STATE_CHANGED.equals(action)) {
                    BluetoothDevice device =
                            intent.getParcelableExtra(
                                    BluetoothDevice.EXTRA_DEVICE, BluetoothDevice.class);
@@ -163,12 +182,30 @@ public class HidHostDualModeTest {
                    BluetoothDevice device =
                            intent.getParcelableExtra(
                                    BluetoothDevice.EXTRA_DEVICE, BluetoothDevice.class);
                    ParcelUuid[] uuids =
                    Parcelable[] uuidsRaw =
                            intent.getParcelableArrayExtra(
                                    BluetoothDevice.EXTRA_UUID, ParcelUuid.class);
                    if (uuidsRaw == null) {
                        Log.e(TAG, "onReceive(): device " + device + " null uuid list");
                    } else if (uuidsRaw.length == 0) {
                        Log.e(TAG, "onReceive(): device " + device + " 0 length uuid list");
                    } else {
                        ParcelUuid[] uuids =
                                Arrays.copyOf(uuidsRaw, uuidsRaw.length, ParcelUuid[].class);
                        Log.d(
                                TAG,
                            "onReceive(): device " + device + ", UUID=" + Arrays.toString(uuids));
                                "onReceive(): device "
                                        + device
                                        + ", UUID="
                                        + Arrays.toString(uuids));
                    }
                } else if (BluetoothDevice.ACTION_FOUND.equals(action)) {
                    BluetoothDevice device =
                            intent.getParcelableExtra(
                                    BluetoothDevice.EXTRA_DEVICE, BluetoothDevice.class);
                    String deviceName =
                            String.valueOf(intent.getStringExtra(BluetoothDevice.EXTRA_NAME));
                    Log.i(TAG, "Discovered device: " + device + " with name: " + deviceName);
                } else if (BluetoothHidHost.ACTION_PROTOCOL_MODE_CHANGED.equals(action)) {
                    BluetoothDevice device =
                            intent.getParcelableExtra(
@@ -206,7 +243,6 @@ public class HidHostDualModeTest {
                return null;
            };

    @SuppressLint("MissingPermission")
    @Before
    public void setUp() throws Exception {
        MockitoAnnotations.initMocks(this);
@@ -216,10 +252,12 @@ public class HidHostDualModeTest {
        mInOrder = inOrder(mReceiver);

        final IntentFilter filter = new IntentFilter();
        filter.addAction(BluetoothHidHost.ACTION_CONNECTION_STATE_CHANGED);
        filter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED);
        filter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
        filter.addAction(BluetoothDevice.ACTION_PAIRING_REQUEST);
        filter.addAction(BluetoothDevice.ACTION_UUID);
        filter.addAction(BluetoothDevice.ACTION_FOUND);
        filter.addAction(BluetoothHidHost.ACTION_CONNECTION_STATE_CHANGED);
        filter.addAction(BluetoothHidHost.ACTION_PROTOCOL_MODE_CHANGED);
        filter.addAction(BluetoothHidHost.ACTION_HANDSHAKE);
        filter.addAction(BluetoothHidHost.ACTION_REPORT);
@@ -245,7 +283,11 @@ public class HidHostDualModeTest {

        mDevice = mBumble.getRemoteDevice();

        assertThat(mDevice.createBond()).isTrue();
        // Remove bond if the device is already bonded
        if (mDevice.getBondState() == BluetoothDevice.BOND_BONDED) {
            removeBond(mDevice);
        }
        assertThat(mDevice.createBond(TRANSPORT_BREDR)).isTrue();
        verifyIntentReceived(
                hasAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED),
                hasExtra(BluetoothDevice.EXTRA_DEVICE, mDevice),
@@ -253,8 +295,6 @@ public class HidHostDualModeTest {
        verifyIntentReceived(
                hasAction(BluetoothDevice.ACTION_PAIRING_REQUEST),
                hasExtra(BluetoothDevice.EXTRA_DEVICE, mDevice));
        verifyIntentReceived(hasAction(BluetoothDevice.ACTION_UUID));
        // Two ACTION_UUIDs are returned after pairing with dual mode HID device
        verifyIntentReceived(
                hasAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED),
                hasExtra(BluetoothDevice.EXTRA_DEVICE, mDevice),
@@ -274,51 +314,48 @@ public class HidHostDualModeTest {
                    .isTrue();
        }

        verifyIntentReceived(
                hasAction(BluetoothHidHost.ACTION_CONNECTION_STATE_CHANGED),
                hasExtra(BluetoothDevice.EXTRA_DEVICE, mDevice),
                hasExtra(BluetoothDevice.EXTRA_TRANSPORT, BluetoothDevice.TRANSPORT_BREDR),
                hasExtra(BluetoothProfile.EXTRA_STATE, BluetoothProfile.STATE_CONNECTING));
        verifyIntentReceived(
                hasAction(BluetoothHidHost.ACTION_CONNECTION_STATE_CHANGED),
                hasExtra(BluetoothDevice.EXTRA_DEVICE, mDevice),
                hasExtra(BluetoothDevice.EXTRA_TRANSPORT, BluetoothDevice.TRANSPORT_BREDR),
                hasExtra(BluetoothProfile.EXTRA_STATE, BluetoothProfile.STATE_CONNECTED));
        // 2nd ACTION_UUID and ACTION_CONNECTION_STATE_CHANGED has a race condition, hence unordered
        verifyIntentReceivedUnordered(
        // Have to use Hamcrest matchers instead of Mockito matchers in MockitoHamcrest context
        verifyConnectionState(
                mDevice, oneOf(TRANSPORT_BREDR, TRANSPORT_LE), equalTo(STATE_CONNECTING));
        verifyConnectionState(
                mDevice, oneOf(TRANSPORT_BREDR, TRANSPORT_LE), equalTo(STATE_CONNECTED));
        // Two ACTION_UUIDs are returned after pairing with dual mode HID device
        // 2nd ACTION_UUID and ACTION_CONNECTION_STATE_CHANGED has race condition, hence unordered
        verifyIntentReceivedUnorderedAtLeast(
                1,
                hasAction(BluetoothDevice.ACTION_UUID),
                hasExtra(BluetoothDevice.EXTRA_DEVICE, mDevice),
                hasExtra(BluetoothDevice.EXTRA_UUID, Matchers.hasItemInArray(BluetoothUuid.HOGP)));

        assertThat(mHidService.getPreferredTransport(mDevice))
                .isEqualTo(BluetoothDevice.TRANSPORT_BREDR);
                hasExtra(BluetoothDevice.EXTRA_UUID,
                        allOf(Matchers.hasItemInArray(BluetoothUuid.HOGP),
                                Matchers.hasItemInArray(BluetoothUuid.HID))));

        // Cannot guarantee TRANSPORT_BREDR at this moment, hence we need to check
        if (mHidService.getPreferredTransport(mDevice) == TRANSPORT_BREDR) {
            // Switch to LE transport to prepare for test cases
        mHidService.setPreferredTransport(mDevice, BluetoothDevice.TRANSPORT_LE);
        // Verifies BR/EDR transport is disconnected
        verifyIntentReceived(
                hasAction(BluetoothHidHost.ACTION_CONNECTION_STATE_CHANGED),
                hasExtra(BluetoothDevice.EXTRA_TRANSPORT, BluetoothDevice.TRANSPORT_BREDR),
                hasExtra(BluetoothProfile.EXTRA_STATE, BluetoothProfile.STATE_DISCONNECTED));
        // Verifies LE transport is connected
        verifyIntentReceived(
                hasAction(BluetoothHidHost.ACTION_CONNECTION_STATE_CHANGED),
                hasExtra(BluetoothDevice.EXTRA_TRANSPORT, BluetoothDevice.TRANSPORT_LE),
                hasExtra(BluetoothProfile.EXTRA_STATE, BluetoothProfile.STATE_CONNECTED));
            mHidService.setPreferredTransport(mDevice, TRANSPORT_LE);
            verifyTransportSwitch(mDevice, TRANSPORT_BREDR, TRANSPORT_LE);
        }

        assertThat(mHidService.getPreferredTransport(mDevice))
                .isEqualTo(BluetoothDevice.TRANSPORT_LE);
        assertThat(mHidService.getPreferredTransport(mDevice)).isEqualTo(TRANSPORT_LE);
    }

    @SuppressLint("MissingPermission")
    @After
    public void tearDown() throws Exception {
        if (mDevice.getBondState() == BluetoothDevice.BOND_BONDED) {
            mDevice.removeBond();
            verifyIntentReceived(
                    hasAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED),
                    hasExtra(BluetoothDevice.EXTRA_DEVICE, mDevice),
                    hasExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.BOND_NONE));
            // Restore transport to BR/EDR
            if (mHidService.getPreferredTransport(mDevice) == TRANSPORT_LE) {
                boolean connected = mHidService.getConnectedDevices().contains(mDevice);
                mHidService.setPreferredTransport(mDevice, TRANSPORT_BREDR);
                if (connected) {
                    verifyTransportSwitch(mDevice, TRANSPORT_LE, TRANSPORT_BREDR);
                } else {
                    verifyConnectionState(
                            mDevice, equalTo(TRANSPORT_BREDR), equalTo(STATE_CONNECTING));
                    verifyConnectionState(
                            mDevice, equalTo(TRANSPORT_BREDR), equalTo(STATE_CONNECTED));
                }
            }
            removeBond(mDevice);
        }
        mContext.unregisterReceiver(mReceiver);
    }
@@ -332,7 +369,6 @@ public class HidHostDualModeTest {
     *   <li>3. Android switch the transport to BR/EDR and Verifies the transport
     * </ol>
     */
    @SuppressLint("MissingPermission")
    @Test
    @RequiresFlagsEnabled({
        Flags.FLAG_ALLOW_SWITCHING_HID_AND_HOGP,
@@ -340,20 +376,10 @@ public class HidHostDualModeTest {
    })
    public void setPreferredTransportTest() {
        // BR/EDR transport
        mHidService.setPreferredTransport(mDevice, BluetoothDevice.TRANSPORT_BREDR);
        // Verifies LE transport disconnected
        verifyIntentReceived(
                hasAction(BluetoothHidHost.ACTION_CONNECTION_STATE_CHANGED),
                hasExtra(BluetoothDevice.EXTRA_TRANSPORT, BluetoothDevice.TRANSPORT_LE),
                hasExtra(BluetoothProfile.EXTRA_STATE, BluetoothProfile.STATE_DISCONNECTED));
        // Verifies BR/EDR transport is connected
        verifyIntentReceived(
                hasAction(BluetoothHidHost.ACTION_CONNECTION_STATE_CHANGED),
                hasExtra(BluetoothDevice.EXTRA_TRANSPORT, BluetoothDevice.TRANSPORT_BREDR),
                hasExtra(BluetoothProfile.EXTRA_STATE, BluetoothProfile.STATE_CONNECTED));
        mHidService.setPreferredTransport(mDevice, TRANSPORT_BREDR);
        verifyTransportSwitch(mDevice, TRANSPORT_LE, TRANSPORT_BREDR);
        // Check if the API returns the correct transport
        assertThat(mHidService.getPreferredTransport(mDevice))
                .isEqualTo(BluetoothDevice.TRANSPORT_BREDR);
        assertThat(mHidService.getPreferredTransport(mDevice)).isEqualTo(TRANSPORT_BREDR);
    }

    /**
@@ -364,7 +390,6 @@ public class HidHostDualModeTest {
     *   <li>2. Android get report and verifies the report
     * </ol>
     */
    @SuppressLint("MissingPermission")
    @Test
    @RequiresFlagsEnabled({
        Flags.FLAG_ALLOW_SWITCHING_HID_AND_HOGP,
@@ -402,7 +427,6 @@ public class HidHostDualModeTest {
     *   <li>2. Android Gets the Protocol mode and verifies the mode
     * </ol>
     */
    @SuppressLint("MissingPermission")
    @Test
    @RequiresFlagsEnabled({
        Flags.FLAG_ALLOW_SWITCHING_HID_AND_HOGP,
@@ -425,7 +449,6 @@ public class HidHostDualModeTest {
     *   <li>2. Android Sets the Protocol mode and verifies the mode
     * </ol>
     */
    @SuppressLint("MissingPermission")
    @Test
    @RequiresFlagsEnabled({
        Flags.FLAG_ALLOW_SWITCHING_HID_AND_HOGP,
@@ -433,9 +456,11 @@ public class HidHostDualModeTest {
    })
    public void hogpSetProtocolModeTest() throws Exception {
        mHidService.setProtocolMode(mDevice, BluetoothHidHost.PROTOCOL_BOOT_MODE);
        // Must cast ERROR_RSP_SUCCESS, otherwise, it won't match with the int extra
        verifyIntentReceived(
                hasAction(BluetoothHidHost.ACTION_HANDSHAKE),
                hasExtra(BluetoothHidHost.EXTRA_STATUS, BluetoothHidDevice.ERROR_RSP_SUCCESS));
                hasExtra(
                        BluetoothHidHost.EXTRA_STATUS, (int) BluetoothHidDevice.ERROR_RSP_SUCCESS));
    }

    /**
@@ -454,14 +479,18 @@ public class HidHostDualModeTest {
    public void hogpSetReportTest() throws Exception {
        // Keyboard report
        mHidService.setReport(mDevice, BluetoothHidHost.REPORT_TYPE_INPUT, "010203040506070809");
        /// Must cast ERROR_RSP_SUCCESS, otherwise, it won't match with the int extra
        verifyIntentReceived(
                hasAction(BluetoothHidHost.ACTION_HANDSHAKE),
                hasExtra(BluetoothHidHost.EXTRA_STATUS, BluetoothHidDevice.ERROR_RSP_SUCCESS));
                hasExtra(
                        BluetoothHidHost.EXTRA_STATUS, (int) BluetoothHidDevice.ERROR_RSP_SUCCESS));
        // Mouse report
        mHidService.setReport(mDevice, BluetoothHidHost.REPORT_TYPE_INPUT, "02030405");
        // Must cast ERROR_RSP_SUCCESS, otherwise, it won't match with the int extra
        verifyIntentReceived(
                hasAction(BluetoothHidHost.ACTION_HANDSHAKE),
                hasExtra(BluetoothHidHost.EXTRA_STATUS, BluetoothHidDevice.ERROR_RSP_SUCCESS));
                hasExtra(
                        BluetoothHidHost.EXTRA_STATUS, (int) BluetoothHidDevice.ERROR_RSP_SUCCESS));
    }

    /**
@@ -485,6 +514,90 @@ public class HidHostDualModeTest {
                hasExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.BOND_NONE));
    }

    /**
     * CONNECTING and DISCONNECTING intents can go out of order, hence need a special function to
     * verify transport switches if we want to verify connecting and disconnected states
     *
     * <p>Four intents are expected: 1. fromTransport DISCONNECTING 2. toTransport CONNECTING 3.
     * fromTransport DISCONNECTED 4. toTransport CONNECTED
     *
     * <p>Currently, the order of 2 and 3 is unstable and hence we need this method to work with
     * both 2 -> 3 AND 3 -> 2
     *
     * <p>This function is complicated because we cannot mix ordered verification and unordered
     * verification if the same set of argument will appear more than once.
     *
     * @param device target dual mode HID device
     * @param fromTransport from which transport
     * @param toTransport to which transport
     */
    private void verifyTransportSwitch(BluetoothDevice device, int fromTransport, int toTransport) {
        assertThat(fromTransport).isNotEqualTo(toTransport);
        verifyConnectionState(mDevice, equalTo(fromTransport), equalTo(STATE_DISCONNECTING));

        // Capture the next intent with filter
        // Filter is necessary as otherwise it will corrupt all other unordered verifications
        final Intent[] savedIntent = {null};
        verifyIntentReceived(
                new CustomTypeSafeMatcher<>("Intent Matcher") {
                    public boolean matchesSafely(Intent intent) {
                        savedIntent[0] = intent;
                        return AllOf.allOf(
                                        hasAction(BluetoothHidHost.ACTION_CONNECTION_STATE_CHANGED),
                                        hasExtra(BluetoothDevice.EXTRA_DEVICE, device),
                                        hasExtra(
                                                BluetoothDevice.EXTRA_TRANSPORT,
                                                oneOf(fromTransport, toTransport)),
                                        hasExtra(
                                                BluetoothProfile.EXTRA_STATE,
                                                oneOf(STATE_CONNECTING, STATE_DISCONNECTED)))
                                .matches(intent);
                    }
                });

        // Verify saved intent is correct
        assertThat(savedIntent[0]).isNotNull();
        Intent intent = savedIntent[0];
        assertThat(intent.getAction()).isNotNull();
        assertThat(intent.getAction()).isEqualTo(BluetoothHidHost.ACTION_CONNECTION_STATE_CHANGED);
        assertThat(intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE, BluetoothDevice.class))
                .isEqualTo(device);
        assertThat(intent.hasExtra(BluetoothProfile.EXTRA_STATE)).isTrue();
        int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, STATE_CONNECTED);
        assertThat(state).isAnyOf(STATE_CONNECTING, STATE_DISCONNECTED);
        assertThat(intent.hasExtra(BluetoothDevice.EXTRA_TRANSPORT)).isTrue();
        int transport =
                intent.getIntExtra(BluetoothDevice.EXTRA_TRANSPORT, BluetoothDevice.TRANSPORT_AUTO);
        assertThat(transport).isAnyOf(TRANSPORT_BREDR, TRANSPORT_LE);

        // Conditionally verify the next intent
        if (transport == fromTransport) {
            assertThat(state).isEqualTo(STATE_DISCONNECTED);
            verifyConnectionState(mDevice, equalTo(toTransport), equalTo(STATE_CONNECTING));
        } else {
            assertThat(state).isEqualTo(STATE_CONNECTING);
            verifyConnectionState(mDevice, equalTo(fromTransport), equalTo(STATE_DISCONNECTED));
        }
        verifyConnectionState(mDevice, equalTo(toTransport), equalTo(STATE_CONNECTED));
    }

    private void verifyConnectionState(
            BluetoothDevice device, Matcher<Integer> transport, Matcher<Integer> state) {
        verifyIntentReceived(
                hasAction(BluetoothHidHost.ACTION_CONNECTION_STATE_CHANGED),
                hasExtra(BluetoothDevice.EXTRA_DEVICE, device),
                hasExtra(BluetoothDevice.EXTRA_TRANSPORT, transport),
                hasExtra(BluetoothProfile.EXTRA_STATE, state));
    }

    private void removeBond(BluetoothDevice device) {
        assertThat(device.removeBond()).isTrue();
        verifyIntentReceived(
                hasAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED),
                hasExtra(BluetoothDevice.EXTRA_DEVICE, device),
                hasExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.BOND_NONE));
    }

    @SafeVarargs
    private void verifyIntentReceived(Matcher<Intent>... matchers) {
        mInOrder.verify(mReceiver, timeout(INTENT_TIMEOUT.toMillis()))
@@ -492,16 +605,11 @@ public class HidHostDualModeTest {
    }

    @SafeVarargs
    private void verifyIntentReceivedUnordered(int num, Matcher<Intent>... matchers) {
        verify(mReceiver, timeout(INTENT_TIMEOUT.toMillis()).times(num))
    private void verifyIntentReceivedUnorderedAtLeast(int atLeast, Matcher<Intent>... matchers) {
        verify(mReceiver, timeout(INTENT_TIMEOUT.toMillis()).atLeast(atLeast))
                .onReceive(any(Context.class), MockitoHamcrest.argThat(AllOf.allOf(matchers)));
    }

    @SafeVarargs
    private void verifyIntentReceivedUnordered(Matcher<Intent>... matchers) {
        verifyIntentReceivedUnordered(1, matchers);
    }

    private BluetoothProfile verifyProfileServiceConnected(int profile) {
        ArgumentCaptor<BluetoothProfile> proxyCaptor =
                ArgumentCaptor.forClass(BluetoothProfile.class);