Loading services/core/java/com/android/server/audio/AudioDeviceInventory.java +54 −15 Original line number Diff line number Diff line Loading @@ -31,6 +31,7 @@ import static android.media.AudioSystem.isBluetoothScoOutDevice; import static android.media.audio.Flags.automaticBtDeviceType; import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE; import static com.android.media.audio.Flags.asDeviceConnectionFailure; import android.annotation.NonNull; import android.annotation.Nullable; Loading Loading @@ -529,6 +530,17 @@ public class AudioDeviceInventory { } }; /** * package-protected for unit testing only * Returns the currently connected devices * @return the collection of connected devices */ /*package*/ @NonNull Collection<DeviceInfo> getConnectedDevices() { synchronized (mDevicesLock) { return mConnectedDevices.values(); } } // List of devices actually connected to AudioPolicy (through AudioSystem), only one // by device type, which is used as the key, value is the DeviceInfo generated key. // For the moment only for A2DP sink devices. Loading Loading @@ -598,8 +610,9 @@ public class AudioDeviceInventory { /** * Class to store info about connected devices. * Use makeDeviceListKey() to make a unique key for this list. * Package-protected for unit tests */ private static class DeviceInfo { /*package*/ static class DeviceInfo { final int mDeviceType; final @NonNull String mDeviceName; final @NonNull String mDeviceAddress; Loading Loading @@ -762,13 +775,27 @@ public class AudioDeviceInventory { // Always executed on AudioDeviceBroker message queue /*package*/ void onRestoreDevices() { synchronized (mDevicesLock) { int res; List<DeviceInfo> failedReconnectionDeviceList = new ArrayList<>(/*initialCapacity*/ 0); //TODO iterate on mApmConnectedDevices instead once it handles all device types for (DeviceInfo di : mConnectedDevices.values()) { mAudioSystem.setDeviceConnectionState(new AudioDeviceAttributes(di.mDeviceType, res = mAudioSystem.setDeviceConnectionState(new AudioDeviceAttributes( di.mDeviceType, di.mDeviceAddress, di.mDeviceName), AudioSystem.DEVICE_STATE_AVAILABLE, di.mDeviceCodecFormat); if (asDeviceConnectionFailure() && res != AudioSystem.AUDIO_STATUS_OK) { failedReconnectionDeviceList.add(di); } } if (asDeviceConnectionFailure()) { for (DeviceInfo di : failedReconnectionDeviceList) { AudioService.sDeviceLogger.enqueueAndSlog( "Device inventory restore failed to reconnect " + di, EventLogger.Event.ALOGE, TAG); mConnectedDevices.remove(di.getKey(), di); } } mAppliedStrategyRolesInt.clear(); mAppliedPresetRolesInt.clear(); Loading Loading @@ -2070,8 +2097,9 @@ public class AudioDeviceInventory { "APM failed to make available A2DP device addr=" + Utils.anonymizeBluetoothAddress(address) + " error=" + res).printSlog(EventLogger.Event.ALOGE, TAG)); // TODO: connection failed, stop here // TODO: return; if (asDeviceConnectionFailure()) { return; } } else { AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent( "A2DP sink device addr=" + Utils.anonymizeBluetoothAddress(address) Loading Loading @@ -2336,8 +2364,7 @@ public class AudioDeviceInventory { "APM failed to make unavailable A2DP device addr=" + Utils.anonymizeBluetoothAddress(address) + " error=" + res).printSlog(EventLogger.Event.ALOGE, TAG)); // TODO: failed to disconnect, stop here // TODO: return; // not taking further action: proceeding as if disconnection from APM worked } else { AudioService.sDeviceLogger.enqueue((new EventLogger.StringEvent( "A2DP device addr=" + Utils.anonymizeBluetoothAddress(address) Loading Loading @@ -2383,8 +2410,9 @@ public class AudioDeviceInventory { "APM failed to make available A2DP source device addr=" + Utils.anonymizeBluetoothAddress(address) + " error=" + res).printSlog(EventLogger.Event.ALOGE, TAG)); // TODO: connection failed, stop here // TODO: return if (asDeviceConnectionFailure()) { return; } } else { AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent( "A2DP source device addr=" + Utils.anonymizeBluetoothAddress(address) Loading @@ -2402,6 +2430,7 @@ public class AudioDeviceInventory { mAudioSystem.setDeviceConnectionState(ada, AudioSystem.DEVICE_STATE_UNAVAILABLE, AudioSystem.AUDIO_FORMAT_DEFAULT); // always remove regardless of the result mConnectedDevices.remove( DeviceInfo.makeDeviceListKey(AudioSystem.DEVICE_IN_BLUETOOTH_A2DP, address)); mDeviceBroker.postCheckCommunicationDeviceRemoval(ada); Loading @@ -2418,9 +2447,18 @@ public class AudioDeviceInventory { AudioDeviceAttributes ada = new AudioDeviceAttributes( DEVICE_OUT_HEARING_AID, address, name); mAudioSystem.setDeviceConnectionState(ada, final int res = mAudioSystem.setDeviceConnectionState(ada, AudioSystem.DEVICE_STATE_AVAILABLE, AudioSystem.AUDIO_FORMAT_DEFAULT); if (asDeviceConnectionFailure() && res != AudioSystem.AUDIO_STATUS_OK) { AudioService.sDeviceLogger.enqueueAndSlog( "APM failed to make available HearingAid addr=" + address + " error=" + res, EventLogger.Event.ALOGE, TAG); return; } AudioService.sDeviceLogger.enqueueAndSlog("HearingAid made available addr=" + address, EventLogger.Event.ALOGI, TAG); mConnectedDevices.put( DeviceInfo.makeDeviceListKey(DEVICE_OUT_HEARING_AID, address), new DeviceInfo(DEVICE_OUT_HEARING_AID, name, address)); Loading @@ -2447,6 +2485,7 @@ public class AudioDeviceInventory { mAudioSystem.setDeviceConnectionState(ada, AudioSystem.DEVICE_STATE_UNAVAILABLE, AudioSystem.AUDIO_FORMAT_DEFAULT); // always remove regardless of return code mConnectedDevices.remove( DeviceInfo.makeDeviceListKey(DEVICE_OUT_HEARING_AID, address)); // Remove Hearing Aid routes as well Loading Loading @@ -2540,11 +2579,12 @@ public class AudioDeviceInventory { final int res = mAudioSystem.setDeviceConnectionState(ada, AudioSystem.DEVICE_STATE_AVAILABLE, codec); if (res != AudioSystem.AUDIO_STATUS_OK) { AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent( AudioService.sDeviceLogger.enqueueAndSlog( "APM failed to make available LE Audio device addr=" + address + " error=" + res).printSlog(EventLogger.Event.ALOGE, TAG)); // TODO: connection failed, stop here // TODO: return; + " error=" + res, EventLogger.Event.ALOGE, TAG); if (asDeviceConnectionFailure()) { return; } } else { AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent( "LE Audio " + (AudioSystem.isInputDevice(device) ? "source" : "sink") Loading Loading @@ -2596,8 +2636,7 @@ public class AudioDeviceInventory { AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent( "APM failed to make unavailable LE Audio device addr=" + address + " error=" + res).printSlog(EventLogger.Event.ALOGE, TAG)); // TODO: failed to disconnect, stop here // TODO: return; // not taking further action: proceeding as if disconnection from APM worked } else { AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent( "LE Audio device addr=" + Utils.anonymizeBluetoothAddress(address) Loading services/core/java/com/android/server/audio/AudioService.java +3 −0 Original line number Diff line number Diff line Loading @@ -63,6 +63,7 @@ import static android.provider.Settings.Secure.VOLUME_HUSH_VIBRATE; import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE; import static com.android.media.audio.Flags.absVolumeIndexFix; import static com.android.media.audio.Flags.alarmMinVolumeZero; import static com.android.media.audio.Flags.asDeviceConnectionFailure; import static com.android.media.audio.Flags.audioserverPermissions; import static com.android.media.audio.Flags.disablePrescaleAbsoluteVolume; import static com.android.media.audio.Flags.replaceStreamBtSco; Loading Loading @@ -4768,6 +4769,8 @@ public class AudioService extends IAudioService.Stub private void dumpFlags(PrintWriter pw) { pw.println("\nFun with Flags:"); pw.println("\tcom.android.media.audio.as_device_connection_failure:" + asDeviceConnectionFailure()); pw.println("\tandroid.media.audio.autoPublicVolumeApiHardening:" + autoPublicVolumeApiHardening()); pw.println("\tandroid.media.audio.Flags.automaticBtDeviceType:" Loading services/tests/servicestests/src/com/android/server/audio/AudioDeviceInventoryTest.java 0 → 100644 +144 −0 Original line number Diff line number Diff line /* * Copyright 2024 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 com.android.media.audio.Flags.asDeviceConnectionFailure; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; import android.Manifest; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothProfile; import android.content.Context; import android.media.AudioDeviceAttributes; import android.media.AudioManager; import android.media.AudioSystem; import android.platform.test.annotations.Presubmit; import android.util.Log; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.MediumTest; import androidx.test.platform.app.InstrumentationRegistry; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.Spy; @MediumTest @Presubmit @RunWith(AndroidJUnit4.class) public class AudioDeviceInventoryTest { private static final String TAG = "AudioDeviceInventoryTest"; @Mock private AudioService mMockAudioService; private AudioDeviceInventory mDevInventory; @Spy private AudioDeviceBroker mSpyAudioDeviceBroker; @Spy private AudioSystemAdapter mSpyAudioSystem; private SystemServerAdapter mSystemServer; private BluetoothDevice mFakeBtDevice; @Before public void setUp() throws Exception { Context context = InstrumentationRegistry.getInstrumentation().getTargetContext(); mMockAudioService = mock(AudioService.class); mSpyAudioSystem = spy(new NoOpAudioSystemAdapter()); mDevInventory = new AudioDeviceInventory(mSpyAudioSystem); mSystemServer = new NoOpSystemServerAdapter(); mSpyAudioDeviceBroker = spy(new AudioDeviceBroker(context, mMockAudioService, mDevInventory, mSystemServer, mSpyAudioSystem)); mDevInventory.setDeviceBroker(mSpyAudioDeviceBroker); BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); mFakeBtDevice = adapter.getRemoteDevice("00:01:02:03:04:05"); } @After public void tearDown() throws Exception { } /** * test that for DEVICE_OUT_BLUETOOTH_A2DP devices, when the device connects, it's only * added to the connected devices when the connection through AudioSystem is successful * @throws Exception on error */ @Test public void testSetDeviceConnectionStateA2dp() throws Exception { Log.i(TAG, "starting testSetDeviceConnectionStateA2dp"); assertTrue("collection of connected devices not empty at start", mDevInventory.getConnectedDevices().isEmpty()); final AudioDeviceAttributes ada = new AudioDeviceAttributes( AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, mFakeBtDevice.getAddress()); AudioDeviceBroker.BtDeviceInfo btInfo = new AudioDeviceBroker.BtDeviceInfo(mFakeBtDevice, BluetoothProfile.A2DP, BluetoothProfile.STATE_CONNECTED, AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, AudioSystem.AUDIO_FORMAT_SBC); // test that no device is added when AudioSystem returns AUDIO_STATUS_ERROR // when setDeviceConnectionState is called for the connection // NOTE: for now this is only when flag asDeviceConnectionFailure is true if (asDeviceConnectionFailure()) { when(mSpyAudioSystem.setDeviceConnectionState(ada, AudioSystem.DEVICE_STATE_AVAILABLE, AudioSystem.AUDIO_FORMAT_DEFAULT)) .thenReturn(AudioSystem.AUDIO_STATUS_ERROR); runWithBluetoothPrivilegedPermission( () -> mDevInventory.onSetBtActiveDevice(/*btInfo*/ btInfo, /*codec*/ AudioSystem.AUDIO_FORMAT_DEFAULT, AudioManager.STREAM_MUSIC)); assertEquals(0, mDevInventory.getConnectedDevices().size()); } // test that the device is added when AudioSystem returns AUDIO_STATUS_OK // when setDeviceConnectionState is called for the connection when(mSpyAudioSystem.setDeviceConnectionState(ada, AudioSystem.DEVICE_STATE_AVAILABLE, AudioSystem.AUDIO_FORMAT_DEFAULT)) .thenReturn(AudioSystem.AUDIO_STATUS_OK); runWithBluetoothPrivilegedPermission( () -> mDevInventory.onSetBtActiveDevice(/*btInfo*/ btInfo, /*codec*/ AudioSystem.AUDIO_FORMAT_DEFAULT, AudioManager.STREAM_MUSIC)); assertEquals(1, mDevInventory.getConnectedDevices().size()); } // TODO add test for hearing aid // TODO add test for BLE /** * Executes a Runnable while holding the BLUETOOTH_PRIVILEGED permission * @param toRunWithPermission the runnable to run with BT privileges */ private void runWithBluetoothPrivilegedPermission(Runnable toRunWithPermission) { try { InstrumentationRegistry.getInstrumentation().getUiAutomation() .adoptShellPermissionIdentity(Manifest.permission.BLUETOOTH_PRIVILEGED); toRunWithPermission.run(); } finally { InstrumentationRegistry.getInstrumentation().getUiAutomation() .dropShellPermissionIdentity(); } } } Loading
services/core/java/com/android/server/audio/AudioDeviceInventory.java +54 −15 Original line number Diff line number Diff line Loading @@ -31,6 +31,7 @@ import static android.media.AudioSystem.isBluetoothScoOutDevice; import static android.media.audio.Flags.automaticBtDeviceType; import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE; import static com.android.media.audio.Flags.asDeviceConnectionFailure; import android.annotation.NonNull; import android.annotation.Nullable; Loading Loading @@ -529,6 +530,17 @@ public class AudioDeviceInventory { } }; /** * package-protected for unit testing only * Returns the currently connected devices * @return the collection of connected devices */ /*package*/ @NonNull Collection<DeviceInfo> getConnectedDevices() { synchronized (mDevicesLock) { return mConnectedDevices.values(); } } // List of devices actually connected to AudioPolicy (through AudioSystem), only one // by device type, which is used as the key, value is the DeviceInfo generated key. // For the moment only for A2DP sink devices. Loading Loading @@ -598,8 +610,9 @@ public class AudioDeviceInventory { /** * Class to store info about connected devices. * Use makeDeviceListKey() to make a unique key for this list. * Package-protected for unit tests */ private static class DeviceInfo { /*package*/ static class DeviceInfo { final int mDeviceType; final @NonNull String mDeviceName; final @NonNull String mDeviceAddress; Loading Loading @@ -762,13 +775,27 @@ public class AudioDeviceInventory { // Always executed on AudioDeviceBroker message queue /*package*/ void onRestoreDevices() { synchronized (mDevicesLock) { int res; List<DeviceInfo> failedReconnectionDeviceList = new ArrayList<>(/*initialCapacity*/ 0); //TODO iterate on mApmConnectedDevices instead once it handles all device types for (DeviceInfo di : mConnectedDevices.values()) { mAudioSystem.setDeviceConnectionState(new AudioDeviceAttributes(di.mDeviceType, res = mAudioSystem.setDeviceConnectionState(new AudioDeviceAttributes( di.mDeviceType, di.mDeviceAddress, di.mDeviceName), AudioSystem.DEVICE_STATE_AVAILABLE, di.mDeviceCodecFormat); if (asDeviceConnectionFailure() && res != AudioSystem.AUDIO_STATUS_OK) { failedReconnectionDeviceList.add(di); } } if (asDeviceConnectionFailure()) { for (DeviceInfo di : failedReconnectionDeviceList) { AudioService.sDeviceLogger.enqueueAndSlog( "Device inventory restore failed to reconnect " + di, EventLogger.Event.ALOGE, TAG); mConnectedDevices.remove(di.getKey(), di); } } mAppliedStrategyRolesInt.clear(); mAppliedPresetRolesInt.clear(); Loading Loading @@ -2070,8 +2097,9 @@ public class AudioDeviceInventory { "APM failed to make available A2DP device addr=" + Utils.anonymizeBluetoothAddress(address) + " error=" + res).printSlog(EventLogger.Event.ALOGE, TAG)); // TODO: connection failed, stop here // TODO: return; if (asDeviceConnectionFailure()) { return; } } else { AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent( "A2DP sink device addr=" + Utils.anonymizeBluetoothAddress(address) Loading Loading @@ -2336,8 +2364,7 @@ public class AudioDeviceInventory { "APM failed to make unavailable A2DP device addr=" + Utils.anonymizeBluetoothAddress(address) + " error=" + res).printSlog(EventLogger.Event.ALOGE, TAG)); // TODO: failed to disconnect, stop here // TODO: return; // not taking further action: proceeding as if disconnection from APM worked } else { AudioService.sDeviceLogger.enqueue((new EventLogger.StringEvent( "A2DP device addr=" + Utils.anonymizeBluetoothAddress(address) Loading Loading @@ -2383,8 +2410,9 @@ public class AudioDeviceInventory { "APM failed to make available A2DP source device addr=" + Utils.anonymizeBluetoothAddress(address) + " error=" + res).printSlog(EventLogger.Event.ALOGE, TAG)); // TODO: connection failed, stop here // TODO: return if (asDeviceConnectionFailure()) { return; } } else { AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent( "A2DP source device addr=" + Utils.anonymizeBluetoothAddress(address) Loading @@ -2402,6 +2430,7 @@ public class AudioDeviceInventory { mAudioSystem.setDeviceConnectionState(ada, AudioSystem.DEVICE_STATE_UNAVAILABLE, AudioSystem.AUDIO_FORMAT_DEFAULT); // always remove regardless of the result mConnectedDevices.remove( DeviceInfo.makeDeviceListKey(AudioSystem.DEVICE_IN_BLUETOOTH_A2DP, address)); mDeviceBroker.postCheckCommunicationDeviceRemoval(ada); Loading @@ -2418,9 +2447,18 @@ public class AudioDeviceInventory { AudioDeviceAttributes ada = new AudioDeviceAttributes( DEVICE_OUT_HEARING_AID, address, name); mAudioSystem.setDeviceConnectionState(ada, final int res = mAudioSystem.setDeviceConnectionState(ada, AudioSystem.DEVICE_STATE_AVAILABLE, AudioSystem.AUDIO_FORMAT_DEFAULT); if (asDeviceConnectionFailure() && res != AudioSystem.AUDIO_STATUS_OK) { AudioService.sDeviceLogger.enqueueAndSlog( "APM failed to make available HearingAid addr=" + address + " error=" + res, EventLogger.Event.ALOGE, TAG); return; } AudioService.sDeviceLogger.enqueueAndSlog("HearingAid made available addr=" + address, EventLogger.Event.ALOGI, TAG); mConnectedDevices.put( DeviceInfo.makeDeviceListKey(DEVICE_OUT_HEARING_AID, address), new DeviceInfo(DEVICE_OUT_HEARING_AID, name, address)); Loading @@ -2447,6 +2485,7 @@ public class AudioDeviceInventory { mAudioSystem.setDeviceConnectionState(ada, AudioSystem.DEVICE_STATE_UNAVAILABLE, AudioSystem.AUDIO_FORMAT_DEFAULT); // always remove regardless of return code mConnectedDevices.remove( DeviceInfo.makeDeviceListKey(DEVICE_OUT_HEARING_AID, address)); // Remove Hearing Aid routes as well Loading Loading @@ -2540,11 +2579,12 @@ public class AudioDeviceInventory { final int res = mAudioSystem.setDeviceConnectionState(ada, AudioSystem.DEVICE_STATE_AVAILABLE, codec); if (res != AudioSystem.AUDIO_STATUS_OK) { AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent( AudioService.sDeviceLogger.enqueueAndSlog( "APM failed to make available LE Audio device addr=" + address + " error=" + res).printSlog(EventLogger.Event.ALOGE, TAG)); // TODO: connection failed, stop here // TODO: return; + " error=" + res, EventLogger.Event.ALOGE, TAG); if (asDeviceConnectionFailure()) { return; } } else { AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent( "LE Audio " + (AudioSystem.isInputDevice(device) ? "source" : "sink") Loading Loading @@ -2596,8 +2636,7 @@ public class AudioDeviceInventory { AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent( "APM failed to make unavailable LE Audio device addr=" + address + " error=" + res).printSlog(EventLogger.Event.ALOGE, TAG)); // TODO: failed to disconnect, stop here // TODO: return; // not taking further action: proceeding as if disconnection from APM worked } else { AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent( "LE Audio device addr=" + Utils.anonymizeBluetoothAddress(address) Loading
services/core/java/com/android/server/audio/AudioService.java +3 −0 Original line number Diff line number Diff line Loading @@ -63,6 +63,7 @@ import static android.provider.Settings.Secure.VOLUME_HUSH_VIBRATE; import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE; import static com.android.media.audio.Flags.absVolumeIndexFix; import static com.android.media.audio.Flags.alarmMinVolumeZero; import static com.android.media.audio.Flags.asDeviceConnectionFailure; import static com.android.media.audio.Flags.audioserverPermissions; import static com.android.media.audio.Flags.disablePrescaleAbsoluteVolume; import static com.android.media.audio.Flags.replaceStreamBtSco; Loading Loading @@ -4768,6 +4769,8 @@ public class AudioService extends IAudioService.Stub private void dumpFlags(PrintWriter pw) { pw.println("\nFun with Flags:"); pw.println("\tcom.android.media.audio.as_device_connection_failure:" + asDeviceConnectionFailure()); pw.println("\tandroid.media.audio.autoPublicVolumeApiHardening:" + autoPublicVolumeApiHardening()); pw.println("\tandroid.media.audio.Flags.automaticBtDeviceType:" Loading
services/tests/servicestests/src/com/android/server/audio/AudioDeviceInventoryTest.java 0 → 100644 +144 −0 Original line number Diff line number Diff line /* * Copyright 2024 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 com.android.media.audio.Flags.asDeviceConnectionFailure; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; import android.Manifest; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothProfile; import android.content.Context; import android.media.AudioDeviceAttributes; import android.media.AudioManager; import android.media.AudioSystem; import android.platform.test.annotations.Presubmit; import android.util.Log; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.MediumTest; import androidx.test.platform.app.InstrumentationRegistry; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.Spy; @MediumTest @Presubmit @RunWith(AndroidJUnit4.class) public class AudioDeviceInventoryTest { private static final String TAG = "AudioDeviceInventoryTest"; @Mock private AudioService mMockAudioService; private AudioDeviceInventory mDevInventory; @Spy private AudioDeviceBroker mSpyAudioDeviceBroker; @Spy private AudioSystemAdapter mSpyAudioSystem; private SystemServerAdapter mSystemServer; private BluetoothDevice mFakeBtDevice; @Before public void setUp() throws Exception { Context context = InstrumentationRegistry.getInstrumentation().getTargetContext(); mMockAudioService = mock(AudioService.class); mSpyAudioSystem = spy(new NoOpAudioSystemAdapter()); mDevInventory = new AudioDeviceInventory(mSpyAudioSystem); mSystemServer = new NoOpSystemServerAdapter(); mSpyAudioDeviceBroker = spy(new AudioDeviceBroker(context, mMockAudioService, mDevInventory, mSystemServer, mSpyAudioSystem)); mDevInventory.setDeviceBroker(mSpyAudioDeviceBroker); BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); mFakeBtDevice = adapter.getRemoteDevice("00:01:02:03:04:05"); } @After public void tearDown() throws Exception { } /** * test that for DEVICE_OUT_BLUETOOTH_A2DP devices, when the device connects, it's only * added to the connected devices when the connection through AudioSystem is successful * @throws Exception on error */ @Test public void testSetDeviceConnectionStateA2dp() throws Exception { Log.i(TAG, "starting testSetDeviceConnectionStateA2dp"); assertTrue("collection of connected devices not empty at start", mDevInventory.getConnectedDevices().isEmpty()); final AudioDeviceAttributes ada = new AudioDeviceAttributes( AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, mFakeBtDevice.getAddress()); AudioDeviceBroker.BtDeviceInfo btInfo = new AudioDeviceBroker.BtDeviceInfo(mFakeBtDevice, BluetoothProfile.A2DP, BluetoothProfile.STATE_CONNECTED, AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, AudioSystem.AUDIO_FORMAT_SBC); // test that no device is added when AudioSystem returns AUDIO_STATUS_ERROR // when setDeviceConnectionState is called for the connection // NOTE: for now this is only when flag asDeviceConnectionFailure is true if (asDeviceConnectionFailure()) { when(mSpyAudioSystem.setDeviceConnectionState(ada, AudioSystem.DEVICE_STATE_AVAILABLE, AudioSystem.AUDIO_FORMAT_DEFAULT)) .thenReturn(AudioSystem.AUDIO_STATUS_ERROR); runWithBluetoothPrivilegedPermission( () -> mDevInventory.onSetBtActiveDevice(/*btInfo*/ btInfo, /*codec*/ AudioSystem.AUDIO_FORMAT_DEFAULT, AudioManager.STREAM_MUSIC)); assertEquals(0, mDevInventory.getConnectedDevices().size()); } // test that the device is added when AudioSystem returns AUDIO_STATUS_OK // when setDeviceConnectionState is called for the connection when(mSpyAudioSystem.setDeviceConnectionState(ada, AudioSystem.DEVICE_STATE_AVAILABLE, AudioSystem.AUDIO_FORMAT_DEFAULT)) .thenReturn(AudioSystem.AUDIO_STATUS_OK); runWithBluetoothPrivilegedPermission( () -> mDevInventory.onSetBtActiveDevice(/*btInfo*/ btInfo, /*codec*/ AudioSystem.AUDIO_FORMAT_DEFAULT, AudioManager.STREAM_MUSIC)); assertEquals(1, mDevInventory.getConnectedDevices().size()); } // TODO add test for hearing aid // TODO add test for BLE /** * Executes a Runnable while holding the BLUETOOTH_PRIVILEGED permission * @param toRunWithPermission the runnable to run with BT privileges */ private void runWithBluetoothPrivilegedPermission(Runnable toRunWithPermission) { try { InstrumentationRegistry.getInstrumentation().getUiAutomation() .adoptShellPermissionIdentity(Manifest.permission.BLUETOOTH_PRIVILEGED); toRunWithPermission.run(); } finally { InstrumentationRegistry.getInstrumentation().getUiAutomation() .dropShellPermissionIdentity(); } } }