| /* |
| * 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.Nullable; |
| import android.annotation.SystemApi; |
| import android.app.ActivityManager; |
| 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.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.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. |
| */ |
| 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; |
| |
| 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. |
| */ |
| 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}). |
| * <br>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; |
| |
| 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 isVolumeController() { return mVolCb != null; } |
| /** @hide */ |
| public @Nullable MediaProjection getMediaProjection() { |
| return mProjection; |
| } |
| |
| /** |
| * 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, @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; |
| 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<AudioMix> mMixes; |
| private Context mContext; |
| private Looper mLooper; |
| private AudioPolicyFocusListener mFocusListener; |
| private AudioPolicyStatusListener mStatusListener; |
| private boolean mIsFocusPolicy = 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<AudioMix>(); |
| 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 |
| */ |
| 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 |
| */ |
| 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 enforce true if the policy will govern audio focus decisions. |
| * @return the same Builder instance. |
| */ |
| public Builder setIsAudioFocusPolicy(boolean isFocusPolicy) { |
| mIsFocusPolicy = isFocusPolicy; |
| 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. |
| */ |
| 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 |
| */ |
| 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)}. |
| */ |
| 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, mProjection); |
| } |
| } |
| |
| /** |
| * 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. |
| */ |
| public int attachMixes(@NonNull List<AudioMix> 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<AudioMix> zeMixes = new ArrayList<AudioMix>(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; |
| } |
| } |
| } |
| |
| /** |
| * 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. |
| */ |
| public int detachMixes(@NonNull List<AudioMix> 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<AudioMix> zeMixes = new ArrayList<AudioMix>(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; |
| } |
| } |
| } |
| |
| /** |
| * @hide |
| * Configures the audio framework so that all audio stream 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<AudioDeviceInfo> 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; |
| } |
| } |
| } |
| |
| 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 (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 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) || 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)); |
| } |
| } |
| |
| /** |
| * 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; |
| } |
| } |
| } |
| |
| /** |
| * 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(); |
| // 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 |
| */ |
| 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; |
| } |
| |
| 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() { |
| 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 {} |
| } |