| /* |
| * 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); |
| } |
| } |
| } |