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