Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit bb5f51b1 authored by Ján Sebechlebský's avatar Ján Sebechlebský Committed by Android (Google) Code Review
Browse files

Merge changes Ib4ee69ec,I39af1ea6

* changes:
  Fix mix role deduction and validation in AudioMixingRule.Builder
  Fix AudioMixingRule validation
parents 0cd2c709 2089f2c3
Loading
Loading
Loading
Loading
+45 −94
Original line number Diff line number Diff line
@@ -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<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;
@@ -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<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;
@@ -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<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
@@ -382,7 +389,7 @@ public class AudioMixingRule {
         * Constructs a new Builder with no rules.
         */
        public Builder() {
            mCriteria = new ArrayList<AudioMixMatchCriterion>();
            mCriteria = new HashSet<>();
        }

        /**
@@ -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;
@@ -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))
@@ -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));
@@ -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);
        }
    }
}
+1 −0
Original line number Diff line number Diff line
@@ -14,6 +14,7 @@ android_test {
        "androidx.test.ext.junit",
        "androidx.test.rules",
        "guava",
        "hamcrest-library",
        "platform-test-annotations",
    ],
    platform_apis: true,
+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);
    }


}