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