1312 lines
53 KiB
Java
1312 lines
53 KiB
Java
![]() |
/*
|
||
|
* 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<WeakReference<AudioTrack>> mInjectors;
|
||
|
/**
|
||
|
* The list AudioRecord instances created to capture audio from the associated mixes
|
||
|
* Lazy initialization in {@link #createAudioRecordSink(AudioMix)}
|
||
|
*/
|
||
|
@GuardedBy("mLock")
|
||
|
@Nullable private ArrayList<WeakReference<AudioRecord>> 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}).
|
||
|
* <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;
|
||
|
|
||
|
@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<AudioMix> 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<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
|
||
|
*/
|
||
|
@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.
|
||
|
* <p>A value of <code>true</code> 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<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 {
|
||
|
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<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");
|
||
|
} 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<Pair<AudioMix, AudioMixingRule>> 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<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;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @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<AudioDeviceInfo> 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<AudioMix> 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}
|
||
|
*
|
||
|
* <p>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}
|
||
|
*
|
||
|
* <p>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<AudioFocusInfo> 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<AudioRecord>(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<AudioTrack>(at));
|
||
|
}
|
||
|
return at;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @hide
|
||
|
*/
|
||
|
public void invalidateCaptorsAndInjectors() {
|
||
|
if (!policyReadyToUse()) {
|
||
|
return;
|
||
|
}
|
||
|
synchronized (mLock) {
|
||
|
if (mInjectors != null) {
|
||
|
for (final WeakReference<AudioTrack> 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<AudioRecord> 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 {}
|
||
|
}
|