diff options
| -rw-r--r-- | api/current.txt | 5 | ||||
| -rw-r--r-- | api/system-current.txt | 2 | ||||
| -rw-r--r-- | core/jni/android_media_AudioSystem.cpp | 5 | ||||
| -rw-r--r-- | core/res/AndroidManifest.xml | 20 | ||||
| -rw-r--r-- | core/res/res/values/attrs_manifest.xml | 15 | ||||
| -rw-r--r-- | media/java/android/media/AudioAttributes.java | 97 | ||||
| -rw-r--r-- | media/java/android/media/AudioPlaybackCaptureConfiguration.java | 2 | ||||
| -rw-r--r-- | media/java/android/media/audiopolicy/AudioMix.java | 3 | ||||
| -rw-r--r-- | media/java/android/media/audiopolicy/AudioMixingRule.java | 35 | ||||
| -rw-r--r-- | media/java/android/media/audiopolicy/AudioPolicyConfig.java | 11 | ||||
| -rw-r--r-- | services/core/java/com/android/server/audio/AudioService.java | 20 |
11 files changed, 187 insertions, 28 deletions
diff --git a/api/current.txt b/api/current.txt index 9661dcb7ce3d..1e6bf2c91711 100644 --- a/api/current.txt +++ b/api/current.txt @@ -23044,6 +23044,9 @@ package android.media { method public int getUsage(); method public int getVolumeControlStream(); method public void writeToParcel(android.os.Parcel, int); + field public static final int ALLOW_CAPTURE_BY_ALL = 1; // 0x1 + field public static final int ALLOW_CAPTURE_BY_NONE = 3; // 0x3 + field public static final int ALLOW_CAPTURE_BY_SYSTEM = 2; // 0x2 field public static final int CONTENT_TYPE_MOVIE = 3; // 0x3 field public static final int CONTENT_TYPE_MUSIC = 2; // 0x2 field public static final int CONTENT_TYPE_SONIFICATION = 4; // 0x4 @@ -23075,7 +23078,7 @@ package android.media { ctor public AudioAttributes.Builder(); ctor public AudioAttributes.Builder(android.media.AudioAttributes); method public android.media.AudioAttributes build(); - method @NonNull public android.media.AudioAttributes.Builder setAllowCapture(boolean); + method @NonNull public android.media.AudioAttributes.Builder setAllowedCapturePolicy(int); method public android.media.AudioAttributes.Builder setContentType(int); method public android.media.AudioAttributes.Builder setFlags(int); method public android.media.AudioAttributes.Builder setLegacyStreamType(int); diff --git a/api/system-current.txt b/api/system-current.txt index 106918c72259..0dd82500869f 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -49,6 +49,7 @@ package android { field @Deprecated public static final String BROADCAST_NETWORK_PRIVILEGED = "android.permission.BROADCAST_NETWORK_PRIVILEGED"; field public static final String CAMERA_DISABLE_TRANSMIT_LED = "android.permission.CAMERA_DISABLE_TRANSMIT_LED"; field public static final String CAPTURE_AUDIO_HOTWORD = "android.permission.CAPTURE_AUDIO_HOTWORD"; + field public static final String CAPTURE_MEDIA_OUTPUT = "android.permission.CAPTURE_MEDIA_OUTPUT"; field public static final String CAPTURE_TV_INPUT = "android.permission.CAPTURE_TV_INPUT"; field public static final String CHANGE_APP_IDLE_STATE = "android.permission.CHANGE_APP_IDLE_STATE"; field public static final String CHANGE_DEVICE_IDLE_TEMP_WHITELIST = "android.permission.CHANGE_DEVICE_IDLE_TEMP_WHITELIST"; @@ -3602,6 +3603,7 @@ package android.media.audiopolicy { ctor public AudioMixingRule.Builder(); method public android.media.audiopolicy.AudioMixingRule.Builder addMixRule(int, Object) throws java.lang.IllegalArgumentException; method public android.media.audiopolicy.AudioMixingRule.Builder addRule(android.media.AudioAttributes, int) throws java.lang.IllegalArgumentException; + method @NonNull public android.media.audiopolicy.AudioMixingRule.Builder allowPrivilegedPlaybackCapture(boolean); method public android.media.audiopolicy.AudioMixingRule build(); method public android.media.audiopolicy.AudioMixingRule.Builder excludeMixRule(int, Object) throws java.lang.IllegalArgumentException; method public android.media.audiopolicy.AudioMixingRule.Builder excludeRule(android.media.AudioAttributes, int) throws java.lang.IllegalArgumentException; diff --git a/core/jni/android_media_AudioSystem.cpp b/core/jni/android_media_AudioSystem.cpp index c8f81e2193c8..88713d10bb2d 100644 --- a/core/jni/android_media_AudioSystem.cpp +++ b/core/jni/android_media_AudioSystem.cpp @@ -144,6 +144,7 @@ static struct { static jclass gAudioMixingRuleClass; static struct { jfieldID mCriteria; + jfieldID mAllowPrivilegedPlaybackCapture; // other fields unused by JNI } gAudioMixingRuleFields; @@ -1868,6 +1869,8 @@ static jint convertAudioMixToNative(JNIEnv *env, jobject jRule = env->GetObjectField(jAudioMix, gAudioMixFields.mRule); jobject jRuleCriteria = env->GetObjectField(jRule, gAudioMixingRuleFields.mCriteria); + nAudioMix->mAllowPrivilegedPlaybackCapture = + env->GetBooleanField(jRule, gAudioMixingRuleFields.mAllowPrivilegedPlaybackCapture); env->DeleteLocalRef(jRule); jobjectArray jCriteria = (jobjectArray)env->CallObjectMethod(jRuleCriteria, gArrayListMethods.toArray); @@ -2456,6 +2459,8 @@ int register_android_media_AudioSystem(JNIEnv *env) gAudioMixingRuleClass = MakeGlobalRefOrDie(env, audioMixingRuleClass); gAudioMixingRuleFields.mCriteria = GetFieldIDOrDie(env, audioMixingRuleClass, "mCriteria", "Ljava/util/ArrayList;"); + gAudioMixingRuleFields.mAllowPrivilegedPlaybackCapture = + GetFieldIDOrDie(env, audioMixingRuleClass, "mAllowPrivilegedPlaybackCapture", "Z"); jclass audioMixMatchCriterionClass = FindClassOrDie(env, "android/media/audiopolicy/AudioMixingRule$AudioMixMatchCriterion"); diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 2d7cfa44f109..cffa46c64b36 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -3562,10 +3562,30 @@ android:protectionLevel="signature" /> <!-- Allows an application to capture audio output. + Use the {@code CAPTURE_MEDIA_OUTPUT} permission if only the {@code USAGE_UNKNOWN}), + {@code USAGE_MEDIA}) or {@code USAGE_GAME}) usages are intended to be captured. <p>Not for use by third-party applications.</p> --> <permission android:name="android.permission.CAPTURE_AUDIO_OUTPUT" android:protectionLevel="signature|privileged" /> + <!-- @SystemApi Allows an application to capture the audio played by other apps + that have set an allow capture policy of + {@link android.media.AudioAttributes#ALLOW_CAPTURE_BY_SYSTEM}. + + Without this permission, only audio with an allow capture policy of + {@link android.media.AudioAttributes#ALLOW_CAPTURE_BY_ALL} can be used. + + There are strong restriction listed at + {@link android.media.AudioAttributes#ALLOW_CAPTURE_BY_SYSTEM} + on what an app can do with the captured audio. + + See {@code CAPTURE_AUDIO_OUTPUT} for capturing audio use cases other than media playback. + + <p>Not for use by third-party applications.</p> + @hide --> + <permission android:name="android.permission.CAPTURE_MEDIA_OUTPUT" + android:protectionLevel="signature|privileged" /> + <!-- @SystemApi Allows an application to capture audio for hotword detection. <p>Not for use by third-party applications.</p> @hide --> diff --git a/core/res/res/values/attrs_manifest.xml b/core/res/res/values/attrs_manifest.xml index fe2c6655a6ed..362d01c30487 100644 --- a/core/res/res/values/attrs_manifest.xml +++ b/core/res/res/values/attrs_manifest.xml @@ -1673,8 +1673,19 @@ This flag is turned on by default. <em>This attribute is usable only by system apps. </em> --> <attr name="allowClearUserDataOnFailedRestore"/> - <!-- If {@code true} the app's non sensitive audio can be capture by other apps. - The default value is true. --> + <!-- If {@code true} the app's non sensitive audio can be capture by other apps with + {@code AudioPlaybackCaptureConfiguration} and a {@code MediaProjection}. + + <p> + Non sensitive audio is defined as audio whose {@code AttributeUsage} is + {@code USAGE_UNKNOWN}), {@code USAGE_MEDIA}) or {@code USAGE_GAME}). + All other usages (eg. {@code USAGE_VOICE_COMMUNICATION}) will not be captured. + + <p> + The default value is: + - {@code true} for apps with targetSdkVersion >= 29 (Q). + - {@code false} for apps with targetSdkVersion < 29. + --> <attr name="allowAudioPlaybackCapture" format="boolean" /> </declare-styleable> <!-- The <code>permission</code> tag declares a security permission that can be diff --git a/media/java/android/media/AudioAttributes.java b/media/java/android/media/AudioAttributes.java index c2f29bc6e3bc..efb7f4698797 100644 --- a/media/java/android/media/AudioAttributes.java +++ b/media/java/android/media/AudioAttributes.java @@ -370,9 +370,10 @@ public final class AudioAttributes implements Parcelable { /** * @hide - * Flag specifying that the audio shall not be captured by other apps. + * Flag specifying that the audio shall not be captured by third-party apps + * with a MediaProjection. */ - public static final int FLAG_NO_CAPTURE = 0x1 << 10; + public static final int FLAG_NO_MEDIA_PROJECTION = 0x1 << 10; /** * @hide @@ -380,12 +381,63 @@ public final class AudioAttributes implements Parcelable { */ public static final int FLAG_MUTE_HAPTIC = 0x1 << 11; + /** + * @hide + * Flag specifying that the audio shall not be captured by any apps, not even system apps. + */ + public static final int FLAG_NO_SYSTEM_CAPTURE = 0x1 << 12; + private final static int FLAG_ALL = FLAG_AUDIBILITY_ENFORCED | FLAG_SECURE | FLAG_SCO | FLAG_BEACON | FLAG_HW_AV_SYNC | FLAG_HW_HOTWORD | FLAG_BYPASS_INTERRUPTION_POLICY | FLAG_BYPASS_MUTE | FLAG_LOW_LATENCY | FLAG_DEEP_BUFFER | FLAG_MUTE_HAPTIC; private final static int FLAG_ALL_PUBLIC = FLAG_AUDIBILITY_ENFORCED | FLAG_HW_AV_SYNC | FLAG_LOW_LATENCY; + /** + * Indicates that the audio may be captured by any app. + * + * For privacy, the following usages can not be recorded: VOICE_COMMUNICATION*, + * USAGE_NOTIFICATION*, USAGE_ASSISTANCE* and USAGE_ASSISTANT. + * + * On {@link android.os.Build.VERSION_CODES#Q}, this means only {@link #USAGE_UNKNOWN}, + * {@link #USAGE_MEDIA} and {@link #USAGE_GAME} may be captured. + * + * See {@link android.media.projection.MediaProjection} and + * {@link Builder#setAllowedCapturePolicy}. + */ + public static final int ALLOW_CAPTURE_BY_ALL = 1; + /** + * Indicates that the audio may only be captured by system apps. + * + * System apps can capture for many purposes like accessibility, user guidance... + * but abide to the following restrictions: + * - the audio can not leave the device + * - the audio can not be passed to a third party app + * - the audio can not be recorded at a higher quality then 16kHz 16bit mono + * + * See {@link Builder#setAllowedCapturePolicy}. + */ + public static final int ALLOW_CAPTURE_BY_SYSTEM = 2; + /** + * Indicates that the audio is not to be recorded by any app, even if it is a system app. + * + * It is encouraged to use {@link #ALLOW_CAPTURE_BY_SYSTEM} instead of this value as system apps + * provide significant and useful features for the user (such as live captioning + * and accessibility). + * + * See {@link Builder#setAllowedCapturePolicy}. + */ + public static final int ALLOW_CAPTURE_BY_NONE = 3; + + /** @hide */ + @IntDef({ + ALLOW_CAPTURE_BY_ALL, + ALLOW_CAPTURE_BY_SYSTEM, + ALLOW_CAPTURE_BY_NONE, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface CapturePolicy {} + @UnsupportedAppUsage private int mUsage = USAGE_UNKNOWN; @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) @@ -593,10 +645,10 @@ public final class AudioAttributes implements Parcelable { case USAGE_GAME: case USAGE_VIRTUAL_SOURCE: case USAGE_ASSISTANT: - mUsage = usage; - break; + mUsage = usage; + break; default: - mUsage = USAGE_UNKNOWN; + mUsage = USAGE_UNKNOWN; } return this; } @@ -642,17 +694,34 @@ public final class AudioAttributes implements Parcelable { } /** - * Specifying if audio shall or shall not be captured by other apps. - * By default, capture is allowed. - * @param allowCapture false to forbid capture of the audio by any apps, - * true to allow apps to capture the audio + * Specifying if audio may or may not be captured by other apps or the system. + * + * The default is {@link AudioAttributes#ALLOW_CAPTURE_BY_ALL}. + * + * Note that an application can also set its global policy, in which case the most + * restrictive policy is always applied. + * + * @param capturePolicy one of + * {@link #ALLOW_CAPTURE_BY_ALL}, + * {@link #ALLOW_CAPTURE_BY_SYSTEM}, + * {@link #ALLOW_CAPTURE_BY_NONE}. * @return the same Builder instance + * @throws IllegalArgumentException if the argument is not a valid value. */ - public @NonNull Builder setAllowCapture(boolean allowCapture) { - if (allowCapture) { - mFlags &= ~FLAG_NO_CAPTURE; - } else { - mFlags |= FLAG_NO_CAPTURE; + public @NonNull Builder setAllowedCapturePolicy(@CapturePolicy int capturePolicy) { + switch (capturePolicy) { + case ALLOW_CAPTURE_BY_NONE: + mFlags |= FLAG_NO_MEDIA_PROJECTION | FLAG_NO_SYSTEM_CAPTURE; + break; + case ALLOW_CAPTURE_BY_SYSTEM: + mFlags |= FLAG_NO_MEDIA_PROJECTION; + mFlags &= ~FLAG_NO_SYSTEM_CAPTURE; + break; + case ALLOW_CAPTURE_BY_ALL: + mFlags &= ~FLAG_NO_SYSTEM_CAPTURE & ~FLAG_NO_MEDIA_PROJECTION; + break; + default: + throw new IllegalArgumentException("Unknown allow playback capture policy"); } return this; } diff --git a/media/java/android/media/AudioPlaybackCaptureConfiguration.java b/media/java/android/media/AudioPlaybackCaptureConfiguration.java index 4aa0b903bcdf..9ee6f8740568 100644 --- a/media/java/android/media/AudioPlaybackCaptureConfiguration.java +++ b/media/java/android/media/AudioPlaybackCaptureConfiguration.java @@ -28,7 +28,7 @@ import com.android.internal.util.Preconditions; /** * Configuration for capturing audio played by other apps. * - * For privacy and copyright reason, only the following audio can be captured: + * Only the following audio can be captured: * - usage MUST be UNKNOWN or GAME or MEDIA. All other usages CAN NOT be capturable. * - audio attributes MUST NOT have the FLAG_NO_CAPTURE * - played by apps that MUST be in the same user profile as the capturing app diff --git a/media/java/android/media/audiopolicy/AudioMix.java b/media/java/android/media/audiopolicy/AudioMix.java index 6fd6298adb7d..09f17c099609 100644 --- a/media/java/android/media/audiopolicy/AudioMix.java +++ b/media/java/android/media/audiopolicy/AudioMix.java @@ -142,7 +142,8 @@ public class AudioMix { return mFormat; } - AudioMixingRule getRule() { + /** @hide */ + public AudioMixingRule getRule() { return mRule; } diff --git a/media/java/android/media/audiopolicy/AudioMixingRule.java b/media/java/android/media/audiopolicy/AudioMixingRule.java index d41f416f3d51..947b06cdf6b2 100644 --- a/media/java/android/media/audiopolicy/AudioMixingRule.java +++ b/media/java/android/media/audiopolicy/AudioMixingRule.java @@ -16,6 +16,7 @@ package android.media.audiopolicy; +import android.annotation.NonNull; import android.annotation.SystemApi; import android.annotation.UnsupportedAppUsage; import android.media.AudioAttributes; @@ -43,9 +44,11 @@ import java.util.Objects; @SystemApi public class AudioMixingRule { - private AudioMixingRule(int mixType, ArrayList<AudioMixMatchCriterion> criteria) { + private AudioMixingRule(int mixType, ArrayList<AudioMixMatchCriterion> criteria, + boolean allowPrivilegedPlaybackCapture) { mCriteria = criteria; mTargetMixType = mixType; + mAllowPrivilegedPlaybackCapture = allowPrivilegedPlaybackCapture; } /** @@ -161,6 +164,13 @@ public class AudioMixingRule { @UnsupportedAppUsage private final ArrayList<AudioMixMatchCriterion> mCriteria; ArrayList<AudioMixMatchCriterion> getCriteria() { return mCriteria; } + @UnsupportedAppUsage + private boolean mAllowPrivilegedPlaybackCapture = false; + + /** @hide */ + public boolean allowPrivilegedPlaybackCapture() { + return mAllowPrivilegedPlaybackCapture; + } /** @hide */ @Override @@ -170,12 +180,13 @@ public class AudioMixingRule { final AudioMixingRule that = (AudioMixingRule) o; return (this.mTargetMixType == that.mTargetMixType) - && (areCriteriaEquivalent(this.mCriteria, that.mCriteria)); + && (areCriteriaEquivalent(this.mCriteria, that.mCriteria) + && this.mAllowPrivilegedPlaybackCapture == that.mAllowPrivilegedPlaybackCapture); } @Override public int hashCode() { - return Objects.hash(mTargetMixType, mCriteria); + return Objects.hash(mTargetMixType, mCriteria, mAllowPrivilegedPlaybackCapture); } private static boolean isValidSystemApiRule(int rule) { @@ -239,6 +250,7 @@ public class AudioMixingRule { public static class Builder { private ArrayList<AudioMixMatchCriterion> mCriteria; private int mTargetMixType = AudioMix.MIX_TYPE_INVALID; + private boolean mAllowPrivilegedPlaybackCapture = false; /** * Constructs a new Builder with no rules. @@ -343,6 +355,21 @@ public class AudioMixingRule { } /** + * Set if the audio of app that opted out of audio playback capture should be captured. + * + * The permission {@link CAPTURE_AUDIO_OUTPUT} or {@link CAPTURE_MEDIA_OUTPUT} is needed + * to ignore the opt-out. + * + * Only affects LOOPBACK|RENDER mix. + * + * @return the same Builder instance. + */ + public @NonNull Builder allowPrivilegedPlaybackCapture(boolean allow) { + mAllowPrivilegedPlaybackCapture = allow; + return this; + } + + /** * Add or exclude a rule for the selection of which streams are mixed together. * Does error checking on the parameters. * @param rule @@ -507,7 +534,7 @@ public class AudioMixingRule { * @return a new {@link AudioMixingRule} object */ public AudioMixingRule build() { - return new AudioMixingRule(mTargetMixType, mCriteria); + return new AudioMixingRule(mTargetMixType, mCriteria, mAllowPrivilegedPlaybackCapture); } } } diff --git a/media/java/android/media/audiopolicy/AudioPolicyConfig.java b/media/java/android/media/audiopolicy/AudioPolicyConfig.java index a6e63c7841d0..c4ba0c1fc835 100644 --- a/media/java/android/media/audiopolicy/AudioPolicyConfig.java +++ b/media/java/android/media/audiopolicy/AudioPolicyConfig.java @@ -96,6 +96,8 @@ public class AudioPolicyConfig implements Parcelable { dest.writeInt(mix.getFormat().getSampleRate()); dest.writeInt(mix.getFormat().getEncoding()); dest.writeInt(mix.getFormat().getChannelMask()); + // write opt-out respect + dest.writeBoolean(mix.getRule().allowPrivilegedPlaybackCapture()); // write mix rules final ArrayList<AudioMixMatchCriterion> criteria = mix.getRule().getCriteria(); dest.writeInt(criteria.size()); @@ -124,9 +126,12 @@ public class AudioPolicyConfig implements Parcelable { final AudioFormat format = new AudioFormat.Builder().setSampleRate(sampleRate) .setChannelMask(channelMask).setEncoding(encoding).build(); mixBuilder.setFormat(format); + + AudioMixingRule.Builder ruleBuilder = new AudioMixingRule.Builder(); + // write opt-out respect + ruleBuilder.allowPrivilegedPlaybackCapture(in.readBoolean()); // read mix rules int nbRules = in.readInt(); - AudioMixingRule.Builder ruleBuilder = new AudioMixingRule.Builder(); for (int j = 0 ; j < nbRules ; j++) { // read the matching rules ruleBuilder.addRuleFromParcel(in); @@ -161,7 +166,9 @@ public class AudioPolicyConfig implements Parcelable { textDump += " rate=" + mix.getFormat().getSampleRate() + "Hz\n"; textDump += " encoding=" + mix.getFormat().getEncoding() + "\n"; textDump += " channels=0x"; - textDump += Integer.toHexString(mix.getFormat().getChannelMask()).toUpperCase() +"\n"; + textDump += Integer.toHexString(mix.getFormat().getChannelMask()).toUpperCase() + "\n"; + textDump += " ignore playback capture opt out=" + + mix.getRule().allowPrivilegedPlaybackCapture() + "\n"; // write mix rules final ArrayList<AudioMixMatchCriterion> criteria = mix.getRule().getCriteria(); for (AudioMixMatchCriterion criterion : criteria) { diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index 93f7831e8886..82a4f1df2e10 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -6360,9 +6360,20 @@ public class AudioService extends IAudioService.Stub boolean isLoopbackRenderPolicy = policyConfig.getMixes().stream().allMatch( mix -> mix.getRouteFlags() == (mix.ROUTE_FLAG_RENDER | mix.ROUTE_FLAG_LOOP_BACK)); - // Policy that do not modify the audio routing only need an audio projection - if (isLoopbackRenderPolicy && canProjectAudio(projection)) { - return true; + if (isLoopbackRenderPolicy) { + boolean allowPrivilegedPlaybackCapture = policyConfig.getMixes().stream().anyMatch( + mix -> mix.getRule().allowPrivilegedPlaybackCapture()); + if (allowPrivilegedPlaybackCapture + && !(hasPermission(android.Manifest.permission.CAPTURE_AUDIO_OUTPUT) + || hasPermission(android.Manifest.permission.CAPTURE_MEDIA_OUTPUT))) { + // Opt-out can not be bypassed without a system permission + return false; + } + + if (canProjectAudio(projection)) { + // Policy that do not modify the audio routing only need an audio projection + return true; + } } boolean hasPermissionModifyAudioRouting = @@ -6373,6 +6384,9 @@ public class AudioService extends IAudioService.Stub } return false; } + private boolean hasPermission(String permission) { + return PackageManager.PERMISSION_GRANTED == mContext.checkCallingPermission(permission); + } /** @return true if projection is a valid MediaProjection that can project audio. */ private boolean canProjectAudio(IMediaProjection projection) { |