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

Commit 24e0298b authored by Nadav Bar's avatar Nadav Bar
Browse files

Fix voice communication audio playback capture

This change fixes the CTS failure in AudioPlaybackCaptureTest caused by
ag/10111312 and ag/10111311.
It contains the following fixes/changes:
  - Capturing of voice communication is only allowed when the caller is
    explicitly asking for an attribute match USAGE_VOICE_COMMUNICATION.
    If an app adds that match rule, then the permission check for
    CAPTURE_VOICE_COMMUNICATION_OUTPUT will take place. For all other
    rules (like UID), the caller will receive silenced audio for voice
    communication.
  - Capture of voice communication will be only allowed for privileged
    capture. Hence other then CAPTURE_VOICE_COMMUNUCATION_OUTPUT, the
    calling app is also assume to have CAPTURE_MEDIA_OUTPUT.
  - Code cleanup, mainly in AudioService.java

This change is accompanied by ag/10242954 on the native side.

Bug: 148559127
Test: Manually
Test: atest PlaybackCaptureTest (with the version prior to ag/10220852)
Test: atest com.google.android.gts.audio.AudioHostTest
Change-Id: I1077db2e0f3c4133fca97d7b461f673bac693676
parent 68021fb9
Loading
Loading
Loading
Loading
+6 −0
Original line number Diff line number Diff line
@@ -147,6 +147,7 @@ static jclass gAudioMixingRuleClass;
static struct {
    jfieldID    mCriteria;
    jfieldID    mAllowPrivilegedPlaybackCapture;
    jfieldID    mVoiceCommunicationCaptureAllowed;
    // other fields unused by JNI
} gAudioMixingRuleFields;

