blob: aba24fbd55b76e5525e48d9f6123d549c1a946d2 [file] [log] [blame]
/*
* Copyright 2019 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.server.audio;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.anyInt;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
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.content.Context;
import android.content.Intent;
import android.media.AudioDeviceAttributes;
import android.media.AudioDeviceInfo;
import android.media.AudioManager;
import android.media.AudioSystem;
import android.media.BluetoothProfileConnectionInfo;
import android.util.Log;
import androidx.test.filters.MediumTest;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentMatchers;
import org.mockito.Mock;
import org.mockito.Spy;
@MediumTest
@RunWith(AndroidJUnit4.class)
public class AudioDeviceBrokerTest {
private static final String TAG = "AudioDeviceBrokerTest";
private static final int MAX_MESSAGE_HANDLING_DELAY_MS = 100;
// the actual class under test
private AudioDeviceBroker mAudioDeviceBroker;
@Mock private AudioService mMockAudioService;
@Spy private AudioDeviceInventory mSpyDevInventory;
@Spy private AudioSystemAdapter mSpyAudioSystem;
@Spy private SystemServerAdapter mSpySystemServer;
private BluetoothDevice mFakeBtDevice;
@Before
public void setUp() throws Exception {
Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
mMockAudioService = mock(AudioService.class);
mSpyAudioSystem = spy(new NoOpAudioSystemAdapter());
mSpyDevInventory = spy(new AudioDeviceInventory(mSpyAudioSystem));
mSpySystemServer = spy(new NoOpSystemServerAdapter());
mAudioDeviceBroker = new AudioDeviceBroker(context, mMockAudioService, mSpyDevInventory,
mSpySystemServer, mSpyAudioSystem);
mSpyDevInventory.setDeviceBroker(mAudioDeviceBroker);
BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
mFakeBtDevice = adapter.getRemoteDevice("00:01:02:03:04:05");
Assert.assertNotNull("invalid null BT device", mFakeBtDevice);
}
@After
public void tearDown() throws Exception { }
// @Test
// public void testSetUpAndTearDown() { }
/**
* postBluetoothA2dpDeviceConnectionStateSuppressNoisyIntent() for connection:
* - verify it calls into AudioDeviceInventory with the right params
* - verify it calls into AudioSystem and stays connected (no 2nd call to disconnect)
* @throws Exception
*/
@Test
public void testPostA2dpDeviceConnectionChange() throws Exception {
Log.i(TAG, "starting testPostA2dpDeviceConnectionChange");
Assert.assertNotNull("invalid null BT device", mFakeBtDevice);
mAudioDeviceBroker.queueOnBluetoothActiveDeviceChanged(
new AudioDeviceBroker.BtDeviceChangedData(mFakeBtDevice, null,
BluetoothProfileConnectionInfo.createA2dpInfo(true, 1), "testSource"));
Thread.sleep(2 * MAX_MESSAGE_HANDLING_DELAY_MS);
verify(mSpyDevInventory, times(1)).setBluetoothActiveDevice(
any(AudioDeviceBroker.BtDeviceInfo.class)
);
// verify the connection was reported to AudioSystem
checkSingleSystemConnection(mFakeBtDevice);
}
/**
* Verify call to postBluetoothA2dpDeviceConnectionStateSuppressNoisyIntent() for
* connection > pause > disconnection > connection
* keeps the device connected
* @throws Exception
*/
@Test
public void testA2dpDeviceConnectionDisconnectionConnectionChange() throws Exception {
Log.i(TAG, "starting testA2dpDeviceConnectionDisconnectionConnectionChange");
doTestConnectionDisconnectionReconnection(0, false,
// cannot guarantee single connection since commands are posted in separate thread
// than they are processed
false);
}
/**
* Verify device disconnection and reconnection within the BECOMING_NOISY window
* in the absence of media playback
* @throws Exception
*/
@Test
public void testA2dpDeviceReconnectionWithinBecomingNoisyDelay() throws Exception {
Log.i(TAG, "starting testA2dpDeviceReconnectionWithinBecomingNoisyDelay");
doTestConnectionDisconnectionReconnection(AudioService.BECOMING_NOISY_DELAY_MS / 2,
false,
// do not check single connection since the connection command will come much
// after the disconnection command
false);
}
/**
* Same as testA2dpDeviceConnectionDisconnectionConnectionChange() but with mock media playback
* @throws Exception
*/
@Test
public void testA2dpConnectionDisconnectionConnectionChange_MediaPlayback() throws Exception {
Log.i(TAG, "starting testA2dpConnectionDisconnectionConnectionChange_MediaPlayback");
doTestConnectionDisconnectionReconnection(0, true,
// guarantee single connection since because of media playback the disconnection
// is supposed to be delayed, and thus cancelled because of the connection
true);
}
/**
* Same as testA2dpDeviceReconnectionWithinBecomingNoisyDelay() but with mock media playback
* @throws Exception
*/
@Test
public void testA2dpReconnectionWithinBecomingNoisyDelay_MediaPlayback() throws Exception {
Log.i(TAG, "starting testA2dpReconnectionWithinBecomingNoisyDelay_MediaPlayback");
doTestConnectionDisconnectionReconnection(AudioService.BECOMING_NOISY_DELAY_MS / 2,
true,
// guarantee single connection since because of media playback the disconnection
// is supposed to be delayed, and thus cancelled because of the connection
true);
}
/**
* Test that device wired state intents are broadcasted on connection state change
* @throws Exception
*/
@Test
public void testSetWiredDeviceConnectionState() throws Exception {
Log.i(TAG, "starting postSetWiredDeviceConnectionState");
final String address = "testAddress";
final String name = "testName";
final String caller = "testCaller";
doNothing().when(mSpySystemServer).broadcastStickyIntentToCurrentProfileGroup(
any(Intent.class));
mSpyDevInventory.setWiredDeviceConnectionState(new AudioDeviceAttributes(
AudioSystem.DEVICE_OUT_WIRED_HEADSET, address, name),
AudioService.CONNECTION_STATE_CONNECTED, caller);
Thread.sleep(MAX_MESSAGE_HANDLING_DELAY_MS);
// Verify that the sticky intent is broadcasted
verify(mSpySystemServer, times(1)).broadcastStickyIntentToCurrentProfileGroup(
any(Intent.class));
}
/**
* Test that constructing an AdiDeviceState instance requires a non-null address for a
* wireless type, but can take null for a non-wireless type;
* @throws Exception
*/
@Test
public void testAdiDeviceStateNullAddressCtor() throws Exception {
try {
new AdiDeviceState(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER,
AudioManager.DEVICE_OUT_SPEAKER, null);
new AdiDeviceState(AudioDeviceInfo.TYPE_BLUETOOTH_A2DP,
AudioManager.DEVICE_OUT_BLUETOOTH_A2DP, null);
Assert.fail();
} catch (NullPointerException e) { }
}
@Test
public void testAdiDeviceStateStringSerialization() throws Exception {
Log.i(TAG, "starting testAdiDeviceStateStringSerialization");
final AdiDeviceState devState = new AdiDeviceState(
AudioDeviceInfo.TYPE_BUILTIN_SPEAKER, AudioManager.DEVICE_OUT_SPEAKER, "bla");
devState.setHasHeadTracker(false);
devState.setHeadTrackerEnabled(false);
devState.setSAEnabled(true);
final String persistString = devState.toPersistableString();
final AdiDeviceState result = AdiDeviceState.fromPersistedString(persistString);
Log.i(TAG, "original:" + devState);
Log.i(TAG, "result :" + result);
Assert.assertEquals(devState, result);
}
private void doTestConnectionDisconnectionReconnection(int delayAfterDisconnection,
boolean mockMediaPlayback, boolean guaranteeSingleConnection) throws Exception {
when(mMockAudioService.getDeviceForStream(AudioManager.STREAM_MUSIC))
.thenReturn(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP);
when(mMockAudioService.isInCommunication()).thenReturn(false);
when(mMockAudioService.hasMediaDynamicPolicy()).thenReturn(false);
when(mMockAudioService.hasAudioFocusUsers()).thenReturn(false);
((NoOpAudioSystemAdapter) mSpyAudioSystem).configureIsStreamActive(mockMediaPlayback);
// first connection: ensure the device is connected as a starting condition for the test
mAudioDeviceBroker.queueOnBluetoothActiveDeviceChanged(
new AudioDeviceBroker.BtDeviceChangedData(mFakeBtDevice, null,
BluetoothProfileConnectionInfo.createA2dpInfo(true, 1), "testSource"));
Thread.sleep(MAX_MESSAGE_HANDLING_DELAY_MS);
// disconnection
mAudioDeviceBroker.queueOnBluetoothActiveDeviceChanged(
new AudioDeviceBroker.BtDeviceChangedData(null, mFakeBtDevice,
BluetoothProfileConnectionInfo.createA2dpInfo(false, -1), "testSource"));
if (delayAfterDisconnection > 0) {
Thread.sleep(delayAfterDisconnection);
}
// reconnection
mAudioDeviceBroker.queueOnBluetoothActiveDeviceChanged(
new AudioDeviceBroker.BtDeviceChangedData(mFakeBtDevice, null,
BluetoothProfileConnectionInfo.createA2dpInfo(true, 2), "testSource"));
Thread.sleep(AudioService.BECOMING_NOISY_DELAY_MS + MAX_MESSAGE_HANDLING_DELAY_MS);
// Verify disconnection has been cancelled and we're seeing two connections attempts,
// with the device connected at the end of the test
verify(mSpyDevInventory, times(2)).onSetBtActiveDevice(
any(AudioDeviceBroker.BtDeviceInfo.class), anyInt());
Assert.assertTrue("Mock device not connected",
mSpyDevInventory.isA2dpDeviceConnected(mFakeBtDevice));
if (guaranteeSingleConnection) {
// when the disconnection was expected to be cancelled, there should have been a single
// call to AudioSystem to declare the device connected (available)
checkSingleSystemConnection(mFakeBtDevice);
}
}
/**
* Verifies the given device was reported to AudioSystem exactly once as available
* @param btDevice
* @throws Exception
*/
private void checkSingleSystemConnection(BluetoothDevice btDevice) throws Exception {
final String expectedName = btDevice.getName() == null ? "" : btDevice.getName();
AudioDeviceAttributes expected = new AudioDeviceAttributes(
AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, btDevice.getAddress(), expectedName);
verify(mSpyAudioSystem, times(1)).setDeviceConnectionState(
ArgumentMatchers.argThat(x -> x.equalTypeAddress(expected)),
ArgumentMatchers.eq(AudioSystem.DEVICE_STATE_AVAILABLE),
anyInt() /*codec*/);
}
}