/* * 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 android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.SystemApi; import android.content.Context; import android.content.pm.PackageManager; import android.media.AudioAttributes; import android.media.AudioFocusInfo; import android.media.AudioFormat; import android.media.AudioManager; import android.media.AudioRecord; import android.media.AudioTrack; import android.media.IAudioService; import android.media.MediaRecorder; 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.Slog; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.List; /** * @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. */ @SystemApi public static final int POLICY_STATUS_UNREGISTERED = 1; /** * The status of an audio policy that is valid, successfully registered and thus active. */ @SystemApi public static final int POLICY_STATUS_REGISTERED = 2; private int mStatus; private String mRegistrationId; private AudioPolicyStatusListener mStatusListener; private boolean mIsFocusPolicy; /** * 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. */ @SystemApi 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)}. */ @SystemApi public static final int FOCUS_POLICY_DUCKING_IN_POLICY = 1; private AudioPolicyFocusListener mFocusListener; private final AudioPolicyVolumeCallback mVolCb; private Context mContext; private AudioPolicyConfig mConfig; /** @hide */ public AudioPolicyConfig getConfig() { return mConfig; } /** @hide */ public boolean hasFocusListener() { return mFocusListener != null; } /** @hide */ public boolean isFocusPolicy() { return mIsFocusPolicy; } /** @hide */ public boolean isVolumeController() { return mVolCb != null; } /** * The parameter is guaranteed non-null through the Builder */ private AudioPolicy(AudioPolicyConfig config, Context context, Looper looper, AudioPolicyFocusListener fl, AudioPolicyStatusListener sl, boolean isFocusPolicy, AudioPolicyVolumeCallback vc) { 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; mVolCb = vc; } /** * Builder class for {@link AudioPolicy} objects. * By default the policy to be created doesn't govern audio focus decisions. */ @SystemApi public static class Builder { private ArrayList mMixes; private Context mContext; private Looper mLooper; private AudioPolicyFocusListener mFocusListener; private AudioPolicyStatusListener mStatusListener; private boolean mIsFocusPolicy = false; private AudioPolicyVolumeCallback mVolCb; /** * Constructs a new Builder with no audio mixes. * @param context the context for the policy */ @SystemApi 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 */ @SystemApi public Builder addMix(@NonNull AudioMix mix) throws IllegalArgumentException { if (mix == null) { throw new IllegalArgumentException("Illegal null AudioMix argument"); } 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 */ @SystemApi 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} */ @SystemApi 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 enforce true if the policy will govern audio focus decisions. * @return the same Builder instance. */ @SystemApi public Builder setIsAudioFocusPolicy(boolean isFocusPolicy) { mIsFocusPolicy = isFocusPolicy; return this; } /** * Sets the audio policy status listener. * @param l a {@link AudioPolicy.AudioPolicyStatusListener} */ @SystemApi public void setAudioPolicyStatusListener(AudioPolicyStatusListener l) { mStatusListener = l; } @SystemApi /** * 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. */ public Builder setAudioPolicyVolumeCallback(@NonNull AudioPolicyVolumeCallback vc) { if (vc == null) { throw new IllegalArgumentException("Invalid null volume callback"); } mVolCb = vc; 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)}. */ @SystemApi 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, mVolCb); } } /** * @hide * Update the current configuration of the set of audio mixes by adding new ones, while * keeping the policy 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. */ @SystemApi 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 { 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; } } } /** * @hide * Update the current configuration of the set of audio mixes by removing some, while * keeping the policy registered. * This method can only be called on a registered policy. * @param mixes the list of {@link AudioMix} to remove * @return {@link AudioManager#SUCCESS} if the change was successful, {@link AudioManager#ERROR} * otherwise. */ @SystemApi 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"); // TODO also check mix is currently contained in list of mixes } else { 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; } } } public void setRegistration(String regId) { synchronized (mLock) { mRegistrationId = regId; mConfig.setRegistration(regId); if (regId != null) { mStatus = POLICY_STATUS_REGISTERED; } else { mStatus = POLICY_STATUS_UNREGISTERED; } } sendMsg(MSG_POLICY_STATUS_CHANGE); } private boolean policyReadyToUse() { synchronized (mLock) { if (mStatus != POLICY_STATUS_REGISTERED) { Log.e(TAG, "Cannot use unregistered AudioPolicy"); return false; } if (mContext == null) { Log.e(TAG, "Cannot use AudioPolicy without context"); return false; } if (mRegistrationId == null) { Log.e(TAG, "Cannot use unregistered AudioPolicy"); return false; } } if (!(PackageManager.PERMISSION_GRANTED == mContext.checkCallingOrSelfPermission( android.Manifest.permission.MODIFY_AUDIO_ROUTING))) { Slog.w(TAG, "Cannot use AudioPolicy for pid " + Binder.getCallingPid() + " / uid " + Binder.getCallingUid() + ", needs MODIFY_AUDIO_ROUTING"); return false; } return true; } 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} */ @SystemApi 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 */ @SystemApi 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; } } } /** * 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 */ @SystemApi 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(); // create the AudioRecord, configured for loop back, using the same format as the mix AudioRecord ar = new AudioRecord( new AudioAttributes.Builder() .setInternalCapturePreset(MediaRecorder.AudioSource.REMOTE_SUBMIX) .addTag(addressForTag(mix)) .addTag(AudioRecord.SUBMIX_FIXED_VOLUME) .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 ); 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 */ @SystemApi 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 AudioTrack at = new AudioTrack( new AudioAttributes.Builder() .setUsage(AudioAttributes.USAGE_VIRTUAL_SOURCE) .addTag(addressForTag(mix)) .build(), mix.getFormat(), AudioTrack.getMinBufferSize(mix.getFormat().getSampleRate(), mix.getFormat().getChannelMask(), mix.getFormat().getEncoding()), AudioTrack.MODE_STREAM, AudioManager.AUDIO_SESSION_ID_GENERATE ); return at; } @SystemApi public int getStatus() { return mStatus; } @SystemApi public static abstract class AudioPolicyStatusListener { public void onStatusChange() {} public void onMixStateUpdate(AudioMix mix) {} } @SystemApi 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) {} } @SystemApi /** * 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 { /** @hide */ 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() { AudioPolicyStatusListener l; synchronized (mLock) { if (mStatusListener == null) { return; } l = mStatusListener; } l.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); } } }; //================================================== // 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 {} }