diff --git a/core/java/android/companion/virtual/VirtualDeviceManager.java b/core/java/android/companion/virtual/VirtualDeviceManager.java index d50a6ba3b56dc..99ce14743a6f8 100644 --- a/core/java/android/companion/virtual/VirtualDeviceManager.java +++ b/core/java/android/companion/virtual/VirtualDeviceManager.java @@ -148,6 +148,8 @@ public final class VirtualDeviceManager { } } }; + @Nullable + private VirtualAudioDevice mVirtualAudioDevice; private VirtualDevice( IVirtualDeviceManager service, @@ -255,8 +257,8 @@ public final class VirtualDeviceManager { } /** - * Closes the virtual device, stopping and tearing down any virtual displays, - * audio policies, and event injection that's currently in progress. + * Closes the virtual device, stopping and tearing down any virtual displays, associated + * virtual audio device, and event injection that's currently in progress. */ @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void close() { @@ -265,6 +267,10 @@ public final class VirtualDeviceManager { } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } + if (mVirtualAudioDevice != null) { + mVirtualAudioDevice.close(); + mVirtualAudioDevice = null; + } } /** @@ -351,8 +357,10 @@ public final class VirtualDeviceManager { * Creates a VirtualAudioDevice, capable of recording audio emanating from this device, * or injecting audio from another device. * - *

Note: This object does not support capturing privileged playback, such as voice call - * audio. + *

Note: One {@link VirtualDevice} can only create one {@link VirtualAudioDevice}, so + * calling this method multiple times will return the same instance. When + * {@link VirtualDevice#close()} is called, the associated {@link VirtualAudioDevice} will + * also be closed automatically. * * @param display The target virtual display to capture from and inject into. * @param executor The {@link Executor} object for the thread on which to execute @@ -368,7 +376,11 @@ public final class VirtualDeviceManager { @NonNull VirtualDisplay display, @Nullable Executor executor, @Nullable AudioConfigurationChangeCallback callback) { - return new VirtualAudioDevice(mContext, mVirtualDevice, display, executor, callback); + if (mVirtualAudioDevice == null) { + mVirtualAudioDevice = new VirtualAudioDevice( + mContext, mVirtualDevice, display, executor, callback); + } + return mVirtualAudioDevice; } /** diff --git a/core/java/android/companion/virtual/audio/UserRestrictionsDetector.java b/core/java/android/companion/virtual/audio/UserRestrictionsDetector.java index 5c246d3657518..c816da72b0fff 100644 --- a/core/java/android/companion/virtual/audio/UserRestrictionsDetector.java +++ b/core/java/android/companion/virtual/audio/UserRestrictionsDetector.java @@ -60,7 +60,6 @@ final class UserRestrictionsDetector extends BroadcastReceiver { /** Registers user restrictions change. */ void register(@NonNull UserRestrictionsCallback callback) { mUserRestrictionsCallback = callback; - IntentFilter filter = new IntentFilter(); filter.addAction(UserManager.ACTION_USER_RESTRICTIONS_CHANGED); mContext.registerReceiver(/* receiver= */ this, filter); @@ -73,8 +72,10 @@ final class UserRestrictionsDetector extends BroadcastReceiver { /** Unregisters user restrictions change. */ void unregister() { - mUserRestrictionsCallback = null; - mContext.unregisterReceiver(/* receiver= */ this); + if (mUserRestrictionsCallback != null) { + mUserRestrictionsCallback = null; + mContext.unregisterReceiver(/* receiver= */ this); + } } @Override diff --git a/core/java/android/companion/virtual/audio/VirtualAudioDevice.java b/core/java/android/companion/virtual/audio/VirtualAudioDevice.java index 38e37ec0c316c..3f7299fbb09e2 100644 --- a/core/java/android/companion/virtual/audio/VirtualAudioDevice.java +++ b/core/java/android/companion/virtual/audio/VirtualAudioDevice.java @@ -24,6 +24,7 @@ import android.companion.virtual.IVirtualDevice; import android.content.Context; import android.hardware.display.VirtualDisplay; import android.media.AudioFormat; +import android.media.AudioManager; import android.media.AudioPlaybackConfiguration; import android.media.AudioRecordingConfiguration; import android.os.RemoteException; @@ -96,7 +97,7 @@ public final class VirtualAudioDevice implements Closeable { if (mOngoingSession != null && mOngoingSession.getAudioInjection() != null) { throw new IllegalStateException("Cannot start an audio injection while a session is " - + "ongoing. Call close() on this device first to end the previous injection."); + + "ongoing. Call close() on this device first to end the previous session."); } if (mOngoingSession == null) { mOngoingSession = new VirtualAudioSession(mContext, mCallback, mExecutor); @@ -114,6 +115,9 @@ public final class VirtualAudioDevice implements Closeable { /** * Begins recording audio emanating from this device. * + *

Note: This method does not support capturing privileged playback, which means the + * application can opt out of capturing by {@link AudioManager#setAllowedCapturePolicy(int)}. + * * @return An {@link AudioCapture} containing the recorded audio. */ @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) @@ -150,6 +154,7 @@ public final class VirtualAudioDevice implements Closeable { return mOngoingSession != null ? mOngoingSession.getAudioInjection() : null; } + /** Stops audio capture and injection then releases all the resources */ @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) @Override public void close() { diff --git a/core/java/android/companion/virtual/audio/VirtualAudioSession.java b/core/java/android/companion/virtual/audio/VirtualAudioSession.java index bc71bd65e9440..c6a10456c3319 100644 --- a/core/java/android/companion/virtual/audio/VirtualAudioSession.java +++ b/core/java/android/companion/virtual/audio/VirtualAudioSession.java @@ -120,7 +120,7 @@ public final class VirtualAudioSession extends IAudioSessionCallback.Stub implem @NonNull public AudioInjection startAudioInjection(@NonNull AudioFormat injectionFormat) { Objects.requireNonNull(injectionFormat, "injectionFormat must not be null"); - mUserRestrictionsDetector.register(/* callback= */ this); + synchronized (mLock) { if (mAudioInjection != null) { throw new IllegalStateException( @@ -130,6 +130,8 @@ public final class VirtualAudioSession extends IAudioSessionCallback.Stub implem mInjectionFormat = injectionFormat; mAudioInjection = new AudioInjection(); mAudioInjection.play(); + + mUserRestrictionsDetector.register(/* callback= */ this); mAudioInjection.setSilent(mUserRestrictionsDetector.isUnmuteMicrophoneDisallowed()); return mAudioInjection; } diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java index 387d911672a8f..c0a904fe3d9a6 100644 --- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java +++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java @@ -235,6 +235,10 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub }); mPerDisplayWakelocks.clear(); } + if (mVirtualAudioController != null) { + mVirtualAudioController.stopListening(); + mVirtualAudioController = null; + } } mListener.onClose(mAssociationInfo.getId()); mAppToken.unlinkToDeath(this, 0); diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java index 2ae285409b731..9aac81c34edb9 100644 --- a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java @@ -360,6 +360,16 @@ public class VirtualDeviceManagerServiceTest { assertThat(mDeviceImpl.getVirtualAudioControllerForTesting()).isNull(); } + @Test + public void close_cleanVirtualAudioController() { + mDeviceImpl.onVirtualDisplayCreatedLocked(DISPLAY_ID); + mDeviceImpl.onAudioSessionStarting(DISPLAY_ID, mCallback); + + mDeviceImpl.close(); + + assertThat(mDeviceImpl.getVirtualAudioControllerForTesting()).isNull(); + } + @Test public void sendKeyEvent_noFd() { assertThrows(