diff --git a/core/java/android/companion/virtual/VirtualDeviceManager.java b/core/java/android/companion/virtual/VirtualDeviceManager.java index 088ac069dd7a6..856101875b041 100644 --- a/core/java/android/companion/virtual/VirtualDeviceManager.java +++ b/core/java/android/companion/virtual/VirtualDeviceManager.java @@ -57,6 +57,7 @@ import android.hardware.input.VirtualNavigationTouchpad; import android.hardware.input.VirtualNavigationTouchpadConfig; import android.hardware.input.VirtualTouchscreen; import android.hardware.input.VirtualTouchscreenConfig; +import android.media.AudioManager; import android.os.Binder; import android.os.Bundle; import android.os.Handler; @@ -302,6 +303,22 @@ public final class VirtualDeviceManager { } } + /** + * Requests sound effect to be played on virtual device. + * + * @see android.media.AudioManager#playSoundEffect(int) + * + * @param deviceId - id of the virtual audio device + * @param effectType the type of sound effect + * @hide + */ + public void playSoundEffect(int deviceId, @AudioManager.SystemSoundEffect int effectType) { + //TODO - handle requests to play sound effects by custom callbacks or SoundPool asociated + // with device session id. + // For now, this is intentionally left empty and effectively disables sound effects for + // virtual devices with custom device audio policy. + } + /** * A virtual device has its own virtual display, audio output, microphone, and camera etc. The * creator of a virtual device can take the output from the virtual display and stream it over diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java index 3ed2c4b6ebc7b..44ecee79f90d4 100644 --- a/media/java/android/media/AudioManager.java +++ b/media/java/android/media/AudioManager.java @@ -16,6 +16,10 @@ package android.media; +import static android.companion.virtual.VirtualDeviceManager.DEVICE_ID_DEFAULT; +import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_DEFAULT; +import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_AUDIO; + import android.annotation.CallbackExecutor; import android.annotation.IntDef; import android.annotation.IntRange; @@ -34,6 +38,7 @@ import android.app.compat.CompatChanges; import android.bluetooth.BluetoothCodecConfig; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothLeAudioCodecConfig; +import android.companion.virtual.VirtualDeviceManager; import android.compat.annotation.ChangeId; import android.compat.annotation.EnabledSince; import android.compat.annotation.UnsupportedAppUsage; @@ -100,6 +105,7 @@ public class AudioManager { private Context mOriginalContext; private Context mApplicationContext; + private @Nullable VirtualDeviceManager mVirtualDeviceManager; // Lazy initialized. private long mVolumeKeyUpTime; private static final String TAG = "AudioManager"; private static final boolean DEBUG = false; @@ -858,6 +864,14 @@ public class AudioManager { return sService; } + private VirtualDeviceManager getVirtualDeviceManager() { + if (mVirtualDeviceManager != null) { + return mVirtualDeviceManager; + } + mVirtualDeviceManager = getContext().getSystemService(VirtualDeviceManager.class); + return mVirtualDeviceManager; + } + /** * Sends a simulated key event for a media button. * To simulate a key press, you must first send a KeyEvent built with a @@ -3635,11 +3649,15 @@ public class AudioManager { * whether sounds are heard or not. * @hide */ - public void playSoundEffect(@SystemSoundEffect int effectType, int userId) { + public void playSoundEffect(@SystemSoundEffect int effectType, int userId) { if (effectType < 0 || effectType >= NUM_SOUND_EFFECTS) { return; } + if (delegateSoundEffectToVdm(effectType)) { + return; + } + final IAudioService service = getService(); try { service.playSoundEffect(effectType, userId); @@ -3657,11 +3675,15 @@ public class AudioManager { * NOTE: This version is for applications that have their own * settings panel for enabling and controlling volume. */ - public void playSoundEffect(@SystemSoundEffect int effectType, float volume) { + public void playSoundEffect(@SystemSoundEffect int effectType, float volume) { if (effectType < 0 || effectType >= NUM_SOUND_EFFECTS) { return; } + if (delegateSoundEffectToVdm(effectType)) { + return; + } + final IAudioService service = getService(); try { service.playSoundEffectVolume(effectType, volume); @@ -3670,6 +3692,28 @@ public class AudioManager { } } + /** + * Checks whether this {@link AudioManager} instance is asociated with {@link VirtualDevice} + * configured with custom device policy for audio. If there is such device, request to play + * sound effect is forwarded to {@link VirtualDeviceManager}. + * + * @param effectType - The type of sound effect. + * @return true if the request was forwarded to {@link VirtualDeviceManager} instance, + * false otherwise. + */ + private boolean delegateSoundEffectToVdm(@SystemSoundEffect int effectType) { + int deviceId = getContext().getDeviceId(); + if (deviceId != DEVICE_ID_DEFAULT) { + VirtualDeviceManager vdm = getVirtualDeviceManager(); + if (vdm != null && vdm.getDevicePolicy(deviceId, POLICY_TYPE_AUDIO) + != DEVICE_POLICY_DEFAULT) { + vdm.playSoundEffect(deviceId, effectType); + return true; + } + } + return false; + } + /** * Load Sound effects. * This method must be called when sound effects are enabled. diff --git a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/AudioManagerUnitTest.java b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/AudioManagerUnitTest.java new file mode 100644 index 0000000000000..76543f4ce9890 --- /dev/null +++ b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/AudioManagerUnitTest.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2022 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.mediaframeworktest.unit; + + +import static android.companion.virtual.VirtualDeviceManager.DEVICE_ID_DEFAULT; +import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_CUSTOM; +import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_DEFAULT; +import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_AUDIO; +import static android.media.AudioManager.FX_KEY_CLICK; + +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; + +import android.companion.virtual.VirtualDeviceManager; +import android.content.Context; +import android.media.AudioManager; +import android.test.mock.MockContext; + +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class AudioManagerUnitTest { + private static final int TEST_VIRTUAL_DEVICE_ID = 42; + + @Test + public void testAudioManager_playSoundWithDefaultDeviceContext() { + VirtualDeviceManager mockVdm = getMockVirtualDeviceManager(TEST_VIRTUAL_DEVICE_ID, + DEVICE_POLICY_CUSTOM); + Context defaultDeviceContext = getVirtualDeviceMockContext(DEVICE_ID_DEFAULT, /*vdm=*/ + mockVdm); + AudioManager audioManager = new AudioManager(defaultDeviceContext); + + audioManager.playSoundEffect(FX_KEY_CLICK); + + // We expect no interactions with VDM when running on default device. + verifyZeroInteractions(mockVdm); + } + + @Test + public void testAudioManager_playSoundWithVirtualDeviceContextDefaultPolicy() { + VirtualDeviceManager mockVdm = getMockVirtualDeviceManager(TEST_VIRTUAL_DEVICE_ID, + DEVICE_POLICY_DEFAULT); + Context defaultDeviceContext = getVirtualDeviceMockContext(TEST_VIRTUAL_DEVICE_ID, /*vdm=*/ + mockVdm); + AudioManager audioManager = new AudioManager(defaultDeviceContext); + + audioManager.playSoundEffect(FX_KEY_CLICK); + + // We expect playback not to be delegated to VDM because of default device policy for audio. + verify(mockVdm, never()).playSoundEffect(anyInt(), anyInt()); + } + + @Test + public void testAudioManager_playSoundWithVirtualDeviceContextCustomPolicy() { + VirtualDeviceManager mockVdm = getMockVirtualDeviceManager(TEST_VIRTUAL_DEVICE_ID, + DEVICE_POLICY_CUSTOM); + Context defaultDeviceContext = getVirtualDeviceMockContext(TEST_VIRTUAL_DEVICE_ID, /*vdm=*/ + mockVdm); + AudioManager audioManager = new AudioManager(defaultDeviceContext); + + audioManager.playSoundEffect(FX_KEY_CLICK); + + // We expect playback to be delegated to VDM because of custom device policy for audio. + verify(mockVdm, times(1)).playSoundEffect(TEST_VIRTUAL_DEVICE_ID, FX_KEY_CLICK); + } + + private static Context getVirtualDeviceMockContext(int deviceId, VirtualDeviceManager vdm) { + MockContext mockContext = mock(MockContext.class); + when(mockContext.getDeviceId()).thenReturn(deviceId); + when(mockContext.getSystemService(VirtualDeviceManager.class)).thenReturn(vdm); + return mockContext; + } + + private static VirtualDeviceManager getMockVirtualDeviceManager( + int deviceId, int audioDevicePolicy) { + VirtualDeviceManager vdmMock = mock(VirtualDeviceManager.class); + when(vdmMock.getDevicePolicy(anyInt(), anyInt())).thenReturn(DEVICE_POLICY_DEFAULT); + when(vdmMock.getDevicePolicy(deviceId, POLICY_TYPE_AUDIO)).thenReturn(audioDevicePolicy); + return vdmMock; + } +}