Introduce audio playback capture API
This API allows an app to record what other apps are playing
with some privacy restrictions.
Test: CTS
Bug: 111453086
Change-Id: I98ed789afb792acf90876499aa5eb8a47359b265
diff --git a/api/current.txt b/api/current.txt
index f2fa672..969d7bd 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -23371,6 +23371,18 @@
method public void onAudioFocusChange(int);
}
+ public final class AudioPlaybackCaptureConfiguration {
+ }
+
+ public static final class AudioPlaybackCaptureConfiguration.Builder {
+ ctor public AudioPlaybackCaptureConfiguration.Builder();
+ method public android.media.AudioPlaybackCaptureConfiguration.Builder addMatchingUid(int);
+ method public android.media.AudioPlaybackCaptureConfiguration.Builder addMatchingUsage(@NonNull android.media.AudioAttributes);
+ method public android.media.AudioPlaybackCaptureConfiguration build();
+ method public android.media.AudioPlaybackCaptureConfiguration.Builder excludeUid(int);
+ method public android.media.AudioPlaybackCaptureConfiguration.Builder excludeUsage(@NonNull android.media.AudioAttributes);
+ }
+
public final class AudioPlaybackConfiguration implements android.os.Parcelable {
method public int describeContents();
method public android.media.AudioAttributes getAudioAttributes();
@@ -23469,6 +23481,7 @@
ctor public AudioRecord.Builder();
method public android.media.AudioRecord build() throws java.lang.UnsupportedOperationException;
method public android.media.AudioRecord.Builder setAudioFormat(@NonNull android.media.AudioFormat) throws java.lang.IllegalArgumentException;
+ method public android.media.AudioRecord.Builder setAudioPlaybackCaptureConfig(@NonNull android.media.AudioPlaybackCaptureConfiguration);
method public android.media.AudioRecord.Builder setAudioSource(int) throws java.lang.IllegalArgumentException;
method public android.media.AudioRecord.Builder setBufferSizeInBytes(int) throws java.lang.IllegalArgumentException;
}
diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java
index f996d38..7de7f8f 100644
--- a/media/java/android/media/AudioManager.java
+++ b/media/java/android/media/AudioManager.java
@@ -3188,6 +3188,10 @@
@SystemApi
@RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
public int registerAudioPolicy(@NonNull AudioPolicy policy) {
+ return registerAudioPolicyStatic(policy);
+ }
+
+ static int registerAudioPolicyStatic(@NonNull AudioPolicy policy) {
if (policy == null) {
throw new IllegalArgumentException("Illegal null AudioPolicy argument");
}
@@ -3214,6 +3218,10 @@
@SystemApi
@RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)
public void unregisterAudioPolicyAsync(@NonNull AudioPolicy policy) {
+ unregisterAudioPolicyAsyncStatic(policy);
+ }
+
+ static void unregisterAudioPolicyAsyncStatic(@NonNull AudioPolicy policy) {
if (policy == null) {
throw new IllegalArgumentException("Illegal null AudioPolicy argument");
}
diff --git a/media/java/android/media/AudioPlaybackCaptureConfiguration.java b/media/java/android/media/AudioPlaybackCaptureConfiguration.java
new file mode 100644
index 0000000..22f14ae
--- /dev/null
+++ b/media/java/android/media/AudioPlaybackCaptureConfiguration.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2019 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 android.media;
+
+import android.annotation.NonNull;
+import android.media.audiopolicy.AudioMix;
+import android.media.audiopolicy.AudioMixingRule;
+
+import com.android.internal.util.Preconditions;
+
+/**
+ * Configuration for capturing audio played by other apps.
+ *
+ * <p>An example for creating a capture configuration for capturing all media playback:
+ *
+ * <pre>
+ * AudioAttributes mediaAttr = new AudioAttributes.Builder()
+ * .setUsage(AudioAttributes.USAGE_MEDIA)
+ * .build();
+ * AudioPlaybackCaptureConfiguration config = new AudioPlaybackCaptureConfiguration.Builder()
+ * .addMatchingUsage(mediaAttr)
+ * .build();
+ * AudioRecord record = new AudioRecord.Builder()
+ * .setPlaybackCaptureConfig(config)
+ * .build();
+ * </pre>
+ *
+ * @see AudioRecord.Builder#setPlaybackCaptureConfig(AudioPlaybackCaptureConfiguration)
+ */
+public final class AudioPlaybackCaptureConfiguration {
+
+ private final AudioMixingRule mAudioMixingRule;
+
+ private AudioPlaybackCaptureConfiguration(AudioMixingRule audioMixingRule) {
+ mAudioMixingRule = audioMixingRule;
+ }
+
+ /**
+ * Returns a mix that routes audio back into the app while still playing it from the speakers.
+ *
+ * @param audioFormat The format in which to capture the audio.
+ */
+ AudioMix createAudioMix(AudioFormat audioFormat) {
+ return new AudioMix.Builder(mAudioMixingRule)
+ .setFormat(audioFormat)
+ .setRouteFlags(AudioMix.ROUTE_FLAG_LOOP_BACK | AudioMix.ROUTE_FLAG_RENDER)
+ .build();
+ }
+
+ /** Builder for creating {@link AudioPlaybackCaptureConfiguration} instances. */
+ public static final class Builder {
+
+ private static final int MATCH_TYPE_UNSPECIFIED = 0;
+ private static final int MATCH_TYPE_INCLUSIVE = 1;
+ private static final int MATCH_TYPE_EXCLUSIVE = 2;
+
+ private static final String ERROR_MESSAGE_MISMATCHED_RULES =
+ "Inclusive and exclusive usage rules cannot be combined";
+
+ private final AudioMixingRule.Builder mAudioMixingRuleBuilder;
+ private int mUsageMatchType = MATCH_TYPE_UNSPECIFIED;
+ private int mUidMatchType = MATCH_TYPE_UNSPECIFIED;
+
+ public Builder() {
+ mAudioMixingRuleBuilder = new AudioMixingRule.Builder();
+ }
+
+ /**
+ * Only capture audio output with the given {@link AudioAttributes}.
+ *
+ * <p>If called multiple times, will capture audio output that matches any of the given
+ * attributes.
+ *
+ * @throws IllegalStateException if called in conjunction with
+ * {@link #excludeUsage(AudioAttributes)}.
+ */
+ public Builder addMatchingUsage(@NonNull AudioAttributes audioAttributes) {
+ Preconditions.checkNotNull(audioAttributes);
+ Preconditions.checkState(
+ mUsageMatchType != MATCH_TYPE_EXCLUSIVE, ERROR_MESSAGE_MISMATCHED_RULES);
+ mAudioMixingRuleBuilder
+ .addRule(audioAttributes, AudioMixingRule.RULE_MATCH_ATTRIBUTE_USAGE);
+ mUsageMatchType = MATCH_TYPE_INCLUSIVE;
+ return this;
+ }
+
+ /**
+ * Only capture audio output by app with the matching {@code uid}.
+ *
+ * <p>If called multiple times, will capture audio output by apps whose uid is any of the
+ * given uids.
+ *
+ * @throws IllegalStateException if called in conjunction with {@link #excludeUid(int)}.
+ */
+ public Builder addMatchingUid(int uid) {
+ Preconditions.checkState(
+ mUidMatchType != MATCH_TYPE_EXCLUSIVE, ERROR_MESSAGE_MISMATCHED_RULES);
+ mAudioMixingRuleBuilder.addMixRule(AudioMixingRule.RULE_MATCH_UID, uid);
+ mUidMatchType = MATCH_TYPE_INCLUSIVE;
+ return this;
+ }
+
+ /**
+ * Only capture audio output that does not match the given {@link AudioAttributes}.
+ *
+ * <p>If called multiple times, will capture audio output that does not match any of the
+ * given attributes.
+ *
+ * @throws IllegalStateException if called in conjunction with
+ * {@link #addMatchingUsage(AudioAttributes)}.
+ */
+ public Builder excludeUsage(@NonNull AudioAttributes audioAttributes) {
+ Preconditions.checkNotNull(audioAttributes);
+ Preconditions.checkState(
+ mUsageMatchType != MATCH_TYPE_INCLUSIVE, ERROR_MESSAGE_MISMATCHED_RULES);
+ mAudioMixingRuleBuilder.excludeRule(audioAttributes,
+ AudioMixingRule.RULE_MATCH_ATTRIBUTE_USAGE);
+ mUsageMatchType = MATCH_TYPE_EXCLUSIVE;
+ return this;
+ }
+
+ /**
+ * Only capture audio output by apps that do not have the matching {@code uid}.
+ *
+ * <p>If called multiple times, will capture audio output by apps whose uid is not any of
+ * the given uids.
+ *
+ * @throws IllegalStateException if called in conjunction with {@link #addMatchingUid(int)}.
+ */
+ public Builder excludeUid(int uid) {
+ Preconditions.checkState(
+ mUidMatchType != MATCH_TYPE_INCLUSIVE, ERROR_MESSAGE_MISMATCHED_RULES);
+ mAudioMixingRuleBuilder.excludeMixRule(AudioMixingRule.RULE_MATCH_UID, uid);
+ mUidMatchType = MATCH_TYPE_EXCLUSIVE;
+ return this;
+ }
+
+ /**
+ * Builds the configuration instance.
+ *
+ * @throws UnsupportedOperationException if the parameters set are incompatible.
+ */
+ public AudioPlaybackCaptureConfiguration build() {
+ return new AudioPlaybackCaptureConfiguration(mAudioMixingRuleBuilder.build());
+ }
+ }
+}
diff --git a/media/java/android/media/AudioRecord.java b/media/java/android/media/AudioRecord.java
index 24a3a9b..10accf2 100644
--- a/media/java/android/media/AudioRecord.java
+++ b/media/java/android/media/AudioRecord.java
@@ -24,6 +24,8 @@
import android.annotation.SystemApi;
import android.annotation.UnsupportedAppUsage;
import android.app.ActivityThread;
+import android.media.audiopolicy.AudioMix;
+import android.media.audiopolicy.AudioPolicy;
import android.os.Binder;
import android.os.Handler;
import android.os.IBinder;
@@ -37,6 +39,7 @@
import android.util.Pair;
import com.android.internal.annotations.GuardedBy;
+import com.android.internal.util.Preconditions;
import java.io.IOException;
import java.lang.annotation.Retention;
@@ -182,6 +185,8 @@
//---------------------------------------------------------
// Member variables
//--------------------
+ private AudioPolicy mAudioCapturePolicy;
+
/**
* The audio data sampling rate in Hz.
* Never {@link AudioFormat#SAMPLE_RATE_UNSPECIFIED}.
@@ -429,6 +434,16 @@
}
/**
+ * Sets an {@link AudioPolicy} to automatically unregister when the record is released.
+ *
+ * <p>This is to prevent users of the audio capture API from having to manually unregister the
+ * policy that was used to create the record.
+ */
+ private void unregisterAudioPolicyOnRelease(AudioPolicy audioPolicy) {
+ mAudioCapturePolicy = audioPolicy;
+ }
+
+ /**
* @hide
*/
/* package */ void deferred_connect(long nativeRecordInJavaObj) {
@@ -491,6 +506,11 @@
* the minimum buffer size for the source is used.
*/
public static class Builder {
+
+ private static final String ERROR_MESSAGE_SOURCE_MISMATCH =
+ "Cannot both set audio source and set playback capture config";
+
+ private AudioPlaybackCaptureConfiguration mAudioPlaybackCaptureConfiguration;
private AudioAttributes mAttributes;
private AudioFormat mFormat;
private int mBufferSizeInBytes;
@@ -509,6 +529,9 @@
* @throws IllegalArgumentException
*/
public Builder setAudioSource(int source) throws IllegalArgumentException {
+ Preconditions.checkState(
+ mAudioPlaybackCaptureConfiguration == null,
+ ERROR_MESSAGE_SOURCE_MISMATCH);
if ( (source < MediaRecorder.AudioSource.DEFAULT) ||
(source > MediaRecorder.getAudioSourceMax()) ) {
throw new IllegalArgumentException("Invalid audio source " + source);
@@ -578,6 +601,25 @@
}
/**
+ * Sets the {@link AudioRecord} to record audio played by other apps.
+ *
+ * @param config Defines what apps to record audio from (i.e., via either their uid or
+ * the type of audio).
+ * @throws IllegalStateException if called in conjunction with {@link #setAudioSource(int)}.
+ * @throws NullPointerException if {@code config} is null.
+ */
+ public Builder setAudioPlaybackCaptureConfig(
+ @NonNull AudioPlaybackCaptureConfiguration config) {
+ Preconditions.checkNotNull(
+ config, "Illegal null AudioPlaybackCaptureConfiguration argument");
+ Preconditions.checkState(
+ mAttributes == null,
+ ERROR_MESSAGE_SOURCE_MISMATCH);
+ mAudioPlaybackCaptureConfiguration = config;
+ return this;
+ }
+
+ /**
* @hide
* To be only used by system components.
* @param sessionId ID of audio session the AudioRecord must be attached to, or
@@ -595,6 +637,15 @@
return this;
}
+ private AudioRecord buildAudioPlaybackCaptureRecord() {
+ AudioMix audioMix = mAudioPlaybackCaptureConfiguration.createAudioMix(mFormat);
+ AudioPolicy audioPolicy = new AudioPolicy.Builder(/*context=*/ null)
+ .addMix(audioMix).build();
+ AudioRecord record = audioPolicy.createAudioRecordSink(audioMix);
+ record.unregisterAudioPolicyOnRelease(audioPolicy);
+ return record;
+ }
+
/**
* @return a new {@link AudioRecord} instance successfully initialized with all
* the parameters set on this <code>Builder</code>.
@@ -603,6 +654,10 @@
* or if the device was not available.
*/
public AudioRecord build() throws UnsupportedOperationException {
+ if (mAudioPlaybackCaptureConfiguration != null) {
+ return buildAudioPlaybackCaptureRecord();
+ }
+
if (mFormat == null) {
mFormat = new AudioFormat.Builder()
.setEncoding(AudioFormat.ENCODING_PCM_16BIT)
@@ -757,6 +812,9 @@
} catch(IllegalStateException ise) {
// don't raise an exception, we're releasing the resources.
}
+ if (mAudioCapturePolicy != null) {
+ AudioManager.unregisterAudioPolicyAsyncStatic(mAudioCapturePolicy);
+ }
native_release();
mState = STATE_UNINITIALIZED;
}