@@ -1919,6 +1920,8 @@ static jint convertAudioMixToNative(JNIEnv *env,
    jobject jRuleCriteria = env->GetObjectField(jRule, gAudioMixingRuleFields.mCriteria);
    nAudioMix->mAllowPrivilegedPlaybackCapture =
            env->GetBooleanField(jRule, gAudioMixingRuleFields.mAllowPrivilegedPlaybackCapture);
    nAudioMix->mVoiceCommunicationCaptureAllowed =
            env->GetBooleanField(jRule, gAudioMixingRuleFields.mVoiceCommunicationCaptureAllowed);
    env->DeleteLocalRef(jRule);
    jobjectArray jCriteria = (jobjectArray)env->CallObjectMethod(jRuleCriteria,
                                                                 gArrayListMethods.toArray);
@@ -2682,6 +2685,9 @@ int register_android_media_AudioSystem(JNIEnv *env)
    gAudioMixingRuleFields.mAllowPrivilegedPlaybackCapture =
            GetFieldIDOrDie(env, audioMixingRuleClass, "mAllowPrivilegedPlaybackCapture", "Z");

    gAudioMixingRuleFields.mVoiceCommunicationCaptureAllowed =
            GetFieldIDOrDie(env, audioMixingRuleClass, "mVoiceCommunicationCaptureAllowed", "Z");

    jclass audioMixMatchCriterionClass =
                FindClassOrDie(env, "android/media/audiopolicy/AudioMixingRule$AudioMixMatchCriterion");
    gAudioMixMatchCriterionClass = MakeGlobalRefOrDie(env,audioMixMatchCriterionClass);
+10 −0
Original line number Diff line number Diff line
@@ -192,6 +192,16 @@ public class AudioMix {
        return mRule.isAffectingUsage(usage);
    }

    /**
      * Returns {@code true} if the rule associated with this mix contains a
      * RULE_MATCH_ATTRIBUTE_USAGE criterion for the given usage
      *
      * @hide
      */
    public boolean containsMatchAttributeRuleForUsage(int usage) {
        return mRule.containsMatchAttributeRuleForUsage(usage);
    }

    /** @hide */
    public boolean isRoutedToDevice(int deviceType, @NonNull String deviceAddress) {
        if ((mRouteFlags & ROUTE_FLAG_RENDER) != ROUTE_FLAG_RENDER) {
+61 −4
Original line number Diff line number Diff line
@@ -47,10 +47,12 @@ import java.util.Objects;
public class AudioMixingRule {

    private AudioMixingRule(int mixType, ArrayList<AudioMixMatchCriterion> criteria,
                            boolean allowPrivilegedPlaybackCapture) {
                            boolean allowPrivilegedPlaybackCapture,
                            boolean voiceCommunicationCaptureAllowed) {
        mCriteria = criteria;
        mTargetMixType = mixType;
        mAllowPrivilegedPlaybackCapture = allowPrivilegedPlaybackCapture;
        mVoiceCommunicationCaptureAllowed = voiceCommunicationCaptureAllowed;
    }

    /**
@@ -171,6 +173,23 @@ public class AudioMixingRule {
        return false;
    }

    /**
      * Returns {@code true} if this rule contains a RULE_MATCH_ATTRIBUTE_USAGE criterion for
      * the given usage
      *
      * @hide
      */
    boolean containsMatchAttributeRuleForUsage(int usage) {
        for (AudioMixMatchCriterion criterion : mCriteria) {
            if (criterion.mRule == RULE_MATCH_ATTRIBUTE_USAGE
                    && criterion.mAttr != null
                    && criterion.mAttr.getUsage() == usage) {
                return true;
            }
        }
        return false;
    }

    private static boolean areCriteriaEquivalent(ArrayList<AudioMixMatchCriterion> cr1,
            ArrayList<AudioMixMatchCriterion> cr2) {
        if (cr1 == null || cr2 == null) return false;
@@ -188,12 +207,24 @@ public class AudioMixingRule {
    public ArrayList<AudioMixMatchCriterion> getCriteria() { return mCriteria; }
    @UnsupportedAppUsage
    private boolean mAllowPrivilegedPlaybackCapture = false;
    @UnsupportedAppUsage
    private boolean mVoiceCommunicationCaptureAllowed = false;

    /** @hide */
    public boolean allowPrivilegedPlaybackCapture() {
        return mAllowPrivilegedPlaybackCapture;
    }

    /** @hide */
    public boolean voiceCommunicationCaptureAllowed() {
        return mVoiceCommunicationCaptureAllowed;
    }

    /** @hide */
    public void setVoiceCommunicationCaptureAllowed(boolean allowed) {
        mVoiceCommunicationCaptureAllowed = allowed;
    }

    /** @hide */
    @Override
    public boolean equals(Object o) {
@@ -203,12 +234,18 @@ public class AudioMixingRule {
        final AudioMixingRule that = (AudioMixingRule) o;
        return (this.mTargetMixType == that.mTargetMixType)
                && (areCriteriaEquivalent(this.mCriteria, that.mCriteria)
                && this.mAllowPrivilegedPlaybackCapture == that.mAllowPrivilegedPlaybackCapture);
                && this.mAllowPrivilegedPlaybackCapture == that.mAllowPrivilegedPlaybackCapture
                && this.mVoiceCommunicationCaptureAllowed
                    == that.mVoiceCommunicationCaptureAllowed);
    }

    @Override
    public int hashCode() {
        return Objects.hash(mTargetMixType, mCriteria, mAllowPrivilegedPlaybackCapture);
        return Objects.hash(
            mTargetMixType,
            mCriteria,
            mAllowPrivilegedPlaybackCapture,
            mVoiceCommunicationCaptureAllowed);
    }

    private static boolean isValidSystemApiRule(int rule) {
@@ -276,6 +313,8 @@ public class AudioMixingRule {
        private ArrayList<AudioMixMatchCriterion> mCriteria;
        private int mTargetMixType = AudioMix.MIX_TYPE_INVALID;
        private boolean mAllowPrivilegedPlaybackCapture = false;
        // This value should be set internally according to a permission check
        private boolean mVoiceCommunicationCaptureAllowed = false;

        /**
         * Constructs a new Builder with no rules.
@@ -400,6 +439,23 @@ public class AudioMixingRule {
            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;
        }

        /**
         * Add or exclude a rule for the selection of which streams are mixed together.
         * Does error checking on the parameters.
@@ -583,7 +639,8 @@ public class AudioMixingRule {
         * @return a new {@link AudioMixingRule} object
         */
        public AudioMixingRule build() {
            return new AudioMixingRule(mTargetMixType, mCriteria, mAllowPrivilegedPlaybackCapture);
            return new AudioMixingRule(mTargetMixType, mCriteria,
                mAllowPrivilegedPlaybackCapture, mVoiceCommunicationCaptureAllowed);
        }
    }
}
+7 −1
Original line number Diff line number Diff line
@@ -98,6 +98,8 @@ public class AudioPolicyConfig implements Parcelable {
            dest.writeInt(mix.getFormat().getChannelMask());
            // write opt-out respect
            dest.writeBoolean(mix.getRule().allowPrivilegedPlaybackCapture());
            // write voice communication capture allowed flag
            dest.writeBoolean(mix.getRule().voiceCommunicationCaptureAllowed());
            // write mix rules
            final ArrayList<AudioMixMatchCriterion> criteria = mix.getRule().getCriteria();
            dest.writeInt(criteria.size());
@@ -128,8 +130,10 @@ public class AudioPolicyConfig implements Parcelable {
            mixBuilder.setFormat(format);

            AudioMixingRule.Builder ruleBuilder = new AudioMixingRule.Builder();
            // write opt-out respect
            // read opt-out respect
            ruleBuilder.allowPrivilegedPlaybackCapture(in.readBoolean());
            // read voice capture allowed flag
            ruleBuilder.voiceCommunicationCaptureAllowed(in.readBoolean());
            // read mix rules
            int nbRules = in.readInt();
            for (int j = 0 ; j < nbRules ; j++) {
@@ -169,6 +173,8 @@ public class AudioPolicyConfig implements Parcelable {
            textDump += Integer.toHexString(mix.getFormat().getChannelMask()).toUpperCase() + "\n";
            textDump += "  ignore playback capture opt out="
                    + mix.getRule().allowPrivilegedPlaybackCapture() + "\n";
            textDump += "  allow voice communication capture="
                    + mix.getRule().voiceCommunicationCaptureAllowed() + "\n";
            // write mix rules
            final ArrayList<AudioMixMatchCriterion> criteria = mix.getRule().getCriteria();
            for (AudioMixMatchCriterion criterion : criteria) {
+29 −53
Original line number Diff line number Diff line
@@ -21,7 +21,6 @@ import static android.media.AudioManager.RINGER_MODE_NORMAL;
import static android.media.AudioManager.RINGER_MODE_SILENT;
import static android.media.AudioManager.RINGER_MODE_VIBRATE;
import static android.media.AudioManager.STREAM_SYSTEM;
import static android.media.audiopolicy.AudioMixingRule.RULE_MATCH_ATTRIBUTE_USAGE;
import static android.os.Process.FIRST_APPLICATION_UID;
import static android.provider.Settings.Secure.VOLUME_HUSH_MUTE;
import static android.provider.Settings.Secure.VOLUME_HUSH_OFF;
@@ -90,7 +89,6 @@ import android.media.PlayerBase;
import android.media.VolumePolicy;
import android.media.audiofx.AudioEffect;
import android.media.audiopolicy.AudioMix;
import android.media.audiopolicy.AudioMixingRule.AudioMixMatchCriterion;
import android.media.audiopolicy.AudioPolicy;
import android.media.audiopolicy.AudioPolicyConfig;
import android.media.audiopolicy.AudioProductStrategy;
@@ -6805,8 +6803,9 @@ public class AudioService extends IAudioService.Stub

        boolean requireValidProjection = false;
        boolean requireCaptureAudioOrMediaOutputPerm = false;
        boolean requireVoiceComunicationOutputPerm = false;
        boolean requireModifyRouting = false;
        ArrayList<AudioMix> voiceCommunicationCaptureMixes = null;


        if (hasFocusAccess || isVolumeController) {
            requireModifyRouting |= true;
@@ -6815,23 +6814,29 @@ public class AudioService extends IAudioService.Stub
            requireModifyRouting |= true;
        }
        for (AudioMix mix : policyConfig.getMixes()) {
            // If mix is trying to capture USAGE_VOICE_COMMUNICATION using playback capture
            if (isVoiceCommunicationPlaybackCaptureMix(mix)) {
                // then it must have CAPTURE_USAGE_VOICE_COMMUNICATION_OUTPUT permission
                requireVoiceComunicationOutputPerm |= true;
            }
            // If mix is requesting privileged capture and is capturing at
            // least one usage which is not USAGE_VOICE_COMMUNICATION.
            if (mix.getRule().allowPrivilegedPlaybackCapture()
                    && isNonVoiceCommunicationCaptureMix(mix)) {
            // If mix is requesting privileged capture
            if (mix.getRule().allowPrivilegedPlaybackCapture()) {
                // then it must have CAPTURE_MEDIA_OUTPUT or CAPTURE_AUDIO_OUTPUT permission
                requireCaptureAudioOrMediaOutputPerm |= true;

                // and its format must be low quality enough
                String error = mix.canBeUsedForPrivilegedCapture(mix.getFormat());
                if (error != null) {
                    Log.e(TAG, error);
                    return false;
                }

                // If mix is trying to excplicitly capture USAGE_VOICE_COMMUNICATION
                if (mix.containsMatchAttributeRuleForUsage(
                        AudioAttributes.USAGE_VOICE_COMMUNICATION)) {
                    // then it must have CAPTURE_USAGE_VOICE_COMMUNICATION_OUTPUT permission
                    // Note that for UID, USERID or EXCLDUE rules, the capture will be silenced
                    // in AudioPolicyMix
                    if (voiceCommunicationCaptureMixes == null) {
                        voiceCommunicationCaptureMixes = new ArrayList<AudioMix>();
                    }
                    voiceCommunicationCaptureMixes.add(mix);
                }
            }

            // If mix is RENDER|LOOPBACK, then an audio MediaProjection is enough
@@ -6851,14 +6856,20 @@ public class AudioService extends IAudioService.Stub
            return false;
        }

        if (requireVoiceComunicationOutputPerm
                && !callerHasPermission(
        if (voiceCommunicationCaptureMixes != null && voiceCommunicationCaptureMixes.size() > 0) {
            if (!callerHasPermission(
                    android.Manifest.permission.CAPTURE_VOICE_COMMUNICATION_OUTPUT)) {
                Log.e(TAG, "Privileged audio capture for voice communication requires "
                        + "CAPTURE_VOICE_COMMUNICATION_OUTPUT system permission");
                return false;
            }

            // If permission check succeeded, we set the flag in each of the mixing rules
            for (AudioMix mix : voiceCommunicationCaptureMixes) {
                mix.getRule().setVoiceCommunicationCaptureAllowed(true);
            }
        }

        if (requireValidProjection && !canProjectAudio(projection)) {
            return false;
        }
@@ -6872,41 +6883,6 @@ public class AudioService extends IAudioService.Stub
        return true;
    }

    /**
    * Checks whether a given AudioMix is used for playback capture
    * (has the ROUTE_FLAG_LOOP_BACK_RENDER flag) and has a matching
    * criterion for USAGE_VOICE_COMMUNICATION.
    */
    private boolean isVoiceCommunicationPlaybackCaptureMix(AudioMix mix) {
        if (mix.getRouteFlags() != mix.ROUTE_FLAG_LOOP_BACK_RENDER) {
            return false;
        }

        for (AudioMixMatchCriterion criterion : mix.getRule().getCriteria()) {
            if (criterion.getRule() == RULE_MATCH_ATTRIBUTE_USAGE
                    && criterion.getAudioAttributes().getUsage()
                    == AudioAttributes.USAGE_VOICE_COMMUNICATION) {
                return true;
            }
        }
        return false;
    }

    /**
    * Checks whether a given AudioMix has a matching
    * criterion for a usage which is not USAGE_VOICE_COMMUNICATION.
    */
    private boolean isNonVoiceCommunicationCaptureMix(AudioMix mix) {
        for (AudioMixMatchCriterion criterion : mix.getRule().getCriteria()) {
            if (criterion.getRule() == RULE_MATCH_ATTRIBUTE_USAGE
                    && criterion.getAudioAttributes().getUsage()
                    != AudioAttributes.USAGE_VOICE_COMMUNICATION) {
                return true;
            }
        }
        return false;
    }

    private boolean callerHasPermission(String permission) {
        return mContext.checkCallingPermission(permission) == PackageManager.PERMISSION_GRANTED;
    }