diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java index 00fc6d06245c6..75d9dd8174875 100644 --- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java +++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java @@ -77,6 +77,9 @@ public class AudioDeviceInventory { // List of preferred devices for strategies private final ArrayMap mPreferredDevices = new ArrayMap<>(); + // the wrapper for AudioSystem static methods, allows us to spy AudioSystem + private final @NonNull AudioSystemAdapter mAudioSystem; + private @NonNull AudioDeviceBroker mDeviceBroker; // Monitoring of audio routes. Protected by mAudioRoutes. @@ -86,12 +89,14 @@ public class AudioDeviceInventory { /*package*/ AudioDeviceInventory(@NonNull AudioDeviceBroker broker) { mDeviceBroker = broker; + mAudioSystem = AudioSystemAdapter.getDefaultAdapter(); } //----------------------------------------------------------- - /** for mocking only */ - /*package*/ AudioDeviceInventory() { + /** for mocking only, allows to inject AudioSystem adapter */ + /*package*/ AudioDeviceInventory(@NonNull AudioSystemAdapter audioSystem) { mDeviceBroker = null; + mAudioSystem = audioSystem; } /*package*/ void setDeviceBroker(@NonNull AudioDeviceBroker broker) { @@ -185,7 +190,7 @@ public class AudioDeviceInventory { synchronized (mDevicesLock) { //TODO iterate on mApmConnectedDevices instead once it handles all device types for (DeviceInfo di : mConnectedDevices.values()) { - AudioSystem.setDeviceConnectionState( + mAudioSystem.setDeviceConnectionState( di.mDeviceType, AudioSystem.DEVICE_STATE_AVAILABLE, di.mDeviceAddress, @@ -195,7 +200,7 @@ public class AudioDeviceInventory { } synchronized (mPreferredDevices) { mPreferredDevices.forEach((strategy, device) -> { - AudioSystem.setPreferredDeviceForStrategy(strategy, device); }); + mAudioSystem.setPreferredDeviceForStrategy(strategy, device); }); } } @@ -355,7 +360,7 @@ public class AudioDeviceInventory { mConnectedDevices.replace(key, di); } } - final int res = AudioSystem.handleDeviceConfigChange( + final int res = mAudioSystem.handleDeviceConfigChange( AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, address, BtHelper.getName(btDevice), a2dpCodec); @@ -477,7 +482,7 @@ public class AudioDeviceInventory { /*package*/ int setPreferredDeviceForStrategySync(int strategy, @NonNull AudioDeviceAddress device) { final long identity = Binder.clearCallingIdentity(); - final int status = AudioSystem.setPreferredDeviceForStrategy(strategy, device); + final int status = mAudioSystem.setPreferredDeviceForStrategy(strategy, device); Binder.restoreCallingIdentity(identity); if (status == AudioSystem.SUCCESS) { @@ -488,7 +493,7 @@ public class AudioDeviceInventory { /*package*/ int removePreferredDeviceForStrategySync(int strategy) { final long identity = Binder.clearCallingIdentity(); - final int status = AudioSystem.removePreferredDeviceForStrategy(strategy); + final int status = mAudioSystem.removePreferredDeviceForStrategy(strategy); Binder.restoreCallingIdentity(identity); if (status == AudioSystem.SUCCESS) { @@ -523,7 +528,7 @@ public class AudioDeviceInventory { Slog.i(TAG, "deviceInfo:" + di + " is(already)Connected:" + isConnected); } if (connect && !isConnected) { - final int res = AudioSystem.setDeviceConnectionState(device, + final int res = mAudioSystem.setDeviceConnectionState(device, AudioSystem.DEVICE_STATE_AVAILABLE, address, deviceName, AudioSystem.AUDIO_FORMAT_DEFAULT); if (res != AudioSystem.AUDIO_STATUS_OK) { @@ -536,7 +541,7 @@ public class AudioDeviceInventory { mDeviceBroker.postAccessoryPlugMediaUnmute(device); return true; } else if (!connect && isConnected) { - AudioSystem.setDeviceConnectionState(device, + mAudioSystem.setDeviceConnectionState(device, AudioSystem.DEVICE_STATE_UNAVAILABLE, address, deviceName, AudioSystem.AUDIO_FORMAT_DEFAULT); // always remove even if disconnection failed @@ -713,7 +718,7 @@ public class AudioDeviceInventory { mDeviceBroker.setBluetoothA2dpOnInt(true, eventSource); // at this point there could be another A2DP device already connected in APM, but it // doesn't matter as this new one will overwrite the previous one - final int res = AudioSystem.setDeviceConnectionState(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, + final int res = mAudioSystem.setDeviceConnectionState(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, AudioSystem.DEVICE_STATE_AVAILABLE, address, name, a2dpCodec); if (res != AudioSystem.AUDIO_STATUS_OK) { @@ -728,7 +733,7 @@ public class AudioDeviceInventory { } // Reset A2DP suspend state each time a new sink is connected - AudioSystem.setParameters("A2dpSuspended=false"); + mAudioSystem.setParameters("A2dpSuspended=false"); final DeviceInfo di = new DeviceInfo(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, name, address, a2dpCodec); @@ -761,7 +766,7 @@ public class AudioDeviceInventory { // device to remove was visible by APM, update APM mDeviceBroker.setAvrcpAbsoluteVolumeSupported(false); - final int res = AudioSystem.setDeviceConnectionState(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, + final int res = mAudioSystem.setDeviceConnectionState(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, AudioSystem.DEVICE_STATE_UNAVAILABLE, address, "", a2dpCodec); if (res != AudioSystem.AUDIO_STATUS_OK) { @@ -783,7 +788,7 @@ public class AudioDeviceInventory { private void makeA2dpDeviceUnavailableLater(String address, int delayMs) { // prevent any activity on the A2DP audio output to avoid unwanted // reconnection of the sink. - AudioSystem.setParameters("A2dpSuspended=true"); + mAudioSystem.setParameters("A2dpSuspended=true"); // retrieve DeviceInfo before removing device final String deviceKey = DeviceInfo.makeDeviceListKey(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, address); @@ -799,7 +804,7 @@ public class AudioDeviceInventory { @GuardedBy("mDevicesLock") private void makeA2dpSrcAvailable(String address) { - AudioSystem.setDeviceConnectionState(AudioSystem.DEVICE_IN_BLUETOOTH_A2DP, + mAudioSystem.setDeviceConnectionState(AudioSystem.DEVICE_IN_BLUETOOTH_A2DP, AudioSystem.DEVICE_STATE_AVAILABLE, address, "", AudioSystem.AUDIO_FORMAT_DEFAULT); mConnectedDevices.put( @@ -810,7 +815,7 @@ public class AudioDeviceInventory { @GuardedBy("mDevicesLock") private void makeA2dpSrcUnavailable(String address) { - AudioSystem.setDeviceConnectionState(AudioSystem.DEVICE_IN_BLUETOOTH_A2DP, + mAudioSystem.setDeviceConnectionState(AudioSystem.DEVICE_IN_BLUETOOTH_A2DP, AudioSystem.DEVICE_STATE_UNAVAILABLE, address, "", AudioSystem.AUDIO_FORMAT_DEFAULT); mConnectedDevices.remove( @@ -824,7 +829,7 @@ public class AudioDeviceInventory { AudioSystem.DEVICE_OUT_HEARING_AID); mDeviceBroker.postSetHearingAidVolumeIndex(hearingAidVolIndex, streamType); - AudioSystem.setDeviceConnectionState(AudioSystem.DEVICE_OUT_HEARING_AID, + mAudioSystem.setDeviceConnectionState(AudioSystem.DEVICE_OUT_HEARING_AID, AudioSystem.DEVICE_STATE_AVAILABLE, address, name, AudioSystem.AUDIO_FORMAT_DEFAULT); mConnectedDevices.put( @@ -839,7 +844,7 @@ public class AudioDeviceInventory { @GuardedBy("mDevicesLock") private void makeHearingAidDeviceUnavailable(String address) { - AudioSystem.setDeviceConnectionState(AudioSystem.DEVICE_OUT_HEARING_AID, + mAudioSystem.setDeviceConnectionState(AudioSystem.DEVICE_OUT_HEARING_AID, AudioSystem.DEVICE_STATE_UNAVAILABLE, address, "", AudioSystem.AUDIO_FORMAT_DEFAULT); mConnectedDevices.remove( diff --git a/services/core/java/com/android/server/audio/AudioSystemAdapter.java b/services/core/java/com/android/server/audio/AudioSystemAdapter.java new file mode 100644 index 0000000000000..9d06b4257b62b --- /dev/null +++ b/services/core/java/com/android/server/audio/AudioSystemAdapter.java @@ -0,0 +1,155 @@ +/* + * 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 android.annotation.NonNull; +import android.media.AudioDeviceAddress; +import android.media.AudioSystem; +import android.util.Log; + +/** + * Provides an adapter to access functionality of the android.media.AudioSystem class for device + * related functionality. + * Use the "real" AudioSystem through the default adapter. + * Use the "always ok" adapter to avoid dealing with the APM behaviors during a test. + */ +public class AudioSystemAdapter { + + /** + * Create a wrapper around the {@link AudioSystem} static methods, all functions are directly + * forwarded to the AudioSystem class. + * @return an adapter around AudioSystem + */ + static final @NonNull AudioSystemAdapter getDefaultAdapter() { + return new AudioSystemAdapter(); + } + + /** + * Create an adapter for AudioSystem that always succeeds, and does nothing. + * @return a no-op AudioSystem adapter + */ + static final @NonNull AudioSystemAdapter getAlwaysOkAdapter() { + return new AudioSystemOkAdapter(); + } + + /** + * Same as {@link AudioSystem#setDeviceConnectionState(int, int, String, String, int)} + * @param device + * @param state + * @param deviceAddress + * @param deviceName + * @param codecFormat + * @return + */ + public int setDeviceConnectionState(int device, int state, String deviceAddress, + String deviceName, int codecFormat) { + return AudioSystem.setDeviceConnectionState(device, state, deviceAddress, deviceName, + codecFormat); + } + + /** + * Same as {@link AudioSystem#getDeviceConnectionState(int, String)} + * @param device + * @param deviceAddress + * @return + */ + public int getDeviceConnectionState(int device, String deviceAddress) { + return AudioSystem.getDeviceConnectionState(device, deviceAddress); + } + + /** + * Same as {@link AudioSystem#handleDeviceConfigChange(int, String, String, int)} + * @param device + * @param deviceAddress + * @param deviceName + * @param codecFormat + * @return + */ + public int handleDeviceConfigChange(int device, String deviceAddress, + String deviceName, int codecFormat) { + return AudioSystem.handleDeviceConfigChange(device, deviceAddress, deviceName, + codecFormat); + } + + /** + * Same as {@link AudioSystem#setPreferredDeviceForStrategy(int, AudioDeviceAddress)} + * @param strategy + * @param device + * @return + */ + public int setPreferredDeviceForStrategy(int strategy, @NonNull AudioDeviceAddress device) { + return AudioSystem.setPreferredDeviceForStrategy(strategy, device); + } + + /** + * Same as {@link AudioSystem#removePreferredDeviceForStrategy(int)} + * @param strategy + * @return + */ + public int removePreferredDeviceForStrategy(int strategy) { + return AudioSystem.removePreferredDeviceForStrategy(strategy); + } + + /** + * Same as {@link AudioSystem#setParameters(String)} + * @param keyValuePairs + * @return + */ + public int setParameters(String keyValuePairs) { + return AudioSystem.setParameters(keyValuePairs); + } + + //-------------------------------------------------------------------- + protected static class AudioSystemOkAdapter extends AudioSystemAdapter { + private static final String TAG = "ASA"; + + @Override + public int setDeviceConnectionState(int device, int state, String deviceAddress, + String deviceName, int codecFormat) { + Log.i(TAG, String.format("setDeviceConnectionState(0x%s, %s, %s, 0x%s", + Integer.toHexString(device), state, deviceAddress, deviceName, + Integer.toHexString(codecFormat))); + return AudioSystem.AUDIO_STATUS_OK; + } + + @Override + public int getDeviceConnectionState(int device, String deviceAddress) { + return AudioSystem.AUDIO_STATUS_OK; + } + + @Override + public int handleDeviceConfigChange(int device, String deviceAddress, + String deviceName, int codecFormat) { + return AudioSystem.AUDIO_STATUS_OK; + } + + @Override + public int setPreferredDeviceForStrategy(int strategy, @NonNull AudioDeviceAddress device) { + return AudioSystem.AUDIO_STATUS_OK; + } + + @Override + public int removePreferredDeviceForStrategy(int strategy) { + return AudioSystem.AUDIO_STATUS_OK; + } + + @Override + public int setParameters(String keyValuePairs) { + return AudioSystem.AUDIO_STATUS_OK; + } + } +} diff --git a/services/tests/servicestests/src/com/android/server/audio/AudioDeviceBrokerTest.java b/services/tests/servicestests/src/com/android/server/audio/AudioDeviceBrokerTest.java index 5c2ad94720f01..73a191dc78ccc 100644 --- a/services/tests/servicestests/src/com/android/server/audio/AudioDeviceBrokerTest.java +++ b/services/tests/servicestests/src/com/android/server/audio/AudioDeviceBrokerTest.java @@ -57,6 +57,7 @@ public class AudioDeviceBrokerTest { @Mock private AudioService mMockAudioService; @Spy private AudioDeviceInventory mSpyDevInventory; + @Spy private AudioSystemAdapter mSpyAudioSystem; private BluetoothDevice mFakeBtDevice; @@ -65,7 +66,8 @@ public class AudioDeviceBrokerTest { mContext = InstrumentationRegistry.getTargetContext(); mMockAudioService = mock(AudioService.class); - mSpyDevInventory = spy(new AudioDeviceInventory()); + mSpyAudioSystem = spy(AudioSystemAdapter.getAlwaysOkAdapter()); + mSpyDevInventory = spy(new AudioDeviceInventory(mSpyAudioSystem)); mAudioDeviceBroker = new AudioDeviceBroker(mContext, mMockAudioService, mSpyDevInventory); mSpyDevInventory.setDeviceBroker(mAudioDeviceBroker); @@ -81,8 +83,9 @@ public class AudioDeviceBrokerTest { public void testSetUpAndTearDown() { } /** - * Verify call to postBluetoothA2dpDeviceConnectionStateSuppressNoisyIntent() for connection - * calls into AudioDeviceInventory with the right params + * 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 @@ -92,7 +95,7 @@ public class AudioDeviceBrokerTest { mAudioDeviceBroker.postBluetoothA2dpDeviceConnectionStateSuppressNoisyIntent(mFakeBtDevice, BluetoothProfile.STATE_CONNECTED, BluetoothProfile.A2DP, true, 1); - Thread.sleep(MAX_MESSAGE_HANDLING_DELAY_MS); + Thread.sleep(2 * MAX_MESSAGE_HANDLING_DELAY_MS); verify(mSpyDevInventory, times(1)).setBluetoothA2dpDeviceConnectionState( any(BluetoothDevice.class), ArgumentMatchers.eq(BluetoothProfile.STATE_CONNECTED) /*state*/, @@ -100,6 +103,14 @@ public class AudioDeviceBrokerTest { ArgumentMatchers.eq(true) /*suppressNoisyIntent*/, anyInt() /*musicDevice*/, ArgumentMatchers.eq(1) /*a2dpVolume*/ ); + + final String expectedName = mFakeBtDevice.getName() == null ? "" : mFakeBtDevice.getName(); + verify(mSpyAudioSystem, times(1)).setDeviceConnectionState( + ArgumentMatchers.eq(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP), + ArgumentMatchers.eq(AudioSystem.DEVICE_STATE_AVAILABLE), + ArgumentMatchers.eq(mFakeBtDevice.getAddress()), + ArgumentMatchers.eq(expectedName), + anyInt() /*codec*/); } /**