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 private final Context mContext; 68 private final IVirtualDevice mVirtualDevice; 69 private final VirtualDisplay mVirtualDisplay; 70 private final AudioConfigurationChangeCallback mCallback; 71 private final Executor mExecutor; 72 @Nullable 73 private VirtualAudioSession mOngoingSession; 74 75 /** 76 * @hide 77 */ VirtualAudioDevice(Context context, IVirtualDevice virtualDevice, @NonNull VirtualDisplay virtualDisplay, @Nullable Executor executor, @Nullable AudioConfigurationChangeCallback callback)78 public VirtualAudioDevice(Context context, IVirtualDevice virtualDevice, 79 @NonNull VirtualDisplay virtualDisplay, @Nullable Executor executor, 80 @Nullable AudioConfigurationChangeCallback callback) { 81 mContext = context; 82 mVirtualDevice = virtualDevice; 83 mVirtualDisplay = virtualDisplay; 84 mExecutor = executor; 85 mCallback = callback; 86 } 87 88 /** 89 * Begins injecting audio from a remote device into this device. 90 * 91 * @return An {@link AudioInjection} containing the injected audio. 92 */ 93 @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) 94 @NonNull startAudioInjection(@onNull AudioFormat injectionFormat)95 public AudioInjection startAudioInjection(@NonNull AudioFormat injectionFormat) { 96 Objects.requireNonNull(injectionFormat, "injectionFormat must not be null"); 97 98 if (mOngoingSession != null && mOngoingSession.getAudioInjection() != null) { 99 throw new IllegalStateException("Cannot start an audio injection while a session is " 100 + "ongoing. Call close() on this device first to end the previous session."); 101 } 102 if (mOngoingSession == null) { 103 mOngoingSession = new VirtualAudioSession(mContext, mCallback, mExecutor); 104 } 105 106 try { 107 mVirtualDevice.onAudioSessionStarting(mVirtualDisplay.getDisplay().getDisplayId(), 108 /* routingCallback= */ mOngoingSession, 109 /* configChangedCallback= */ mOngoingSession.getAudioConfigChangedListener()); 110 } catch (RemoteException e) { 111 throw e.rethrowFromSystemServer(); 112 } 113 return mOngoingSession.startAudioInjection(injectionFormat); 114 } 115 116 /** 117 * Begins recording audio emanating from this device. 118 * 119 * <p>Note: This method does not support capturing privileged playback, which means the 120 * application can opt out of capturing by {@link AudioManager#setAllowedCapturePolicy(int)}. 121 * 122 * @return An {@link AudioCapture} containing the recorded audio. 123 */ 124 @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) 125 @NonNull startAudioCapture(@onNull AudioFormat captureFormat)126 public AudioCapture startAudioCapture(@NonNull AudioFormat captureFormat) { 127 Objects.requireNonNull(captureFormat, "captureFormat must not be null"); 128 129 if (mOngoingSession != null && mOngoingSession.getAudioCapture() != null) { 130 throw new IllegalStateException("Cannot start an audio capture while a session is " 131 + "ongoing. Call close() on this device first to end the previous session."); 132 } 133 if (mOngoingSession == null) { 134 mOngoingSession = new VirtualAudioSession(mContext, mCallback, mExecutor); 135 } 136 137 try { 138 mVirtualDevice.onAudioSessionStarting(mVirtualDisplay.getDisplay().getDisplayId(), 139 /* routingCallback= */ mOngoingSession, 140 /* configChangedCallback= */ mOngoingSession.getAudioConfigChangedListener()); 141 } catch (RemoteException e) { 142 throw e.rethrowFromSystemServer(); 143 } 144 return mOngoingSession.startAudioCapture(captureFormat); 145 } 146 147 /** Returns the {@link AudioCapture} instance. */ 148 @Nullable getAudioCapture()149 public AudioCapture getAudioCapture() { 150 return mOngoingSession != null ? mOngoingSession.getAudioCapture() : null; 151 } 152 153 /** Returns the {@link AudioInjection} instance. */ 154 @Nullable getAudioInjection()155 public AudioInjection getAudioInjection() { 156 return mOngoingSession != null ? mOngoingSession.getAudioInjection() : null; 157 } 158 159 /** Stops audio capture and injection then releases all the resources */ 160 @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) 161 @Override close()162 public void close() { 163 if (mOngoingSession != null) { 164 mOngoingSession.close(); 165 mOngoingSession = null; 166 167 try { 168 mVirtualDevice.onAudioSessionEnded(); 169 } catch (RemoteException e) { 170 throw e.rethrowFromSystemServer(); 171 } 172 } 173 } 174 } 175