1 /* 2 * Copyright 2019 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.server.audio; 17 18 import static org.mockito.Mockito.any; 19 import static org.mockito.Mockito.anyInt; 20 import static org.mockito.Mockito.doNothing; 21 import static org.mockito.Mockito.mock; 22 import static org.mockito.Mockito.spy; 23 import static org.mockito.Mockito.times; 24 import static org.mockito.Mockito.verify; 25 import static org.mockito.Mockito.when; 26 27 import android.bluetooth.BluetoothAdapter; 28 import android.bluetooth.BluetoothDevice; 29 import android.content.Context; 30 import android.content.Intent; 31 import android.media.AudioDeviceAttributes; 32 import android.media.AudioDeviceInfo; 33 import android.media.AudioManager; 34 import android.media.AudioSystem; 35 import android.media.BluetoothProfileConnectionInfo; 36 import android.util.Log; 37 38 import androidx.test.filters.MediumTest; 39 import androidx.test.platform.app.InstrumentationRegistry; 40 import androidx.test.runner.AndroidJUnit4; 41 42 import org.junit.After; 43 import org.junit.Assert; 44 import org.junit.Before; 45 import org.junit.Test; 46 import org.junit.runner.RunWith; 47 import org.mockito.ArgumentMatchers; 48 import org.mockito.Mock; 49 import org.mockito.Spy; 50 51 @MediumTest 52 @RunWith(AndroidJUnit4.class) 53 public class AudioDeviceBrokerTest { 54 55 private static final String TAG = "AudioDeviceBrokerTest"; 56 private static final int MAX_MESSAGE_HANDLING_DELAY_MS = 100; 57 58 // the actual class under test 59 private AudioDeviceBroker mAudioDeviceBroker; 60 61 @Mock private AudioService mMockAudioService; 62 @Spy private AudioDeviceInventory mSpyDevInventory; 63 @Spy private AudioSystemAdapter mSpyAudioSystem; 64 @Spy private SystemServerAdapter mSpySystemServer; 65 66 private BluetoothDevice mFakeBtDevice; 67 68 @Before setUp()69 public void setUp() throws Exception { 70 Context context = InstrumentationRegistry.getInstrumentation().getTargetContext(); 71 72 mMockAudioService = mock(AudioService.class); 73 mSpyAudioSystem = spy(new NoOpAudioSystemAdapter()); 74 mSpyDevInventory = spy(new AudioDeviceInventory(mSpyAudioSystem)); 75 mSpySystemServer = spy(new NoOpSystemServerAdapter()); 76 mAudioDeviceBroker = new AudioDeviceBroker(context, mMockAudioService, mSpyDevInventory, 77 mSpySystemServer, mSpyAudioSystem); 78 mSpyDevInventory.setDeviceBroker(mAudioDeviceBroker); 79 80 BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); 81 mFakeBtDevice = adapter.getRemoteDevice("00:01:02:03:04:05"); 82 Assert.assertNotNull("invalid null BT device", mFakeBtDevice); 83 } 84 85 @After tearDown()86 public void tearDown() throws Exception { } 87 88 // @Test 89 // public void testSetUpAndTearDown() { } 90 91 /** 92 * postBluetoothA2dpDeviceConnectionStateSuppressNoisyIntent() for connection: 93 * - verify it calls into AudioDeviceInventory with the right params 94 * - verify it calls into AudioSystem and stays connected (no 2nd call to disconnect) 95 * @throws Exception 96 */ 97 @Test testPostA2dpDeviceConnectionChange()98 public void testPostA2dpDeviceConnectionChange() throws Exception { 99 Log.i(TAG, "starting testPostA2dpDeviceConnectionChange"); 100 Assert.assertNotNull("invalid null BT device", mFakeBtDevice); 101 102 mAudioDeviceBroker.queueOnBluetoothActiveDeviceChanged( 103 new AudioDeviceBroker.BtDeviceChangedData(mFakeBtDevice, null, 104 BluetoothProfileConnectionInfo.createA2dpInfo(true, 1), "testSource")); 105 Thread.sleep(2 * MAX_MESSAGE_HANDLING_DELAY_MS); 106 verify(mSpyDevInventory, times(1)).setBluetoothActiveDevice( 107 any(AudioDeviceBroker.BtDeviceInfo.class) 108 ); 109 110 // verify the connection was reported to AudioSystem 111 checkSingleSystemConnection(mFakeBtDevice); 112 } 113 114 /** 115 * Verify call to postBluetoothA2dpDeviceConnectionStateSuppressNoisyIntent() for 116 * connection > pause > disconnection > connection 117 * keeps the device connected 118 * @throws Exception 119 */ 120 @Test testA2dpDeviceConnectionDisconnectionConnectionChange()121 public void testA2dpDeviceConnectionDisconnectionConnectionChange() throws Exception { 122 Log.i(TAG, "starting testA2dpDeviceConnectionDisconnectionConnectionChange"); 123 124 doTestConnectionDisconnectionReconnection(0, false, 125 // cannot guarantee single connection since commands are posted in separate thread 126 // than they are processed 127 false); 128 } 129 130 /** 131 * Verify device disconnection and reconnection within the BECOMING_NOISY window 132 * in the absence of media playback 133 * @throws Exception 134 */ 135 @Test testA2dpDeviceReconnectionWithinBecomingNoisyDelay()136 public void testA2dpDeviceReconnectionWithinBecomingNoisyDelay() throws Exception { 137 Log.i(TAG, "starting testA2dpDeviceReconnectionWithinBecomingNoisyDelay"); 138 139 doTestConnectionDisconnectionReconnection(AudioService.BECOMING_NOISY_DELAY_MS / 2, 140 false, 141 // do not check single connection since the connection command will come much 142 // after the disconnection command 143 false); 144 } 145 146 /** 147 * Same as testA2dpDeviceConnectionDisconnectionConnectionChange() but with mock media playback 148 * @throws Exception 149 */ 150 @Test testA2dpConnectionDisconnectionConnectionChange_MediaPlayback()151 public void testA2dpConnectionDisconnectionConnectionChange_MediaPlayback() throws Exception { 152 Log.i(TAG, "starting testA2dpConnectionDisconnectionConnectionChange_MediaPlayback"); 153 154 doTestConnectionDisconnectionReconnection(0, true, 155 // guarantee single connection since because of media playback the disconnection 156 // is supposed to be delayed, and thus cancelled because of the connection 157 true); 158 } 159 160 /** 161 * Same as testA2dpDeviceReconnectionWithinBecomingNoisyDelay() but with mock media playback 162 * @throws Exception 163 */ 164 @Test testA2dpReconnectionWithinBecomingNoisyDelay_MediaPlayback()165 public void testA2dpReconnectionWithinBecomingNoisyDelay_MediaPlayback() throws Exception { 166 Log.i(TAG, "starting testA2dpReconnectionWithinBecomingNoisyDelay_MediaPlayback"); 167 168 doTestConnectionDisconnectionReconnection(AudioService.BECOMING_NOISY_DELAY_MS / 2, 169 true, 170 // guarantee single connection since because of media playback the disconnection 171 // is supposed to be delayed, and thus cancelled because of the connection 172 true); 173 } 174 175 /** 176 * Test that device wired state intents are broadcasted on connection state change 177 * @throws Exception 178 */ 179 @Test testSetWiredDeviceConnectionState()180 public void testSetWiredDeviceConnectionState() throws Exception { 181 Log.i(TAG, "starting postSetWiredDeviceConnectionState"); 182 183 final String address = "testAddress"; 184 final String name = "testName"; 185 final String caller = "testCaller"; 186 187 doNothing().when(mSpySystemServer).broadcastStickyIntentToCurrentProfileGroup( 188 any(Intent.class)); 189 190 mSpyDevInventory.setWiredDeviceConnectionState(new AudioDeviceAttributes( 191 AudioSystem.DEVICE_OUT_WIRED_HEADSET, address, name), 192 AudioService.CONNECTION_STATE_CONNECTED, caller); 193 Thread.sleep(MAX_MESSAGE_HANDLING_DELAY_MS); 194 195 // Verify that the sticky intent is broadcasted 196 verify(mSpySystemServer, times(1)).broadcastStickyIntentToCurrentProfileGroup( 197 any(Intent.class)); 198 } 199 200 /** 201 * Test that constructing an AdiDeviceState instance requires a non-null address for a 202 * wireless type, but can take null for a non-wireless type; 203 * @throws Exception 204 */ 205 @Test testAdiDeviceStateNullAddressCtor()206 public void testAdiDeviceStateNullAddressCtor() throws Exception { 207 try { 208 new AdiDeviceState(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER, 209 AudioManager.DEVICE_OUT_SPEAKER, null); 210 new AdiDeviceState(AudioDeviceInfo.TYPE_BLUETOOTH_A2DP, 211 AudioManager.DEVICE_OUT_BLUETOOTH_A2DP, null); 212 Assert.fail(); 213 } catch (NullPointerException e) { } 214 } 215 216 @Test testAdiDeviceStateStringSerialization()217 public void testAdiDeviceStateStringSerialization() throws Exception { 218 Log.i(TAG, "starting testAdiDeviceStateStringSerialization"); 219 final AdiDeviceState devState = new AdiDeviceState( 220 AudioDeviceInfo.TYPE_BUILTIN_SPEAKER, AudioManager.DEVICE_OUT_SPEAKER, "bla"); 221 devState.setHasHeadTracker(false); 222 devState.setHeadTrackerEnabled(false); 223 devState.setSAEnabled(true); 224 final String persistString = devState.toPersistableString(); 225 final AdiDeviceState result = AdiDeviceState.fromPersistedString(persistString); 226 Log.i(TAG, "original:" + devState); 227 Log.i(TAG, "result :" + result); 228 Assert.assertEquals(devState, result); 229 } 230 doTestConnectionDisconnectionReconnection(int delayAfterDisconnection, boolean mockMediaPlayback, boolean guaranteeSingleConnection)231 private void doTestConnectionDisconnectionReconnection(int delayAfterDisconnection, 232 boolean mockMediaPlayback, boolean guaranteeSingleConnection) throws Exception { 233 when(mMockAudioService.getDeviceForStream(AudioManager.STREAM_MUSIC)) 234 .thenReturn(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP); 235 when(mMockAudioService.isInCommunication()).thenReturn(false); 236 when(mMockAudioService.hasMediaDynamicPolicy()).thenReturn(false); 237 when(mMockAudioService.hasAudioFocusUsers()).thenReturn(false); 238 239 ((NoOpAudioSystemAdapter) mSpyAudioSystem).configureIsStreamActive(mockMediaPlayback); 240 241 // first connection: ensure the device is connected as a starting condition for the test 242 mAudioDeviceBroker.queueOnBluetoothActiveDeviceChanged( 243 new AudioDeviceBroker.BtDeviceChangedData(mFakeBtDevice, null, 244 BluetoothProfileConnectionInfo.createA2dpInfo(true, 1), "testSource")); 245 Thread.sleep(MAX_MESSAGE_HANDLING_DELAY_MS); 246 247 // disconnection 248 mAudioDeviceBroker.queueOnBluetoothActiveDeviceChanged( 249 new AudioDeviceBroker.BtDeviceChangedData(null, mFakeBtDevice, 250 BluetoothProfileConnectionInfo.createA2dpInfo(false, -1), "testSource")); 251 if (delayAfterDisconnection > 0) { 252 Thread.sleep(delayAfterDisconnection); 253 } 254 255 // reconnection 256 mAudioDeviceBroker.queueOnBluetoothActiveDeviceChanged( 257 new AudioDeviceBroker.BtDeviceChangedData(mFakeBtDevice, null, 258 BluetoothProfileConnectionInfo.createA2dpInfo(true, 2), "testSource")); 259 Thread.sleep(AudioService.BECOMING_NOISY_DELAY_MS + MAX_MESSAGE_HANDLING_DELAY_MS); 260 261 // Verify disconnection has been cancelled and we're seeing two connections attempts, 262 // with the device connected at the end of the test 263 verify(mSpyDevInventory, times(2)).onSetBtActiveDevice( 264 any(AudioDeviceBroker.BtDeviceInfo.class), anyInt() /*codec*/, 265 anyInt() /*streamType*/); 266 Assert.assertTrue("Mock device not connected", 267 mSpyDevInventory.isA2dpDeviceConnected(mFakeBtDevice)); 268 269 if (guaranteeSingleConnection) { 270 // when the disconnection was expected to be cancelled, there should have been a single 271 // call to AudioSystem to declare the device connected (available) 272 checkSingleSystemConnection(mFakeBtDevice); 273 } 274 } 275 276 /** 277 * Verifies the given device was reported to AudioSystem exactly once as available 278 * @param btDevice 279 * @throws Exception 280 */ checkSingleSystemConnection(BluetoothDevice btDevice)281 private void checkSingleSystemConnection(BluetoothDevice btDevice) throws Exception { 282 final String expectedName = btDevice.getName() == null ? "" : btDevice.getName(); 283 AudioDeviceAttributes expected = new AudioDeviceAttributes( 284 AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, btDevice.getAddress(), expectedName); 285 verify(mSpyAudioSystem, times(1)).setDeviceConnectionState( 286 ArgumentMatchers.argThat(x -> x.equalTypeAddress(expected)), 287 ArgumentMatchers.eq(AudioSystem.DEVICE_STATE_AVAILABLE), 288 anyInt() /*codec*/); 289 } 290 } 291