bass: Add BassClientStateMachine unit tests
Bug: 239546757
Fixes: 239546757
Tag: #feature
Sponsor: jpawlowski@
Test: atest BassClientStateMachineTest
Change-Id: Ief24ac1d180028fa05bb7d815358eb256f1e33e9
diff --git a/android/app/src/com/android/bluetooth/bass_client/BassClientStateMachine.java b/android/app/src/com/android/bluetooth/bass_client/BassClientStateMachine.java
index 448dc54..d7d5db3 100755
--- a/android/app/src/com/android/bluetooth/bass_client/BassClientStateMachine.java
+++ b/android/app/src/com/android/bluetooth/bass_client/BassClientStateMachine.java
@@ -138,6 +138,10 @@
static final int PSYNC_ACTIVE_TIMEOUT = 14;
static final int CONNECT_TIMEOUT = 15;
+ // NOTE: the value is not "final" - it is modified in the unit tests
+ @VisibleForTesting
+ static int sConnectTimeoutMs = BassConstants.CONNECT_TIMEOUT_MS;
+
/*key is combination of sourceId, Address and advSid for this hashmap*/
private final Map<Integer, BluetoothLeBroadcastReceiveState>
mBluetoothLeBroadcastReceiveStates =
@@ -159,7 +163,6 @@
private boolean mDiscoveryInitiated = false;
@VisibleForTesting
BassClientService mService;
- private BluetoothGatt mBluetoothGatt = null;
private BluetoothGattCharacteristic mBroadcastScanControlPoint;
private boolean mFirstTimeBisDiscovery = false;
@@ -185,6 +188,9 @@
private int mBroadcastSourceIdLength = 3;
private byte mNextSourceId = 0;
+ BluetoothGatt mBluetoothGatt = null;
+ BluetoothGattCallback mGattCallback = null;
+
BassClientStateMachine(BluetoothDevice device, BassClientService svc, Looper looper) {
super(TAG + "(" + device.toString() + ")", looper);
mDevice = device;
@@ -242,6 +248,7 @@
mBluetoothGatt.disconnect();
mBluetoothGatt.close();
mBluetoothGatt = null;
+ mGattCallback = null;
}
mPendingOperation = -1;
mPendingSourceId = -1;
@@ -825,8 +832,7 @@
// Implements callback methods for GATT events that the app cares about.
// For example, connection change and services discovered.
- private final BluetoothGattCallback mGattCallback =
- new BluetoothGattCallback() {
+ final class GattCallback extends BluetoothGattCallback {
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
boolean isStateChanged = false;
@@ -980,6 +986,25 @@
};
/**
+ * Connects to the GATT server of the device.
+ *
+ * @return {@code true} if it successfully connects to the GATT server.
+ */
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+ public boolean connectGatt(Boolean autoConnect) {
+ if (mGattCallback == null) {
+ mGattCallback = new GattCallback();
+ }
+
+ mBluetoothGatt = mDevice.connectGatt(mService, autoConnect,
+ mGattCallback, BluetoothDevice.TRANSPORT_LE,
+ (BluetoothDevice.PHY_LE_1M_MASK
+ | BluetoothDevice.PHY_LE_2M_MASK
+ | BluetoothDevice.PHY_LE_CODED_MASK), null);
+ return mBluetoothGatt != null;
+ }
+
+ /**
* getAllSources
*/
public List<BluetoothLeBroadcastReceiveState> getAllSources() {
@@ -1052,11 +1077,7 @@
if (mLastConnectionState != BluetoothProfile.STATE_DISCONNECTED) {
// Reconnect in background if not disallowed by the service
if (mService.okToConnect(mDevice)) {
- mBluetoothGatt = mDevice.connectGatt(mService, true,
- mGattCallback, BluetoothDevice.TRANSPORT_LE,
- (BluetoothDevice.PHY_LE_1M_MASK
- | BluetoothDevice.PHY_LE_2M_MASK
- | BluetoothDevice.PHY_LE_CODED_MASK), null);
+ connectGatt(false);
}
}
}
@@ -1082,16 +1103,10 @@
mBluetoothGatt.close();
mBluetoothGatt = null;
}
- mBluetoothGatt = mDevice.connectGatt(mService, mIsAllowedList,
- mGattCallback, BluetoothDevice.TRANSPORT_LE, false,
- (BluetoothDevice.PHY_LE_1M_MASK
- | BluetoothDevice.PHY_LE_2M_MASK
- | BluetoothDevice.PHY_LE_CODED_MASK), null);
- if (mBluetoothGatt == null) {
- Log.e(TAG, "Disconnected: error connecting to " + mDevice);
- break;
- } else {
+ if (connectGatt(mIsAllowedList)) {
transitionTo(mConnecting);
+ } else {
+ Log.e(TAG, "Disconnected: error connecting to " + mDevice);
}
break;
case DISCONNECT:
@@ -1132,7 +1147,7 @@
public void enter() {
log("Enter Connecting(" + mDevice + "): "
+ messageWhatToString(getCurrentMessage().what));
- sendMessageDelayed(CONNECT_TIMEOUT, mDevice, BassConstants.CONNECT_TIMEOUT_MS);
+ sendMessageDelayed(CONNECT_TIMEOUT, mDevice, sConnectTimeoutMs);
broadcastConnectionState(
mDevice, mLastConnectionState, BluetoothProfile.STATE_CONNECTING);
}
@@ -1788,7 +1803,7 @@
public void enter() {
log("Enter Disconnecting(" + mDevice + "): "
+ messageWhatToString(getCurrentMessage().what));
- sendMessageDelayed(CONNECT_TIMEOUT, mDevice, BassConstants.CONNECT_TIMEOUT_MS);
+ sendMessageDelayed(CONNECT_TIMEOUT, mDevice, sConnectTimeoutMs);
broadcastConnectionState(
mDevice, mLastConnectionState, BluetoothProfile.STATE_DISCONNECTING);
}
diff --git a/android/app/tests/unit/src/com/android/bluetooth/bass_client/BassClientStateMachineTest.java b/android/app/tests/unit/src/com/android/bluetooth/bass_client/BassClientStateMachineTest.java
new file mode 100644
index 0000000..e021c42
--- /dev/null
+++ b/android/app/tests/unit/src/com/android/bluetooth/bass_client/BassClientStateMachineTest.java
@@ -0,0 +1,250 @@
+/*
+ * Copyright 2022 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.bluetooth.bass_client;
+
+import static android.bluetooth.BluetoothGatt.GATT_SUCCESS;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.after;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.reset;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothProfile;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.HandlerThread;
+import android.os.Looper;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.LargeTest;
+import androidx.test.filters.MediumTest;
+
+import com.android.bluetooth.R;
+import com.android.bluetooth.TestUtils;
+import com.android.bluetooth.btservice.AdapterService;
+
+import org.hamcrest.core.IsInstanceOf;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Assume;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+@MediumTest
+@RunWith(JUnit4.class)
+public class BassClientStateMachineTest {
+ @Rule
+ public final MockitoRule mockito = MockitoJUnit.rule();
+
+ private BluetoothAdapter mAdapter;
+ private Context mTargetContext;
+ private HandlerThread mHandlerThread;
+ private StubBassClientStateMachine mBassClientStateMachine;
+ private static final int CONNECTION_TIMEOUT_MS = 1_000;
+ private static final int TIMEOUT_MS = 2_000;
+ private static final int WAIT_MS = 1_200;
+
+ private BluetoothDevice mTestDevice;
+ @Mock private AdapterService mAdapterService;
+ @Mock private BassClientService mBassClientService;
+
+ @Before
+ public void setUp() throws Exception {
+ mTargetContext = InstrumentationRegistry.getTargetContext();
+ TestUtils.setAdapterService(mAdapterService);
+
+ mAdapter = BluetoothAdapter.getDefaultAdapter();
+
+ // Get a device for testing
+ mTestDevice = mAdapter.getRemoteDevice("00:01:02:03:04:05");
+
+ // Set up thread and looper
+ mHandlerThread = new HandlerThread("BassClientStateMachineTestHandlerThread");
+ mHandlerThread.start();
+ mBassClientStateMachine = new StubBassClientStateMachine(mTestDevice,
+ mBassClientService, mHandlerThread.getLooper());
+ // Override the timeout value to speed up the test
+ BassClientStateMachine.sConnectTimeoutMs = CONNECTION_TIMEOUT_MS;
+ mBassClientStateMachine.start();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ mBassClientStateMachine.doQuit();
+ mHandlerThread.quit();
+ TestUtils.clearAdapterService(mAdapterService);
+ }
+
+ /**
+ * Test that default state is disconnected
+ */
+ @Test
+ public void testDefaultDisconnectedState() {
+ Assert.assertEquals(BluetoothProfile.STATE_DISCONNECTED,
+ mBassClientStateMachine.getConnectionState());
+ }
+
+ /**
+ * Allow/disallow connection to any device.
+ *
+ * @param allow if true, connection is allowed
+ */
+ private void allowConnection(boolean allow) {
+ when(mBassClientService.okToConnect(any(BluetoothDevice.class))).thenReturn(allow);
+ }
+
+ private void allowConnectGatt(boolean allow) {
+ mBassClientStateMachine.mShouldAllowGatt = allow;
+ }
+
+ /**
+ * Test that an incoming connection with policy forbidding connection is rejected
+ */
+ @Test
+ public void testOkToConnectFails() {
+ allowConnection(false);
+ allowConnectGatt(true);
+
+ // Inject an event for when incoming connection is requested
+ mBassClientStateMachine.sendMessage(BassClientStateMachine.CONNECT);
+
+ // Verify that no connection state broadcast is executed
+ verify(mBassClientService, after(WAIT_MS).never()).sendBroadcast(any(Intent.class),
+ anyString());
+
+ // Check that we are in Disconnected state
+ Assert.assertThat(mBassClientStateMachine.getCurrentState(),
+ IsInstanceOf.instanceOf(BassClientStateMachine.Disconnected.class));
+ }
+
+ @Test
+ public void testFailToConnectGatt() {
+ allowConnection(true);
+ allowConnectGatt(false);
+
+ // Inject an event for when incoming connection is requested
+ mBassClientStateMachine.sendMessage(BassClientStateMachine.CONNECT);
+
+ // Verify that no connection state broadcast is executed
+ verify(mBassClientService, after(WAIT_MS).never()).sendBroadcast(any(Intent.class),
+ anyString());
+
+ // Check that we are in Disconnected state
+ Assert.assertThat(mBassClientStateMachine.getCurrentState(),
+ IsInstanceOf.instanceOf(BassClientStateMachine.Disconnected.class));
+ assertNull(mBassClientStateMachine.mBluetoothGatt);
+ }
+
+ @Test
+ public void testSuccessfullyConnected() {
+ allowConnection(true);
+ allowConnectGatt(true);
+
+ // Inject an event for when incoming connection is requested
+ mBassClientStateMachine.sendMessage(BassClientStateMachine.CONNECT);
+
+ // Verify that one connection state broadcast is executed
+ ArgumentCaptor<Intent> intentArgument1 = ArgumentCaptor.forClass(Intent.class);
+ verify(mBassClientService, timeout(TIMEOUT_MS).times(1)).sendBroadcast(
+ intentArgument1.capture(), anyString(), any(Bundle.class));
+ Assert.assertEquals(BluetoothProfile.STATE_CONNECTING,
+ intentArgument1.getValue().getIntExtra(BluetoothProfile.EXTRA_STATE, -1));
+
+ Assert.assertThat(mBassClientStateMachine.getCurrentState(),
+ IsInstanceOf.instanceOf(BassClientStateMachine.Connecting.class));
+
+ assertNotNull(mBassClientStateMachine.mGattCallback);
+ mBassClientStateMachine.notifyConnectionStateChanged(
+ GATT_SUCCESS, BluetoothProfile.STATE_CONNECTED);
+
+ // Verify that the expected number of broadcasts are executed:
+ // - two calls to broadcastConnectionState(): Disconnected -> Connecting -> Connected
+ ArgumentCaptor<Intent> intentArgument2 = ArgumentCaptor.forClass(Intent.class);
+ verify(mBassClientService, timeout(TIMEOUT_MS).times(2)).sendBroadcast(
+ intentArgument2.capture(), anyString(), any(Bundle.class));
+
+ Assert.assertThat(mBassClientStateMachine.getCurrentState(),
+ IsInstanceOf.instanceOf(BassClientStateMachine.Connected.class));
+ }
+
+ @Test
+ public void testConnectGattTimeout() {
+ allowConnection(true);
+ allowConnectGatt(true);
+
+ // Inject an event for when incoming connection is requested
+ mBassClientStateMachine.sendMessage(BassClientStateMachine.CONNECT);
+
+ // Verify that one connection state broadcast is executed
+ ArgumentCaptor<Intent> intentArgument1 = ArgumentCaptor.forClass(Intent.class);
+ verify(mBassClientService, timeout(TIMEOUT_MS).times(1)).sendBroadcast(
+ intentArgument1.capture(), anyString(), any(Bundle.class));
+ Assert.assertEquals(BluetoothProfile.STATE_CONNECTING,
+ intentArgument1.getValue().getIntExtra(BluetoothProfile.EXTRA_STATE, -1));
+
+ Assert.assertThat(mBassClientStateMachine.getCurrentState(),
+ IsInstanceOf.instanceOf(BassClientStateMachine.Connecting.class));
+
+ // Verify that one connection state broadcast is executed
+ ArgumentCaptor<Intent> intentArgument2 = ArgumentCaptor.forClass(Intent.class);
+ verify(mBassClientService, timeout(TIMEOUT_MS).times(
+ 2)).sendBroadcast(intentArgument2.capture(), anyString(), any(Bundle.class));
+ Assert.assertEquals(BluetoothProfile.STATE_DISCONNECTED,
+ intentArgument2.getValue().getIntExtra(BluetoothProfile.EXTRA_STATE, -1));
+
+ Assert.assertThat(mBassClientStateMachine.getCurrentState(),
+ IsInstanceOf.instanceOf(BassClientStateMachine.Disconnected.class));
+ }
+
+ // It simulates GATT connection for testing.
+ public class StubBassClientStateMachine extends BassClientStateMachine {
+ boolean mShouldAllowGatt = true;
+
+ StubBassClientStateMachine(BluetoothDevice device, BassClientService service, Looper looper) {
+ super(device, service, looper);
+ }
+
+ @Override
+ public boolean connectGatt(Boolean autoConnect) {
+ mGattCallback = new GattCallback();
+ return mShouldAllowGatt;
+ }
+
+ public void notifyConnectionStateChanged(int status, int newState) {
+ if (mGattCallback != null) {
+ mGattCallback.onConnectionStateChange(mBluetoothGatt, status, newState);
+ }
+ }
+ }
+}