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.companion.virtual.audio.UserRestrictionsDetector.UserRestrictionsCallback;
23 import android.companion.virtual.audio.VirtualAudioDevice.AudioConfigurationChangeCallback;
24 import android.content.Context;
25 import android.media.AudioFormat;
26 import android.media.AudioManager;
27 import android.media.AudioPlaybackConfiguration;
28 import android.media.AudioRecord;
29 import android.media.AudioRecordingConfiguration;
30 import android.media.AudioTrack;
31 import android.media.audiopolicy.AudioMix;
32 import android.media.audiopolicy.AudioMixingRule;
33 import android.media.audiopolicy.AudioPolicy;
34 import android.util.IntArray;
35 import android.util.Log;
36 
37 import com.android.internal.annotations.GuardedBy;
38 import com.android.internal.annotations.VisibleForTesting;
39 
40 import java.io.Closeable;
41 import java.util.Arrays;
42 import java.util.List;
43 import java.util.Objects;
44 import java.util.concurrent.Executor;
45 
46 /**
47  * Manages an ongiong audio session in which audio can be captured (recorded) and/or
48  * injected from a remote device.
49  *
50  * @hide
51  */
52 @VisibleForTesting
53 public final class VirtualAudioSession extends IAudioRoutingCallback.Stub implements
54         UserRestrictionsCallback, Closeable {
55     private static final String TAG = "VirtualAudioSession";
56 
57     private final Context mContext;
58     private final UserRestrictionsDetector mUserRestrictionsDetector;
59     @Nullable
60     private final AudioConfigChangedCallback mAudioConfigChangedCallback;
61     private final Object mLock = new Object();
62     @GuardedBy("mLock")
63     private final IntArray mReroutedAppUids = new IntArray();
64     @Nullable
65     @GuardedBy("mLock")
66     private AudioPolicy mAudioPolicy;
67     @Nullable
68     @GuardedBy("mLock")
69     private AudioCapture mAudioCapture;
70     @Nullable
71     @GuardedBy("mLock")
72     private AudioInjection mAudioInjection;
73 
74     /**
75      * Class to receive {@link IAudioConfigChangedCallback} callbacks from service.
76      *
77      * @hide
78      */
79     @VisibleForTesting
80     public static final class AudioConfigChangedCallback extends IAudioConfigChangedCallback.Stub {
81         private final Executor mExecutor;
82         private final AudioConfigurationChangeCallback mCallback;
83 
AudioConfigChangedCallback(Context context, Executor executor, AudioConfigurationChangeCallback callback)84         AudioConfigChangedCallback(Context context, Executor executor,
85                 AudioConfigurationChangeCallback callback) {
86             mExecutor = executor != null ? executor : context.getMainExecutor();
87             mCallback = callback;
88         }
89 
90         @Override
onPlaybackConfigChanged(List<AudioPlaybackConfiguration> configs)91         public void onPlaybackConfigChanged(List<AudioPlaybackConfiguration> configs) {
92             if (mCallback != null) {
93                 mExecutor.execute(() -> mCallback.onPlaybackConfigChanged(configs));
94             }
95         }
96 
97         @Override
onRecordingConfigChanged(List<AudioRecordingConfiguration> configs)98         public void onRecordingConfigChanged(List<AudioRecordingConfiguration> configs) {
99             if (mCallback != null) {
100                 mExecutor.execute(() -> mCallback.onRecordingConfigChanged(configs));
101             }
102         }
103     }
104 
105     @VisibleForTesting
VirtualAudioSession(Context context, @Nullable AudioConfigurationChangeCallback callback, @Nullable Executor executor)106     public VirtualAudioSession(Context context,
107             @Nullable AudioConfigurationChangeCallback callback, @Nullable Executor executor) {
108         mContext = context;
109         mUserRestrictionsDetector = new UserRestrictionsDetector(context);
110         mAudioConfigChangedCallback = callback == null ? null : new AudioConfigChangedCallback(
111                 context, executor, callback);
112     }
113 
114     /**
115      * Begins recording audio emanating from this device.
116      *
117      * @return An {@link AudioCapture} containing the recorded audio.
118      */
119     @VisibleForTesting
120     @NonNull
startAudioCapture(@onNull AudioFormat captureFormat)121     public AudioCapture startAudioCapture(@NonNull AudioFormat captureFormat) {
122         Objects.requireNonNull(captureFormat, "captureFormat must not be null");
123 
124         synchronized (mLock) {
125             if (mAudioCapture != null) {
126                 throw new IllegalStateException(
127                         "Cannot start capture while another capture is ongoing.");
128             }
129 
130             mAudioCapture = new AudioCapture(captureFormat);
131             mAudioCapture.startRecording();
132             return mAudioCapture;
133         }
134     }
135 
136     /**
137      * Begins injecting audio from a remote device into this device.
138      *
139      * @return An {@link AudioInjection} containing the injected audio.
140      */
141     @VisibleForTesting
142     @NonNull
startAudioInjection(@onNull AudioFormat injectionFormat)143     public AudioInjection startAudioInjection(@NonNull AudioFormat injectionFormat) {
144         Objects.requireNonNull(injectionFormat, "injectionFormat must not be null");
145 
146         synchronized (mLock) {
147             if (mAudioInjection != null) {
148                 throw new IllegalStateException(
149                         "Cannot start injection while injection is already ongoing.");
150             }
151 
152             mAudioInjection = new AudioInjection(injectionFormat);
153             mAudioInjection.play();
154 
155             mUserRestrictionsDetector.register(/* callback= */ this);
156             mAudioInjection.setSilent(mUserRestrictionsDetector.isUnmuteMicrophoneDisallowed());
157             return mAudioInjection;
158         }
159     }
160 
161     /** @hide */
162     @VisibleForTesting
163     @Nullable
getAudioConfigChangedListener()164     public AudioConfigChangedCallback getAudioConfigChangedListener() {
165         return mAudioConfigChangedCallback;
166     }
167 
168     /** @hide */
169     @VisibleForTesting
170     @Nullable
getAudioCapture()171     public AudioCapture getAudioCapture() {
172         synchronized (mLock) {
173             return mAudioCapture;
174         }
175     }
176 
177     /** @hide */
178     @VisibleForTesting
179     @Nullable
getAudioInjection()180     public AudioInjection getAudioInjection() {
181         synchronized (mLock) {
182             return mAudioInjection;
183         }
184     }
185 
186     @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
187     @Override
onAppsNeedingAudioRoutingChanged(int[] appUids)188     public void onAppsNeedingAudioRoutingChanged(int[] appUids) {
189         synchronized (mLock) {
190             if (Arrays.equals(mReroutedAppUids.toArray(), appUids)) {
191                 return;
192             }
193         }
194 
195         releaseAudioStreams();
196 
197         if (appUids.length == 0) {
198             return;
199         }
200 
201         createAudioStreams(appUids);
202     }
203 
204     @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
205     @Override
close()206     public void close() {
207         mUserRestrictionsDetector.unregister();
208         releaseAudioStreams();
209         synchronized (mLock) {
210             if (mAudioCapture != null) {
211                 mAudioCapture.close();
212                 mAudioCapture = null;
213             }
214             if (mAudioInjection != null) {
215                 mAudioInjection.close();
216                 mAudioInjection = null;
217             }
218         }
219     }
220 
221     @Override
onMicrophoneRestrictionChanged(boolean isUnmuteMicDisallowed)222     public void onMicrophoneRestrictionChanged(boolean isUnmuteMicDisallowed) {
223         synchronized (mLock) {
224             if (mAudioInjection != null) {
225                 mAudioInjection.setSilent(isUnmuteMicDisallowed);
226             }
227         }
228     }
229 
230     @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
createAudioStreams(int[] appUids)231     private void createAudioStreams(int[] appUids) {
232         synchronized (mLock) {
233             if (mAudioCapture == null && mAudioInjection == null) {
234                 throw new IllegalStateException(
235                         "At least one of AudioCapture and AudioInjection must be started.");
236             }
237             if (mAudioPolicy != null) {
238                 throw new IllegalStateException(
239                         "Cannot create audio streams while the audio policy is registered. Call "
240                                 + "releaseAudioStreams() first to unregister the previous audio "
241                                 + "policy."
242                 );
243             }
244 
245             mReroutedAppUids.clear();
246             for (int appUid : appUids) {
247                 mReroutedAppUids.add(appUid);
248             }
249 
250             AudioMix audioRecordMix = null;
251             AudioMix audioTrackMix = null;
252             AudioPolicy.Builder builder = new AudioPolicy.Builder(mContext);
253             if (mAudioCapture != null) {
254                 audioRecordMix = createAudioRecordMix(mAudioCapture.getFormat(), appUids);
255                 builder.addMix(audioRecordMix);
256             }
257             if (mAudioInjection != null) {
258                 audioTrackMix = createAudioTrackMix(mAudioInjection.getFormat(), appUids);
259                 builder.addMix(audioTrackMix);
260             }
261             mAudioPolicy = builder.build();
262             AudioManager audioManager = mContext.getSystemService(AudioManager.class);
263             if (audioManager.registerAudioPolicy(mAudioPolicy) == AudioManager.ERROR) {
264                 Log.e(TAG, "Failed to register audio policy!");
265             }
266 
267             AudioRecord audioRecord =
268                     audioRecordMix != null ? mAudioPolicy.createAudioRecordSink(audioRecordMix)
269                             : null;
270             AudioTrack audioTrack =
271                     audioTrackMix != null ? mAudioPolicy.createAudioTrackSource(audioTrackMix)
272                             : null;
273 
274             if (mAudioCapture != null) {
275                 mAudioCapture.setAudioRecord(audioRecord);
276             }
277             if (mAudioInjection != null) {
278                 mAudioInjection.setAudioTrack(audioTrack);
279             }
280         }
281     }
282 
283     @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
releaseAudioStreams()284     private void releaseAudioStreams() {
285         synchronized (mLock) {
286             if (mAudioCapture != null) {
287                 mAudioCapture.setAudioRecord(null);
288             }
289             if (mAudioInjection != null) {
290                 mAudioInjection.setAudioTrack(null);
291             }
292             mReroutedAppUids.clear();
293             if (mAudioPolicy != null) {
294                 AudioManager audioManager = mContext.getSystemService(AudioManager.class);
295                 audioManager.unregisterAudioPolicy(mAudioPolicy);
296                 mAudioPolicy = null;
297                 Log.i(TAG, "AudioPolicy unregistered");
298             }
299         }
300     }
301 
302     /** @hide */
303     @VisibleForTesting
getReroutedAppUids()304     public IntArray getReroutedAppUids() {
305         synchronized (mLock) {
306             return mReroutedAppUids;
307         }
308     }
309 
createAudioRecordMix(@onNull AudioFormat audioFormat, int[] appUids)310     private static AudioMix createAudioRecordMix(@NonNull AudioFormat audioFormat, int[] appUids) {
311         AudioMixingRule.Builder builder = new AudioMixingRule.Builder();
312         builder.setTargetMixRole(AudioMixingRule.MIX_ROLE_PLAYERS);
313         for (int uid : appUids) {
314             builder.addMixRule(AudioMixingRule.RULE_MATCH_UID, uid);
315         }
316         AudioMixingRule audioMixingRule = builder.allowPrivilegedPlaybackCapture(false).build();
317         AudioMix audioMix =
318                 new AudioMix.Builder(audioMixingRule)
319                         .setFormat(audioFormat)
320                         .setRouteFlags(AudioMix.ROUTE_FLAG_LOOP_BACK)
321                         .build();
322         return audioMix;
323     }
324 
createAudioTrackMix(@onNull AudioFormat audioFormat, int[] appUids)325     private static AudioMix createAudioTrackMix(@NonNull AudioFormat audioFormat, int[] appUids) {
326         AudioMixingRule.Builder builder = new AudioMixingRule.Builder();
327         builder.setTargetMixRole(AudioMixingRule.MIX_ROLE_INJECTOR);
328         for (int uid : appUids) {
329             builder.addMixRule(AudioMixingRule.RULE_MATCH_UID, uid);
330         }
331         AudioMixingRule audioMixingRule = builder.build();
332         AudioMix audioMix =
333                 new AudioMix.Builder(audioMixingRule)
334                         .setFormat(audioFormat)
335                         .setRouteFlags(AudioMix.ROUTE_FLAG_LOOP_BACK)
336                         .build();
337         return audioMix;
338     }
339 }
340