| /* |
| * 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 |
| } |
| } |