| /* |
| * Copyright 2020 HIMSA II K/S - www.himsa.com. |
| * Represented by EHIMA - www.ehima.com |
| * |
| * 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.vc; |
| |
| import static org.mockito.Mockito.*; |
| |
| import android.bluetooth.BluetoothAdapter; |
| import android.bluetooth.BluetoothDevice; |
| import android.bluetooth.BluetoothProfile; |
| import android.bluetooth.BluetoothUuid; |
| import android.bluetooth.BluetoothVolumeControl; |
| import android.bluetooth.IBluetoothVolumeControlCallback; |
| import android.content.AttributionSource; |
| import android.content.BroadcastReceiver; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.IntentFilter; |
| import android.media.AudioManager; |
| import android.os.Binder; |
| import android.os.Looper; |
| import android.os.ParcelUuid; |
| |
| import androidx.test.InstrumentationRegistry; |
| import androidx.test.filters.MediumTest; |
| import androidx.test.rule.ServiceTestRule; |
| import androidx.test.runner.AndroidJUnit4; |
| |
| import com.android.bluetooth.btservice.AdapterService; |
| import com.android.bluetooth.btservice.ServiceFactory; |
| import com.android.bluetooth.btservice.storage.DatabaseManager; |
| import com.android.bluetooth.csip.CsipSetCoordinatorService; |
| import com.android.bluetooth.TestUtils; |
| import com.android.bluetooth.x.com.android.modules.utils.SynchronousResultReceiver; |
| |
| import org.junit.After; |
| import org.junit.Assert; |
| import org.junit.Before; |
| import org.junit.Rule; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| import org.mockito.Mock; |
| import org.mockito.Mockito; |
| import org.mockito.MockitoAnnotations; |
| |
| import java.time.Duration; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.concurrent.LinkedBlockingQueue; |
| import java.util.concurrent.TimeoutException; |
| import java.util.stream.IntStream; |
| |
| @MediumTest |
| @RunWith(AndroidJUnit4.class) |
| public class VolumeControlServiceTest { |
| private BluetoothAdapter mAdapter; |
| private AttributionSource mAttributionSource; |
| private Context mTargetContext; |
| private VolumeControlService mService; |
| private VolumeControlService.BluetoothVolumeControlBinder mServiceBinder; |
| private BluetoothDevice mDevice; |
| private BluetoothDevice mDeviceTwo; |
| private HashMap<BluetoothDevice, LinkedBlockingQueue<Intent>> mDeviceQueueMap; |
| private static final int TIMEOUT_MS = 1000; |
| private static final int BT_LE_AUDIO_MAX_VOL = 255; |
| private static final int MEDIA_MIN_VOL = 0; |
| private static final int MEDIA_MAX_VOL = 25; |
| private static final int CALL_MIN_VOL = 1; |
| private static final int CALL_MAX_VOL = 8; |
| |
| private BroadcastReceiver mVolumeControlIntentReceiver; |
| |
| @Mock private AdapterService mAdapterService; |
| @Mock private DatabaseManager mDatabaseManager; |
| @Mock private VolumeControlNativeInterface mNativeInterface; |
| @Mock private AudioManager mAudioManager; |
| @Mock private ServiceFactory mServiceFactory; |
| @Mock private CsipSetCoordinatorService mCsipService; |
| |
| @Rule public final ServiceTestRule mServiceRule = new ServiceTestRule(); |
| |
| @Before |
| public void setUp() throws Exception { |
| mTargetContext = InstrumentationRegistry.getTargetContext(); |
| // Set up mocks and test assets |
| MockitoAnnotations.initMocks(this); |
| |
| if (Looper.myLooper() == null) { |
| Looper.prepare(); |
| } |
| |
| TestUtils.setAdapterService(mAdapterService); |
| doReturn(mDatabaseManager).when(mAdapterService).getDatabase(); |
| doReturn(true, false).when(mAdapterService).isStartedProfile(anyString()); |
| |
| mAdapter = BluetoothAdapter.getDefaultAdapter(); |
| mAttributionSource = mAdapter.getAttributionSource(); |
| |
| doReturn(MEDIA_MIN_VOL).when(mAudioManager) |
| .getStreamMinVolume(eq(AudioManager.STREAM_MUSIC)); |
| doReturn(MEDIA_MAX_VOL).when(mAudioManager) |
| .getStreamMaxVolume(eq(AudioManager.STREAM_MUSIC)); |
| doReturn(CALL_MIN_VOL).when(mAudioManager) |
| .getStreamMinVolume(eq(AudioManager.STREAM_VOICE_CALL)); |
| doReturn(CALL_MAX_VOL).when(mAudioManager) |
| .getStreamMaxVolume(eq(AudioManager.STREAM_VOICE_CALL)); |
| |
| VolumeControlNativeInterface.setInstance(mNativeInterface); |
| startService(); |
| mService.mAudioManager = mAudioManager; |
| mService.mFactory = mServiceFactory; |
| mServiceBinder = (VolumeControlService.BluetoothVolumeControlBinder) mService.initBinder(); |
| mServiceBinder.mIsTesting = true; |
| |
| doReturn(mCsipService).when(mServiceFactory).getCsipSetCoordinatorService(); |
| |
| // Override the timeout value to speed up the test |
| VolumeControlStateMachine.sConnectTimeoutMs = TIMEOUT_MS; // 1s |
| |
| // Set up the Connection State Changed receiver |
| IntentFilter filter = new IntentFilter(); |
| filter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY); |
| filter.addAction(BluetoothVolumeControl.ACTION_CONNECTION_STATE_CHANGED); |
| |
| mVolumeControlIntentReceiver = new VolumeControlIntentReceiver(); |
| mTargetContext.registerReceiver(mVolumeControlIntentReceiver, filter); |
| |
| // Get a device for testing |
| mDevice = TestUtils.getTestDevice(mAdapter, 0); |
| mDeviceTwo = TestUtils.getTestDevice(mAdapter, 1); |
| mDeviceQueueMap = new HashMap<>(); |
| mDeviceQueueMap.put(mDevice, new LinkedBlockingQueue<>()); |
| mDeviceQueueMap.put(mDeviceTwo, new LinkedBlockingQueue<>()); |
| doReturn(BluetoothDevice.BOND_BONDED).when(mAdapterService) |
| .getBondState(any(BluetoothDevice.class)); |
| doReturn(new ParcelUuid[]{BluetoothUuid.VOLUME_CONTROL}).when(mAdapterService) |
| .getRemoteUuids(any(BluetoothDevice.class)); |
| } |
| |
| @After |
| public void tearDown() throws Exception { |
| if (mService == null) { |
| return; |
| } |
| |
| stopService(); |
| VolumeControlNativeInterface.setInstance(null); |
| mTargetContext.unregisterReceiver(mVolumeControlIntentReceiver); |
| mDeviceQueueMap.clear(); |
| TestUtils.clearAdapterService(mAdapterService); |
| reset(mAudioManager); |
| } |
| |
| private void startService() throws TimeoutException { |
| TestUtils.startService(mServiceRule, VolumeControlService.class); |
| mService = VolumeControlService.getVolumeControlService(); |
| Assert.assertNotNull(mService); |
| } |
| |
| private void stopService() throws TimeoutException { |
| TestUtils.stopService(mServiceRule, VolumeControlService.class); |
| mService = VolumeControlService.getVolumeControlService(); |
| Assert.assertNull(mService); |
| } |
| |
| private class VolumeControlIntentReceiver extends BroadcastReceiver { |
| @Override |
| public void onReceive(Context context, Intent intent) { |
| try { |
| BluetoothDevice device = intent.getParcelableExtra( |
| BluetoothDevice.EXTRA_DEVICE); |
| Assert.assertNotNull(device); |
| LinkedBlockingQueue<Intent> queue = mDeviceQueueMap.get(device); |
| Assert.assertNotNull(queue); |
| queue.put(intent); |
| } catch (InterruptedException e) { |
| Assert.fail("Cannot add Intent to the Connection State queue: " |
| + e.getMessage()); |
| } |
| } |
| } |
| |
| private void verifyConnectionStateIntent(int timeoutMs, BluetoothDevice device, |
| int newState, int prevState) { |
| Intent intent = TestUtils.waitForIntent(timeoutMs, mDeviceQueueMap.get(device)); |
| Assert.assertNotNull(intent); |
| Assert.assertEquals(BluetoothVolumeControl.ACTION_CONNECTION_STATE_CHANGED, |
| intent.getAction()); |
| Assert.assertEquals(device, intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)); |
| Assert.assertEquals(newState, intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1)); |
| Assert.assertEquals(prevState, intent.getIntExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, |
| -1)); |
| } |
| |
| private void verifyNoConnectionStateIntent(int timeoutMs, BluetoothDevice device) { |
| Intent intent = TestUtils.waitForNoIntent(timeoutMs, mDeviceQueueMap.get(device)); |
| Assert.assertNull(intent); |
| } |
| |
| /** |
| * Test getting VolumeControl Service: getVolumeControlService() |
| */ |
| @Test |
| public void testGetVolumeControlService() { |
| Assert.assertEquals(mService, VolumeControlService.getVolumeControlService()); |
| } |
| |
| /** |
| * Test stop VolumeControl Service |
| */ |
| @Test |
| public void testStopVolumeControlService() throws Exception { |
| // Prepare: connect |
| connectDevice(mDevice); |
| // VolumeControl Service is already running: test stop(). |
| // Note: must be done on the main thread |
| InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() { |
| public void run() { |
| Assert.assertTrue(mService.stop()); |
| } |
| }); |
| // Try to restart the service. Note: must be done on the main thread |
| InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() { |
| public void run() { |
| Assert.assertTrue(mService.start()); |
| } |
| }); |
| } |
| |
| /** |
| * Test get/set policy for BluetoothDevice |
| */ |
| @Test |
| public void testGetSetPolicy() { |
| when(mAdapterService.getDatabase()).thenReturn(mDatabaseManager); |
| when(mDatabaseManager |
| .getProfileConnectionPolicy(mDevice, BluetoothProfile.VOLUME_CONTROL)) |
| .thenReturn(BluetoothProfile.CONNECTION_POLICY_UNKNOWN); |
| Assert.assertEquals("Initial device policy", |
| BluetoothProfile.CONNECTION_POLICY_UNKNOWN, |
| mService.getConnectionPolicy(mDevice)); |
| |
| when(mAdapterService.getDatabase()).thenReturn(mDatabaseManager); |
| when(mDatabaseManager |
| .getProfileConnectionPolicy(mDevice, BluetoothProfile.VOLUME_CONTROL)) |
| .thenReturn(BluetoothProfile.CONNECTION_POLICY_FORBIDDEN); |
| Assert.assertEquals("Setting device policy to POLICY_FORBIDDEN", |
| BluetoothProfile.CONNECTION_POLICY_FORBIDDEN, |
| mService.getConnectionPolicy(mDevice)); |
| |
| when(mAdapterService.getDatabase()).thenReturn(mDatabaseManager); |
| when(mDatabaseManager |
| .getProfileConnectionPolicy(mDevice, BluetoothProfile.VOLUME_CONTROL)) |
| .thenReturn(BluetoothProfile.CONNECTION_POLICY_ALLOWED); |
| Assert.assertEquals("Setting device policy to POLICY_ALLOWED", |
| BluetoothProfile.CONNECTION_POLICY_ALLOWED, |
| mService.getConnectionPolicy(mDevice)); |
| } |
| |
| /** |
| * Test if getProfileConnectionPolicy works after the service is stopped. |
| */ |
| @Test |
| public void testGetPolicyAfterStopped() throws Exception { |
| mService.stop(); |
| when(mDatabaseManager |
| .getProfileConnectionPolicy(mDevice, BluetoothProfile.VOLUME_CONTROL)) |
| .thenReturn(BluetoothProfile.CONNECTION_POLICY_UNKNOWN); |
| final SynchronousResultReceiver<Integer> recv = SynchronousResultReceiver.get(); |
| int defaultRecvValue = -1000; |
| mServiceBinder.getConnectionPolicy(mDevice, mAttributionSource, recv); |
| int policy = recv.awaitResultNoInterrupt(Duration.ofMillis(TIMEOUT_MS)) |
| .getValue(defaultRecvValue); |
| Assert.assertEquals("Initial device policy", |
| BluetoothProfile.CONNECTION_POLICY_UNKNOWN, policy); |
| } |
| |
| /** |
| * Test okToConnect method using various test cases |
| */ |
| @Test |
| public void testOkToConnect() { |
| int badPolicyValue = 1024; |
| int badBondState = 42; |
| testOkToConnectCase(mDevice, |
| BluetoothDevice.BOND_NONE, BluetoothProfile.CONNECTION_POLICY_UNKNOWN, false); |
| testOkToConnectCase(mDevice, |
| BluetoothDevice.BOND_NONE, BluetoothProfile.CONNECTION_POLICY_FORBIDDEN, false); |
| testOkToConnectCase(mDevice, |
| BluetoothDevice.BOND_NONE, BluetoothProfile.CONNECTION_POLICY_ALLOWED, false); |
| testOkToConnectCase(mDevice, |
| BluetoothDevice.BOND_NONE, badPolicyValue, false); |
| testOkToConnectCase(mDevice, |
| BluetoothDevice.BOND_BONDING, BluetoothProfile.CONNECTION_POLICY_UNKNOWN, false); |
| testOkToConnectCase(mDevice, |
| BluetoothDevice.BOND_BONDING, BluetoothProfile.CONNECTION_POLICY_FORBIDDEN, false); |
| testOkToConnectCase(mDevice, |
| BluetoothDevice.BOND_BONDING, BluetoothProfile.CONNECTION_POLICY_ALLOWED, false); |
| testOkToConnectCase(mDevice, |
| BluetoothDevice.BOND_BONDING, badPolicyValue, false); |
| testOkToConnectCase(mDevice, |
| BluetoothDevice.BOND_BONDED, BluetoothProfile.CONNECTION_POLICY_UNKNOWN, true); |
| testOkToConnectCase(mDevice, |
| BluetoothDevice.BOND_BONDED, BluetoothProfile.CONNECTION_POLICY_FORBIDDEN, false); |
| testOkToConnectCase(mDevice, |
| BluetoothDevice.BOND_BONDED, BluetoothProfile.CONNECTION_POLICY_ALLOWED, true); |
| testOkToConnectCase(mDevice, |
| BluetoothDevice.BOND_BONDED, badPolicyValue, false); |
| testOkToConnectCase(mDevice, |
| badBondState, BluetoothProfile.CONNECTION_POLICY_UNKNOWN, false); |
| testOkToConnectCase(mDevice, |
| badBondState, BluetoothProfile.CONNECTION_POLICY_FORBIDDEN, false); |
| testOkToConnectCase(mDevice, |
| badBondState, BluetoothProfile.CONNECTION_POLICY_ALLOWED, false); |
| testOkToConnectCase(mDevice, |
| badBondState, badPolicyValue, false); |
| } |
| |
| /** |
| * Test that an outgoing connection to device that does not have Volume Control UUID is rejected |
| */ |
| @Test |
| public void testOutgoingConnectMissingVolumeControlUuid() { |
| // Update the device policy so okToConnect() returns true |
| when(mAdapterService.getDatabase()).thenReturn(mDatabaseManager); |
| when(mDatabaseManager |
| .getProfileConnectionPolicy(mDevice, BluetoothProfile.VOLUME_CONTROL)) |
| .thenReturn(BluetoothProfile.CONNECTION_POLICY_ALLOWED); |
| doReturn(true).when(mNativeInterface).connectVolumeControl(any(BluetoothDevice.class)); |
| doReturn(true).when(mNativeInterface).disconnectVolumeControl(any(BluetoothDevice.class)); |
| |
| // Return No UUID |
| doReturn(new ParcelUuid[]{}).when(mAdapterService) |
| .getRemoteUuids(any(BluetoothDevice.class)); |
| |
| // Send a connect request |
| Assert.assertFalse("Connect expected to fail", mService.connect(mDevice)); |
| } |
| |
| /** |
| * Test that an outgoing connection to device that have Volume Control UUID is successful |
| */ |
| @Test |
| public void testOutgoingConnectDisconnectExistingVolumeControlUuid() throws Exception { |
| // Update the device policy so okToConnect() returns true |
| when(mAdapterService.getDatabase()).thenReturn(mDatabaseManager); |
| when(mDatabaseManager |
| .getProfileConnectionPolicy(mDevice, BluetoothProfile.VOLUME_CONTROL)) |
| .thenReturn(BluetoothProfile.CONNECTION_POLICY_ALLOWED); |
| doReturn(true).when(mNativeInterface).connectVolumeControl(any(BluetoothDevice.class)); |
| doReturn(true).when(mNativeInterface).disconnectVolumeControl(any(BluetoothDevice.class)); |
| |
| // Return Volume Control UUID |
| doReturn(new ParcelUuid[]{BluetoothUuid.VOLUME_CONTROL}).when(mAdapterService) |
| .getRemoteUuids(any(BluetoothDevice.class)); |
| |
| // Send a connect request via binder |
| SynchronousResultReceiver<Boolean> recv = SynchronousResultReceiver.get(); |
| mServiceBinder.connect(mDevice, mAttributionSource, recv); |
| Assert.assertTrue("Connect expected to succeed", |
| recv.awaitResultNoInterrupt(Duration.ofMillis(TIMEOUT_MS)).getValue(false)); |
| |
| // Verify the connection state broadcast, and that we are in Connecting state |
| verifyConnectionStateIntent(TIMEOUT_MS, mDevice, BluetoothProfile.STATE_CONNECTING, |
| BluetoothProfile.STATE_DISCONNECTED); |
| |
| // Send a disconnect request via binder |
| recv = SynchronousResultReceiver.get(); |
| mServiceBinder.disconnect(mDevice, mAttributionSource, recv); |
| Assert.assertTrue("Disconnect expected to succeed", |
| recv.awaitResultNoInterrupt(Duration.ofMillis(TIMEOUT_MS)).getValue(false)); |
| |
| // Verify the connection state broadcast, and that we are in Connecting state |
| verifyConnectionStateIntent(TIMEOUT_MS, mDevice, BluetoothProfile.STATE_DISCONNECTED, |
| BluetoothProfile.STATE_CONNECTING); |
| } |
| |
| /** |
| * Test that an outgoing connection to device with POLICY_FORBIDDEN is rejected |
| */ |
| @Test |
| public void testOutgoingConnectPolicyForbidden() { |
| doReturn(true).when(mNativeInterface).connectVolumeControl(any(BluetoothDevice.class)); |
| doReturn(true).when(mNativeInterface).disconnectVolumeControl(any(BluetoothDevice.class)); |
| |
| // Set the device policy to POLICY_FORBIDDEN so connect() should fail |
| when(mAdapterService.getDatabase()).thenReturn(mDatabaseManager); |
| when(mDatabaseManager |
| .getProfileConnectionPolicy(mDevice, BluetoothProfile.VOLUME_CONTROL)) |
| .thenReturn(BluetoothProfile.CONNECTION_POLICY_FORBIDDEN); |
| |
| // Send a connect request |
| Assert.assertFalse("Connect expected to fail", mService.connect(mDevice)); |
| } |
| |
| /** |
| * Test that an outgoing connection times out |
| */ |
| @Test |
| public void testOutgoingConnectTimeout() throws Exception { |
| // Update the device policy so okToConnect() returns true |
| when(mAdapterService.getDatabase()).thenReturn(mDatabaseManager); |
| when(mDatabaseManager |
| .getProfileConnectionPolicy(mDevice, BluetoothProfile.VOLUME_CONTROL)) |
| .thenReturn(BluetoothProfile.CONNECTION_POLICY_ALLOWED); |
| doReturn(true).when(mNativeInterface).connectVolumeControl(any(BluetoothDevice.class)); |
| doReturn(true).when(mNativeInterface).disconnectVolumeControl(any(BluetoothDevice.class)); |
| |
| // Send a connect request |
| Assert.assertTrue("Connect failed", mService.connect(mDevice)); |
| |
| // Verify the connection state broadcast, and that we are in Connecting state |
| verifyConnectionStateIntent(TIMEOUT_MS, mDevice, BluetoothProfile.STATE_CONNECTING, |
| BluetoothProfile.STATE_DISCONNECTED); |
| Assert.assertEquals(BluetoothProfile.STATE_CONNECTING, |
| mService.getConnectionState(mDevice)); |
| |
| // Verify the connection state broadcast, and that we are in Disconnected state |
| verifyConnectionStateIntent(VolumeControlStateMachine.sConnectTimeoutMs * 2, |
| mDevice, BluetoothProfile.STATE_DISCONNECTED, |
| BluetoothProfile.STATE_CONNECTING); |
| |
| final SynchronousResultReceiver<Integer> recv = SynchronousResultReceiver.get(); |
| int defaultRecvValue = -1000; |
| mServiceBinder.getConnectionState(mDevice, mAttributionSource, recv); |
| int state = recv.awaitResultNoInterrupt(Duration.ofMillis(TIMEOUT_MS)) |
| .getValue(defaultRecvValue); |
| Assert.assertEquals(BluetoothProfile.STATE_DISCONNECTED, state); |
| } |
| |
| /** |
| * Test that only CONNECTION_STATE_CONNECTED or CONNECTION_STATE_CONNECTING Volume Control stack |
| * events will create a state machine. |
| */ |
| @Test |
| public void testCreateStateMachineStackEvents() { |
| // Update the device policy so okToConnect() returns true |
| when(mAdapterService.getDatabase()).thenReturn(mDatabaseManager); |
| when(mDatabaseManager |
| .getProfileConnectionPolicy(mDevice, BluetoothProfile.VOLUME_CONTROL)) |
| .thenReturn(BluetoothProfile.CONNECTION_POLICY_ALLOWED); |
| doReturn(true).when(mNativeInterface).connectVolumeControl(any(BluetoothDevice.class)); |
| doReturn(true).when(mNativeInterface).disconnectVolumeControl(any(BluetoothDevice.class)); |
| |
| // stack event: CONNECTION_STATE_CONNECTING - state machine should be created |
| generateConnectionMessageFromNative(mDevice, BluetoothProfile.STATE_CONNECTING, |
| BluetoothProfile.STATE_DISCONNECTED); |
| Assert.assertEquals(BluetoothProfile.STATE_CONNECTING, |
| mService.getConnectionState(mDevice)); |
| Assert.assertTrue(mService.getDevices().contains(mDevice)); |
| |
| // stack event: CONNECTION_STATE_DISCONNECTED - state machine should be removed |
| generateConnectionMessageFromNative(mDevice, BluetoothProfile.STATE_DISCONNECTED, |
| BluetoothProfile.STATE_CONNECTING); |
| Assert.assertEquals(BluetoothProfile.STATE_DISCONNECTED, |
| mService.getConnectionState(mDevice)); |
| Assert.assertTrue(mService.getDevices().contains(mDevice)); |
| mService.bondStateChanged(mDevice, BluetoothDevice.BOND_NONE); |
| Assert.assertFalse(mService.getDevices().contains(mDevice)); |
| |
| // stack event: CONNECTION_STATE_CONNECTED - state machine should be created |
| generateConnectionMessageFromNative(mDevice, BluetoothProfile.STATE_CONNECTED, |
| BluetoothProfile.STATE_DISCONNECTED); |
| Assert.assertEquals(BluetoothProfile.STATE_CONNECTED, |
| mService.getConnectionState(mDevice)); |
| Assert.assertTrue(mService.getDevices().contains(mDevice)); |
| |
| // stack event: CONNECTION_STATE_DISCONNECTED - state machine should be removed |
| generateConnectionMessageFromNative(mDevice, BluetoothProfile.STATE_DISCONNECTED, |
| BluetoothProfile.STATE_CONNECTED); |
| Assert.assertEquals(BluetoothProfile.STATE_DISCONNECTED, |
| mService.getConnectionState(mDevice)); |
| Assert.assertTrue(mService.getDevices().contains(mDevice)); |
| mService.bondStateChanged(mDevice, BluetoothDevice.BOND_NONE); |
| Assert.assertFalse(mService.getDevices().contains(mDevice)); |
| |
| // stack event: CONNECTION_STATE_DISCONNECTING - state machine should not be created |
| generateUnexpectedConnectionMessageFromNative(mDevice, |
| BluetoothProfile.STATE_DISCONNECTING, |
| BluetoothProfile.STATE_DISCONNECTED); |
| Assert.assertEquals(BluetoothProfile.STATE_DISCONNECTED, |
| mService.getConnectionState(mDevice)); |
| Assert.assertFalse(mService.getDevices().contains(mDevice)); |
| |
| // stack event: CONNECTION_STATE_DISCONNECTED - state machine should not be created |
| generateUnexpectedConnectionMessageFromNative(mDevice, |
| BluetoothProfile.STATE_DISCONNECTED, |
| BluetoothProfile.STATE_DISCONNECTED); |
| Assert.assertEquals(BluetoothProfile.STATE_DISCONNECTED, |
| mService.getConnectionState(mDevice)); |
| Assert.assertFalse(mService.getDevices().contains(mDevice)); |
| } |
| |
| /** |
| * Test that a CONNECTION_STATE_DISCONNECTED Volume Control stack event will remove the state |
| * machine only if the device is unbond. |
| */ |
| @Test |
| public void testDeleteStateMachineDisconnectEvents() { |
| // Update the device policy so okToConnect() returns true |
| when(mAdapterService.getDatabase()).thenReturn(mDatabaseManager); |
| when(mDatabaseManager |
| .getProfileConnectionPolicy(mDevice, BluetoothProfile.VOLUME_CONTROL)) |
| .thenReturn(BluetoothProfile.CONNECTION_POLICY_ALLOWED); |
| doReturn(true).when(mNativeInterface).connectVolumeControl(any(BluetoothDevice.class)); |
| doReturn(true).when(mNativeInterface).disconnectVolumeControl(any(BluetoothDevice.class)); |
| |
| // stack event: CONNECTION_STATE_CONNECTING - state machine should be created |
| generateConnectionMessageFromNative(mDevice, BluetoothProfile.STATE_CONNECTING, |
| BluetoothProfile.STATE_DISCONNECTED); |
| Assert.assertEquals(BluetoothProfile.STATE_CONNECTING, |
| mService.getConnectionState(mDevice)); |
| Assert.assertTrue(mService.getDevices().contains(mDevice)); |
| |
| // stack event: CONNECTION_STATE_DISCONNECTED - state machine is not removed |
| generateConnectionMessageFromNative(mDevice, BluetoothProfile.STATE_DISCONNECTED, |
| BluetoothProfile.STATE_CONNECTING); |
| Assert.assertEquals(BluetoothProfile.STATE_DISCONNECTED, |
| mService.getConnectionState(mDevice)); |
| Assert.assertTrue(mService.getDevices().contains(mDevice)); |
| |
| // stack event: CONNECTION_STATE_CONNECTING - state machine remains |
| generateConnectionMessageFromNative(mDevice, BluetoothProfile.STATE_CONNECTING, |
| BluetoothProfile.STATE_DISCONNECTED); |
| Assert.assertEquals(BluetoothProfile.STATE_CONNECTING, |
| mService.getConnectionState(mDevice)); |
| Assert.assertTrue(mService.getDevices().contains(mDevice)); |
| |
| // device bond state marked as unbond - state machine is not removed |
| doReturn(BluetoothDevice.BOND_NONE).when(mAdapterService) |
| .getBondState(any(BluetoothDevice.class)); |
| Assert.assertTrue(mService.getDevices().contains(mDevice)); |
| |
| // stack event: CONNECTION_STATE_DISCONNECTED - state machine is removed |
| generateConnectionMessageFromNative(mDevice, BluetoothProfile.STATE_DISCONNECTED, |
| BluetoothProfile.STATE_CONNECTING); |
| Assert.assertEquals(BluetoothProfile.STATE_DISCONNECTED, |
| mService.getConnectionState(mDevice)); |
| Assert.assertFalse(mService.getDevices().contains(mDevice)); |
| } |
| |
| /** |
| * Test that various Volume Control stack events will broadcast related states. |
| */ |
| @Test |
| public void testVolumeControlStackEvents() { |
| int group_id = -1; |
| int volume = 6; |
| boolean mute = false; |
| |
| // Send a message to trigger volume state changed broadcast |
| VolumeControlStackEvent stackEvent = new VolumeControlStackEvent( |
| VolumeControlStackEvent.EVENT_TYPE_VOLUME_STATE_CHANGED); |
| stackEvent.device = mDevice; |
| stackEvent.valueInt1 = group_id; |
| stackEvent.valueInt2 = volume; |
| stackEvent.valueBool1 = mute; |
| mService.messageFromNative(stackEvent); |
| } |
| |
| int getLeAudioVolume(int index, int minIndex, int maxIndex, int streamType) { |
| // Note: This has to be the same as mBtHelper.setLeAudioVolume() |
| return (int) Math.round((double) index * BT_LE_AUDIO_MAX_VOL / maxIndex); |
| } |
| |
| void testVolumeCalculations(int streamType, int minIdx, int maxIdx) { |
| // Send a message to trigger volume state changed broadcast |
| final VolumeControlStackEvent stackEvent = new VolumeControlStackEvent( |
| VolumeControlStackEvent.EVENT_TYPE_VOLUME_STATE_CHANGED); |
| stackEvent.device = null; |
| stackEvent.valueInt1 = 1; // groupId |
| stackEvent.valueBool1 = false; // isMuted |
| stackEvent.valueBool2 = true; // isAutonomous |
| |
| IntStream.range(minIdx, maxIdx).forEach(idx -> { |
| // Given the reference volume index, set the LeAudio Volume |
| stackEvent.valueInt2 = getLeAudioVolume(idx, |
| mAudioManager.getStreamMinVolume(streamType), |
| mAudioManager.getStreamMaxVolume(streamType), streamType); |
| mService.messageFromNative(stackEvent); |
| |
| // Verify that setting LeAudio Volume, sets the original volume index to Audio FW |
| verify(mAudioManager, times(1)).setStreamVolume(eq(streamType), eq(idx), anyInt()); |
| }); |
| } |
| |
| @Test |
| public void testAutonomousVolumeStateChange() { |
| doReturn(AudioManager.MODE_IN_CALL).when(mAudioManager).getMode(); |
| testVolumeCalculations(AudioManager.STREAM_VOICE_CALL, CALL_MIN_VOL, CALL_MAX_VOL); |
| |
| doReturn(AudioManager.MODE_NORMAL).when(mAudioManager).getMode(); |
| testVolumeCalculations(AudioManager.STREAM_MUSIC, MEDIA_MIN_VOL, MEDIA_MAX_VOL); |
| } |
| |
| /** |
| * Test if autonomous Mute/Unmute propagates the event to audio manager. |
| */ |
| @Test |
| public void testAutonomousMuteUnmute() { |
| int streamType = AudioManager.STREAM_MUSIC; |
| int streamVol = getLeAudioVolume(19, MEDIA_MIN_VOL, MEDIA_MAX_VOL, streamType); |
| |
| // Send a message to trigger volume state changed broadcast |
| final VolumeControlStackEvent stackEvent = new VolumeControlStackEvent( |
| VolumeControlStackEvent.EVENT_TYPE_VOLUME_STATE_CHANGED); |
| stackEvent.device = null; |
| stackEvent.valueInt1 = 1; // groupId |
| stackEvent.valueInt2 = streamVol; |
| stackEvent.valueBool1 = false; // isMuted |
| stackEvent.valueBool2 = true; // isAutonomous |
| |
| doReturn(false).when(mAudioManager) |
| .isStreamMute(eq(AudioManager.STREAM_MUSIC)); |
| |
| // Verify that muting LeAudio device, sets the mute state on the audio device |
| stackEvent.valueBool1 = true; |
| mService.messageFromNative(stackEvent); |
| verify(mAudioManager, times(1)).adjustStreamVolume(eq(streamType), |
| eq(AudioManager.ADJUST_MUTE), anyInt()); |
| |
| doReturn(true).when(mAudioManager) |
| .isStreamMute(eq(AudioManager.STREAM_MUSIC)); |
| |
| // Verify that unmuting LeAudio device, unsets the mute state on the audio device |
| stackEvent.valueBool1 = false; |
| mService.messageFromNative(stackEvent); |
| verify(mAudioManager, times(1)).adjustStreamVolume(eq(streamType), |
| eq(AudioManager.ADJUST_UNMUTE), anyInt()); |
| } |
| |
| /** |
| * Test Volume Control cache. |
| */ |
| @Test |
| public void testVolumeCache() throws Exception { |
| int groupId = 1; |
| int volume = 6; |
| |
| Assert.assertEquals(-1, mService.getGroupVolume(groupId)); |
| final SynchronousResultReceiver<Void> voidRecv = SynchronousResultReceiver.get(); |
| mServiceBinder.setGroupVolume(groupId, volume, mAttributionSource, voidRecv); |
| voidRecv.awaitResultNoInterrupt(Duration.ofMillis(TIMEOUT_MS)); |
| |
| final SynchronousResultReceiver<Integer> intRecv = SynchronousResultReceiver.get(); |
| int defaultRecvValue = -100; |
| mServiceBinder.getGroupVolume(groupId, mAttributionSource, intRecv); |
| int groupVolume = intRecv.awaitResultNoInterrupt( |
| Duration.ofMillis(TIMEOUT_MS)).getValue(defaultRecvValue); |
| Assert.assertEquals(volume, groupVolume); |
| |
| volume = 10; |
| // Send autonomous volume change. |
| VolumeControlStackEvent stackEvent = new VolumeControlStackEvent( |
| VolumeControlStackEvent.EVENT_TYPE_VOLUME_STATE_CHANGED); |
| stackEvent.device = null; |
| stackEvent.valueInt1 = groupId; |
| stackEvent.valueInt2 = volume; |
| stackEvent.valueBool1 = false; |
| stackEvent.valueBool2 = true; /* autonomous */ |
| mService.messageFromNative(stackEvent); |
| |
| Assert.assertEquals(volume, mService.getGroupVolume(groupId)); |
| } |
| |
| /** |
| * Test Volume Control Mute cache. |
| */ |
| @Test |
| public void testMuteCache() throws Exception { |
| int groupId = 1; |
| int volume = 6; |
| |
| Assert.assertEquals(false, mService.getGroupMute(groupId)); |
| |
| // Send autonomous volume change |
| VolumeControlStackEvent stackEvent = new VolumeControlStackEvent( |
| VolumeControlStackEvent.EVENT_TYPE_VOLUME_STATE_CHANGED); |
| stackEvent.device = null; |
| stackEvent.valueInt1 = groupId; |
| stackEvent.valueInt2 = volume; |
| stackEvent.valueBool1 = false; /* unmuted */ |
| stackEvent.valueBool2 = true; /* autonomous */ |
| mService.messageFromNative(stackEvent); |
| |
| // Mute |
| final SynchronousResultReceiver<Void> voidRecv = SynchronousResultReceiver.get(); |
| mServiceBinder.muteGroup(groupId, mAttributionSource, voidRecv); |
| voidRecv.awaitResultNoInterrupt(Duration.ofMillis(TIMEOUT_MS)); |
| Assert.assertEquals(true, mService.getGroupMute(groupId)); |
| |
| // Make sure the volume is kept even when muted |
| Assert.assertEquals(volume, mService.getGroupVolume(groupId)); |
| |
| // Send autonomous unmute |
| stackEvent.valueBool1 = false; /* unmuted */ |
| mService.messageFromNative(stackEvent); |
| |
| Assert.assertEquals(false, mService.getGroupMute(groupId)); |
| } |
| |
| /** |
| * Test Volume Control with muted stream. |
| */ |
| @Test |
| public void testVolumeChangeWhileMuted() throws Exception { |
| int groupId = 1; |
| int volume = 6; |
| |
| Assert.assertEquals(false, mService.getGroupMute(groupId)); |
| |
| // Set the initial volume state |
| VolumeControlStackEvent stackEvent = new VolumeControlStackEvent( |
| VolumeControlStackEvent.EVENT_TYPE_VOLUME_STATE_CHANGED); |
| stackEvent.device = null; |
| stackEvent.valueInt1 = groupId; |
| stackEvent.valueInt2 = volume; |
| stackEvent.valueBool1 = false; /* unmuted */ |
| stackEvent.valueBool2 = true; /* autonomous */ |
| mService.messageFromNative(stackEvent); |
| |
| // Mute |
| mService.muteGroup(groupId); |
| Assert.assertEquals(true, mService.getGroupMute(groupId)); |
| verify(mNativeInterface, times(1)).muteGroup(eq(groupId)); |
| |
| // Make sure the volume is kept even when muted |
| doReturn(true).when(mAudioManager) |
| .isStreamMute(eq(AudioManager.STREAM_MUSIC)); |
| Assert.assertEquals(volume, mService.getGroupVolume(groupId)); |
| |
| // Lower the volume and keep it mute |
| mService.setGroupVolume(groupId, --volume); |
| Assert.assertEquals(true, mService.getGroupMute(groupId)); |
| verify(mNativeInterface, times(1)).setGroupVolume(eq(groupId), eq(volume)); |
| verify(mNativeInterface, times(0)).unmuteGroup(eq(groupId)); |
| |
| // Don't unmute on consecutive calls either |
| mService.setGroupVolume(groupId, --volume); |
| Assert.assertEquals(true, mService.getGroupMute(groupId)); |
| verify(mNativeInterface, times(1)).setGroupVolume(eq(groupId), eq(volume)); |
| verify(mNativeInterface, times(0)).unmuteGroup(eq(groupId)); |
| |
| // Raise the volume and unmute |
| volume += 10; // avoid previous volume levels and simplify mock verification |
| doReturn(false).when(mAudioManager) |
| .isStreamMute(eq(AudioManager.STREAM_MUSIC)); |
| mService.setGroupVolume(groupId, ++volume); |
| Assert.assertEquals(false, mService.getGroupMute(groupId)); |
| verify(mNativeInterface, times(1)).setGroupVolume(eq(groupId), eq(volume)); |
| // Verify the number of unmute calls after the second volume change |
| mService.setGroupVolume(groupId, ++volume); |
| Assert.assertEquals(false, mService.getGroupMute(groupId)); |
| verify(mNativeInterface, times(1)).setGroupVolume(eq(groupId), eq(volume)); |
| // Make sure we unmuted only once |
| verify(mNativeInterface, times(1)).unmuteGroup(eq(groupId)); |
| } |
| |
| /** |
| * Test setting volume for a group member who connects after the volume level |
| * for a group was already changed and cached. |
| */ |
| @Test |
| public void testLateConnectingDevice() throws Exception { |
| int groupId = 1; |
| int groupVolume = 56; |
| |
| // Both devices are in the same group |
| when(mCsipService.getGroupId(mDevice, BluetoothUuid.CAP)).thenReturn(groupId); |
| when(mCsipService.getGroupId(mDeviceTwo, BluetoothUuid.CAP)).thenReturn(groupId); |
| |
| // Update the device policy so okToConnect() returns true |
| when(mAdapterService.getDatabase()).thenReturn(mDatabaseManager); |
| when(mDatabaseManager |
| .getProfileConnectionPolicy(any(BluetoothDevice.class), |
| eq(BluetoothProfile.VOLUME_CONTROL))) |
| .thenReturn(BluetoothProfile.CONNECTION_POLICY_ALLOWED); |
| doReturn(true).when(mNativeInterface).connectVolumeControl(any(BluetoothDevice.class)); |
| doReturn(true).when(mNativeInterface).disconnectVolumeControl(any(BluetoothDevice.class)); |
| |
| generateConnectionMessageFromNative(mDevice, BluetoothProfile.STATE_CONNECTED, |
| BluetoothProfile.STATE_DISCONNECTED); |
| Assert.assertEquals(BluetoothProfile.STATE_CONNECTED, |
| mService.getConnectionState(mDevice)); |
| Assert.assertTrue(mService.getDevices().contains(mDevice)); |
| |
| mService.setGroupVolume(groupId, groupVolume); |
| verify(mNativeInterface, times(1)).setGroupVolume(eq(groupId), eq(groupVolume)); |
| verify(mNativeInterface, times(0)).setVolume(eq(mDeviceTwo), eq(groupVolume)); |
| |
| // Verify that second device gets the proper group volume level when connected |
| generateConnectionMessageFromNative(mDeviceTwo, BluetoothProfile.STATE_CONNECTED, |
| BluetoothProfile.STATE_DISCONNECTED); |
| Assert.assertEquals(BluetoothProfile.STATE_CONNECTED, |
| mService.getConnectionState(mDeviceTwo)); |
| Assert.assertTrue(mService.getDevices().contains(mDeviceTwo)); |
| verify(mNativeInterface, times(1)).setVolume(eq(mDeviceTwo), eq(groupVolume)); |
| } |
| |
| /** |
| * Test setting volume for a new group member who is discovered after the volume level |
| * for a group was already changed and cached. |
| */ |
| @Test |
| public void testLateDiscoveredGroupMember() throws Exception { |
| int groupId = 1; |
| int groupVolume = 56; |
| |
| // For now only one device is in the group |
| when(mCsipService.getGroupId(mDevice, BluetoothUuid.CAP)).thenReturn(groupId); |
| when(mCsipService.getGroupId(mDeviceTwo, BluetoothUuid.CAP)).thenReturn(-1); |
| |
| // Update the device policy so okToConnect() returns true |
| when(mAdapterService.getDatabase()).thenReturn(mDatabaseManager); |
| when(mDatabaseManager |
| .getProfileConnectionPolicy(any(BluetoothDevice.class), |
| eq(BluetoothProfile.VOLUME_CONTROL))) |
| .thenReturn(BluetoothProfile.CONNECTION_POLICY_ALLOWED); |
| doReturn(true).when(mNativeInterface).connectVolumeControl(any(BluetoothDevice.class)); |
| doReturn(true).when(mNativeInterface).disconnectVolumeControl(any(BluetoothDevice.class)); |
| |
| generateConnectionMessageFromNative(mDevice, BluetoothProfile.STATE_CONNECTED, |
| BluetoothProfile.STATE_DISCONNECTED); |
| Assert.assertEquals(BluetoothProfile.STATE_CONNECTED, |
| mService.getConnectionState(mDevice)); |
| Assert.assertTrue(mService.getDevices().contains(mDevice)); |
| |
| // Set the group volume |
| mService.setGroupVolume(groupId, groupVolume); |
| |
| // Verify that second device will not get the group volume level if it is not a group member |
| generateConnectionMessageFromNative(mDeviceTwo, BluetoothProfile.STATE_CONNECTED, |
| BluetoothProfile.STATE_DISCONNECTED); |
| Assert.assertEquals(BluetoothProfile.STATE_CONNECTED, |
| mService.getConnectionState(mDeviceTwo)); |
| Assert.assertTrue(mService.getDevices().contains(mDeviceTwo)); |
| verify(mNativeInterface, times(0)).setVolume(eq(mDeviceTwo), eq(groupVolume)); |
| |
| // But gets the volume when it becomes the group member |
| when(mCsipService.getGroupId(mDeviceTwo, BluetoothUuid.CAP)).thenReturn(groupId); |
| mService.handleGroupNodeAdded(groupId, mDeviceTwo); |
| verify(mNativeInterface, times(1)).setVolume(eq(mDeviceTwo), eq(groupVolume)); |
| } |
| |
| /** |
| * Test setting volume to 0 for a group member who connects after the volume level |
| * for a group was already changed and cached. LeAudio has no knowledge of mute |
| * for anything else than telephony, thus setting volume level to 0 is considered |
| * as muting. |
| */ |
| @Test |
| public void testMuteLateConnectingDevice() throws Exception { |
| int groupId = 1; |
| int volume = 100; |
| |
| // Both devices are in the same group |
| when(mCsipService.getGroupId(mDevice, BluetoothUuid.CAP)).thenReturn(groupId); |
| when(mCsipService.getGroupId(mDeviceTwo, BluetoothUuid.CAP)).thenReturn(groupId); |
| |
| // Update the device policy so okToConnect() returns true |
| when(mAdapterService.getDatabase()).thenReturn(mDatabaseManager); |
| when(mDatabaseManager |
| .getProfileConnectionPolicy(any(BluetoothDevice.class), |
| eq(BluetoothProfile.VOLUME_CONTROL))) |
| .thenReturn(BluetoothProfile.CONNECTION_POLICY_ALLOWED); |
| doReturn(true).when(mNativeInterface).connectVolumeControl(any(BluetoothDevice.class)); |
| doReturn(true).when(mNativeInterface).disconnectVolumeControl(any(BluetoothDevice.class)); |
| |
| generateConnectionMessageFromNative(mDevice, BluetoothProfile.STATE_CONNECTED, |
| BluetoothProfile.STATE_DISCONNECTED); |
| Assert.assertEquals(BluetoothProfile.STATE_CONNECTED, |
| mService.getConnectionState(mDevice)); |
| Assert.assertTrue(mService.getDevices().contains(mDevice)); |
| |
| // Set the initial volume and mute conditions |
| doReturn(true).when(mAudioManager).isStreamMute(anyInt()); |
| mService.setGroupVolume(groupId, volume); |
| |
| verify(mNativeInterface, times(1)).setGroupVolume(eq(groupId), eq(volume)); |
| verify(mNativeInterface, times(0)).setVolume(eq(mDeviceTwo), eq(volume)); |
| // Check if it was muted |
| verify(mNativeInterface, times(1)).muteGroup(eq(groupId)); |
| |
| Assert.assertEquals(true, mService.getGroupMute(groupId)); |
| |
| // Verify that second device gets the proper group volume level when connected |
| generateConnectionMessageFromNative(mDeviceTwo, BluetoothProfile.STATE_CONNECTED, |
| BluetoothProfile.STATE_DISCONNECTED); |
| Assert.assertEquals(BluetoothProfile.STATE_CONNECTED, |
| mService.getConnectionState(mDeviceTwo)); |
| Assert.assertTrue(mService.getDevices().contains(mDeviceTwo)); |
| verify(mNativeInterface, times(1)).setVolume(eq(mDeviceTwo), eq(volume)); |
| // Check if new device was muted |
| verify(mNativeInterface, times(1)).mute(eq(mDeviceTwo)); |
| } |
| |
| /** |
| * Test setting volume to 0 for a new group member who is discovered after the volume level |
| * for a group was already changed and cached. LeAudio has no knowledge of mute |
| * for anything else than telephony, thus setting volume level to 0 is considered |
| * as muting. |
| */ |
| @Test |
| public void testMuteLateDiscoveredGroupMember() throws Exception { |
| int groupId = 1; |
| int volume = 100; |
| |
| // For now only one device is in the group |
| when(mCsipService.getGroupId(mDevice, BluetoothUuid.CAP)).thenReturn(groupId); |
| when(mCsipService.getGroupId(mDeviceTwo, BluetoothUuid.CAP)).thenReturn(-1); |
| |
| // Update the device policy so okToConnect() returns true |
| when(mAdapterService.getDatabase()).thenReturn(mDatabaseManager); |
| when(mDatabaseManager |
| .getProfileConnectionPolicy(any(BluetoothDevice.class), |
| eq(BluetoothProfile.VOLUME_CONTROL))) |
| .thenReturn(BluetoothProfile.CONNECTION_POLICY_ALLOWED); |
| doReturn(true).when(mNativeInterface).connectVolumeControl(any(BluetoothDevice.class)); |
| doReturn(true).when(mNativeInterface).disconnectVolumeControl(any(BluetoothDevice.class)); |
| |
| generateConnectionMessageFromNative(mDevice, BluetoothProfile.STATE_CONNECTED, |
| BluetoothProfile.STATE_DISCONNECTED); |
| Assert.assertEquals(BluetoothProfile.STATE_CONNECTED, |
| mService.getConnectionState(mDevice)); |
| Assert.assertTrue(mService.getDevices().contains(mDevice)); |
| |
| // Set the initial volume and mute conditions |
| doReturn(true).when(mAudioManager).isStreamMute(anyInt()); |
| mService.setGroupVolume(groupId, volume); |
| |
| // Verify that second device will not get the group volume level if it is not a group member |
| generateConnectionMessageFromNative(mDeviceTwo, BluetoothProfile.STATE_CONNECTED, |
| BluetoothProfile.STATE_DISCONNECTED); |
| Assert.assertEquals(BluetoothProfile.STATE_CONNECTED, |
| mService.getConnectionState(mDeviceTwo)); |
| Assert.assertTrue(mService.getDevices().contains(mDeviceTwo)); |
| verify(mNativeInterface, times(0)).setVolume(eq(mDeviceTwo), eq(volume)); |
| // Check if it was not muted |
| verify(mNativeInterface, times(0)).mute(eq(mDeviceTwo)); |
| |
| // But gets the volume when it becomes the group member |
| when(mCsipService.getGroupId(mDeviceTwo, BluetoothUuid.CAP)).thenReturn(groupId); |
| mService.handleGroupNodeAdded(groupId, mDeviceTwo); |
| verify(mNativeInterface, times(1)).setVolume(eq(mDeviceTwo), eq(volume)); |
| verify(mNativeInterface, times(1)).mute(eq(mDeviceTwo)); |
| } |
| |
| @Test |
| public void testServiceBinderGetDevicesMatchingConnectionStates() throws Exception { |
| final SynchronousResultReceiver<List<BluetoothDevice>> recv = |
| SynchronousResultReceiver.get(); |
| mServiceBinder.getDevicesMatchingConnectionStates(null, mAttributionSource, recv); |
| List<BluetoothDevice> devices = recv.awaitResultNoInterrupt(Duration.ofMillis(TIMEOUT_MS)) |
| .getValue(null); |
| Assert.assertEquals(0, devices.size()); |
| } |
| |
| @Test |
| public void testServiceBinderSetConnectionPolicy() throws Exception { |
| final SynchronousResultReceiver<Boolean> recv = SynchronousResultReceiver.get(); |
| boolean defaultRecvValue = false; |
| mServiceBinder.setConnectionPolicy( |
| mDevice, BluetoothProfile.CONNECTION_POLICY_UNKNOWN, mAttributionSource, recv); |
| Assert.assertTrue(recv.awaitResultNoInterrupt(Duration.ofMillis(TIMEOUT_MS)) |
| .getValue(defaultRecvValue)); |
| verify(mDatabaseManager).setProfileConnectionPolicy( |
| mDevice, BluetoothProfile.VOLUME_CONTROL, BluetoothProfile.CONNECTION_POLICY_UNKNOWN); |
| } |
| |
| @Test |
| public void testServiceBinderVolumeOffsetMethods() throws Exception { |
| // Send a message to trigger connection completed |
| VolumeControlStackEvent event = new VolumeControlStackEvent( |
| VolumeControlStackEvent.EVENT_TYPE_DEVICE_AVAILABLE); |
| event.device = mDevice; |
| event.valueInt1 = 2; // number of external outputs |
| mService.messageFromNative(event); |
| |
| final SynchronousResultReceiver<Boolean> boolRecv = SynchronousResultReceiver.get(); |
| boolean defaultRecvValue = false; |
| mServiceBinder.isVolumeOffsetAvailable(mDevice, mAttributionSource, boolRecv); |
| Assert.assertTrue(boolRecv.awaitResultNoInterrupt(Duration.ofMillis(TIMEOUT_MS)) |
| .getValue(defaultRecvValue)); |
| |
| int volumeOffset = 100; |
| final SynchronousResultReceiver<Void> voidRecv = SynchronousResultReceiver.get(); |
| mServiceBinder.setVolumeOffset(mDevice, volumeOffset, mAttributionSource, voidRecv); |
| voidRecv.awaitResultNoInterrupt(Duration.ofMillis(TIMEOUT_MS)); |
| verify(mNativeInterface).setExtAudioOutVolumeOffset(mDevice, 1, volumeOffset); |
| } |
| |
| @Test |
| public void testServiceBinderRegisterUnregisterCallback() throws Exception { |
| IBluetoothVolumeControlCallback callback = |
| Mockito.mock(IBluetoothVolumeControlCallback.class); |
| Binder binder = Mockito.mock(Binder.class); |
| when(callback.asBinder()).thenReturn(binder); |
| |
| int size = mService.mCallbacks.getRegisteredCallbackCount(); |
| SynchronousResultReceiver<Void> recv = SynchronousResultReceiver.get(); |
| mServiceBinder.registerCallback(callback, mAttributionSource, recv); |
| recv.awaitResultNoInterrupt(Duration.ofMillis(TIMEOUT_MS)).getValue(null); |
| Assert.assertEquals(size + 1, mService.mCallbacks.getRegisteredCallbackCount()); |
| |
| recv = SynchronousResultReceiver.get(); |
| mServiceBinder.unregisterCallback(callback, mAttributionSource, recv); |
| recv.awaitResultNoInterrupt(Duration.ofMillis(TIMEOUT_MS)).getValue(null); |
| Assert.assertEquals(size, mService.mCallbacks.getRegisteredCallbackCount()); |
| } |
| |
| @Test |
| public void testServiceBinderMuteMethods() throws Exception { |
| SynchronousResultReceiver<Void> voidRecv = SynchronousResultReceiver.get(); |
| mServiceBinder.mute(mDevice, mAttributionSource, voidRecv); |
| voidRecv.awaitResultNoInterrupt(Duration.ofMillis(TIMEOUT_MS)); |
| verify(mNativeInterface).mute(mDevice); |
| |
| voidRecv = SynchronousResultReceiver.get(); |
| mServiceBinder.unmute(mDevice, mAttributionSource, voidRecv); |
| voidRecv.awaitResultNoInterrupt(Duration.ofMillis(TIMEOUT_MS)); |
| verify(mNativeInterface).unmute(mDevice); |
| |
| int groupId = 1; |
| voidRecv = SynchronousResultReceiver.get(); |
| mServiceBinder.muteGroup(groupId, mAttributionSource, voidRecv); |
| voidRecv.awaitResultNoInterrupt(Duration.ofMillis(TIMEOUT_MS)); |
| verify(mNativeInterface).muteGroup(groupId); |
| |
| voidRecv = SynchronousResultReceiver.get(); |
| mServiceBinder.unmuteGroup(groupId, mAttributionSource, voidRecv); |
| voidRecv.awaitResultNoInterrupt(Duration.ofMillis(TIMEOUT_MS)); |
| verify(mNativeInterface).unmuteGroup(groupId); |
| } |
| |
| @Test |
| public void testVolumeControlOffsetDescriptor() { |
| VolumeControlService.VolumeControlOffsetDescriptor descriptor = |
| new VolumeControlService.VolumeControlOffsetDescriptor(); |
| int invalidId = -1; |
| int validId = 10; |
| int testValue = 100; |
| String testDesc = "testDescription"; |
| int testLocation = 10000; |
| |
| Assert.assertEquals(0, descriptor.size()); |
| descriptor.add(validId); |
| Assert.assertEquals(1, descriptor.size()); |
| |
| Assert.assertFalse(descriptor.setValue(invalidId, testValue)); |
| Assert.assertTrue(descriptor.setValue(validId, testValue)); |
| Assert.assertEquals(0, descriptor.getValue(invalidId)); |
| Assert.assertEquals(testValue, descriptor.getValue(validId)); |
| |
| Assert.assertFalse(descriptor.setDescription(invalidId, testDesc)); |
| Assert.assertTrue(descriptor.setDescription(validId, testDesc)); |
| Assert.assertEquals(null, descriptor.getDescription(invalidId)); |
| Assert.assertEquals(testDesc, descriptor.getDescription(validId)); |
| |
| Assert.assertFalse(descriptor.setLocation(invalidId, testLocation)); |
| Assert.assertTrue(descriptor.setLocation(validId, testLocation)); |
| Assert.assertEquals(0, descriptor.getLocation(invalidId)); |
| Assert.assertEquals(testLocation, descriptor.getLocation(validId)); |
| |
| StringBuilder sb = new StringBuilder(); |
| descriptor.dump(sb); |
| Assert.assertTrue(sb.toString().contains(testDesc)); |
| |
| descriptor.add(validId + 1); |
| Assert.assertEquals(2, descriptor.size()); |
| descriptor.remove(validId); |
| Assert.assertEquals(1, descriptor.size()); |
| descriptor.clear(); |
| Assert.assertEquals(0, descriptor.size()); |
| } |
| |
| @Test |
| public void testDump_doesNotCrash() throws Exception { |
| connectDevice(mDevice); |
| |
| StringBuilder sb = new StringBuilder(); |
| mService.dump(sb); |
| } |
| |
| private void connectDevice(BluetoothDevice device) throws Exception { |
| VolumeControlStackEvent connCompletedEvent; |
| |
| List<BluetoothDevice> prevConnectedDevices = mService.getConnectedDevices(); |
| |
| // Update the device policy so okToConnect() returns true |
| when(mAdapterService.getDatabase()).thenReturn(mDatabaseManager); |
| when(mDatabaseManager.getProfileConnectionPolicy(device, BluetoothProfile.VOLUME_CONTROL)) |
| .thenReturn(BluetoothProfile.CONNECTION_POLICY_ALLOWED); |
| doReturn(true).when(mNativeInterface).connectVolumeControl(device); |
| doReturn(true).when(mNativeInterface).disconnectVolumeControl(device); |
| |
| // Send a connect request |
| Assert.assertTrue("Connect failed", mService.connect(device)); |
| |
| // Verify the connection state broadcast, and that we are in Connecting state |
| verifyConnectionStateIntent(TIMEOUT_MS, device, BluetoothProfile.STATE_CONNECTING, |
| BluetoothProfile.STATE_DISCONNECTED); |
| Assert.assertEquals(BluetoothProfile.STATE_CONNECTING, |
| mService.getConnectionState(device)); |
| |
| // Send a message to trigger connection completed |
| connCompletedEvent = new VolumeControlStackEvent( |
| VolumeControlStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED); |
| connCompletedEvent.device = device; |
| connCompletedEvent.valueInt1 = VolumeControlStackEvent.CONNECTION_STATE_CONNECTED; |
| mService.messageFromNative(connCompletedEvent); |
| |
| // Verify the connection state broadcast, and that we are in Connected state |
| verifyConnectionStateIntent(TIMEOUT_MS, device, BluetoothProfile.STATE_CONNECTED, |
| BluetoothProfile.STATE_CONNECTING); |
| Assert.assertEquals(BluetoothProfile.STATE_CONNECTED, |
| mService.getConnectionState(device)); |
| |
| // Verify that the device is in the list of connected devices |
| final SynchronousResultReceiver<List<BluetoothDevice>> recv = |
| SynchronousResultReceiver.get(); |
| mServiceBinder.getConnectedDevices(mAttributionSource, recv); |
| List<BluetoothDevice> connectedDevices = |
| recv.awaitResultNoInterrupt(Duration.ofMillis(TIMEOUT_MS)).getValue(null); |
| Assert.assertTrue(connectedDevices.contains(device)); |
| // Verify the list of previously connected devices |
| for (BluetoothDevice prevDevice : prevConnectedDevices) { |
| Assert.assertTrue(connectedDevices.contains(prevDevice)); |
| } |
| } |
| |
| private void generateConnectionMessageFromNative(BluetoothDevice device, int newConnectionState, |
| int oldConnectionState) { |
| VolumeControlStackEvent stackEvent = |
| new VolumeControlStackEvent( |
| VolumeControlStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED); |
| stackEvent.device = device; |
| stackEvent.valueInt1 = newConnectionState; |
| mService.messageFromNative(stackEvent); |
| // Verify the connection state broadcast |
| verifyConnectionStateIntent(TIMEOUT_MS, device, newConnectionState, oldConnectionState); |
| } |
| |
| private void generateUnexpectedConnectionMessageFromNative(BluetoothDevice device, |
| int newConnectionState, int oldConnectionState) { |
| VolumeControlStackEvent stackEvent = |
| new VolumeControlStackEvent( |
| VolumeControlStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED); |
| stackEvent.device = device; |
| stackEvent.valueInt1 = newConnectionState; |
| mService.messageFromNative(stackEvent); |
| // Verify the connection state broadcast |
| verifyNoConnectionStateIntent(TIMEOUT_MS, device); |
| } |
| |
| /** |
| * Helper function to test okToConnect() method |
| * |
| * @param device test device |
| * @param bondState bond state value, could be invalid |
| * @param policy value, could be invalid |
| * @param expected expected result from okToConnect() |
| */ |
| private void testOkToConnectCase(BluetoothDevice device, int bondState, int policy, |
| boolean expected) { |
| doReturn(bondState).when(mAdapterService).getBondState(device); |
| when(mAdapterService.getDatabase()).thenReturn(mDatabaseManager); |
| when(mDatabaseManager.getProfileConnectionPolicy(device, BluetoothProfile.VOLUME_CONTROL)) |
| .thenReturn(policy); |
| Assert.assertEquals(expected, mService.okToConnect(device)); |
| } |
| } |