/* * 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 android.companion.virtual.audio; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.SystemApi; 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; import java.io.Closeable; import java.util.List; import java.util.Objects; import java.util.concurrent.Executor; /** * The class stores an {@link AudioCapture} for audio capturing and an {@link AudioInjection} for * audio injection. * * @hide */ @SystemApi public final class VirtualAudioDevice implements Closeable { /** * Interface to be notified when playback or recording configuration of applications running on * virtual display was changed. * * @hide */ @SystemApi public interface AudioConfigurationChangeCallback { /** * Notifies when playback configuration of applications running on virtual display was * changed. */ void onPlaybackConfigChanged(@NonNull List configs); /** * Notifies when recording configuration of applications running on virtual display was * changed. */ void onRecordingConfigChanged(@NonNull List configs); } /** * Interface to be notified when {@link #close()} is called. * * @hide */ public interface CloseListener { /** * Notifies when {@link #close()} is called. */ void onClosed(); } private final Context mContext; private final IVirtualDevice mVirtualDevice; private final VirtualDisplay mVirtualDisplay; private final AudioConfigurationChangeCallback mCallback; private final Executor mExecutor; private final CloseListener mListener; @Nullable private VirtualAudioSession mOngoingSession; /** * @hide */ public VirtualAudioDevice(Context context, IVirtualDevice virtualDevice, @NonNull VirtualDisplay virtualDisplay, @Nullable Executor executor, @Nullable AudioConfigurationChangeCallback callback, @Nullable CloseListener listener) { mContext = context; mVirtualDevice = virtualDevice; mVirtualDisplay = virtualDisplay; mExecutor = executor; mCallback = callback; mListener = listener; } /** * Begins injecting audio from a remote device into this device. * * @return An {@link AudioInjection} containing the injected audio. */ @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) @NonNull public AudioInjection startAudioInjection(@NonNull AudioFormat injectionFormat) { Objects.requireNonNull(injectionFormat, "injectionFormat must not be null"); 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 session."); } if (mOngoingSession == null) { mOngoingSession = new VirtualAudioSession(mContext, mCallback, mExecutor); } try { mVirtualDevice.onAudioSessionStarting(mVirtualDisplay.getDisplay().getDisplayId(), /* routingCallback= */ mOngoingSession, /* configChangedCallback= */ mOngoingSession.getAudioConfigChangedListener()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } return mOngoingSession.startAudioInjection(injectionFormat); } /** * 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) @NonNull public AudioCapture startAudioCapture(@NonNull AudioFormat captureFormat) { Objects.requireNonNull(captureFormat, "captureFormat must not be null"); if (mOngoingSession != null && mOngoingSession.getAudioCapture() != null) { throw new IllegalStateException("Cannot start an audio capture while a session is " + "ongoing. Call close() on this device first to end the previous session."); } if (mOngoingSession == null) { mOngoingSession = new VirtualAudioSession(mContext, mCallback, mExecutor); } try { mVirtualDevice.onAudioSessionStarting(mVirtualDisplay.getDisplay().getDisplayId(), /* routingCallback= */ mOngoingSession, /* configChangedCallback= */ mOngoingSession.getAudioConfigChangedListener()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } return mOngoingSession.startAudioCapture(captureFormat); } /** Returns the {@link AudioCapture} instance. */ @Nullable public AudioCapture getAudioCapture() { return mOngoingSession != null ? mOngoingSession.getAudioCapture() : null; } /** Returns the {@link AudioInjection} instance. */ @Nullable public AudioInjection getAudioInjection() { 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() { if (mOngoingSession != null) { mOngoingSession.close(); mOngoingSession = null; try { mVirtualDevice.onAudioSessionEnded(); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } if (mListener != null) { mListener.onClosed(); } } } }