From 2859215d99d92257b92c0f6678fe406140626723 Mon Sep 17 00:00:00 2001 From: Jan Sebechlebsky Date: Tue, 20 Sep 2022 10:40:13 +0200 Subject: [PATCH 1/2] Fix AudioMixingRule validation ... to match validation in AudioServer performed on mix registration. Bug: 247536210 Test: atest AudioMixingRuleUnitTests Change-Id: I39af1ea6ba5b3c2d3b703d7d449d5d14d7855b7e --- .../media/audiopolicy/AudioMixingRule.java | 115 +++-------- media/tests/AudioPolicyTest/Android.bp | 1 + .../AudioMixingRuleUnitTests.java | 189 ++++++++++++++++++ 3 files changed, 220 insertions(+), 85 deletions(-) create mode 100644 media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioMixingRuleUnitTests.java diff --git a/media/java/android/media/audiopolicy/AudioMixingRule.java b/media/java/android/media/audiopolicy/AudioMixingRule.java index 1f89f9990e753..11d51f91027dd 100644 --- a/media/java/android/media/audiopolicy/AudioMixingRule.java +++ b/media/java/android/media/audiopolicy/AudioMixingRule.java @@ -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; /** @@ -50,10 +52,10 @@ import java.util.Objects; @SystemApi public class AudioMixingRule { - private AudioMixingRule(int mixType, ArrayList criteria, + private AudioMixingRule(int mixType, Collection criteria, boolean allowPrivilegedMediaPlaybackCapture, boolean voiceCommunicationCaptureAllowed) { - mCriteria = criteria; + mCriteria = new ArrayList<>(criteria); mTargetMixType = mixType; mAllowPrivilegedPlaybackCapture = allowPrivilegedMediaPlaybackCapture; mVoiceCommunicationCaptureAllowed = voiceCommunicationCaptureAllowed; @@ -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; @@ -192,15 +208,6 @@ public class AudioMixingRule { return false; } - private static boolean areCriteriaEquivalent(ArrayList cr1, - ArrayList 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; @@ -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); } @@ -372,7 +379,7 @@ public class AudioMixingRule { * Builder class for {@link AudioMixingRule} objects */ public static class Builder { - private ArrayList mCriteria; + private final Set mCriteria; private int mTargetMixType = AudioMix.MIX_TYPE_INVALID; private boolean mAllowPrivilegedMediaPlaybackCapture = false; // This value should be set internally according to a permission check @@ -382,7 +389,7 @@ public class AudioMixingRule { * Constructs a new Builder with no rules. */ public Builder() { - mCriteria = new ArrayList(); + mCriteria = new HashSet<>(); } /** @@ -622,75 +629,13 @@ public class AudioMixingRule { throw new IllegalArgumentException("Incompatible rule for mix"); } synchronized (mCriteria) { - Iterator 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 - } - 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; - } + 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."); } - // 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)); diff --git a/media/tests/AudioPolicyTest/Android.bp b/media/tests/AudioPolicyTest/Android.bp index fcf91f0794427..7963ff25b3a7e 100644 --- a/media/tests/AudioPolicyTest/Android.bp +++ b/media/tests/AudioPolicyTest/Android.bp @@ -14,6 +14,7 @@ android_test { "androidx.test.ext.junit", "androidx.test.rules", "guava", + "hamcrest-library", "platform-test-annotations", ], platform_apis: true, diff --git a/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioMixingRuleUnitTests.java b/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioMixingRuleUnitTests.java new file mode 100644 index 0000000000000..3fb618207d512 --- /dev/null +++ b/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioMixingRuleUnitTests.java @@ -0,0 +1,189 @@ +/* + * 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.audiopolicy.AudioMixingRule.MIX_ROLE_PLAYERS; +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_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 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()); + } + + + private static Matcher isAudioMixUidCriterion(int uid, boolean exclude) { + return new CustomTypeSafeMatcher("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 isAudioMixUsageCriterion(int usage, boolean exclude) { + return new CustomTypeSafeMatcher("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); + } + + +} From 2089f2c351a0e1b6f749f824dc1b3e4d8add613e Mon Sep 17 00:00:00 2001 From: Jan Sebechlebsky Date: Tue, 20 Sep 2022 16:59:18 +0200 Subject: [PATCH 2/2] Fix mix role deduction and validation in AudioMixingRule.Builder Bug: 247783063 Test: atest AudioMixingRuleUnitTests Change-Id: Ib4ee69ec3465af835eb10643b3568f01ef2034cb --- .../media/audiopolicy/AudioMixingRule.java | 24 ++++--- .../AudioMixingRuleUnitTests.java | 72 +++++++++++++++++++ 2 files changed, 87 insertions(+), 9 deletions(-) diff --git a/media/java/android/media/audiopolicy/AudioMixingRule.java b/media/java/android/media/audiopolicy/AudioMixingRule.java index 11d51f91027dd..6aead4337245c 100644 --- a/media/java/android/media/audiopolicy/AudioMixingRule.java +++ b/media/java/android/media/audiopolicy/AudioMixingRule.java @@ -554,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; @@ -611,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)) @@ -679,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); } } } diff --git a/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioMixingRuleUnitTests.java b/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioMixingRuleUnitTests.java index 3fb618207d512..ad7ab971d5f2e 100644 --- a/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioMixingRuleUnitTests.java +++ b/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioMixingRuleUnitTests.java @@ -17,9 +17,13 @@ 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; @@ -53,6 +57,8 @@ import org.junit.runner.RunWith; 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; @@ -137,6 +143,46 @@ public class AudioMixingRuleUnitTests { .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("uid mix criterion") { @@ -160,6 +206,32 @@ public class AudioMixingRuleUnitTests { return isAudioMixUidCriterion(uid, /*exclude=*/ false); } + private static Matcher isAudioMixCapturePresetCriterion(int audioSource, boolean exclude) { + return new CustomTypeSafeMatcher("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("usage mix criterion") { @Override