blob: c91877aad47e2cf0245f81990fee7dc455478e1f [file] [log] [blame]
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.companion.virtual.audio;
import static android.media.AudioPlaybackConfiguration.PLAYER_STATE_STARTED;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.companion.virtual.audio.IAudioConfigChangedCallback;
import android.companion.virtual.audio.IAudioRoutingCallback;
import android.content.Context;
import android.media.AudioManager;
import android.media.AudioPlaybackConfiguration;
import android.media.AudioRecordingConfiguration;
import android.os.Handler;
import android.os.Looper;
import android.os.RemoteException;
import android.util.ArraySet;
import android.util.Slog;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.companion.virtual.GenericWindowPolicyController;
import com.android.server.companion.virtual.GenericWindowPolicyController.RunningAppsChangedListener;
import com.android.server.companion.virtual.audio.AudioPlaybackDetector.AudioPlaybackCallback;
import com.android.server.companion.virtual.audio.AudioRecordingDetector.AudioRecordingCallback;
import java.util.ArrayList;
import java.util.List;
/**
* Manages audio streams associated with a {@link VirtualAudioDevice}. Responsible for monitoring
* running applications and playback configuration changes in order to correctly re-route audio and
* then notify clients of these changes.
*/
public final class VirtualAudioController implements AudioPlaybackCallback,
AudioRecordingCallback, RunningAppsChangedListener {
private static final String TAG = "VirtualAudioController";
private static final int UPDATE_REROUTING_APPS_DELAY_MS = 2000;
private final Context mContext;
private final Handler mHandler = new Handler(Looper.getMainLooper());
private final Runnable mUpdateAudioRoutingRunnable = this::notifyAppsNeedingAudioRoutingChanged;
private final AudioPlaybackDetector mAudioPlaybackDetector;
private final AudioRecordingDetector mAudioRecordingDetector;
private final Object mLock = new Object();
@GuardedBy("mLock")
private final ArraySet<Integer> mRunningAppUids = new ArraySet<>();
@GuardedBy("mLock")
private ArraySet<Integer> mPlayingAppUids = new ArraySet<>();
private GenericWindowPolicyController mGenericWindowPolicyController;
private final Object mCallbackLock = new Object();
@GuardedBy("mCallbackLock")
private IAudioRoutingCallback mRoutingCallback;
@GuardedBy("mCallbackLock")
private IAudioConfigChangedCallback mConfigChangedCallback;
public VirtualAudioController(Context context) {
mContext = context;
mAudioPlaybackDetector = new AudioPlaybackDetector(context);
mAudioRecordingDetector = new AudioRecordingDetector(context);
}
/**
* Starts to listen to running applications and audio configuration changes on virtual display
* for audio capture and injection.
*/
public void startListening(
@NonNull GenericWindowPolicyController genericWindowPolicyController,
@NonNull IAudioRoutingCallback routingCallback,
@Nullable IAudioConfigChangedCallback configChangedCallback) {
mGenericWindowPolicyController = genericWindowPolicyController;
mGenericWindowPolicyController.registerRunningAppsChangedListener(/* listener= */ this);
synchronized (mCallbackLock) {
mRoutingCallback = routingCallback;
mConfigChangedCallback = configChangedCallback;
}
synchronized (mLock) {
mRunningAppUids.clear();
mPlayingAppUids.clear();
}
if (configChangedCallback != null) {
mAudioPlaybackDetector.register(/* callback= */ this);
mAudioRecordingDetector.register(/* callback= */ this);
}
}
/**
* Stops listening to running applications and audio configuration changes on virtual display
* for audio capture and injection.
*/
public void stopListening() {
if (mHandler.hasCallbacks(mUpdateAudioRoutingRunnable)) {
mHandler.removeCallbacks(mUpdateAudioRoutingRunnable);
}
mAudioPlaybackDetector.unregister();
mAudioRecordingDetector.unregister();
if (mGenericWindowPolicyController != null) {
mGenericWindowPolicyController.unregisterRunningAppsChangedListener(
/* listener= */ this);
mGenericWindowPolicyController = null;
}
synchronized (mCallbackLock) {
mRoutingCallback = null;
mConfigChangedCallback = null;
}
}
@Override
public void onRunningAppsChanged(ArraySet<Integer> runningUids) {
synchronized (mLock) {
if (mRunningAppUids.equals(runningUids)) {
// Ignore no-op events.
return;
}
mRunningAppUids.clear();
mRunningAppUids.addAll(runningUids);
ArraySet<Integer> oldPlayingAppUids = mPlayingAppUids;
// Update the list of playing apps after caching the old list, and before checking if
// the list of playing apps is empty. This is a subset of the running apps, so we need
// to update this here as well.
AudioManager audioManager = mContext.getSystemService(AudioManager.class);
List<AudioPlaybackConfiguration> configs =
audioManager.getActivePlaybackConfigurations();
mPlayingAppUids = findPlayingAppUids(configs, mRunningAppUids);
// Do not change rerouted applications while any application is playing, or the sound
// will be leaked from phone during the transition. Delay the change until we detect
// there is no application is playing in onPlaybackConfigChanged().
if (!mPlayingAppUids.isEmpty()) {
Slog.i(TAG, "Audio is playing, do not change rerouted apps");
return;
}
// An application previously playing audio was removed from the display.
if (!oldPlayingAppUids.isEmpty()) {
// Delay changing the rerouted application when the last application playing audio
// was removed from virtual device, or the sound will be leaked from phone side
// during the transition.
Slog.i(TAG, "The last playing app removed, delay change rerouted apps");
if (mHandler.hasCallbacks(mUpdateAudioRoutingRunnable)) {
mHandler.removeCallbacks(mUpdateAudioRoutingRunnable);
}
mHandler.postDelayed(mUpdateAudioRoutingRunnable, UPDATE_REROUTING_APPS_DELAY_MS);
return;
}
}
// Normal case with no application playing, just update routing.
notifyAppsNeedingAudioRoutingChanged();
}
@Override
public void onPlaybackConfigChanged(List<AudioPlaybackConfiguration> configs) {
updatePlayingApplications(configs);
List<AudioPlaybackConfiguration> audioPlaybackConfigurations;
synchronized (mLock) {
// Filter configurations of applications running on virtual device.
audioPlaybackConfigurations = findPlaybackConfigurations(configs, mRunningAppUids);
}
synchronized (mCallbackLock) {
if (mConfigChangedCallback != null) {
try {
mConfigChangedCallback.onPlaybackConfigChanged(audioPlaybackConfigurations);
} catch (RemoteException e) {
Slog.e(TAG, "RemoteException when calling onPlaybackConfigChanged", e);
}
}
}
}
@RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
@Override
public void onRecordingConfigChanged(List<AudioRecordingConfiguration> configs) {
List<AudioRecordingConfiguration> audioRecordingConfigurations;
synchronized (mLock) {
// Filter configurations of applications running on virtual device.
audioRecordingConfigurations = findRecordingConfigurations(configs, mRunningAppUids);
}
synchronized (mCallbackLock) {
if (mConfigChangedCallback != null) {
try {
mConfigChangedCallback.onRecordingConfigChanged(audioRecordingConfigurations);
} catch (RemoteException e) {
Slog.e(TAG, "RemoteException when calling onRecordingConfigChanged", e);
}
}
}
}
private void updatePlayingApplications(List<AudioPlaybackConfiguration> configs) {
synchronized (mLock) {
ArraySet<Integer> playingAppUids = findPlayingAppUids(configs, mRunningAppUids);
if (mPlayingAppUids.equals(playingAppUids)) {
return;
}
mPlayingAppUids = playingAppUids;
}
// Updated rerouted apps, even if the app is already playing. It originally should be done
// when onRunningAppsChanged() is called, but we don't want to interrupt the audio
// streaming and cause the sound leak from phone when it's playing, so delay until here.
notifyAppsNeedingAudioRoutingChanged();
}
private void notifyAppsNeedingAudioRoutingChanged() {
if (mHandler.hasCallbacks(mUpdateAudioRoutingRunnable)) {
mHandler.removeCallbacks(mUpdateAudioRoutingRunnable);
}
int[] runningUids;
synchronized (mLock) {
runningUids = new int[mRunningAppUids.size()];
for (int i = 0; i < mRunningAppUids.size(); i++) {
runningUids[i] = mRunningAppUids.valueAt(i);
}
}
synchronized (mCallbackLock) {
if (mRoutingCallback != null) {
try {
mRoutingCallback.onAppsNeedingAudioRoutingChanged(runningUids);
} catch (RemoteException e) {
Slog.e(TAG, "RemoteException when calling updateReroutingApps", e);
}
}
}
}
/**
* Finds uid of playing applications from the given running applications.
*
* @param configs a list of playback configs which get from {@link AudioManager}
*/
private static ArraySet<Integer> findPlayingAppUids(List<AudioPlaybackConfiguration> configs,
ArraySet<Integer> runningAppUids) {
ArraySet<Integer> playingAppUids = new ArraySet<>();
for (AudioPlaybackConfiguration config : configs) {
if (runningAppUids.contains(config.getClientUid())
&& config.getPlayerState() == PLAYER_STATE_STARTED) {
playingAppUids.add(config.getClientUid());
}
}
return playingAppUids;
}
/** Finds a list of {@link AudioPlaybackConfiguration} for the given running applications. */
private static List<AudioPlaybackConfiguration> findPlaybackConfigurations(
List<AudioPlaybackConfiguration> configs,
ArraySet<Integer> runningAppUids) {
List<AudioPlaybackConfiguration> runningConfigs = new ArrayList<>();
for (AudioPlaybackConfiguration config : configs) {
if (runningAppUids.contains(config.getClientUid())) {
runningConfigs.add(config);
}
}
return runningConfigs;
}
/** Finds a list of {@link AudioRecordingConfiguration} for the given running applications. */
@RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
private static List<AudioRecordingConfiguration> findRecordingConfigurations(
List<AudioRecordingConfiguration> configs, ArraySet<Integer> runningAppUids) {
List<AudioRecordingConfiguration> runningConfigs = new ArrayList<>();
for (AudioRecordingConfiguration config : configs) {
if (runningAppUids.contains(config.getClientUid())) {
runningConfigs.add(config);
}
}
return runningConfigs;
}
@VisibleForTesting
boolean hasPendingRunnable() {
return mHandler.hasCallbacks(mUpdateAudioRoutingRunnable);
}
@VisibleForTesting
void addPlayingAppsForTesting(int appUid) {
synchronized (mLock) {
mPlayingAppUids.add(appUid);
}
}
}