blob: 03d95a3a77bbfd46ad616d28f854dc8d60405862 [file] [log] [blame]
/*
* Copyright 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.bluetooth.a2dpsink;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothAudioConfig;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothProfile;
import android.content.Context;
import android.media.AudioFormat;
import android.os.test.TestLooper;
import androidx.test.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;
import com.android.bluetooth.TestUtils;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
@RunWith(AndroidJUnit4.class)
public class A2dpSinkStateMachineTest {
private static final String DEVICE_ADDRESS = "11:11:11:11:11:11";
private static final int UNHANDLED_MESSAGE = 9999;
@Mock private A2dpSinkService mService;
@Mock private A2dpSinkNativeInterface mNativeInterface;
private A2dpSinkStateMachine mStateMachine;
private BluetoothAdapter mAdapter;
private BluetoothDevice mDevice;
private Context mTargetContext;
private TestLooper mLooper;
@Before
public void setUp() throws Exception {
mTargetContext = InstrumentationRegistry.getTargetContext();
MockitoAnnotations.initMocks(this);
mLooper = new TestLooper();
mAdapter = BluetoothAdapter.getDefaultAdapter();
assertThat(mAdapter).isNotNull();
mDevice = mAdapter.getRemoteDevice(DEVICE_ADDRESS);
doNothing().when(mService).removeStateMachine(any(A2dpSinkStateMachine.class));
mStateMachine =
new A2dpSinkStateMachine(mLooper.getLooper(), mDevice, mService, mNativeInterface);
mStateMachine.start();
syncHandler(-2 /* SM_INIT_CMD */);
assertThat(mStateMachine.getDevice()).isEqualTo(mDevice);
assertThat(mStateMachine.getAudioConfig()).isNull();
assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_DISCONNECTED);
}
@After
public void tearDown() throws Exception {
assertThat(mLooper.nextMessage()).isNull();
mStateMachine = null;
mDevice = null;
mAdapter = null;
}
private void syncHandler(int... what) {
TestUtils.syncHandler(mLooper, what);
}
private void mockDeviceConnectionPolicy(BluetoothDevice device, int policy) {
doReturn(policy).when(mService).getConnectionPolicy(device);
}
private void sendConnectionEvent(int state) {
mStateMachine.sendMessage(A2dpSinkStateMachine.STACK_EVENT,
StackEvent.connectionStateChanged(mDevice, state));
syncHandler(A2dpSinkStateMachine.STACK_EVENT);
}
private void sendAudioConfigChangedEvent(int sampleRate, int channelCount) {
mStateMachine.sendMessage(A2dpSinkStateMachine.STACK_EVENT,
StackEvent.audioConfigChanged(mDevice, sampleRate, channelCount));
syncHandler(A2dpSinkStateMachine.STACK_EVENT);
}
/**********************************************************************************************
* DISCONNECTED STATE TESTS *
*********************************************************************************************/
@Test
public void testConnectInDisconnected() {
mStateMachine.connect();
syncHandler(A2dpSinkStateMachine.CONNECT);
verify(mNativeInterface).connectA2dpSink(mDevice);
assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_CONNECTING);
}
@Test
public void testDisconnectInDisconnected() {
mStateMachine.disconnect();
syncHandler(A2dpSinkStateMachine.DISCONNECT);
assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_DISCONNECTED);
}
@Test
public void testAudioConfigChangedInDisconnected() {
sendAudioConfigChangedEvent(44, 1);
assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_DISCONNECTED);
assertThat(mStateMachine.getAudioConfig()).isNull();
}
@Test
public void testIncomingConnectedInDisconnected() {
sendConnectionEvent(BluetoothProfile.STATE_CONNECTED);
assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_CONNECTED);
}
@Test
public void testAllowedIncomingConnectionInDisconnected() {
mockDeviceConnectionPolicy(mDevice, BluetoothProfile.CONNECTION_POLICY_ALLOWED);
sendConnectionEvent(BluetoothProfile.STATE_CONNECTING);
assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_CONNECTING);
verify(mNativeInterface, times(0)).connectA2dpSink(mDevice);
}
@Test
public void testForbiddenIncomingConnectionInDisconnected() {
mockDeviceConnectionPolicy(mDevice, BluetoothProfile.CONNECTION_POLICY_FORBIDDEN);
sendConnectionEvent(BluetoothProfile.STATE_CONNECTING);
verify(mNativeInterface).disconnectA2dpSink(mDevice);
assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_DISCONNECTED);
}
@Test
public void testUnknownIncomingConnectionInDisconnected() {
mockDeviceConnectionPolicy(mDevice, BluetoothProfile.CONNECTION_POLICY_UNKNOWN);
sendConnectionEvent(BluetoothProfile.STATE_CONNECTING);
assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_CONNECTING);
verify(mNativeInterface, times(0)).connectA2dpSink(mDevice);
}
@Test
public void testIncomingDisconnectInDisconnected() {
sendConnectionEvent(BluetoothProfile.STATE_DISCONNECTED);
assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_DISCONNECTED);
syncHandler(A2dpSinkStateMachine.CLEANUP);
verify(mService).removeStateMachine(mStateMachine);
}
@Test
public void testIncomingDisconnectingInDisconnected() {
sendConnectionEvent(BluetoothProfile.STATE_DISCONNECTING);
assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_DISCONNECTED);
verify(mService, times(0)).removeStateMachine(mStateMachine);
}
@Test
public void testIncomingConnectingInDisconnected() {
sendConnectionEvent(BluetoothProfile.STATE_CONNECTING);
assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_DISCONNECTED);
}
@Test
public void testUnhandledMessageInDisconnected() {
mStateMachine.sendMessage(UNHANDLED_MESSAGE);
mStateMachine.sendMessage(UNHANDLED_MESSAGE, 0 /* arbitrary payload */);
syncHandler(UNHANDLED_MESSAGE, UNHANDLED_MESSAGE);
}
/**********************************************************************************************
* CONNECTING STATE TESTS *
*********************************************************************************************/
@Test
public void testConnectedInConnecting() {
testConnectInDisconnected();
sendConnectionEvent(BluetoothProfile.STATE_CONNECTED);
assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_CONNECTED);
}
@Test
public void testConnectingInConnecting() {
testConnectInDisconnected();
sendConnectionEvent(BluetoothProfile.STATE_CONNECTING);
assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_CONNECTING);
}
@Test
public void testDisconnectingInConnecting() {
testConnectInDisconnected();
sendConnectionEvent(BluetoothProfile.STATE_DISCONNECTING);
assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_CONNECTING);
}
@Test
public void testDisconnectedInConnecting() {
testConnectInDisconnected();
sendConnectionEvent(BluetoothProfile.STATE_DISCONNECTED);
assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_DISCONNECTED);
syncHandler(A2dpSinkStateMachine.CLEANUP);
verify(mService).removeStateMachine(mStateMachine);
}
@Test
public void testConnectionTimeoutInConnecting() {
testConnectInDisconnected();
mLooper.moveTimeForward(120_000); // Skip time so the timeout fires
syncHandler(A2dpSinkStateMachine.CONNECT_TIMEOUT);
assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_DISCONNECTED);
syncHandler(A2dpSinkStateMachine.CLEANUP);
verify(mService).removeStateMachine(mStateMachine);
}
@Test
public void testAudioStateChangeInConnecting() {
testConnectInDisconnected();
sendAudioConfigChangedEvent(44, 1);
assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_CONNECTING);
assertThat(mStateMachine.getAudioConfig()).isNull();
}
@Test
public void testConnectInConnecting() {
testConnectInDisconnected();
mStateMachine.connect();
syncHandler(A2dpSinkStateMachine.CONNECT);
assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_CONNECTING);
}
@Test
public void testDisconnectInConnecting_disconnectDeferredAndProcessed() {
testConnectInDisconnected();
mStateMachine.disconnect();
syncHandler(A2dpSinkStateMachine.DISCONNECT);
assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_CONNECTING);
// send connected, disconnect should get processed
sendConnectionEvent(BluetoothProfile.STATE_CONNECTED);
assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_CONNECTED);
syncHandler(A2dpSinkStateMachine.DISCONNECT); // message was defer
verify(mNativeInterface).disconnectA2dpSink(mDevice);
assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_DISCONNECTED);
syncHandler(A2dpSinkStateMachine.CLEANUP);
verify(mService).removeStateMachine(mStateMachine);
}
/**********************************************************************************************
* CONNECTED STATE TESTS *
*********************************************************************************************/
@Test
public void testConnectInConnected() {
testConnectedInConnecting();
mStateMachine.connect();
syncHandler(A2dpSinkStateMachine.CONNECT);
assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_CONNECTED);
}
@Test
public void testDisconnectInConnected() {
testConnectedInConnecting();
mStateMachine.disconnect();
syncHandler(A2dpSinkStateMachine.DISCONNECT);
verify(mNativeInterface).disconnectA2dpSink(mDevice);
assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_DISCONNECTED);
syncHandler(A2dpSinkStateMachine.CLEANUP);
verify(mService).removeStateMachine(mStateMachine);
}
@Test
public void testAudioStateChangeInConnected() {
testConnectedInConnecting();
sendAudioConfigChangedEvent(44, 1);
assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_CONNECTED);
BluetoothAudioConfig expected =
new BluetoothAudioConfig(44, 1, AudioFormat.ENCODING_PCM_16BIT);
assertThat(mStateMachine.getAudioConfig()).isEqualTo(expected);
}
@Test
public void testConnectedInConnected() {
testConnectedInConnecting();
sendConnectionEvent(BluetoothProfile.STATE_CONNECTED);
assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_CONNECTED);
}
@Test
public void testConnectingInConnected() {
testConnectedInConnecting();
sendConnectionEvent(BluetoothProfile.STATE_CONNECTING);
assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_CONNECTED);
}
@Test
public void testDisconnectingInConnected() {
testConnectedInConnecting();
sendConnectionEvent(BluetoothProfile.STATE_DISCONNECTING);
assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_DISCONNECTED);
syncHandler(A2dpSinkStateMachine.CLEANUP);
verify(mService).removeStateMachine(mStateMachine);
}
@Test
public void testDisconnectedInConnected() {
testConnectedInConnecting();
sendConnectionEvent(BluetoothProfile.STATE_DISCONNECTED);
assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_DISCONNECTED);
syncHandler(A2dpSinkStateMachine.CLEANUP);
verify(mService).removeStateMachine(mStateMachine);
}
/**********************************************************************************************
* OTHER TESTS *
*********************************************************************************************/
@Test
public void testDump() {
StringBuilder sb = new StringBuilder();
mStateMachine.dump(sb);
assertThat(sb.toString()).isNotNull();
}
}