Merge "Fix voice communication audio playback capture"

This commit is contained in:
TreeHugger Robot
2020-02-12 14:35:36 +00:00
committed by Android (Google) Code Review
5 changed files with 113 additions and 58 deletions

View File

@@ -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);

View File

@@ -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) {

View File

@@ -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);
}
}
}

View File

@@ -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) {

View File

@@ -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;
@@ -91,7 +90,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;
@@ -6826,8 +6824,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;
@@ -6836,23 +6835,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
@@ -6872,12 +6877,18 @@ public class AudioService extends IAudioService.Stub
return false;
}
if (requireVoiceComunicationOutputPerm
&& !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 (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)) {
@@ -6893,41 +6904,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;
}