Loading media/java/android/media/audiopolicy/AudioMixingRule.java +45 −94 Original line number Diff line number Diff line Loading @@ -30,8 +30,10 @@ import android.util.Log; import java.lang.annotation.Retention; import java.util.ArrayList; import java.util.Iterator; import java.util.Collection; import java.util.HashSet; import java.util.Objects; import java.util.Set; /** Loading @@ -50,10 +52,10 @@ import java.util.Objects; @SystemApi public class AudioMixingRule { private AudioMixingRule(int mixType, ArrayList<AudioMixMatchCriterion> criteria, private AudioMixingRule(int mixType, Collection<AudioMixMatchCriterion> criteria, boolean allowPrivilegedMediaPlaybackCapture, boolean voiceCommunicationCaptureAllowed) { mCriteria = criteria; mCriteria = new ArrayList<>(criteria); mTargetMixType = mixType; mAllowPrivilegedPlaybackCapture = allowPrivilegedMediaPlaybackCapture; mVoiceCommunicationCaptureAllowed = voiceCommunicationCaptureAllowed; Loading Loading @@ -140,6 +142,20 @@ public class AudioMixingRule { return Objects.hash(mAttr, mIntProp, mRule); } @Override public boolean equals(Object object) { if (object == null || this.getClass() != object.getClass()) { return false; } if (object == this) { return true; } AudioMixMatchCriterion other = (AudioMixMatchCriterion) object; return mRule == other.mRule && mIntProp == other.mIntProp && Objects.equals(mAttr, other.mAttr); } void writeToParcel(Parcel dest) { dest.writeInt(mRule); final int match_rule = mRule & ~RULE_EXCLUSION_MASK; Loading Loading @@ -192,15 +208,6 @@ public class AudioMixingRule { return false; } private static boolean areCriteriaEquivalent(ArrayList<AudioMixMatchCriterion> cr1, ArrayList<AudioMixMatchCriterion> cr2) { if (cr1 == null || cr2 == null) return false; if (cr1 == cr2) return true; if (cr1.size() != cr2.size()) return false; //TODO iterate over rules to check they contain the same criterion return (cr1.hashCode() == cr2.hashCode()); } private final int mTargetMixType; int getTargetMixType() { return mTargetMixType; Loading Loading @@ -286,9 +293,9 @@ public class AudioMixingRule { final AudioMixingRule that = (AudioMixingRule) o; return (this.mTargetMixType == that.mTargetMixType) && (areCriteriaEquivalent(this.mCriteria, that.mCriteria) && this.mAllowPrivilegedPlaybackCapture == that.mAllowPrivilegedPlaybackCapture && this.mVoiceCommunicationCaptureAllowed && Objects.equals(mCriteria, that.mCriteria) && (this.mAllowPrivilegedPlaybackCapture == that.mAllowPrivilegedPlaybackCapture) && (this.mVoiceCommunicationCaptureAllowed == that.mVoiceCommunicationCaptureAllowed); } Loading Loading @@ -372,7 +379,7 @@ public class AudioMixingRule { * Builder class for {@link AudioMixingRule} objects */ public static class Builder { private ArrayList<AudioMixMatchCriterion> mCriteria; private final Set<AudioMixMatchCriterion> mCriteria; private int mTargetMixType = AudioMix.MIX_TYPE_INVALID; private boolean mAllowPrivilegedMediaPlaybackCapture = false; // This value should be set internally according to a permission check Loading @@ -382,7 +389,7 @@ public class AudioMixingRule { * Constructs a new Builder with no rules. */ public Builder() { mCriteria = new ArrayList<AudioMixMatchCriterion>(); mCriteria = new HashSet<>(); } /** Loading Loading @@ -547,7 +554,12 @@ public class AudioMixingRule { throw new IllegalArgumentException("Illegal argument for mix role"); } Log.i("AudioMixingRule", "Builder setTargetMixRole " + mixRole); 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; Loading Loading @@ -604,17 +616,15 @@ public class AudioMixingRule { */ private Builder addRuleInternal(AudioAttributes attrToMatch, Integer intProp, int rule) throws IllegalArgumentException { // as rules are added to the Builder, we verify they are consistent with the type // of mix being built. When adding the first rule, the mix type is MIX_TYPE_INVALID. // 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. if (mTargetMixType == AudioMix.MIX_TYPE_INVALID) { if (isPlayerRule(rule)) { mTargetMixType = AudioMix.MIX_TYPE_PLAYERS; } else if (isRecorderRule(rule)) { mTargetMixType = AudioMix.MIX_TYPE_RECORDERS; } else { // For rules which are not player or recorder specific (e.g. RULE_MATCH_UID), // the default mix type is MIX_TYPE_PLAYERS. mTargetMixType = AudioMix.MIX_TYPE_PLAYERS; } } else if ((isPlayerRule(rule) && (mTargetMixType != AudioMix.MIX_TYPE_PLAYERS)) || (isRecorderRule(rule)) && (mTargetMixType != AudioMix.MIX_TYPE_RECORDERS)) Loading @@ -622,75 +632,13 @@ public class AudioMixingRule { throw new IllegalArgumentException("Incompatible rule for mix"); } synchronized (mCriteria) { Iterator<AudioMixMatchCriterion> crIterator = mCriteria.iterator(); final int match_rule = rule & ~RULE_EXCLUSION_MASK; while (crIterator.hasNext()) { final AudioMixMatchCriterion criterion = crIterator.next(); if ((criterion.mRule & ~RULE_EXCLUSION_MASK) != match_rule) { continue; // The two rules are not of the same type int oppositeRule = rule ^ RULE_EXCLUSION_MASK; if (mCriteria.stream().anyMatch(criterion -> criterion.mRule == oppositeRule)) { throw new IllegalArgumentException("AudioMixingRule cannot contain RULE_MATCH_*" + " and RULE_EXCLUDE_* for the same dimension."); } switch (match_rule) { case RULE_MATCH_ATTRIBUTE_USAGE: // "usage"-based rule if (criterion.mAttr.getSystemUsage() == attrToMatch.getSystemUsage()) { if (criterion.mRule == rule) { // rule already exists, we're done return this; } else { // criterion already exists with a another rule, // it is incompatible throw new IllegalArgumentException("Contradictory rule exists" + " for " + attrToMatch); } } break; case RULE_MATCH_ATTRIBUTE_CAPTURE_PRESET: // "capture preset"-base rule if (criterion.mAttr.getCapturePreset() == attrToMatch.getCapturePreset()) { if (criterion.mRule == rule) { // rule already exists, we're done return this; } else { // criterion already exists with a another rule, // it is incompatible throw new IllegalArgumentException("Contradictory rule exists" + " for " + attrToMatch); } } break; case RULE_MATCH_UID: // "usage"-based rule if (criterion.mIntProp == intProp.intValue()) { if (criterion.mRule == rule) { // rule already exists, we're done return this; } else { // criterion already exists with a another rule, // it is incompatible throw new IllegalArgumentException("Contradictory rule exists" + " for UID " + intProp); } } break; case RULE_MATCH_USERID: // "userid"-based rule if (criterion.mIntProp == intProp.intValue()) { if (criterion.mRule == rule) { // rule already exists, we're done return this; } else { // criterion already exists with a another rule, // it is incompatible throw new IllegalArgumentException("Contradictory rule exists" + " for userId " + intProp); } } break; } } // rule didn't exist, add it switch (match_rule) { int ruleWithoutExclusion = rule & ~RULE_EXCLUSION_MASK; switch (ruleWithoutExclusion) { case RULE_MATCH_ATTRIBUTE_USAGE: case RULE_MATCH_ATTRIBUTE_CAPTURE_PRESET: mCriteria.add(new AudioMixMatchCriterion(attrToMatch, rule)); Loading Loading @@ -734,8 +682,11 @@ public class AudioMixingRule { * @return a new {@link AudioMixingRule} object */ public AudioMixingRule build() { return new AudioMixingRule(mTargetMixType, mCriteria, mAllowPrivilegedMediaPlaybackCapture, mVoiceCommunicationCaptureAllowed); return new AudioMixingRule( mTargetMixType == AudioMix.MIX_TYPE_INVALID ? AudioMix.MIX_TYPE_PLAYERS : mTargetMixType, mCriteria, mAllowPrivilegedMediaPlaybackCapture, mVoiceCommunicationCaptureAllowed); } } } media/tests/AudioPolicyTest/Android.bp +1 −0 Original line number Diff line number Diff line Loading @@ -14,6 +14,7 @@ android_test { "androidx.test.ext.junit", "androidx.test.rules", "guava", "hamcrest-library", "platform-test-annotations", ], platform_apis: true, Loading media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioMixingRuleUnitTests.java 0 → 100644 +261 −0 Original line number Diff line number Diff line /* * Copyright (C) 2022 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 com.android.audiopolicytest; import static android.media.AudioAttributes.USAGE_MEDIA; import static android.media.MediaRecorder.AudioSource.VOICE_RECOGNITION; import static android.media.audiopolicy.AudioMixingRule.MIX_ROLE_INJECTOR; import static android.media.audiopolicy.AudioMixingRule.MIX_ROLE_PLAYERS; import static android.media.audiopolicy.AudioMixingRule.RULE_EXCLUDE_ATTRIBUTE_CAPTURE_PRESET; import static android.media.audiopolicy.AudioMixingRule.RULE_EXCLUDE_ATTRIBUTE_USAGE; import static android.media.audiopolicy.AudioMixingRule.RULE_EXCLUDE_UID; import static android.media.audiopolicy.AudioMixingRule.RULE_MATCH_ATTRIBUTE_CAPTURE_PRESET; import static android.media.audiopolicy.AudioMixingRule.RULE_MATCH_ATTRIBUTE_USAGE; import static android.media.audiopolicy.AudioMixingRule.RULE_MATCH_UID; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.collection.IsCollectionWithSize.hasSize; import static org.hamcrest.collection.IsIterableContainingInAnyOrder.containsInAnyOrder; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThrows; import android.media.AudioAttributes; import android.media.audiopolicy.AudioMixingRule; import android.media.audiopolicy.AudioMixingRule.AudioMixMatchCriterion; import android.platform.test.annotations.Presubmit; import androidx.test.ext.junit.runners.AndroidJUnit4; import org.hamcrest.CustomTypeSafeMatcher; import org.hamcrest.Description; import org.hamcrest.Matcher; import org.junit.Test; import org.junit.runner.RunWith; /** * Unit tests for AudioPolicy. * * Run with "atest AudioMixingRuleUnitTests". */ @Presubmit @RunWith(AndroidJUnit4.class) public class AudioMixingRuleUnitTests { private static final AudioAttributes USAGE_MEDIA_AUDIO_ATTRIBUTES = new AudioAttributes.Builder().setUsage(USAGE_MEDIA).build(); private static final AudioAttributes CAPTURE_PRESET_VOICE_RECOGNITION_AUDIO_ATTRIBUTES = new AudioAttributes.Builder().setCapturePreset(VOICE_RECOGNITION).build(); private static final int TEST_UID = 42; private static final int OTHER_UID = 77; @Test public void testConstructValidRule() { AudioMixingRule rule = new AudioMixingRule.Builder() .addMixRule(RULE_MATCH_ATTRIBUTE_USAGE, USAGE_MEDIA_AUDIO_ATTRIBUTES) .addMixRule(RULE_MATCH_UID, TEST_UID) .build(); // Based on the rules, the mix type should fall back to MIX_ROLE_PLAYERS, // since the rules are valid for both MIX_ROLE_PLAYERS & MIX_ROLE_INJECTOR. assertEquals(rule.getTargetMixRole(), MIX_ROLE_PLAYERS); assertThat(rule.getCriteria(), containsInAnyOrder( isAudioMixMatchUsageCriterion(USAGE_MEDIA), isAudioMixMatchUidCriterion(TEST_UID))); } @Test public void testConstructRuleWithConflictingCriteriaFails() { assertThrows(IllegalArgumentException.class, () -> new AudioMixingRule.Builder() .addMixRule(RULE_MATCH_ATTRIBUTE_USAGE, USAGE_MEDIA_AUDIO_ATTRIBUTES) .addMixRule(RULE_MATCH_UID, TEST_UID) // Conflicts with previous criterion. .addMixRule(RULE_EXCLUDE_UID, OTHER_UID) .build()); } @Test public void testRuleBuilderDedupsCriteria() { AudioMixingRule rule = new AudioMixingRule.Builder() .addMixRule(RULE_MATCH_ATTRIBUTE_USAGE, USAGE_MEDIA_AUDIO_ATTRIBUTES) .addMixRule(RULE_MATCH_UID, TEST_UID) // Identical to previous criterion. .addMixRule(RULE_MATCH_UID, TEST_UID) // Identical to first criterion. .addMixRule(RULE_MATCH_ATTRIBUTE_USAGE, USAGE_MEDIA_AUDIO_ATTRIBUTES) .build(); assertThat(rule.getCriteria(), hasSize(2)); assertThat(rule.getCriteria(), containsInAnyOrder( isAudioMixMatchUsageCriterion(USAGE_MEDIA), isAudioMixMatchUidCriterion(TEST_UID))); } @Test public void failsWhenAddAttributeRuleCalledWithInvalidType() { assertThrows(IllegalArgumentException.class, () -> new AudioMixingRule.Builder() // Rule match attribute usage requires AudioAttributes, not // just the int enum value of the usage. .addMixRule(RULE_MATCH_ATTRIBUTE_USAGE, USAGE_MEDIA) .build()); } @Test public void failsWhenExcludeAttributeRuleCalledWithInvalidType() { assertThrows(IllegalArgumentException.class, () -> new AudioMixingRule.Builder() // Rule match attribute usage requires AudioAttributes, not // just the int enum value of the usage. .excludeMixRule(RULE_MATCH_ATTRIBUTE_USAGE, USAGE_MEDIA) .build()); } @Test public void failsWhenAddIntRuleCalledWithInvalidType() { assertThrows(IllegalArgumentException.class, () -> new AudioMixingRule.Builder() // Rule match uid requires Integer not AudioAttributes. .addMixRule(RULE_MATCH_UID, USAGE_MEDIA_AUDIO_ATTRIBUTES) .build()); } @Test public void failsWhenExcludeIntRuleCalledWithInvalidType() { assertThrows(IllegalArgumentException.class, () -> new AudioMixingRule.Builder() // Rule match uid requires Integer not AudioAttributes. .excludeMixRule(RULE_MATCH_UID, USAGE_MEDIA_AUDIO_ATTRIBUTES) .build()); } @Test public void injectorMixTypeDeductionWithGenericRuleSucceeds() { AudioMixingRule rule = new AudioMixingRule.Builder() // UID rule can be used both with MIX_ROLE_PLAYERS and MIX_ROLE_INJECTOR. .addMixRule(RULE_MATCH_UID, TEST_UID) // Capture preset rule is only valid for injector, MIX_ROLE_INJECTOR should // be deduced. .addMixRule(RULE_MATCH_ATTRIBUTE_CAPTURE_PRESET, CAPTURE_PRESET_VOICE_RECOGNITION_AUDIO_ATTRIBUTES) .build(); assertEquals(rule.getTargetMixRole(), MIX_ROLE_INJECTOR); assertThat(rule.getCriteria(), containsInAnyOrder( isAudioMixMatchUidCriterion(TEST_UID), isAudioMixMatchCapturePresetCriterion(VOICE_RECOGNITION))); } @Test public void settingTheMixTypeToIncompatibleInjectorMixFails() { assertThrows(IllegalArgumentException.class, () -> new AudioMixingRule.Builder() .addMixRule(RULE_MATCH_ATTRIBUTE_CAPTURE_PRESET, CAPTURE_PRESET_VOICE_RECOGNITION_AUDIO_ATTRIBUTES) // Capture preset cannot be defined for MIX_ROLE_PLAYERS. .setTargetMixRole(MIX_ROLE_PLAYERS) .build()); } @Test public void addingPlayersOnlyRuleWithInjectorsOnlyRuleFails() { assertThrows(IllegalArgumentException.class, () -> new AudioMixingRule.Builder() // MIX_ROLE_PLAYERS only rule. .addMixRule(RULE_MATCH_ATTRIBUTE_USAGE, USAGE_MEDIA_AUDIO_ATTRIBUTES) // MIX ROLE_INJECTOR only rule. .addMixRule(RULE_MATCH_ATTRIBUTE_CAPTURE_PRESET, CAPTURE_PRESET_VOICE_RECOGNITION_AUDIO_ATTRIBUTES) .build()); } private static Matcher isAudioMixUidCriterion(int uid, boolean exclude) { return new CustomTypeSafeMatcher<AudioMixMatchCriterion>("uid mix criterion") { @Override public boolean matchesSafely(AudioMixMatchCriterion item) { int expectedRule = exclude ? RULE_EXCLUDE_UID : RULE_MATCH_UID; return item.getRule() == expectedRule && item.getIntProp() == uid; } @Override public void describeMismatchSafely( AudioMixMatchCriterion item, Description mismatchDescription) { mismatchDescription.appendText( String.format("is not %s criterion with uid %d", exclude ? "exclude" : "match", uid)); } }; } private static Matcher isAudioMixMatchUidCriterion(int uid) { return isAudioMixUidCriterion(uid, /*exclude=*/ false); } private static Matcher isAudioMixCapturePresetCriterion(int audioSource, boolean exclude) { return new CustomTypeSafeMatcher<AudioMixMatchCriterion>("uid mix criterion") { @Override public boolean matchesSafely(AudioMixMatchCriterion item) { int expectedRule = exclude ? RULE_EXCLUDE_ATTRIBUTE_CAPTURE_PRESET : RULE_MATCH_ATTRIBUTE_CAPTURE_PRESET; AudioAttributes attributes = item.getAudioAttributes(); return item.getRule() == expectedRule && attributes != null && attributes.getCapturePreset() == audioSource; } @Override public void describeMismatchSafely( AudioMixMatchCriterion item, Description mismatchDescription) { mismatchDescription.appendText( String.format("is not %s criterion with capture preset %d", exclude ? "exclude" : "match", audioSource)); } }; } private static Matcher isAudioMixMatchCapturePresetCriterion(int audioSource) { return isAudioMixCapturePresetCriterion(audioSource, /*exclude=*/ false); } private static Matcher isAudioMixUsageCriterion(int usage, boolean exclude) { return new CustomTypeSafeMatcher<AudioMixMatchCriterion>("usage mix criterion") { @Override public boolean matchesSafely(AudioMixMatchCriterion item) { int expectedRule = exclude ? RULE_EXCLUDE_ATTRIBUTE_USAGE : RULE_MATCH_ATTRIBUTE_USAGE; AudioAttributes attributes = item.getAudioAttributes(); return item.getRule() == expectedRule && attributes != null && attributes.getUsage() == usage; } @Override public void describeMismatchSafely( AudioMixMatchCriterion item, Description mismatchDescription) { mismatchDescription.appendText( String.format("is not %s criterion with usage %d", exclude ? "exclude" : "match", usage)); } }; } private static Matcher isAudioMixMatchUsageCriterion(int usage) { return isAudioMixUsageCriterion(usage, /*exclude=*/ false); } } Loading
media/java/android/media/audiopolicy/AudioMixingRule.java +45 −94 Original line number Diff line number Diff line Loading @@ -30,8 +30,10 @@ import android.util.Log; import java.lang.annotation.Retention; import java.util.ArrayList; import java.util.Iterator; import java.util.Collection; import java.util.HashSet; import java.util.Objects; import java.util.Set; /** Loading @@ -50,10 +52,10 @@ import java.util.Objects; @SystemApi public class AudioMixingRule { private AudioMixingRule(int mixType, ArrayList<AudioMixMatchCriterion> criteria, private AudioMixingRule(int mixType, Collection<AudioMixMatchCriterion> criteria, boolean allowPrivilegedMediaPlaybackCapture, boolean voiceCommunicationCaptureAllowed) { mCriteria = criteria; mCriteria = new ArrayList<>(criteria); mTargetMixType = mixType; mAllowPrivilegedPlaybackCapture = allowPrivilegedMediaPlaybackCapture; mVoiceCommunicationCaptureAllowed = voiceCommunicationCaptureAllowed; Loading Loading @@ -140,6 +142,20 @@ public class AudioMixingRule { return Objects.hash(mAttr, mIntProp, mRule); } @Override public boolean equals(Object object) { if (object == null || this.getClass() != object.getClass()) { return false; } if (object == this) { return true; } AudioMixMatchCriterion other = (AudioMixMatchCriterion) object; return mRule == other.mRule && mIntProp == other.mIntProp && Objects.equals(mAttr, other.mAttr); } void writeToParcel(Parcel dest) { dest.writeInt(mRule); final int match_rule = mRule & ~RULE_EXCLUSION_MASK; Loading Loading @@ -192,15 +208,6 @@ public class AudioMixingRule { return false; } private static boolean areCriteriaEquivalent(ArrayList<AudioMixMatchCriterion> cr1, ArrayList<AudioMixMatchCriterion> cr2) { if (cr1 == null || cr2 == null) return false; if (cr1 == cr2) return true; if (cr1.size() != cr2.size()) return false; //TODO iterate over rules to check they contain the same criterion return (cr1.hashCode() == cr2.hashCode()); } private final int mTargetMixType; int getTargetMixType() { return mTargetMixType; Loading Loading @@ -286,9 +293,9 @@ public class AudioMixingRule { final AudioMixingRule that = (AudioMixingRule) o; return (this.mTargetMixType == that.mTargetMixType) && (areCriteriaEquivalent(this.mCriteria, that.mCriteria) && this.mAllowPrivilegedPlaybackCapture == that.mAllowPrivilegedPlaybackCapture && this.mVoiceCommunicationCaptureAllowed && Objects.equals(mCriteria, that.mCriteria) && (this.mAllowPrivilegedPlaybackCapture == that.mAllowPrivilegedPlaybackCapture) && (this.mVoiceCommunicationCaptureAllowed == that.mVoiceCommunicationCaptureAllowed); } Loading Loading @@ -372,7 +379,7 @@ public class AudioMixingRule { * Builder class for {@link AudioMixingRule} objects */ public static class Builder { private ArrayList<AudioMixMatchCriterion> mCriteria; private final Set<AudioMixMatchCriterion> mCriteria; private int mTargetMixType = AudioMix.MIX_TYPE_INVALID; private boolean mAllowPrivilegedMediaPlaybackCapture = false; // This value should be set internally according to a permission check Loading @@ -382,7 +389,7 @@ public class AudioMixingRule { * Constructs a new Builder with no rules. */ public Builder() { mCriteria = new ArrayList<AudioMixMatchCriterion>(); mCriteria = new HashSet<>(); } /** Loading Loading @@ -547,7 +554,12 @@ public class AudioMixingRule { throw new IllegalArgumentException("Illegal argument for mix role"); } Log.i("AudioMixingRule", "Builder setTargetMixRole " + mixRole); 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; Loading Loading @@ -604,17 +616,15 @@ public class AudioMixingRule { */ private Builder addRuleInternal(AudioAttributes attrToMatch, Integer intProp, int rule) throws IllegalArgumentException { // as rules are added to the Builder, we verify they are consistent with the type // of mix being built. When adding the first rule, the mix type is MIX_TYPE_INVALID. // 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. if (mTargetMixType == AudioMix.MIX_TYPE_INVALID) { if (isPlayerRule(rule)) { mTargetMixType = AudioMix.MIX_TYPE_PLAYERS; } else if (isRecorderRule(rule)) { mTargetMixType = AudioMix.MIX_TYPE_RECORDERS; } else { // For rules which are not player or recorder specific (e.g. RULE_MATCH_UID), // the default mix type is MIX_TYPE_PLAYERS. mTargetMixType = AudioMix.MIX_TYPE_PLAYERS; } } else if ((isPlayerRule(rule) && (mTargetMixType != AudioMix.MIX_TYPE_PLAYERS)) || (isRecorderRule(rule)) && (mTargetMixType != AudioMix.MIX_TYPE_RECORDERS)) Loading @@ -622,75 +632,13 @@ public class AudioMixingRule { throw new IllegalArgumentException("Incompatible rule for mix"); } synchronized (mCriteria) { Iterator<AudioMixMatchCriterion> crIterator = mCriteria.iterator(); final int match_rule = rule & ~RULE_EXCLUSION_MASK; while (crIterator.hasNext()) { final AudioMixMatchCriterion criterion = crIterator.next(); if ((criterion.mRule & ~RULE_EXCLUSION_MASK) != match_rule) { continue; // The two rules are not of the same type int oppositeRule = rule ^ RULE_EXCLUSION_MASK; if (mCriteria.stream().anyMatch(criterion -> criterion.mRule == oppositeRule)) { throw new IllegalArgumentException("AudioMixingRule cannot contain RULE_MATCH_*" + " and RULE_EXCLUDE_* for the same dimension."); } switch (match_rule) { case RULE_MATCH_ATTRIBUTE_USAGE: // "usage"-based rule if (criterion.mAttr.getSystemUsage() == attrToMatch.getSystemUsage()) { if (criterion.mRule == rule) { // rule already exists, we're done return this; } else { // criterion already exists with a another rule, // it is incompatible throw new IllegalArgumentException("Contradictory rule exists" + " for " + attrToMatch); } } break; case RULE_MATCH_ATTRIBUTE_CAPTURE_PRESET: // "capture preset"-base rule if (criterion.mAttr.getCapturePreset() == attrToMatch.getCapturePreset()) { if (criterion.mRule == rule) { // rule already exists, we're done return this; } else { // criterion already exists with a another rule, // it is incompatible throw new IllegalArgumentException("Contradictory rule exists" + " for " + attrToMatch); } } break; case RULE_MATCH_UID: // "usage"-based rule if (criterion.mIntProp == intProp.intValue()) { if (criterion.mRule == rule) { // rule already exists, we're done return this; } else { // criterion already exists with a another rule, // it is incompatible throw new IllegalArgumentException("Contradictory rule exists" + " for UID " + intProp); } } break; case RULE_MATCH_USERID: // "userid"-based rule if (criterion.mIntProp == intProp.intValue()) { if (criterion.mRule == rule) { // rule already exists, we're done return this; } else { // criterion already exists with a another rule, // it is incompatible throw new IllegalArgumentException("Contradictory rule exists" + " for userId " + intProp); } } break; } } // rule didn't exist, add it switch (match_rule) { int ruleWithoutExclusion = rule & ~RULE_EXCLUSION_MASK; switch (ruleWithoutExclusion) { case RULE_MATCH_ATTRIBUTE_USAGE: case RULE_MATCH_ATTRIBUTE_CAPTURE_PRESET: mCriteria.add(new AudioMixMatchCriterion(attrToMatch, rule)); Loading Loading @@ -734,8 +682,11 @@ public class AudioMixingRule { * @return a new {@link AudioMixingRule} object */ public AudioMixingRule build() { return new AudioMixingRule(mTargetMixType, mCriteria, mAllowPrivilegedMediaPlaybackCapture, mVoiceCommunicationCaptureAllowed); return new AudioMixingRule( mTargetMixType == AudioMix.MIX_TYPE_INVALID ? AudioMix.MIX_TYPE_PLAYERS : mTargetMixType, mCriteria, mAllowPrivilegedMediaPlaybackCapture, mVoiceCommunicationCaptureAllowed); } } }
media/tests/AudioPolicyTest/Android.bp +1 −0 Original line number Diff line number Diff line Loading @@ -14,6 +14,7 @@ android_test { "androidx.test.ext.junit", "androidx.test.rules", "guava", "hamcrest-library", "platform-test-annotations", ], platform_apis: true, Loading
media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioMixingRuleUnitTests.java 0 → 100644 +261 −0 Original line number Diff line number Diff line /* * Copyright (C) 2022 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 com.android.audiopolicytest; import static android.media.AudioAttributes.USAGE_MEDIA; import static android.media.MediaRecorder.AudioSource.VOICE_RECOGNITION; import static android.media.audiopolicy.AudioMixingRule.MIX_ROLE_INJECTOR; import static android.media.audiopolicy.AudioMixingRule.MIX_ROLE_PLAYERS; import static android.media.audiopolicy.AudioMixingRule.RULE_EXCLUDE_ATTRIBUTE_CAPTURE_PRESET; import static android.media.audiopolicy.AudioMixingRule.RULE_EXCLUDE_ATTRIBUTE_USAGE; import static android.media.audiopolicy.AudioMixingRule.RULE_EXCLUDE_UID; import static android.media.audiopolicy.AudioMixingRule.RULE_MATCH_ATTRIBUTE_CAPTURE_PRESET; import static android.media.audiopolicy.AudioMixingRule.RULE_MATCH_ATTRIBUTE_USAGE; import static android.media.audiopolicy.AudioMixingRule.RULE_MATCH_UID; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.collection.IsCollectionWithSize.hasSize; import static org.hamcrest.collection.IsIterableContainingInAnyOrder.containsInAnyOrder; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThrows; import android.media.AudioAttributes; import android.media.audiopolicy.AudioMixingRule; import android.media.audiopolicy.AudioMixingRule.AudioMixMatchCriterion; import android.platform.test.annotations.Presubmit; import androidx.test.ext.junit.runners.AndroidJUnit4; import org.hamcrest.CustomTypeSafeMatcher; import org.hamcrest.Description; import org.hamcrest.Matcher; import org.junit.Test; import org.junit.runner.RunWith; /** * Unit tests for AudioPolicy. * * Run with "atest AudioMixingRuleUnitTests". */ @Presubmit @RunWith(AndroidJUnit4.class) public class AudioMixingRuleUnitTests { private static final AudioAttributes USAGE_MEDIA_AUDIO_ATTRIBUTES = new AudioAttributes.Builder().setUsage(USAGE_MEDIA).build(); private static final AudioAttributes CAPTURE_PRESET_VOICE_RECOGNITION_AUDIO_ATTRIBUTES = new AudioAttributes.Builder().setCapturePreset(VOICE_RECOGNITION).build(); private static final int TEST_UID = 42; private static final int OTHER_UID = 77; @Test public void testConstructValidRule() { AudioMixingRule rule = new AudioMixingRule.Builder() .addMixRule(RULE_MATCH_ATTRIBUTE_USAGE, USAGE_MEDIA_AUDIO_ATTRIBUTES) .addMixRule(RULE_MATCH_UID, TEST_UID) .build(); // Based on the rules, the mix type should fall back to MIX_ROLE_PLAYERS, // since the rules are valid for both MIX_ROLE_PLAYERS & MIX_ROLE_INJECTOR. assertEquals(rule.getTargetMixRole(), MIX_ROLE_PLAYERS); assertThat(rule.getCriteria(), containsInAnyOrder( isAudioMixMatchUsageCriterion(USAGE_MEDIA), isAudioMixMatchUidCriterion(TEST_UID))); } @Test public void testConstructRuleWithConflictingCriteriaFails() { assertThrows(IllegalArgumentException.class, () -> new AudioMixingRule.Builder() .addMixRule(RULE_MATCH_ATTRIBUTE_USAGE, USAGE_MEDIA_AUDIO_ATTRIBUTES) .addMixRule(RULE_MATCH_UID, TEST_UID) // Conflicts with previous criterion. .addMixRule(RULE_EXCLUDE_UID, OTHER_UID) .build()); } @Test public void testRuleBuilderDedupsCriteria() { AudioMixingRule rule = new AudioMixingRule.Builder() .addMixRule(RULE_MATCH_ATTRIBUTE_USAGE, USAGE_MEDIA_AUDIO_ATTRIBUTES) .addMixRule(RULE_MATCH_UID, TEST_UID) // Identical to previous criterion. .addMixRule(RULE_MATCH_UID, TEST_UID) // Identical to first criterion. .addMixRule(RULE_MATCH_ATTRIBUTE_USAGE, USAGE_MEDIA_AUDIO_ATTRIBUTES) .build(); assertThat(rule.getCriteria(), hasSize(2)); assertThat(rule.getCriteria(), containsInAnyOrder( isAudioMixMatchUsageCriterion(USAGE_MEDIA), isAudioMixMatchUidCriterion(TEST_UID))); } @Test public void failsWhenAddAttributeRuleCalledWithInvalidType() { assertThrows(IllegalArgumentException.class, () -> new AudioMixingRule.Builder() // Rule match attribute usage requires AudioAttributes, not // just the int enum value of the usage. .addMixRule(RULE_MATCH_ATTRIBUTE_USAGE, USAGE_MEDIA) .build()); } @Test public void failsWhenExcludeAttributeRuleCalledWithInvalidType() { assertThrows(IllegalArgumentException.class, () -> new AudioMixingRule.Builder() // Rule match attribute usage requires AudioAttributes, not // just the int enum value of the usage. .excludeMixRule(RULE_MATCH_ATTRIBUTE_USAGE, USAGE_MEDIA) .build()); } @Test public void failsWhenAddIntRuleCalledWithInvalidType() { assertThrows(IllegalArgumentException.class, () -> new AudioMixingRule.Builder() // Rule match uid requires Integer not AudioAttributes. .addMixRule(RULE_MATCH_UID, USAGE_MEDIA_AUDIO_ATTRIBUTES) .build()); } @Test public void failsWhenExcludeIntRuleCalledWithInvalidType() { assertThrows(IllegalArgumentException.class, () -> new AudioMixingRule.Builder() // Rule match uid requires Integer not AudioAttributes. .excludeMixRule(RULE_MATCH_UID, USAGE_MEDIA_AUDIO_ATTRIBUTES) .build()); } @Test public void injectorMixTypeDeductionWithGenericRuleSucceeds() { AudioMixingRule rule = new AudioMixingRule.Builder() // UID rule can be used both with MIX_ROLE_PLAYERS and MIX_ROLE_INJECTOR. .addMixRule(RULE_MATCH_UID, TEST_UID) // Capture preset rule is only valid for injector, MIX_ROLE_INJECTOR should // be deduced. .addMixRule(RULE_MATCH_ATTRIBUTE_CAPTURE_PRESET, CAPTURE_PRESET_VOICE_RECOGNITION_AUDIO_ATTRIBUTES) .build(); assertEquals(rule.getTargetMixRole(), MIX_ROLE_INJECTOR); assertThat(rule.getCriteria(), containsInAnyOrder( isAudioMixMatchUidCriterion(TEST_UID), isAudioMixMatchCapturePresetCriterion(VOICE_RECOGNITION))); } @Test public void settingTheMixTypeToIncompatibleInjectorMixFails() { assertThrows(IllegalArgumentException.class, () -> new AudioMixingRule.Builder() .addMixRule(RULE_MATCH_ATTRIBUTE_CAPTURE_PRESET, CAPTURE_PRESET_VOICE_RECOGNITION_AUDIO_ATTRIBUTES) // Capture preset cannot be defined for MIX_ROLE_PLAYERS. .setTargetMixRole(MIX_ROLE_PLAYERS) .build()); } @Test public void addingPlayersOnlyRuleWithInjectorsOnlyRuleFails() { assertThrows(IllegalArgumentException.class, () -> new AudioMixingRule.Builder() // MIX_ROLE_PLAYERS only rule. .addMixRule(RULE_MATCH_ATTRIBUTE_USAGE, USAGE_MEDIA_AUDIO_ATTRIBUTES) // MIX ROLE_INJECTOR only rule. .addMixRule(RULE_MATCH_ATTRIBUTE_CAPTURE_PRESET, CAPTURE_PRESET_VOICE_RECOGNITION_AUDIO_ATTRIBUTES) .build()); } private static Matcher isAudioMixUidCriterion(int uid, boolean exclude) { return new CustomTypeSafeMatcher<AudioMixMatchCriterion>("uid mix criterion") { @Override public boolean matchesSafely(AudioMixMatchCriterion item) { int expectedRule = exclude ? RULE_EXCLUDE_UID : RULE_MATCH_UID; return item.getRule() == expectedRule && item.getIntProp() == uid; } @Override public void describeMismatchSafely( AudioMixMatchCriterion item, Description mismatchDescription) { mismatchDescription.appendText( String.format("is not %s criterion with uid %d", exclude ? "exclude" : "match", uid)); } }; } private static Matcher isAudioMixMatchUidCriterion(int uid) { return isAudioMixUidCriterion(uid, /*exclude=*/ false); } private static Matcher isAudioMixCapturePresetCriterion(int audioSource, boolean exclude) { return new CustomTypeSafeMatcher<AudioMixMatchCriterion>("uid mix criterion") { @Override public boolean matchesSafely(AudioMixMatchCriterion item) { int expectedRule = exclude ? RULE_EXCLUDE_ATTRIBUTE_CAPTURE_PRESET : RULE_MATCH_ATTRIBUTE_CAPTURE_PRESET; AudioAttributes attributes = item.getAudioAttributes(); return item.getRule() == expectedRule && attributes != null && attributes.getCapturePreset() == audioSource; } @Override public void describeMismatchSafely( AudioMixMatchCriterion item, Description mismatchDescription) { mismatchDescription.appendText( String.format("is not %s criterion with capture preset %d", exclude ? "exclude" : "match", audioSource)); } }; } private static Matcher isAudioMixMatchCapturePresetCriterion(int audioSource) { return isAudioMixCapturePresetCriterion(audioSource, /*exclude=*/ false); } private static Matcher isAudioMixUsageCriterion(int usage, boolean exclude) { return new CustomTypeSafeMatcher<AudioMixMatchCriterion>("usage mix criterion") { @Override public boolean matchesSafely(AudioMixMatchCriterion item) { int expectedRule = exclude ? RULE_EXCLUDE_ATTRIBUTE_USAGE : RULE_MATCH_ATTRIBUTE_USAGE; AudioAttributes attributes = item.getAudioAttributes(); return item.getRule() == expectedRule && attributes != null && attributes.getUsage() == usage; } @Override public void describeMismatchSafely( AudioMixMatchCriterion item, Description mismatchDescription) { mismatchDescription.appendText( String.format("is not %s criterion with usage %d", exclude ? "exclude" : "match", usage)); } }; } private static Matcher isAudioMixMatchUsageCriterion(int usage) { return isAudioMixUsageCriterion(usage, /*exclude=*/ false); } }