blob: 4ee08000970c742c88d9cb5fc041dfba2c0e95e1 [file] [log] [blame]
/*
* 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.car.bluetooth;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.TruthJUnit.assume;
import static org.mockito.Mockito.after;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothManager;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothStatusCodes;
import android.bluetooth.BluetoothUuid;
import android.content.Intent;
import android.os.ParcelUuid;
import android.os.RemoteException;
import android.os.SystemClock;
import androidx.test.InstrumentationRegistry;
import androidx.test.filters.RequiresDevice;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import java.util.Arrays;
import java.util.List;
/**
* Unit tests for {@link BluetoothConnectionRetryManager}
*
* Run:
* atest BluetoothConnectionRetryManagerTest
*/
@RequiresDevice
@RunWith(MockitoJUnitRunner.class)
public class BluetoothConnectionRetryManagerTest
extends AbstractExtendedMockitoBluetoothTestCase {
private static final String TAG = BluetoothConnectionRetryManagerTest.class.getSimpleName();
private static final boolean VERBOSE = false;
private static final List<String> DEVICE_LIST = Arrays.asList(
"DE:AD:BE:EF:00:00",
"DE:AD:BE:EF:00:01",
"DE:AD:BE:EF:00:02",
"DE:AD:BE:EF:00:03",
"DE:AD:BE:EF:00:04",
"DE:AD:BE:EF:00:05",
"DE:AD:BE:EF:00:06",
"DE:AD:BE:EF:00:07");
private static final int TIMING_BUFFER_MS = 100;
BluetoothConnectionRetryManager mConnectionRetryManager;
// Tests assume HFP is the only supported profile
private final int mProfileId = BluetoothProfile.HEADSET_CLIENT;
private final String mConnectionAction = BluetoothUtils.HFP_CLIENT_CONNECTION_STATE_CHANGED;
private MockContext mMockContext;
@Mock private BluetoothManager mMockBluetoothManager;
@Mock private BluetoothAdapter mMockBluetoothAdapter;
//--------------------------------------------------------------------------------------------//
// Setup/TearDown //
//--------------------------------------------------------------------------------------------//
@Before
public void setUp() {
mMockContext = new MockContext(InstrumentationRegistry.getTargetContext());
mMockContext.addMockedSystemService(BluetoothManager.class, mMockBluetoothManager);
when(mMockBluetoothManager.getAdapter()).thenReturn(mMockBluetoothAdapter);
when(mMockBluetoothAdapter.getUuidsList()).thenReturn(
Arrays.asList(new ParcelUuid[]{BluetoothUuid.HFP}));
mMockContext.addMockedSystemService(BluetoothManager.class, mMockBluetoothManager);
when(mMockBluetoothManager.getAdapter()).thenReturn(mMockBluetoothAdapter);
when(mMockBluetoothAdapter.getUuidsList()).thenReturn(
Arrays.asList(new ParcelUuid[]{BluetoothUuid.HFP}));
mConnectionRetryManager = BluetoothConnectionRetryManager.create(mMockContext);
assertThat(mConnectionRetryManager).isNotNull();
// Override the timeout value to speed up tests
mConnectionRetryManager.sRetryFirstConnectTimeoutMs = 1000;
}
@After
public void tearDown() {
if (mConnectionRetryManager != null) {
mConnectionRetryManager.release();
mConnectionRetryManager = null;
}
if (mMockContext != null) {
mMockContext.release();
mMockContext = null;
}
}
//--------------------------------------------------------------------------------------------//
// Utilities //
//--------------------------------------------------------------------------------------------//
private void sendBondStateChanged(BluetoothDevice device, int newState) {
assertThat(mMockContext).isNotNull();
Intent intent = new Intent(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
intent.putExtra(BluetoothDevice.EXTRA_BOND_STATE, newState);
mMockContext.sendBroadcast(intent);
}
private void sendConnectionStateChanged(BluetoothDevice device, int newState) {
assertThat(mMockContext).isNotNull();
Intent intent = new Intent(mConnectionAction);
intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
intent.putExtra(BluetoothProfile.EXTRA_STATE, newState);
mMockContext.sendBroadcast(intent);
}
private void sendSuccessfulConnection(BluetoothDevice device) {
assertThat(mMockContext).isNotNull();
Intent intent = new Intent(mConnectionAction);
intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
intent.putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, BluetoothProfile.STATE_CONNECTING);
intent.putExtra(BluetoothProfile.EXTRA_STATE, BluetoothProfile.STATE_CONNECTED);
mMockContext.sendBroadcast(intent);
}
private void sendFailedConnectionMethod1(BluetoothDevice device) {
assertThat(mMockContext).isNotNull();
Intent intent = new Intent(mConnectionAction);
intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
intent.putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, BluetoothProfile.STATE_CONNECTING);
intent.putExtra(BluetoothProfile.EXTRA_STATE, BluetoothProfile.STATE_DISCONNECTING);
mMockContext.sendBroadcast(intent);
}
private void sendFailedConnectionMethod2(BluetoothDevice device) {
assertThat(mMockContext).isNotNull();
Intent intent = new Intent(mConnectionAction);
intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
intent.putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, BluetoothProfile.STATE_CONNECTING);
intent.putExtra(BluetoothProfile.EXTRA_STATE, BluetoothProfile.STATE_DISCONNECTED);
mMockContext.sendBroadcast(intent);
}
private void sendFailedConnectionsPeriodic(BluetoothDevice device, int count, long interval) {
for (int i = 0; i < count; i++) {
sendFailedConnectionMethod1(device);
SystemClock.sleep(interval);
}
}
private BluetoothDevice createMockDevice(String bdAddr) {
// Tests assume HFP is the only supported profile
return createMockDevice(bdAddr,
new ParcelUuid[]{BluetoothUuid.HFP_AG, BluetoothUuid.HSP_AG});
}
private BluetoothDevice createMockDevice(String bdAddr, ParcelUuid[] uuids) {
BluetoothDevice device = mock(BluetoothDevice.class);
when(device.getAddress()).thenReturn(bdAddr);
when(device.getName()).thenReturn(bdAddr);
when(device.getUuids()).thenReturn(uuids);
when(device.connect()).thenReturn(BluetoothStatusCodes.SUCCESS);
when(device.disconnect()).thenReturn(BluetoothStatusCodes.SUCCESS);
return device;
}
//--------------------------------------------------------------------------------------------//
// First-connection after bonding tests //
//--------------------------------------------------------------------------------------------//
/**
* Preconditions:
* - The connection retry manager is initialized.
*
* Actions:
* - A device bonds.
* - The device successfully connects.
*
* Outcome:
* - Nothing, no retry attempts should be made.
*/
@Test
public void testSuccessfulFirstConnect_doNothing() {
mConnectionRetryManager.init();
BluetoothDevice device = createMockDevice(DEVICE_LIST.get(0));
sendBondStateChanged(device, BluetoothDevice.BOND_BONDED);
sendSuccessfulConnection(device);
assertThat(mConnectionRetryManager.isRetryPosted(device, mProfileId)).isFalse();
}
/**
* Preconditions:
* - The connection retry manager is initialized.
*
* Actions:
* - A device bonds.
* - The device fails to connect (CONNECTING --> DISCONNECTING)
*
* Outcome:
* - A retry attempt should be posted.
*/
@Test
public void testFailedFirstConnectMethod1_retryPosted() {
mConnectionRetryManager.init();
BluetoothDevice device = createMockDevice(DEVICE_LIST.get(0));
sendBondStateChanged(device, BluetoothDevice.BOND_BONDED);
sendFailedConnectionMethod1(device);
assertThat(mConnectionRetryManager.isRetryPosted(device, mProfileId)).isTrue();
}
/**
* Preconditions:
* - The connection retry manager is initialized.
*
* Actions:
* - A device bonds.
* - The device fails to connect (CONNECTING --> DISCONNECTED)
*
* Outcome:
* - A retry attempt should be posted.
*/
@Test
public void testFailedFirstConnectMethod2_retryPosted() {
mConnectionRetryManager.init();
BluetoothDevice device = createMockDevice(DEVICE_LIST.get(0));
sendBondStateChanged(device, BluetoothDevice.BOND_BONDED);
sendFailedConnectionMethod2(device);
assertThat(mConnectionRetryManager.isRetryPosted(device, mProfileId)).isTrue();
}
/**
* Preconditions:
* - The connection retry manager is initialized.
*
* Actions:
* - A device bonds.
* - Device fails to connect for the first time.
*
* Outcome:
* - Verify the actual connect command is invoked for the retry, not just retry posted.
*/
@Test
public void testFailedFirstConnect_retryConnectInvoked() throws RemoteException {
mConnectionRetryManager.init();
BluetoothDevice device = createMockDevice(DEVICE_LIST.get(0));
sendBondStateChanged(device, BluetoothDevice.BOND_BONDED);
sendFailedConnectionMethod1(device);
// should not be invoked immediately
verify(device, times(0)).connect();
verify(device, after(mConnectionRetryManager.sRetryFirstConnectTimeoutMs
+ TIMING_BUFFER_MS).times(1)).connect();
}
/**
* Preconditions:
* - The connection retry manager is initialized.
*
* Actions:
* - Two devices bond.
* - One device fails to connect.
* - The other device does nothing.
*
* Outcome:
* - Only the device that failed to connect should have a retry attempt.
*/
@Test
public void testFirstConnectTwoDevices_onlyOneDeviceRetries() {
mConnectionRetryManager.init();
BluetoothDevice deviceDoesNothing = createMockDevice(DEVICE_LIST.get(0));
BluetoothDevice deviceFailsConnection = createMockDevice(DEVICE_LIST.get(1));
sendBondStateChanged(deviceDoesNothing, BluetoothDevice.BOND_BONDED);
sendBondStateChanged(deviceFailsConnection, BluetoothDevice.BOND_BONDED);
sendFailedConnectionMethod1(deviceFailsConnection);
assertThat(mConnectionRetryManager.isRetryPosted(deviceDoesNothing, mProfileId)).isFalse();
assertThat(mConnectionRetryManager.isRetryPosted(deviceFailsConnection, mProfileId))
.isTrue();
}
/**
* Preconditions:
* - The connection retry manager is initialized.
*
* Actions:
* - A device never bonds.
* - Send an unsuccessful connection attempt.
*
* Outcome:
* - Nothing, no retry attempts should be made, no exceptions thrown.
*/
@Test
public void testFirstConnectDeviceNeverBonded_doNothing() {
mConnectionRetryManager.init();
BluetoothDevice device = createMockDevice(DEVICE_LIST.get(0));
sendFailedConnectionMethod1(device);
assertThat(mConnectionRetryManager.isRetryPosted(device, mProfileId)).isFalse();
}
/**
* Preconditions:
* - The connection retry manager is initialized.
*
* Actions:
* - A device bonds.
* - Then, device unbonds.
* - Then, send an unsuccessful connection attempt.
*
* Outcome:
* - Nothing, no retry attempts should be made, no exceptions thrown.
*/
@Test
public void testDeviceUnbondsBeforeFirstConnect_doNothing() {
mConnectionRetryManager.init();
BluetoothDevice device = createMockDevice(DEVICE_LIST.get(0));
sendBondStateChanged(device, BluetoothDevice.BOND_BONDED);
sendBondStateChanged(device, BluetoothDevice.BOND_NONE);
sendFailedConnectionMethod1(device);
assertThat(mConnectionRetryManager.isRetryPosted(device, mProfileId)).isFalse();
}
/**
* Preconditions:
* - The connection retry manager is initialized.
*
* Actions:
* - A device bonds.
* - Then, an unsuccessful connection attempt.
* - Then, device unbonds (before a successful connection).
*
* Outcome:
* - Retry attempts should be removed when the device unbonds.
*/
@Test
public void testDeviceUnbondsAfterRetryPosted_retriesRemoved() {
mConnectionRetryManager.init();
BluetoothDevice device = createMockDevice(DEVICE_LIST.get(0));
sendBondStateChanged(device, BluetoothDevice.BOND_BONDED);
sendFailedConnectionMethod1(device);
assertThat(mConnectionRetryManager.isRetryPosted(device, mProfileId)).isTrue();
sendBondStateChanged(device, BluetoothDevice.BOND_NONE);
assertThat(mConnectionRetryManager.isRetryPosted(device, mProfileId)).isFalse();
}
/**
* Preconditions:
* - The connection retry manager is initialized.
*
* Actions:
* - A device bonds.
* - Device successfully connects for the first time.
* - Device disconnects (but remains bonded).
* - Device fails to connect a second time.
*
* Outcome:
* - Nothing, no retry attempts should be made, no exceptions thrown.
* - Only retry failed _first_ attempts, not subsequent ones.
*/
@Test
public void testSuccessfulFirstConnectFailedSecond_doNothing() {
mConnectionRetryManager.init();
BluetoothDevice device = createMockDevice(DEVICE_LIST.get(0));
sendBondStateChanged(device, BluetoothDevice.BOND_BONDED);
sendSuccessfulConnection(device);
sendConnectionStateChanged(device, BluetoothProfile.STATE_DISCONNECTED);
sendFailedConnectionMethod1(device);
assertThat(mConnectionRetryManager.isRetryPosted(device, mProfileId)).isFalse();
}
/**
* Preconditions:
* - The connection retry manager is initialized.
*
* Actions:
* - A device bonds.
* - Device fails first-connect twice.
*
* Outcome:
* - Two retry attempts should be made.
*/
@Test
public void testFailedFirstConnectTwice_retryTwice() throws RemoteException {
mConnectionRetryManager.init();
assume().that(mConnectionRetryManager.getMaxRetriesFirstConnection()).isGreaterThan(1);
BluetoothDevice device = createMockDevice(DEVICE_LIST.get(0));
sendBondStateChanged(device, BluetoothDevice.BOND_BONDED);
sendFailedConnectionsPeriodic(device, 2, mConnectionRetryManager
.sRetryFirstConnectTimeoutMs + TIMING_BUFFER_MS);
verify(device, times(2)).connect();
}
/**
* Preconditions:
* - The connection retry manager is initialized.
*
* Actions:
* - A device bonds.
* - Device fails first-connect greater than {@code N} times, where {@code N} is the maximum
* retry attempts.
*
* Outcome:
* - Only {@code N} retry attempts should be made.
*/
@Test
public void testFailedFirstConnectMoreThanMaxTimes_retryMaxTimes() throws RemoteException {
mConnectionRetryManager.init();
BluetoothDevice device = createMockDevice(DEVICE_LIST.get(0));
int expectedRetryAttempts = mConnectionRetryManager.getMaxRetriesFirstConnection();
int failedAttempts = expectedRetryAttempts + 2;
sendBondStateChanged(device, BluetoothDevice.BOND_BONDED);
sendFailedConnectionsPeriodic(device, failedAttempts, mConnectionRetryManager
.sRetryFirstConnectTimeoutMs + TIMING_BUFFER_MS);
verify(device, times(expectedRetryAttempts)).connect();
}
/**
* Preconditions:
* - The connection retry manager is initialized.
*
* Actions:
* - A device bonds.
* - Device fails first-connect in quick succession: 3 failed attempts spaced {@code T/2} ms
* apart, where {@code T} is
* {@link BluetoothConnectionRetryManager.sRetryFirstConnectTimeoutMs}.
*
* Outcome:
* - Only the first and third failures should trigger retry-connect attempts.
*/
@Test
public void testFailedFirstConnectThreeInQuickSuccession_retryTwice() throws RemoteException {
mConnectionRetryManager.init();
assume().that(mConnectionRetryManager.getMaxRetriesFirstConnection()).isGreaterThan(1);
BluetoothDevice device = createMockDevice(DEVICE_LIST.get(0));
long halfInterval = mConnectionRetryManager.sRetryFirstConnectTimeoutMs / 2;
sendBondStateChanged(device, BluetoothDevice.BOND_BONDED);
sendFailedConnectionMethod1(device); // failure 1
SystemClock.sleep(halfInterval);
sendFailedConnectionMethod1(device); // failure 2
SystemClock.sleep(halfInterval + TIMING_BUFFER_MS); // full interval since failure 1
verify(device, times(1)).connect(); // for #1
sendFailedConnectionMethod1(device); // failure 3
SystemClock.sleep(halfInterval); // full interval since failure 2
verify(device, times(1)).connect(); // for #2
SystemClock.sleep(halfInterval + TIMING_BUFFER_MS); // full interval since failure 3
verify(device, times(2)).connect(); // for #3
}
}