/* * Copyright (C) 2014 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 android.media.audiopolicy; import static android.media.audiopolicy.Flags.FLAG_ENABLE_FADE_MANAGER_CONFIGURATION; import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.SystemApi; import android.annotation.TestApi; import android.annotation.UserIdInt; import android.app.ActivityManager; import android.content.AttributionSource; import android.content.Context; import android.content.pm.PackageManager; import android.media.AudioAttributes; import android.media.AudioDeviceInfo; import android.media.AudioFocusInfo; import android.media.AudioFormat; import android.media.AudioManager; import android.media.AudioRecord; import android.media.AudioTrack; import android.media.FadeManagerConfiguration; import android.media.IAudioService; import android.media.MediaRecorder; import android.media.projection.MediaProjection; import android.os.Binder; import android.os.Handler; import android.os.IBinder; import android.os.Looper; import android.os.Message; import android.os.RemoteException; import android.os.ServiceManager; import android.util.Log; import android.util.Pair; import android.util.Slog; import com.android.internal.annotations.GuardedBy; import com.android.internal.util.Preconditions; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Objects; /** * @hide * AudioPolicy provides access to the management of audio routing and audio focus. */ @SystemApi public class AudioPolicy { private static final String TAG = "AudioPolicy"; private static final boolean DEBUG = false; private final Object mLock = new Object(); /** * The status of an audio policy that is valid but cannot be used because it is not registered. */ public static final int POLICY_STATUS_UNREGISTERED = 1; /** * The status of an audio policy that is valid, successfully registered and thus active. */ public static final int POLICY_STATUS_REGISTERED = 2; @GuardedBy("mLock") private int mStatus; @GuardedBy("mLock") private String mRegistrationId; private final AudioPolicyStatusListener mStatusListener; private final boolean mIsFocusPolicy; private final boolean mIsTestFocusPolicy; /** * The list of AudioTrack instances created to inject audio into the associated mixes * Lazy initialization in {@link #createAudioTrackSource(AudioMix)} */ @GuardedBy("mLock") @Nullable private ArrayList> mInjectors; /** * The list AudioRecord instances created to capture audio from the associated mixes * Lazy initialization in {@link #createAudioRecordSink(AudioMix)} */ @GuardedBy("mLock") @Nullable private ArrayList> mCaptors; /** * The behavior of a policy with regards to audio focus where it relies on the application * to do the ducking, the is the legacy and default behavior. */ public static final int FOCUS_POLICY_DUCKING_IN_APP = 0; public static final int FOCUS_POLICY_DUCKING_DEFAULT = FOCUS_POLICY_DUCKING_IN_APP; /** * The behavior of a policy with regards to audio focus where it handles ducking instead * of the application losing focus and being signaled it can duck (as communicated by * {@link android.media.AudioManager#AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK}). *
Can only be used after having set a listener with * {@link AudioPolicy#setAudioPolicyFocusListener(AudioPolicyFocusListener)}. */ public static final int FOCUS_POLICY_DUCKING_IN_POLICY = 1; private AudioPolicyFocusListener mFocusListener; private final AudioPolicyVolumeCallback mVolCb; private Context mContext; @GuardedBy("mLock") private AudioPolicyConfig mConfig; private final MediaProjection mProjection; /** @hide */ public AudioPolicyConfig getConfig() { return mConfig; } /** @hide */ public boolean hasFocusListener() { return mFocusListener != null; } /** @hide */ public boolean isFocusPolicy() { return mIsFocusPolicy; } /** @hide */ public boolean isTestFocusPolicy() { return mIsTestFocusPolicy; } /** @hide */ public boolean isVolumeController() { return mVolCb != null; } /** @hide */ public @Nullable MediaProjection getMediaProjection() { return mProjection; } /** @hide */ public AttributionSource getAttributionSource() { return getAttributionSource(mContext); } private static AttributionSource getAttributionSource(Context context) { return context == null ? AttributionSource.myAttributionSource() : context.getAttributionSource(); } /** * The parameters are guaranteed non-null through the Builder */ private AudioPolicy(AudioPolicyConfig config, Context context, Looper looper, AudioPolicyFocusListener fl, AudioPolicyStatusListener sl, boolean isFocusPolicy, boolean isTestFocusPolicy, AudioPolicyVolumeCallback vc, @Nullable MediaProjection projection) { mConfig = config; mStatus = POLICY_STATUS_UNREGISTERED; mContext = context; if (looper == null) { looper = Looper.getMainLooper(); } if (looper != null) { mEventHandler = new EventHandler(this, looper); } else { mEventHandler = null; Log.e(TAG, "No event handler due to looper without a thread"); } mFocusListener = fl; mStatusListener = sl; mIsFocusPolicy = isFocusPolicy; mIsTestFocusPolicy = isTestFocusPolicy; mVolCb = vc; mProjection = projection; } /** * Builder class for {@link AudioPolicy} objects. * By default the policy to be created doesn't govern audio focus decisions. */ public static class Builder { private ArrayList mMixes; private Context mContext; private Looper mLooper; private AudioPolicyFocusListener mFocusListener; private AudioPolicyStatusListener mStatusListener; private boolean mIsFocusPolicy = false; private boolean mIsTestFocusPolicy = false; private AudioPolicyVolumeCallback mVolCb; private MediaProjection mProjection; /** * Constructs a new Builder with no audio mixes. * @param context the context for the policy */ public Builder(Context context) { mMixes = new ArrayList(); mContext = context; } /** * Add an {@link AudioMix} to be part of the audio policy being built. * @param mix a non-null {@link AudioMix} to be part of the audio policy. * @return the same Builder instance. * @throws IllegalArgumentException */ @NonNull public Builder addMix(@NonNull AudioMix mix) throws IllegalArgumentException { if (mix == null) { throw new IllegalArgumentException("Illegal null AudioMix argument"); } if (android.permission.flags.Flags.deviceAwarePermissionApisEnabled()) { mix.setVirtualDeviceId(getAttributionSource(mContext).getDeviceId()); } mMixes.add(mix); return this; } /** * Sets the {@link Looper} on which to run the event loop. * @param looper a non-null specific Looper. * @return the same Builder instance. * @throws IllegalArgumentException */ @NonNull public Builder setLooper(@NonNull Looper looper) throws IllegalArgumentException { if (looper == null) { throw new IllegalArgumentException("Illegal null Looper argument"); } mLooper = looper; return this; } /** * Sets the audio focus listener for the policy. * @param l a {@link AudioPolicy.AudioPolicyFocusListener} */ public void setAudioPolicyFocusListener(AudioPolicyFocusListener l) { mFocusListener = l; } /** * Declares whether this policy will grant and deny audio focus through * the {@link AudioPolicy.AudioPolicyFocusListener}. * If set to {@code true}, it is mandatory to set an * {@link AudioPolicy.AudioPolicyFocusListener} in order to successfully build * an {@code AudioPolicy} instance. * @param isFocusPolicy true if the policy will govern audio focus decisions. * @return the same Builder instance. */ @NonNull public Builder setIsAudioFocusPolicy(boolean isFocusPolicy) { mIsFocusPolicy = isFocusPolicy; return this; } /** * @hide * Test method to declare whether this audio focus policy is for test purposes only. * Having a test policy registered will disable the current focus policy and replace it * with this test policy. When unregistered, the previous focus policy will be restored. *

A value of true will be ignored if the AudioPolicy is not also * focus policy. * @param isTestFocusPolicy true if the focus policy to register is for testing purposes. * @return the same Builder instance */ @TestApi @NonNull public Builder setIsTestFocusPolicy(boolean isTestFocusPolicy) { mIsTestFocusPolicy = isTestFocusPolicy; return this; } /** * Sets the audio policy status listener. * @param l a {@link AudioPolicy.AudioPolicyStatusListener} */ public void setAudioPolicyStatusListener(AudioPolicyStatusListener l) { mStatusListener = l; } /** * Sets the callback to receive all volume key-related events. * The callback will only be called if the device is configured to handle volume events * in the PhoneWindowManager (see config_handleVolumeKeysInWindowManager) * @param vc * @return the same Builder instance. */ @NonNull public Builder setAudioPolicyVolumeCallback(@NonNull AudioPolicyVolumeCallback vc) { if (vc == null) { throw new IllegalArgumentException("Invalid null volume callback"); } mVolCb = vc; return this; } /** * Set a media projection obtained through createMediaProjection(). * * A MediaProjection that can project audio allows to register an audio * policy LOOPBACK|RENDER without the MODIFY_AUDIO_ROUTING permission. * * @hide */ @NonNull public Builder setMediaProjection(@NonNull MediaProjection projection) { if (projection == null) { throw new IllegalArgumentException("Invalid null volume callback"); } mProjection = projection; return this; } /** * Combines all of the attributes that have been set on this {@code Builder} and returns a * new {@link AudioPolicy} object. * @return a new {@code AudioPolicy} object. * @throws IllegalStateException if there is no * {@link AudioPolicy.AudioPolicyStatusListener} but the policy was configured * as an audio focus policy with {@link #setIsAudioFocusPolicy(boolean)}. */ @NonNull public AudioPolicy build() { if (mStatusListener != null) { // the AudioPolicy status listener includes updates on each mix activity state for (AudioMix mix : mMixes) { mix.mCallbackFlags |= AudioMix.CALLBACK_FLAG_NOTIFY_ACTIVITY; } } if (mIsFocusPolicy && mFocusListener == null) { throw new IllegalStateException("Cannot be a focus policy without " + "an AudioPolicyFocusListener"); } return new AudioPolicy(new AudioPolicyConfig(mMixes), mContext, mLooper, mFocusListener, mStatusListener, mIsFocusPolicy, mIsTestFocusPolicy, mVolCb, mProjection); } } /** * Update the current configuration of the set of audio mixes by adding new ones, while * keeping the policy registered. If any of the provided audio mixes is invalid then none of * the passed mixes will be registered. * * This method can only be called on a registered policy. * @param mixes the list of {@link AudioMix} to add * @return {@link AudioManager#SUCCESS} if the change was successful, {@link AudioManager#ERROR} * otherwise. */ public int attachMixes(@NonNull List mixes) { if (mixes == null) { throw new IllegalArgumentException("Illegal null list of AudioMix"); } synchronized (mLock) { if (mStatus != POLICY_STATUS_REGISTERED) { throw new IllegalStateException("Cannot alter unregistered AudioPolicy"); } final ArrayList zeMixes = new ArrayList(mixes.size()); for (AudioMix mix : mixes) { if (mix == null) { throw new IllegalArgumentException("Illegal null AudioMix in attachMixes"); } else { if (android.permission.flags.Flags.deviceAwarePermissionApisEnabled()) { mix.setVirtualDeviceId(getAttributionSource(mContext).getDeviceId()); } zeMixes.add(mix); } } final AudioPolicyConfig cfg = new AudioPolicyConfig(zeMixes); IAudioService service = getService(); try { final int status = service.addMixForPolicy(cfg, this.cb()); if (status == AudioManager.SUCCESS) { mConfig.add(zeMixes); } return status; } catch (RemoteException e) { Log.e(TAG, "Dead object in attachMixes", e); return AudioManager.ERROR; } } } /** * Update the current configuration of the set of audio mixes for this audio policy by * removing some, while keeping the policy registered. Will unregister all provided audio * mixes, if possible. * * This method can only be called on a registered policy and only affects this current policy. * @param mixes the list of {@link AudioMix} to remove * @return {@link AudioManager#SUCCESS} if the change was successful, {@link AudioManager#ERROR} * otherwise. If only some of the provided audio mixes were detached but any one mix * failed to be detached, this method returns {@link AudioManager#ERROR}. */ public int detachMixes(@NonNull List mixes) { if (mixes == null) { throw new IllegalArgumentException("Illegal null list of AudioMix"); } synchronized (mLock) { if (mStatus != POLICY_STATUS_REGISTERED) { throw new IllegalStateException("Cannot alter unregistered AudioPolicy"); } final ArrayList zeMixes = new ArrayList(mixes.size()); for (AudioMix mix : mixes) { if (mix == null) { throw new IllegalArgumentException("Illegal null AudioMix in detachMixes"); } else { if (android.permission.flags.Flags.deviceAwarePermissionApisEnabled()) { mix.setVirtualDeviceId(getAttributionSource(mContext).getDeviceId()); } zeMixes.add(mix); } } final AudioPolicyConfig cfg = new AudioPolicyConfig(zeMixes); IAudioService service = getService(); try { final int status = service.removeMixForPolicy(cfg, this.cb()); if (status == AudioManager.SUCCESS) { mConfig.remove(zeMixes); } return status; } catch (RemoteException e) { Log.e(TAG, "Dead object in detachMixes", e); return AudioManager.ERROR; } } } /** * Update {@link AudioMixingRule}-s of already registered {@link AudioMix}-es. * * @param mixingRuleUpdates - {@link List} of {@link Pair}-s, each pair containing * {@link AudioMix} to update and its new corresponding {@link AudioMixingRule}. * * @return {@link AudioManager#SUCCESS} if the update was successful, * {@link AudioManager#ERROR} otherwise. */ @FlaggedApi(Flags.FLAG_AUDIO_POLICY_UPDATE_MIXING_RULES_API) @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public int updateMixingRules( @NonNull List> mixingRuleUpdates) { Objects.requireNonNull(mixingRuleUpdates); IAudioService service = getService(); try { synchronized (mLock) { final int status = service.updateMixingRulesForPolicy( mixingRuleUpdates.stream().map(p -> p.first).toArray(AudioMix[]::new), mixingRuleUpdates.stream().map(p -> p.second).toArray( AudioMixingRule[]::new), cb()); if (status == AudioManager.SUCCESS) { mConfig.updateMixingRules(mixingRuleUpdates); } return status; } } catch (RemoteException e) { Log.e(TAG, "Received remote exeception in updateMixingRules call: ", e); return AudioManager.ERROR; } } /** * @hide * Configures the audio framework so that all audio streams originating from the given UID * can only come from a set of audio devices. * For this routing to be operational, a number of {@link AudioMix} instances must have been * previously registered on this policy, and routed to a super-set of the given audio devices * with {@link AudioMix.Builder#setDevice(android.media.AudioDeviceInfo)}. Note that having * multiple devices in the list doesn't imply the signals will be duplicated on the different * audio devices, final routing will depend on the {@link AudioAttributes} of the sounds being * played. * @param uid UID of the application to affect. * @param devices list of devices to which the audio stream of the application may be routed. * @return true if the change was successful, false otherwise. */ @SystemApi public boolean setUidDeviceAffinity(int uid, @NonNull List devices) { if (devices == null) { throw new IllegalArgumentException("Illegal null list of audio devices"); } synchronized (mLock) { if (mStatus != POLICY_STATUS_REGISTERED) { throw new IllegalStateException("Cannot use unregistered AudioPolicy"); } final int[] deviceTypes = new int[devices.size()]; final String[] deviceAdresses = new String[devices.size()]; int i = 0; for (AudioDeviceInfo device : devices) { if (device == null) { throw new IllegalArgumentException( "Illegal null AudioDeviceInfo in setUidDeviceAffinity"); } deviceTypes[i] = AudioDeviceInfo.convertDeviceTypeToInternalDevice(device.getType()); deviceAdresses[i] = device.getAddress(); i++; } final IAudioService service = getService(); try { final int status = service.setUidDeviceAffinity(this.cb(), uid, deviceTypes, deviceAdresses); return (status == AudioManager.SUCCESS); } catch (RemoteException e) { Log.e(TAG, "Dead object in setUidDeviceAffinity", e); return false; } } } /** * @hide * Removes audio device affinity previously set by * {@link #setUidDeviceAffinity(int, java.util.List)}. * @param uid UID of the application affected. * @return true if the change was successful, false otherwise. */ @SystemApi public boolean removeUidDeviceAffinity(int uid) { synchronized (mLock) { if (mStatus != POLICY_STATUS_REGISTERED) { throw new IllegalStateException("Cannot use unregistered AudioPolicy"); } final IAudioService service = getService(); try { final int status = service.removeUidDeviceAffinity(this.cb(), uid); return (status == AudioManager.SUCCESS); } catch (RemoteException e) { Log.e(TAG, "Dead object in removeUidDeviceAffinity", e); return false; } } } /** * @hide * Removes audio device affinity previously set by * {@link #setUserIdDeviceAffinity(int, java.util.List)}. * @param userId userId of the application affected, as obtained via * {@link UserHandle#getIdentifier}. Not to be confused with application uid. * @return true if the change was successful, false otherwise. */ @SystemApi public boolean removeUserIdDeviceAffinity(@UserIdInt int userId) { synchronized (mLock) { if (mStatus != POLICY_STATUS_REGISTERED) { throw new IllegalStateException("Cannot use unregistered AudioPolicy"); } final IAudioService service = getService(); try { final int status = service.removeUserIdDeviceAffinity(this.cb(), userId); return (status == AudioManager.SUCCESS); } catch (RemoteException e) { Log.e(TAG, "Dead object in removeUserIdDeviceAffinity", e); return false; } } } /** * @hide * Configures the audio framework so that all audio streams originating from the given user * can only come from a set of audio devices. * For this routing to be operational, a number of {@link AudioMix} instances must have been * previously registered on this policy, and routed to a super-set of the given audio devices * with {@link AudioMix.Builder#setDevice(android.media.AudioDeviceInfo)}. Note that having * multiple devices in the list doesn't imply the signals will be duplicated on the different * audio devices, final routing will depend on the {@link AudioAttributes} of the sounds being * played. * @param userId userId of the application affected, as obtained via * {@link UserHandle#getIdentifier}. Not to be confused with application uid. * @param devices list of devices to which the audio stream of the application may be routed. * @return true if the change was successful, false otherwise. */ @SystemApi public boolean setUserIdDeviceAffinity(@UserIdInt int userId, @NonNull List devices) { Objects.requireNonNull(devices, "Illegal null list of audio devices"); synchronized (mLock) { if (mStatus != POLICY_STATUS_REGISTERED) { throw new IllegalStateException("Cannot use unregistered AudioPolicy"); } final int[] deviceTypes = new int[devices.size()]; final String[] deviceAddresses = new String[devices.size()]; int i = 0; for (AudioDeviceInfo device : devices) { if (device == null) { throw new IllegalArgumentException( "Illegal null AudioDeviceInfo in setUserIdDeviceAffinity"); } deviceTypes[i] = AudioDeviceInfo.convertDeviceTypeToInternalDevice(device.getType()); deviceAddresses[i] = device.getAddress(); i++; } final IAudioService service = getService(); try { final int status = service.setUserIdDeviceAffinity(this.cb(), userId, deviceTypes, deviceAddresses); return (status == AudioManager.SUCCESS); } catch (RemoteException e) { Log.e(TAG, "Dead object in setUserIdDeviceAffinity", e); return false; } } } /** @hide */ public void reset() { setRegistration(null); } /** * @hide */ @TestApi @NonNull @FlaggedApi(Flags.FLAG_AUDIO_MIX_TEST_API) public List getMixes() { if (!Flags.audioMixTestApi()) { return Collections.emptyList(); } synchronized (mLock) { return List.copyOf(mConfig.getMixes()); } } public void setRegistration(String regId) { synchronized (mLock) { mRegistrationId = regId; mConfig.setRegistration(regId); if (regId != null) { mStatus = POLICY_STATUS_REGISTERED; } else { mStatus = POLICY_STATUS_UNREGISTERED; mConfig.reset(); } } sendMsg(MSG_POLICY_STATUS_CHANGE); } /**@hide*/ public String getRegistration() { return mRegistrationId; } /** * Sets a custom {@link FadeManagerConfiguration} to handle fade cycle of players during * {@link android.media.AudioManager#AUDIOFOCUS_LOSS} * * @param fmcForFocusLoss custom {@link FadeManagerConfiguration} * @return {@link AudioManager#SUCCESS} if the update was successful, * {@link AudioManager#ERROR} otherwise * @throws IllegalStateException if the audio policy is not registered * @hide */ @FlaggedApi(FLAG_ENABLE_FADE_MANAGER_CONFIGURATION) @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED) @SystemApi public int setFadeManagerConfigurationForFocusLoss( @NonNull FadeManagerConfiguration fmcForFocusLoss) { Objects.requireNonNull(fmcForFocusLoss, "FadeManagerConfiguration for focus loss cannot be null"); IAudioService service = getService(); synchronized (mLock) { Preconditions.checkState(isAudioPolicyRegisteredLocked(), "Cannot set FadeManagerConfiguration with unregistered AudioPolicy"); try { return service.setFadeManagerConfigurationForFocusLoss(fmcForFocusLoss); } catch (RemoteException e) { Log.e(TAG, "Received remote exception for setFadeManagerConfigurationForFocusLoss:", e); throw e.rethrowFromSystemServer(); } } } /** * Clear the current {@link FadeManagerConfiguration} set to handle fade cycles of players * during {@link android.media.AudioManager#AUDIOFOCUS_LOSS} * *

In the absence of custom {@link FadeManagerConfiguration}, the default configurations will * be used to handle fade cycles during audio focus loss. * * @return {@link AudioManager#SUCCESS} if the update was successful, * {@link AudioManager#ERROR} otherwise * @throws IllegalStateException if the audio policy is not registered * @see #setFadeManagerConfigurationForFocusLoss(FadeManagerConfiguration) * @hide */ @FlaggedApi(FLAG_ENABLE_FADE_MANAGER_CONFIGURATION) @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED) @SystemApi public int clearFadeManagerConfigurationForFocusLoss() { IAudioService service = getService(); synchronized (mLock) { Preconditions.checkState(isAudioPolicyRegisteredLocked(), "Cannot clear FadeManagerConfiguration from unregistered AudioPolicy"); try { return service.clearFadeManagerConfigurationForFocusLoss(); } catch (RemoteException e) { Log.e(TAG, "Received remote exception for " + "clearFadeManagerConfigurationForFocusLoss:", e); throw e.rethrowFromSystemServer(); } } } /** * Get the current fade manager configuration used for fade operations during * {@link android.media.AudioManager#AUDIOFOCUS_LOSS} * *

If no custom {@link FadeManagerConfiguration} is set, the default configuration currently * active will be returned. * * @return the active {@link FadeManagerConfiguration} used during audio focus loss * @throws IllegalStateException if the audio policy is not registered * @see #setFadeManagerConfigurationForFocusLoss(FadeManagerConfiguration) * @see #clearFadeManagerConfigurationForFocusLoss() * @hide */ @FlaggedApi(FLAG_ENABLE_FADE_MANAGER_CONFIGURATION) @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED) @SystemApi @NonNull public FadeManagerConfiguration getFadeManagerConfigurationForFocusLoss() { IAudioService service = getService(); synchronized (mLock) { Preconditions.checkState(isAudioPolicyRegisteredLocked(), "Cannot get FadeManagerConfiguration from unregistered AudioPolicy"); try { return service.getFadeManagerConfigurationForFocusLoss(); } catch (RemoteException e) { Log.e(TAG, "Received remote exception for getFadeManagerConfigurationForFocusLoss:", e); throw e.rethrowFromSystemServer(); } } } @GuardedBy("mLock") private boolean isAudioPolicyRegisteredLocked() { return mStatus == POLICY_STATUS_REGISTERED; } private boolean policyReadyToUse() { synchronized (mLock) { if (mStatus != POLICY_STATUS_REGISTERED) { Log.e(TAG, "Cannot use unregistered AudioPolicy"); return false; } if (mRegistrationId == null) { Log.e(TAG, "Cannot use unregistered AudioPolicy"); return false; } } // Loopback|capture only need an audio projection, everything else need MODIFY_AUDIO_ROUTING boolean canModifyAudioRouting = PackageManager.PERMISSION_GRANTED == checkCallingOrSelfPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING); boolean canInterceptCallAudio = PackageManager.PERMISSION_GRANTED == checkCallingOrSelfPermission( android.Manifest.permission.CALL_AUDIO_INTERCEPTION); boolean canProjectAudio; try { canProjectAudio = mProjection != null && mProjection.getProjection().canProjectAudio(); } catch (RemoteException e) { Log.e(TAG, "Failed to check if MediaProjection#canProjectAudio"); throw e.rethrowFromSystemServer(); } if (!((isLoopbackRenderPolicy() && canProjectAudio) || (isCallRedirectionPolicy() && canInterceptCallAudio) || canModifyAudioRouting)) { Slog.w(TAG, "Cannot use AudioPolicy for pid " + Binder.getCallingPid() + " / uid " + Binder.getCallingUid() + ", needs MODIFY_AUDIO_ROUTING or " + "MediaProjection that can project audio."); return false; } return true; } private boolean isLoopbackRenderPolicy() { synchronized (mLock) { return mConfig.mMixes.stream().allMatch(mix -> mix.getRouteFlags() == (mix.ROUTE_FLAG_RENDER | mix.ROUTE_FLAG_LOOP_BACK)); } } private boolean isCallRedirectionPolicy() { synchronized (mLock) { for (AudioMix mix : mConfig.mMixes) { if (mix.isForCallRedirection()) { return true; } } return false; } } /** * Returns {@link PackageManager#PERMISSION_GRANTED} if the caller has the given permission. */ private @PackageManager.PermissionResult int checkCallingOrSelfPermission(String permission) { if (mContext != null) { return mContext.checkCallingOrSelfPermission(permission); } Slog.v(TAG, "Null context, checking permission via ActivityManager"); int pid = Binder.getCallingPid(); int uid = Binder.getCallingUid(); try { return ActivityManager.getService().checkPermission(permission, pid, uid); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } private void checkMixReadyToUse(AudioMix mix, boolean forTrack) throws IllegalArgumentException{ if (mix == null) { String msg = forTrack ? "Invalid null AudioMix for AudioTrack creation" : "Invalid null AudioMix for AudioRecord creation"; throw new IllegalArgumentException(msg); } if (!mConfig.mMixes.contains(mix)) { throw new IllegalArgumentException("Invalid mix: not part of this policy"); } if ((mix.getRouteFlags() & AudioMix.ROUTE_FLAG_LOOP_BACK) != AudioMix.ROUTE_FLAG_LOOP_BACK) { throw new IllegalArgumentException("Invalid AudioMix: not defined for loop back"); } if (forTrack && (mix.getMixType() != AudioMix.MIX_TYPE_RECORDERS)) { throw new IllegalArgumentException( "Invalid AudioMix: not defined for being a recording source"); } if (!forTrack && (mix.getMixType() != AudioMix.MIX_TYPE_PLAYERS)) { throw new IllegalArgumentException( "Invalid AudioMix: not defined for capturing playback"); } } /** * Returns the current behavior for audio focus-related ducking. * @return {@link #FOCUS_POLICY_DUCKING_IN_APP} or {@link #FOCUS_POLICY_DUCKING_IN_POLICY} */ public int getFocusDuckingBehavior() { return mConfig.mDuckingPolicy; } // Note on implementation: not part of the Builder as there can be only one registered policy // that handles ducking but there can be multiple policies /** * Sets the behavior for audio focus-related ducking. * There must be a focus listener if this policy is to handle ducking. * @param behavior {@link #FOCUS_POLICY_DUCKING_IN_APP} or * {@link #FOCUS_POLICY_DUCKING_IN_POLICY} * @return {@link AudioManager#SUCCESS} or {@link AudioManager#ERROR} (for instance if there * is already an audio policy that handles ducking). * @throws IllegalArgumentException * @throws IllegalStateException */ public int setFocusDuckingBehavior(int behavior) throws IllegalArgumentException, IllegalStateException { if ((behavior != FOCUS_POLICY_DUCKING_IN_APP) && (behavior != FOCUS_POLICY_DUCKING_IN_POLICY)) { throw new IllegalArgumentException("Invalid ducking behavior " + behavior); } synchronized (mLock) { if (mStatus != POLICY_STATUS_REGISTERED) { throw new IllegalStateException( "Cannot change ducking behavior for unregistered policy"); } if ((behavior == FOCUS_POLICY_DUCKING_IN_POLICY) && (mFocusListener == null)) { // there must be a focus listener if the policy handles ducking throw new IllegalStateException( "Cannot handle ducking without an audio focus listener"); } IAudioService service = getService(); try { final int status = service.setFocusPropertiesForPolicy(behavior /*duckingBehavior*/, this.cb()); if (status == AudioManager.SUCCESS) { mConfig.mDuckingPolicy = behavior; } return status; } catch (RemoteException e) { Log.e(TAG, "Dead object in setFocusPropertiesForPolicy for behavior", e); return AudioManager.ERROR; } } } /** * Returns the list of entries in the focus stack. * The list is ordered with increasing rank of focus ownership, where the last entry is at the * top of the focus stack and is the current focus owner. * @return the ordered list of focus owners * @see AudioManager#registerAudioPolicy(AudioPolicy) */ @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public @NonNull List getFocusStack() { try { return getService().getFocusStack(); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Send AUDIOFOCUS_LOSS to a specific stack entry, causing it to be notified of the focus * loss, and for it to exit the focus stack (its focus listener will not be invoked after that). * This operation is only valid for a registered policy (with * {@link AudioManager#registerAudioPolicy(AudioPolicy)}) that is also set as the policy focus * listener (with {@link Builder#setAudioPolicyFocusListener(AudioPolicyFocusListener)}. * @param focusLoser the stack entry that is exiting the stack through a focus loss * @return false if the focusLoser wasn't found in the stack, true otherwise * @throws IllegalStateException if used on an unregistered policy, or a registered policy * with no {@link AudioPolicyFocusListener} set * @see AudioManager#registerAudioPolicy(AudioPolicy) * @see Builder#setAudioPolicyStatusListener(AudioPolicyStatusListener) */ @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public boolean sendFocusLoss(@NonNull AudioFocusInfo focusLoser) throws IllegalStateException { Objects.requireNonNull(focusLoser); try { return getService().sendFocusLoss(focusLoser, cb()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Create an {@link AudioRecord} instance that is associated with the given {@link AudioMix}. * Audio buffers recorded through the created instance will contain the mix of the audio * streams that fed the given mixer. * @param mix a non-null {@link AudioMix} instance whose routing flags was defined with * {@link AudioMix#ROUTE_FLAG_LOOP_BACK}, previously added to this policy. * @return a new {@link AudioRecord} instance whose data format is the one defined in the * {@link AudioMix}, or null if this policy was not successfully registered * with {@link AudioManager#registerAudioPolicy(AudioPolicy)}. * @throws IllegalArgumentException */ public AudioRecord createAudioRecordSink(AudioMix mix) throws IllegalArgumentException { if (!policyReadyToUse()) { Log.e(TAG, "Cannot create AudioRecord sink for AudioMix"); return null; } checkMixReadyToUse(mix, false/*not for an AudioTrack*/); // create an AudioFormat from the mix format compatible with recording, as the mix // was defined for playback AudioFormat mixFormat = new AudioFormat.Builder(mix.getFormat()) .setChannelMask(AudioFormat.inChannelMaskFromOutChannelMask( mix.getFormat().getChannelMask())) .build(); AudioAttributes.Builder ab = new AudioAttributes.Builder() .setInternalCapturePreset(MediaRecorder.AudioSource.REMOTE_SUBMIX) .addTag(addressForTag(mix)) .addTag(AudioRecord.SUBMIX_FIXED_VOLUME); if (mix.isForCallRedirection()) { ab.setForCallRedirection(); } // create the AudioRecord, configured for loop back, using the same format as the mix AudioRecord ar = new AudioRecord(ab.build(), mixFormat, AudioRecord.getMinBufferSize(mix.getFormat().getSampleRate(), // using stereo for buffer size to avoid the current poor support for masks AudioFormat.CHANNEL_IN_STEREO, mix.getFormat().getEncoding()), AudioManager.AUDIO_SESSION_ID_GENERATE ); synchronized (mLock) { if (mCaptors == null) { mCaptors = new ArrayList<>(1); } mCaptors.add(new WeakReference(ar)); } return ar; } /** * Create an {@link AudioTrack} instance that is associated with the given {@link AudioMix}. * Audio buffers played through the created instance will be sent to the given mix * to be recorded through the recording APIs. * @param mix a non-null {@link AudioMix} instance whose routing flags was defined with * {@link AudioMix#ROUTE_FLAG_LOOP_BACK}, previously added to this policy. * @return a new {@link AudioTrack} instance whose data format is the one defined in the * {@link AudioMix}, or null if this policy was not successfully registered * with {@link AudioManager#registerAudioPolicy(AudioPolicy)}. * @throws IllegalArgumentException */ public AudioTrack createAudioTrackSource(AudioMix mix) throws IllegalArgumentException { if (!policyReadyToUse()) { Log.e(TAG, "Cannot create AudioTrack source for AudioMix"); return null; } checkMixReadyToUse(mix, true/*for an AudioTrack*/); // create the AudioTrack, configured for loop back, using the same format as the mix AudioAttributes.Builder ab = new AudioAttributes.Builder() .setUsage(AudioAttributes.USAGE_VIRTUAL_SOURCE) .addTag(addressForTag(mix)); if (mix.isForCallRedirection()) { ab.setForCallRedirection(); } AudioTrack at = new AudioTrack(ab.build(), mix.getFormat(), AudioTrack.getMinBufferSize(mix.getFormat().getSampleRate(), mix.getFormat().getChannelMask(), mix.getFormat().getEncoding()), AudioTrack.MODE_STREAM, AudioManager.AUDIO_SESSION_ID_GENERATE ); synchronized (mLock) { if (mInjectors == null) { mInjectors = new ArrayList<>(1); } mInjectors.add(new WeakReference(at)); } return at; } /** * @hide */ public void invalidateCaptorsAndInjectors() { if (!policyReadyToUse()) { return; } synchronized (mLock) { if (mInjectors != null) { for (final WeakReference weakTrack : mInjectors) { final AudioTrack track = weakTrack.get(); if (track == null) { continue; } try { // TODO: add synchronous versions track.stop(); track.flush(); } catch (IllegalStateException e) { // ignore exception, AudioTrack could have already been stopped or // released by the user of the AudioPolicy } } mInjectors.clear(); } if (mCaptors != null) { for (final WeakReference weakRecord : mCaptors) { final AudioRecord record = weakRecord.get(); if (record == null) { continue; } try { // TODO: if needed: implement an invalidate method record.stop(); } catch (IllegalStateException e) { // ignore exception, AudioRecord could have already been stopped or // released by the user of the AudioPolicy } } mCaptors.clear(); } } } public int getStatus() { return mStatus; } public static abstract class AudioPolicyStatusListener { public void onStatusChange() {} public void onMixStateUpdate(AudioMix mix) {} } public static abstract class AudioPolicyFocusListener { public void onAudioFocusGrant(AudioFocusInfo afi, int requestResult) {} public void onAudioFocusLoss(AudioFocusInfo afi, boolean wasNotified) {} /** * Called whenever an application requests audio focus. * Only ever called if the {@link AudioPolicy} was built with * {@link AudioPolicy.Builder#setIsAudioFocusPolicy(boolean)} set to {@code true}. * @param afi information about the focus request and the requester * @param requestResult deprecated after the addition of * {@link AudioManager#setFocusRequestResult(AudioFocusInfo, int, AudioPolicy)} * in Android P, always equal to {@link #AUDIOFOCUS_REQUEST_GRANTED}. */ public void onAudioFocusRequest(AudioFocusInfo afi, int requestResult) {} /** * Called whenever an application abandons audio focus. * Only ever called if the {@link AudioPolicy} was built with * {@link AudioPolicy.Builder#setIsAudioFocusPolicy(boolean)} set to {@code true}. * @param afi information about the focus request being abandoned and the original * requester. */ public void onAudioFocusAbandon(AudioFocusInfo afi) {} } /** * Callback class to receive volume change-related events. * See {@link #Builder.setAudioPolicyVolumeCallback(AudioPolicyCallback)} to configure the * {@link AudioPolicy} to receive those events. * */ public static abstract class AudioPolicyVolumeCallback { public AudioPolicyVolumeCallback() {} /** * Called when volume key-related changes are triggered, on the key down event. * @param adjustment the type of volume adjustment for the key. */ public void onVolumeAdjustment(@AudioManager.VolumeAdjustment int adjustment) {} } private void onPolicyStatusChange() { if (mStatusListener != null) { mStatusListener.onStatusChange(); } } //================================================== // Callback interface /** @hide */ public IAudioPolicyCallback cb() { return mPolicyCb; } private final IAudioPolicyCallback mPolicyCb = new IAudioPolicyCallback.Stub() { public void notifyAudioFocusGrant(AudioFocusInfo afi, int requestResult) { sendMsg(MSG_FOCUS_GRANT, afi, requestResult); if (DEBUG) { Log.v(TAG, "notifyAudioFocusGrant: pack=" + afi.getPackageName() + " client=" + afi.getClientId() + "reqRes=" + requestResult); } } public void notifyAudioFocusLoss(AudioFocusInfo afi, boolean wasNotified) { sendMsg(MSG_FOCUS_LOSS, afi, wasNotified ? 1 : 0); if (DEBUG) { Log.v(TAG, "notifyAudioFocusLoss: pack=" + afi.getPackageName() + " client=" + afi.getClientId() + "wasNotified=" + wasNotified); } } public void notifyAudioFocusRequest(AudioFocusInfo afi, int requestResult) { sendMsg(MSG_FOCUS_REQUEST, afi, requestResult); if (DEBUG) { Log.v(TAG, "notifyAudioFocusRequest: pack=" + afi.getPackageName() + " client=" + afi.getClientId() + " gen=" + afi.getGen()); } } public void notifyAudioFocusAbandon(AudioFocusInfo afi) { sendMsg(MSG_FOCUS_ABANDON, afi, 0 /* ignored */); if (DEBUG) { Log.v(TAG, "notifyAudioFocusAbandon: pack=" + afi.getPackageName() + " client=" + afi.getClientId()); } } public void notifyMixStateUpdate(String regId, int state) { for (AudioMix mix : mConfig.getMixes()) { if (mix.getRegistration().equals(regId)) { mix.mMixState = state; sendMsg(MSG_MIX_STATE_UPDATE, mix, 0/*ignored*/); if (DEBUG) { Log.v(TAG, "notifyMixStateUpdate: regId=" + regId + " state=" + state); } } } } public void notifyVolumeAdjust(int adjustment) { sendMsg(MSG_VOL_ADJUST, null /* ignored */, adjustment); if (DEBUG) { Log.v(TAG, "notifyVolumeAdjust: " + adjustment); } } public void notifyUnregistration() { setRegistration(null); } }; //================================================== // Event handling private final EventHandler mEventHandler; private final static int MSG_POLICY_STATUS_CHANGE = 0; private final static int MSG_FOCUS_GRANT = 1; private final static int MSG_FOCUS_LOSS = 2; private final static int MSG_MIX_STATE_UPDATE = 3; private final static int MSG_FOCUS_REQUEST = 4; private final static int MSG_FOCUS_ABANDON = 5; private final static int MSG_VOL_ADJUST = 6; private class EventHandler extends Handler { public EventHandler(AudioPolicy ap, Looper looper) { super(looper); } @Override public void handleMessage(Message msg) { switch(msg.what) { case MSG_POLICY_STATUS_CHANGE: onPolicyStatusChange(); break; case MSG_FOCUS_GRANT: if (mFocusListener != null) { mFocusListener.onAudioFocusGrant( (AudioFocusInfo) msg.obj, msg.arg1); } break; case MSG_FOCUS_LOSS: if (mFocusListener != null) { mFocusListener.onAudioFocusLoss( (AudioFocusInfo) msg.obj, msg.arg1 != 0); } break; case MSG_MIX_STATE_UPDATE: if (mStatusListener != null) { mStatusListener.onMixStateUpdate((AudioMix) msg.obj); } break; case MSG_FOCUS_REQUEST: if (mFocusListener != null) { mFocusListener.onAudioFocusRequest((AudioFocusInfo) msg.obj, msg.arg1); } else { // should never be null, but don't crash Log.e(TAG, "Invalid null focus listener for focus request event"); } break; case MSG_FOCUS_ABANDON: if (mFocusListener != null) { // should never be null mFocusListener.onAudioFocusAbandon((AudioFocusInfo) msg.obj); } else { // should never be null, but don't crash Log.e(TAG, "Invalid null focus listener for focus abandon event"); } break; case MSG_VOL_ADJUST: if (mVolCb != null) { mVolCb.onVolumeAdjustment(msg.arg1); } else { // should never be null, but don't crash Log.e(TAG, "Invalid null volume event"); } break; default: Log.e(TAG, "Unknown event " + msg.what); } } } //========================================================== // Utils private static String addressForTag(AudioMix mix) { return "addr=" + mix.getRegistration(); } private void sendMsg(int msg) { if (mEventHandler != null) { mEventHandler.sendEmptyMessage(msg); } } private void sendMsg(int msg, Object obj, int i) { if (mEventHandler != null) { mEventHandler.sendMessage( mEventHandler.obtainMessage(msg, i /*arg1*/, 0 /*arg2, ignored*/, obj)); } } private static IAudioService sService; private static IAudioService getService() { if (sService != null) { return sService; } IBinder b = ServiceManager.getService(Context.AUDIO_SERVICE); sService = IAudioService.Stub.asInterface(b); return sService; } public String toLogFriendlyString() { String textDump = new String("android.media.audiopolicy.AudioPolicy:\n"); textDump += "config=" + mConfig.toLogFriendlyString(); return (textDump); } /** @hide */ @IntDef({ POLICY_STATUS_REGISTERED, POLICY_STATUS_UNREGISTERED }) @Retention(RetentionPolicy.SOURCE) public @interface PolicyStatus {} }