1 /* 2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.companion.virtual.audio; 18 19 import android.annotation.NonNull; 20 import android.annotation.Nullable; 21 import android.annotation.RequiresPermission; 22 import android.annotation.SystemApi; 23 import android.companion.virtual.IVirtualDevice; 24 import android.content.Context; 25 import android.hardware.display.VirtualDisplay; 26 import android.media.AudioFormat; 27 import android.media.AudioManager; 28 import android.media.AudioPlaybackConfiguration; 29 import android.media.AudioRecordingConfiguration; 30 import android.os.RemoteException; 31 32 import java.io.Closeable; 33 import java.util.List; 34 import java.util.Objects; 35 import java.util.concurrent.Executor; 36 37 /** 38 * The class stores an {@link AudioCapture} for audio capturing and an {@link AudioInjection} for 39 * audio injection. 40 * 41 * @hide 42 */ 43 @SystemApi 44 public final class VirtualAudioDevice implements Closeable { 45 46 /** 47 * Interface to be notified when playback or recording configuration of applications running on 48 * virtual display was changed. 49 * 50 * @hide 51 */ 52 @SystemApi 53 public interface AudioConfigurationChangeCallback { 54 /** 55 * Notifies when playback configuration of applications running on virtual display was 56 * changed. 57 */ onPlaybackConfigChanged(@onNull List<AudioPlaybackConfiguration> configs)58 void onPlaybackConfigChanged(@NonNull List<AudioPlaybackConfiguration> configs); 59 60 /** 61 * Notifies when recording configuration of applications running on virtual display was 62 * changed. 63 */ onRecordingConfigChanged(@onNull List<AudioRecordingConfiguration> configs)64 void onRecordingConfigChanged(@NonNull List<AudioRecordingConfiguration> configs); 65 } 66 67 /** 68 * Interface to be notified when {@link #close()} is called. 69 * 70 * @hide 71 */ 72 public interface CloseListener { 73 /** 74 * Notifies when {@link #close()} is called. 75 */ onClosed()76 void onClosed(); 77 } 78 79 private final Context mContext; 80 private final IVirtualDevice mVirtualDevice; 81 private final VirtualDisplay mVirtualDisplay; 82 private final AudioConfigurationChangeCallback mCallback; 83 private final Executor mExecutor; 84 private final CloseListener mListener; 85 @Nullable 86 private VirtualAudioSession mOngoingSession; 87 88 /** 89 * @hide 90 */ VirtualAudioDevice(Context context, IVirtualDevice virtualDevice, @NonNull VirtualDisplay virtualDisplay, @Nullable Executor executor, @Nullable AudioConfigurationChangeCallback callback, @Nullable CloseListener listener)91 public VirtualAudioDevice(Context context, IVirtualDevice virtualDevice, 92 @NonNull VirtualDisplay virtualDisplay, @Nullable Executor executor, 93 @Nullable AudioConfigurationChangeCallback callback, @Nullable CloseListener listener) { 94 mContext = context; 95 mVirtualDevice = virtualDevice; 96 mVirtualDisplay = virtualDisplay; 97 mExecutor = executor; 98 mCallback = callback; 99 mListener = listener; 100 } 101 102 /** 103 * Begins injecting audio from a remote device into this device. 104 * 105 * @return An {@link AudioInjection} containing the injected audio. 106 */ 107 @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) 108 @NonNull startAudioInjection(@onNull AudioFormat injectionFormat)109 public AudioInjection startAudioInjection(@NonNull AudioFormat injectionFormat) { 110 Objects.requireNonNull(injectionFormat, "injectionFormat must not be null"); 111 112 if (mOngoingSession != null && mOngoingSession.getAudioInjection() != null) { 113 throw new IllegalStateException("Cannot start an audio injection while a session is " 114 + "ongoing. Call close() on this device first to end the previous session."); 115 } 116 if (mOngoingSession == null) { 117 mOngoingSession = new VirtualAudioSession(mContext, mCallback, mExecutor); 118 } 119 120 try { 121 mVirtualDevice.onAudioSessionStarting(mVirtualDisplay.getDisplay().getDisplayId(), 122 /* routingCallback= */ mOngoingSession, 123 /* configChangedCallback= */ mOngoingSession.getAudioConfigChangedListener()); 124 } catch (RemoteException e) { 125 throw e.rethrowFromSystemServer(); 126 } 127 return mOngoingSession.startAudioInjection(injectionFormat); 128 } 129 130 /** 131 * Begins recording audio emanating from this device. 132 * 133 * <p>Note: This method does not support capturing privileged playback, which means the 134 * application can opt out of capturing by {@link AudioManager#setAllowedCapturePolicy(int)}. 135 * 136 * @return An {@link AudioCapture} containing the recorded audio. 137 */ 138 @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) 139 @NonNull startAudioCapture(@onNull AudioFormat captureFormat)140 public AudioCapture startAudioCapture(@NonNull AudioFormat captureFormat) { 141 Objects.requireNonNull(captureFormat, "captureFormat must not be null"); 142 143 if (mOngoingSession != null && mOngoingSession.getAudioCapture() != null) { 144 throw new IllegalStateException("Cannot start an audio capture while a session is " 145 + "ongoing. Call close() on this device first to end the previous session."); 146 } 147 if (mOngoingSession == null) { 148 mOngoingSession = new VirtualAudioSession(mContext, mCallback, mExecutor); 149 } 150 151 try { 152 mVirtualDevice.onAudioSessionStarting(mVirtualDisplay.getDisplay().getDisplayId(), 153 /* routingCallback= */ mOngoingSession, 154 /* configChangedCallback= */ mOngoingSession.getAudioConfigChangedListener()); 155 } catch (RemoteException e) { 156 throw e.rethrowFromSystemServer(); 157 } 158 return mOngoingSession.startAudioCapture(captureFormat); 159 } 160 161 /** Returns the {@link AudioCapture} instance. */ 162 @Nullable getAudioCapture()163 public AudioCapture getAudioCapture() { 164 return mOngoingSession != null ? mOngoingSession.getAudioCapture() : null; 165 } 166 167 /** Returns the {@link AudioInjection} instance. */ 168 @Nullable getAudioInjection()169 public AudioInjection getAudioInjection() { 170 return mOngoingSession != null ? mOngoingSession.getAudioInjection() : null; 171 } 172 173 /** Stops audio capture and injection then releases all the resources */ 174 @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) 175 @Override close()176 public void close() { 177 if (mOngoingSession != null) { 178 mOngoingSession.close(); 179 mOngoingSession = null; 180 181 try { 182 mVirtualDevice.onAudioSessionEnded(); 183 } catch (RemoteException e) { 184 throw e.rethrowFromSystemServer(); 185 } 186 187 if (mListener != null) { 188 mListener.onClosed(); 189 } 190 } 191 } 192 } 193