/* * 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 java.lang.annotation.RetentionPolicy.SOURCE; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.SystemApi; import android.compat.annotation.UnsupportedAppUsage; import android.media.AudioAttributes; import android.media.MediaRecorder; import android.os.Build; import android.os.Parcel; import android.os.Parcelable; import android.util.Log; import com.android.internal.annotations.VisibleForTesting; import java.lang.annotation.Retention; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.Objects; import java.util.Set; /** * @hide * * Here's an example of creating a mixing rule for all media playback: *
* AudioAttributes mediaAttr = new AudioAttributes.Builder() * .setUsage(AudioAttributes.USAGE_MEDIA) * .build(); * AudioMixingRule mediaRule = new AudioMixingRule.Builder() * .addRule(mediaAttr, AudioMixingRule.RULE_MATCH_ATTRIBUTE_USAGE) * .build(); **/ @SystemApi public class AudioMixingRule implements Parcelable { private AudioMixingRule(int mixType, Collection
The mix role indicates playback streams will be captured or recording source will be
* injected.
*
* @return integer value of {@link #MIX_ROLE_PLAYERS} or {@link #MIX_ROLE_INJECTOR}
*/
public @MixRole int getTargetMixRole() {
return mTargetMixType == AudioMix.MIX_TYPE_RECORDERS ? MIX_ROLE_INJECTOR : MIX_ROLE_PLAYERS;
}
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
private final ArrayList If not specified, the mix role will be decided automatically when
* {@link #addRule(AudioAttributes, int)} or {@link #addMixRule(int, Object)} be called.
*
* @param mixRole integer value of {@link #MIX_ROLE_PLAYERS} or {@link #MIX_ROLE_INJECTOR}
* @return the same Builder instance.
*/
public @NonNull Builder setTargetMixRole(@MixRole int mixRole) {
if (mixRole != MIX_ROLE_PLAYERS && mixRole != MIX_ROLE_INJECTOR) {
throw new IllegalArgumentException("Illegal argument for mix role");
}
if (mCriteria.stream().map(AudioMixMatchCriterion::getRule)
.anyMatch(mixRole == MIX_ROLE_PLAYERS
? AudioMixingRule::isRecorderRule : AudioMixingRule::isPlayerRule)) {
throw new IllegalArgumentException(
"Target mix role is not compatible with mix rules.");
}
mTargetMixType = mixRole == MIX_ROLE_INJECTOR
? AudioMix.MIX_TYPE_RECORDERS : AudioMix.MIX_TYPE_PLAYERS;
return this;
}
/**
* Add or exclude a rule for the selection of which streams are mixed together.
* Does error checking on the parameters.
* @param rule
* @param property
* @return the same Builder instance.
* @throws IllegalArgumentException
*/
private Builder checkAddRuleObjInternal(int rule, Object property)
throws IllegalArgumentException {
if (property == null) {
throw new IllegalArgumentException("Illegal null argument for mixing rule");
}
if (!isValidRule(rule)) {
throw new IllegalArgumentException("Illegal rule value " + rule);
}
final int match_rule = rule & ~RULE_EXCLUSION_MASK;
if (isAudioAttributeRule(match_rule)) {
if (!(property instanceof AudioAttributes)) {
throw new IllegalArgumentException("Invalid AudioAttributes argument");
}
return addRuleInternal(
new AudioMixMatchCriterion((AudioAttributes) property, rule));
} else {
// implies integer match rule
if (!(property instanceof Integer)) {
throw new IllegalArgumentException("Invalid Integer argument");
}
return addRuleInternal(new AudioMixMatchCriterion((Integer) property, rule));
}
}
/**
* Add or exclude a rule on AudioAttributes or integer property for the selection of which
* streams are mixed together.
* No rule-to-parameter type check, all done in {@link #checkAddRuleObjInternal(int, Object)}.
* Exceptions are thrown only when incompatible rules are added.
* @param attrToMatch a non-null AudioAttributes instance for which a contradictory
* rule hasn't been set yet, null if not used.
* @param intProp an integer property to match or exclude, null if not used.
* @param rule one of {@link AudioMixingRule#RULE_EXCLUDE_ATTRIBUTE_USAGE},
* {@link AudioMixingRule#RULE_MATCH_ATTRIBUTE_USAGE},
* {@link AudioMixingRule#RULE_MATCH_ATTRIBUTE_CAPTURE_PRESET},
* {@link AudioMixingRule#RULE_EXCLUDE_ATTRIBUTE_CAPTURE_PRESET},
* {@link AudioMixingRule#RULE_MATCH_UID},
* {@link AudioMixingRule#RULE_EXCLUDE_UID},
* {@link AudioMixingRule#RULE_MATCH_AUDIO_SESSION_ID},
* {@link AudioMixingRule#RULE_EXCLUDE_AUDIO_SESSION_ID}
* {@link AudioMixingRule#RULE_MATCH_USERID},
* {@link AudioMixingRule#RULE_EXCLUDE_USERID}.
* @return the same Builder instance.
* @throws IllegalArgumentException
*/
private Builder addRuleInternal(AudioMixMatchCriterion criterion)
throws IllegalArgumentException {
// If mix type is invalid and added rule is valid only for the players / recorders,
// adjust the mix type accordingly.
// Otherwise, if the mix type was already deduced or set explicitly, verify the rule
// is valid for the mix type.
final int rule = criterion.mRule;
if (mTargetMixType == AudioMix.MIX_TYPE_INVALID) {
if (isPlayerRule(rule)) {
mTargetMixType = AudioMix.MIX_TYPE_PLAYERS;
} else if (isRecorderRule(rule)) {
mTargetMixType = AudioMix.MIX_TYPE_RECORDERS;
}
} else if ((isPlayerRule(rule) && (mTargetMixType != AudioMix.MIX_TYPE_PLAYERS))
|| (isRecorderRule(rule)) && (mTargetMixType != AudioMix.MIX_TYPE_RECORDERS))
{
throw new IllegalArgumentException("Incompatible rule for mix");
}
synchronized (mCriteria) {
int oppositeRule = rule ^ RULE_EXCLUSION_MASK;
if (mCriteria.stream().anyMatch(
otherCriterion -> otherCriterion.mRule == oppositeRule)) {
throw new IllegalArgumentException("AudioMixingRule cannot contain RULE_MATCH_*"
+ " and RULE_EXCLUDE_* for the same dimension.");
}
mCriteria.add(criterion);
}
return this;
}
/**
* Combines all of the matching and exclusion rules that have been set and return a new
* {@link AudioMixingRule} object.
* @return a new {@link AudioMixingRule} object
* @throws IllegalArgumentException if the rule is empty.
*/
public AudioMixingRule build() {
if (mCriteria.isEmpty()) {
throw new IllegalArgumentException("Cannot build AudioMixingRule with no rules.");
}
return new AudioMixingRule(
mTargetMixType == AudioMix.MIX_TYPE_INVALID
? AudioMix.MIX_TYPE_PLAYERS : mTargetMixType,
mCriteria, mAllowPrivilegedMediaPlaybackCapture,
mVoiceCommunicationCaptureAllowed);
}
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
// write opt-out respect
dest.writeBoolean(mAllowPrivilegedPlaybackCapture);
// write voice communication capture allowed flag
dest.writeBoolean(mVoiceCommunicationCaptureAllowed);
// write specified mix type
dest.writeInt(mTargetMixType);
// write mix rules
dest.writeInt(mCriteria.size());
for (AudioMixingRule.AudioMixMatchCriterion criterion : mCriteria) {
criterion.writeToParcel(dest, flags);
}
}
public static final @NonNull Parcelable.Creator
For instance the following code
*
* AudioAttributes mediaAttr = new AudioAttributes.Builder()
* .setUsage(AudioAttributes.USAGE_MEDIA)
* .build();
* AudioMixingRule noMediaRule = new AudioMixingRule.Builder()
* .excludeRule(mediaAttr, AudioMixingRule.RULE_MATCH_ATTRIBUTE_USAGE)
* .build();
*
*
will create a rule which maps to any usage value, except USAGE_MEDIA.
* @param attrToMatch a non-null AudioAttributes instance for which a contradictory
* rule hasn't been set yet.
* @param rule {@link AudioMixingRule#RULE_MATCH_ATTRIBUTE_USAGE} or
* {@link AudioMixingRule#RULE_MATCH_ATTRIBUTE_CAPTURE_PRESET}.
* @return the same Builder instance.
* @throws IllegalArgumentException
* @see #addRule(AudioAttributes, int)
*/
public Builder excludeRule(AudioAttributes attrToMatch, int rule)
throws IllegalArgumentException {
if (!isValidAttributesSystemApiRule(rule)) {
throw new IllegalArgumentException("Illegal rule value " + rule);
}
return checkAddRuleObjInternal(rule | RULE_EXCLUSION_MASK, attrToMatch);
}
/**
* Add a rule for the selection of which streams are mixed together.
* The rule defines what the matching will be made on. It also determines the type of the
* property to match against.
* @param rule one of {@link AudioMixingRule#RULE_MATCH_ATTRIBUTE_USAGE},
* {@link AudioMixingRule#RULE_MATCH_ATTRIBUTE_CAPTURE_PRESET} or
* {@link AudioMixingRule#RULE_MATCH_UID} or
* {@link AudioMixingRule#RULE_MATCH_USERID} or
* {@link AudioMixingRule#RULE_MATCH_AUDIO_SESSION_ID}.
* @param property see the definition of each rule for the type to use (either an
* {@link AudioAttributes} or an {@link java.lang.Integer}).
* @return the same Builder instance.
* @throws IllegalArgumentException
* @see #excludeMixRule(int, Object)
*/
public Builder addMixRule(int rule, Object property) throws IllegalArgumentException {
if (!isValidSystemApiRule(rule)) {
throw new IllegalArgumentException("Illegal rule value " + rule);
}
return checkAddRuleObjInternal(rule, property);
}
/**
* Add a rule by exclusion for the selection of which streams are mixed together.
*
For instance the following code
*
* AudioAttributes mediaAttr = new AudioAttributes.Builder()
* .setUsage(AudioAttributes.USAGE_MEDIA)
* .build();
* AudioMixingRule noMediaRule = new AudioMixingRule.Builder()
* .addMixRule(AudioMixingRule.RULE_MATCH_ATTRIBUTE_USAGE, mediaAttr)
* .excludeMixRule(AudioMixingRule.RULE_MATCH_UID, new Integer(uidToExclude)
* .build();
*
*
will create a rule which maps to usage USAGE_MEDIA, but excludes any stream
* coming from the specified UID.
* @param rule one of {@link AudioMixingRule#RULE_MATCH_ATTRIBUTE_USAGE},
* {@link AudioMixingRule#RULE_MATCH_ATTRIBUTE_CAPTURE_PRESET} or
* {@link AudioMixingRule#RULE_MATCH_UID} or
* {@link AudioMixingRule#RULE_MATCH_USERID} or
* {@link AudioMixingRule#RULE_MATCH_AUDIO_SESSION_ID}.
* @param property see the definition of each rule for the type to use (either an
* {@link AudioAttributes} or an {@link java.lang.Integer}).
* @return the same Builder instance.
* @throws IllegalArgumentException
*/
public Builder excludeMixRule(int rule, Object property) throws IllegalArgumentException {
if (!isValidSystemApiRule(rule)) {
throw new IllegalArgumentException("Illegal rule value " + rule);
}
return checkAddRuleObjInternal(rule | RULE_EXCLUSION_MASK, property);
}
/**
* Set if the audio of app that opted out of audio playback capture should be captured.
*
* Caller of this method with true
, MUST abide to the restriction listed in
* {@link ALLOW_CAPTURE_BY_SYSTEM}, including but not limited to the captured audio
* can not leave the capturing app, and the quality is limited to 16k mono.
*
* The permission {@link CAPTURE_AUDIO_OUTPUT} or {@link CAPTURE_MEDIA_OUTPUT} is needed
* to ignore the opt-out.
*
* Only affects LOOPBACK|RENDER mix.
*
* @return the same Builder instance.
*/
public @NonNull Builder allowPrivilegedPlaybackCapture(boolean allow) {
mAllowPrivilegedMediaPlaybackCapture = allow;
return this;
}
/**
* Set if the caller of the rule is able to capture voice communication output.
* A system app can capture voice communication output only if it is granted with the.
* CAPTURE_VOICE_COMMUNICATION_OUTPUT permission.
*
* Note that this method is for internal use only and should not be called by the app that
* creates the rule.
*
* @return the same Builder instance.
*
* @hide
*/
public @NonNull Builder voiceCommunicationCaptureAllowed(boolean allowed) {
mVoiceCommunicationCaptureAllowed = allowed;
return this;
}
/**
* Sets target mix role of the mixing rule.
*
* As each mixing rule is intended to be associated with an {@link AudioMix},
* explicitly setting the role of a mixing rule allows this {@link Builder} to
* verify validity of the mixing rules to be validated.
* The mix role allows distinguishing between:
*
*
*