• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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