Merge "Delegate playSoundEffect calls to VDM for virtual context"
diff --git a/core/java/android/companion/virtual/VirtualDeviceManager.java b/core/java/android/companion/virtual/VirtualDeviceManager.java
index 088ac06..8561018 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.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;
@@ -303,6 +304,22 @@
     }
 
     /**
+     * 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
      * to another device, and inject input events that are received from the remote device.
diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java
index 3a55993..fdd6233 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.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 @@
 
     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 @@
         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 @@
      * 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 @@
      * 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);
@@ -3671,6 +3693,28 @@
     }
 
     /**
+     * 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 0000000..76543f4
--- /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;
+    }
+}