Public Audio playback capture must have a valid projection

For privacy, require the app wanting to capture other app audio to have
a valid MediaProjection.

Test: adb shell audiorecorder --target /data/file.raw
Bug: 111453086
Change-Id: I1323048fe308282d3719e38915818a0da17567de
Signed-off-by: Kevin Rocard <krocard@google.com>
diff --git a/api/current.txt b/api/current.txt
index f31dfff..9e7872a 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -23383,7 +23383,7 @@
   }
 
   public static final class AudioPlaybackCaptureConfiguration.Builder {
-    ctor public AudioPlaybackCaptureConfiguration.Builder();
+    ctor public AudioPlaybackCaptureConfiguration.Builder(@NonNull android.media.projection.MediaProjection);
     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();
diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java
index 7de7f8f..bd828ee 100644
--- a/media/java/android/media/AudioManager.java
+++ b/media/java/android/media/AudioManager.java
@@ -36,6 +36,7 @@
 import android.content.Intent;
 import android.media.audiopolicy.AudioPolicy;
 import android.media.audiopolicy.AudioPolicy.AudioPolicyFocusListener;
+import android.media.projection.MediaProjection;
 import android.media.session.MediaController;
 import android.media.session.MediaSession;
 import android.media.session.MediaSessionLegacyHelper;
@@ -3197,8 +3198,10 @@
         }
         final IAudioService service = getService();
         try {
+            MediaProjection projection = policy.getMediaProjection();
             String regId = service.registerAudioPolicy(policy.getConfig(), policy.cb(),
-                    policy.hasFocusListener(), policy.isFocusPolicy(), policy.isVolumeController());
+                    policy.hasFocusListener(), policy.isFocusPolicy(), policy.isVolumeController(),
+                    projection == null ? null : projection.getProjection());
             if (regId == null) {
                 return ERROR;
             } else {
diff --git a/media/java/android/media/AudioPlaybackCaptureConfiguration.java b/media/java/android/media/AudioPlaybackCaptureConfiguration.java
index 22f14ae..d714dc7 100644
--- a/media/java/android/media/AudioPlaybackCaptureConfiguration.java
+++ b/media/java/android/media/AudioPlaybackCaptureConfiguration.java
@@ -19,34 +19,52 @@
 import android.annotation.NonNull;
 import android.media.audiopolicy.AudioMix;
 import android.media.audiopolicy.AudioMixingRule;
+import android.media.projection.MediaProjection;
+import android.os.RemoteException;
 
 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:
+ *  - 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
+ *    (eg work profile can not capture user profile apps and vice-versa).
+ *  - played by apps that MUST NOT have in their manifest.xml the application
+ *    attribute android:allowPlaybackCapture="false"
+ *  - played by apps that MUST have a targetSdkVersion higher or equal to 29 (Q).
+ *
  * <p>An example for creating a capture configuration for capturing all media playback:
  *
  * <pre>
+ *     MediaProjection mediaProjection;
+ *     // Retrieve a audio capable projection from the MediaProjectionManager
  *     AudioAttributes mediaAttr = new AudioAttributes.Builder()
  *         .setUsage(AudioAttributes.USAGE_MEDIA)
  *         .build();
- *     AudioPlaybackCaptureConfiguration config = new AudioPlaybackCaptureConfiguration.Builder()
+ *     AudioPlaybackCaptureConfiguration config =
+ *              new AudioPlaybackCaptureConfiguration.Builder(mediaProjection)
  *         .addMatchingUsage(mediaAttr)
  *         .build();
  *     AudioRecord record = new AudioRecord.Builder()
- *         .setPlaybackCaptureConfig(config)
+ *         .setAudioPlaybackCaptureConfig(config)
  *         .build();
  * </pre>
  *
- * @see AudioRecord.Builder#setPlaybackCaptureConfig(AudioPlaybackCaptureConfiguration)
+ * @see MediaProjectionManager#getMediaProjection(int, Intent)
+ * @see AudioRecord.Builder#setAudioPlaybackCaptureConfig(AudioPlaybackCaptureConfiguration)
  */
 public final class AudioPlaybackCaptureConfiguration {
 
     private final AudioMixingRule mAudioMixingRule;
+    private final MediaProjection mProjection;
 
-    private AudioPlaybackCaptureConfiguration(AudioMixingRule audioMixingRule) {
+    private AudioPlaybackCaptureConfiguration(AudioMixingRule audioMixingRule,
+                                              MediaProjection projection) {
         mAudioMixingRule = audioMixingRule;
+        mProjection = projection;
     }
 
     /**
@@ -60,6 +78,9 @@
                 .setRouteFlags(AudioMix.ROUTE_FLAG_LOOP_BACK | AudioMix.ROUTE_FLAG_RENDER)
                 .build();
     }
+    MediaProjection getMediaProjection() {
+        return mProjection;
+    }
 
     /** Builder for creating {@link AudioPlaybackCaptureConfiguration} instances. */
     public static final class Builder {
@@ -70,12 +91,26 @@
 
         private static final String ERROR_MESSAGE_MISMATCHED_RULES =
                 "Inclusive and exclusive usage rules cannot be combined";
+        private static final String ERROR_MESSAGE_START_ACTIVITY_FAILED =
+                "startActivityForResult failed";
+        private static final String ERROR_MESSAGE_NON_AUDIO_PROJECTION =
+                "MediaProjection can not project audio";
 
         private final AudioMixingRule.Builder mAudioMixingRuleBuilder;
+        private final MediaProjection mProjection;
         private int mUsageMatchType = MATCH_TYPE_UNSPECIFIED;
         private int mUidMatchType = MATCH_TYPE_UNSPECIFIED;
 
-        public Builder() {
+        /** @param projection A MediaProjection that supports audio projection. */
+        public Builder(@NonNull MediaProjection projection) {
+            Preconditions.checkNotNull(projection);
+            try {
+                Preconditions.checkArgument(projection.getProjection().canProjectAudio(),
+                                            ERROR_MESSAGE_NON_AUDIO_PROJECTION);
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+            mProjection = projection;
             mAudioMixingRuleBuilder = new AudioMixingRule.Builder();
         }
 
@@ -155,7 +190,8 @@
          * @throws UnsupportedOperationException if the parameters set are incompatible.
          */
         public AudioPlaybackCaptureConfiguration build() {
-            return new AudioPlaybackCaptureConfiguration(mAudioMixingRuleBuilder.build());
+            return new AudioPlaybackCaptureConfiguration(mAudioMixingRuleBuilder.build(),
+                                                         mProjection);
         }
     }
 }
diff --git a/media/java/android/media/AudioRecord.java b/media/java/android/media/AudioRecord.java
index 4f2de23..ec1a854 100644
--- a/media/java/android/media/AudioRecord.java
+++ b/media/java/android/media/AudioRecord.java
@@ -26,6 +26,7 @@
 import android.app.ActivityThread;
 import android.media.audiopolicy.AudioMix;
 import android.media.audiopolicy.AudioPolicy;
+import android.media.projection.MediaProjection;
 import android.os.Binder;
 import android.os.Handler;
 import android.os.IBinder;
@@ -648,7 +649,9 @@
 
         private AudioRecord buildAudioPlaybackCaptureRecord() {
             AudioMix audioMix = mAudioPlaybackCaptureConfiguration.createAudioMix(mFormat);
+            MediaProjection projection = mAudioPlaybackCaptureConfiguration.getMediaProjection();
             AudioPolicy audioPolicy = new AudioPolicy.Builder(/*context=*/ null)
+                    .setMediaProjection(projection)
                     .addMix(audioMix).build();
             AudioRecord record = audioPolicy.createAudioRecordSink(audioMix);
             record.unregisterAudioPolicyOnRelease(audioPolicy);
diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl
index f5aeca7..571e67e 100644
--- a/media/java/android/media/IAudioService.aidl
+++ b/media/java/android/media/IAudioService.aidl
@@ -34,6 +34,7 @@
 import android.media.VolumePolicy;
 import android.media.audiopolicy.AudioPolicyConfig;
 import android.media.audiopolicy.IAudioPolicyCallback;
+import android.media.projection.IMediaProjection;
 
 /**
  * {@hide}
@@ -176,7 +177,7 @@
 
     String registerAudioPolicy(in AudioPolicyConfig policyConfig,
             in IAudioPolicyCallback pcb, boolean hasFocusListener, boolean isFocusPolicy,
-            boolean isVolumeController);
+            boolean isVolumeController, in IMediaProjection projection);
 
     oneway void unregisterAudioPolicyAsync(in IAudioPolicyCallback pcb);
 
diff --git a/media/java/android/media/audiopolicy/AudioMix.java b/media/java/android/media/audiopolicy/AudioMix.java
index 761b625..6fd6298 100644
--- a/media/java/android/media/audiopolicy/AudioMix.java
+++ b/media/java/android/media/audiopolicy/AudioMix.java
@@ -133,7 +133,8 @@
     }
 
 
-    int getRouteFlags() {
+    /** @hide */
+    public int getRouteFlags() {
         return mRouteFlags;
     }
 
diff --git a/media/java/android/media/audiopolicy/AudioPolicy.java b/media/java/android/media/audiopolicy/AudioPolicy.java
index 49867ab..445edd1 100644
--- a/media/java/android/media/audiopolicy/AudioPolicy.java
+++ b/media/java/android/media/audiopolicy/AudioPolicy.java
@@ -18,6 +18,7 @@
 
 import android.annotation.IntDef;
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.annotation.SystemApi;
 import android.app.ActivityManager;
 import android.content.Context;
@@ -31,6 +32,7 @@
 import android.media.AudioTrack;
 import android.media.IAudioService;
 import android.media.MediaRecorder;
+import android.media.projection.MediaProjection;
 import android.os.Binder;
 import android.os.Handler;
 import android.os.IBinder;
@@ -94,6 +96,8 @@
 
     private AudioPolicyConfig mConfig;
 
+    private final MediaProjection mProjection;
+
     /** @hide */
     public AudioPolicyConfig getConfig() { return mConfig; }
     /** @hide */
@@ -102,13 +106,17 @@
     public boolean isFocusPolicy() { return mIsFocusPolicy; }
     /** @hide */
     public boolean isVolumeController() { return mVolCb != null; }
+    /** @hide */
+    public @Nullable MediaProjection getMediaProjection() {
+        return mProjection;
+    }
 
     /**
      * The parameter is guaranteed non-null through the Builder
      */
     private AudioPolicy(AudioPolicyConfig config, Context context, Looper looper,
             AudioPolicyFocusListener fl, AudioPolicyStatusListener sl, boolean isFocusPolicy,
-            AudioPolicyVolumeCallback vc) {
+            AudioPolicyVolumeCallback vc, @Nullable MediaProjection projection) {
         mConfig = config;
         mStatus = POLICY_STATUS_UNREGISTERED;
         mContext = context;
@@ -125,6 +133,7 @@
         mStatusListener = sl;
         mIsFocusPolicy = isFocusPolicy;
         mVolCb = vc;
+        mProjection = projection;
     }
 
     /**
@@ -139,6 +148,7 @@
         private AudioPolicyStatusListener mStatusListener;
         private boolean mIsFocusPolicy = false;
         private AudioPolicyVolumeCallback mVolCb;
+        private MediaProjection mProjection;
 
         /**
          * Constructs a new Builder with no audio mixes.
@@ -223,6 +233,23 @@
         }
 
         /**
+         * Set a media projection obtained through createMediaProjection().
+         *
+         * A MediaProjection that can project audio allows to register an audio
+         * policy LOOPBACK|RENDER without the MODIFY_AUDIO_ROUTING permission.
+         *
+         * @hide
+         */
+        public Builder setMediaProjection(@NonNull MediaProjection projection) {
+            if (projection == null) {
+                throw new IllegalArgumentException("Invalid null volume callback");
+            }
+            mProjection = projection;
+            return this;
+
+        }
+
+        /**
          * Combines all of the attributes that have been set on this {@code Builder} and returns a
          * new {@link AudioPolicy} object.
          * @return a new {@code AudioPolicy} object.
@@ -242,7 +269,7 @@
                         + "an AudioPolicyFocusListener");
             }
             return new AudioPolicy(new AudioPolicyConfig(mMixes), mContext, mLooper,
-                    mFocusListener, mStatusListener, mIsFocusPolicy, mVolCb);
+                    mFocusListener, mStatusListener, mIsFocusPolicy, mVolCb, mProjection);
         }
     }
 
@@ -423,15 +450,35 @@
                 return false;
             }
         }
-        if (!(PackageManager.PERMISSION_GRANTED == checkCallingOrSelfPermission(
-                        android.Manifest.permission.MODIFY_AUDIO_ROUTING))) {
+
+        // Loopback|capture only need an audio projection, everything else need MODIFY_AUDIO_ROUTING
+        boolean canModifyAudioRouting = PackageManager.PERMISSION_GRANTED
+                == checkCallingOrSelfPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING);
+
+        boolean canProjectAudio;
+        try {
+            canProjectAudio = mProjection != null && mProjection.getProjection().canProjectAudio();
+        } catch (RemoteException e) {
+            Log.e(TAG, "Failed to check if MediaProjection#canProjectAudio");
+            throw e.rethrowFromSystemServer();
+        }
+
+        if (!((isLoopbackRenderPolicy() && canProjectAudio) || canModifyAudioRouting)) {
             Slog.w(TAG, "Cannot use AudioPolicy for pid " + Binder.getCallingPid() + " / uid "
-                    + Binder.getCallingUid() + ", needs MODIFY_AUDIO_ROUTING");
+                    + Binder.getCallingUid() + ", needs MODIFY_AUDIO_ROUTING or "
+                    + "MediaProjection that can project audio.");
             return false;
         }
         return true;
     }
 
+    private boolean isLoopbackRenderPolicy() {
+        synchronized (mLock) {
+            return mConfig.mMixes.stream().allMatch(mix -> mix.getRouteFlags()
+                    == (mix.ROUTE_FLAG_RENDER | mix.ROUTE_FLAG_LOOP_BACK));
+        }
+    }
+
     /**
      * Returns {@link PackageManager#PERMISSION_GRANTED} if the caller has the given permission.
      */
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index d902201..afdfbe3 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -89,6 +89,8 @@
 import android.media.audiopolicy.AudioPolicy;
 import android.media.audiopolicy.AudioPolicyConfig;
 import android.media.audiopolicy.IAudioPolicyCallback;
+import android.media.projection.IMediaProjection;
+import android.media.projection.IMediaProjectionManager;
 import android.os.Binder;
 import android.os.Build;
 import android.os.Bundle;
@@ -99,6 +101,7 @@
 import android.os.Message;
 import android.os.PowerManager;
 import android.os.RemoteException;
+import android.os.ServiceManager;
 import android.os.SystemClock;
 import android.os.SystemProperties;
 import android.os.UserHandle;
@@ -453,6 +456,8 @@
     // Broadcast receiver for device connections intent broadcasts
     private final BroadcastReceiver mReceiver = new AudioServiceBroadcastReceiver();
 
+    private IMediaProjectionManager mProjectionService; // to validate projection token
+
     /** Interface for UserManagerService. */
     private final UserManagerInternal mUserManagerInternal;
     private final ActivityManagerInternal mActivityManagerInternal;
@@ -6186,22 +6191,21 @@
     // Audio policy management
     //==========================================================================================
     public String registerAudioPolicy(AudioPolicyConfig policyConfig, IAudioPolicyCallback pcb,
-            boolean hasFocusListener, boolean isFocusPolicy, boolean isVolumeController) {
+            boolean hasFocusListener, boolean isFocusPolicy, boolean isVolumeController,
+            IMediaProjection projection) {
         AudioSystem.setDynamicPolicyCallback(mDynPolicyCallback);
 
-        String regId = null;
-        // error handling
-        boolean hasPermissionForPolicy =
-                (PackageManager.PERMISSION_GRANTED == mContext.checkCallingPermission(
-                        android.Manifest.permission.MODIFY_AUDIO_ROUTING));
-        if (!hasPermissionForPolicy) {
-            Slog.w(TAG, "Can't register audio policy for pid " + Binder.getCallingPid() + " / uid "
-                    + Binder.getCallingUid() + ", need MODIFY_AUDIO_ROUTING");
+        if (!isPolicyRegisterAllowed(policyConfig, projection)) {
+            Slog.w(TAG, "Permission denied to register audio policy for pid "
+                    + Binder.getCallingPid() + " / uid " + Binder.getCallingUid()
+                    + ", need MODIFY_AUDIO_ROUTING or MediaProjection that can project audio");
             return null;
         }
 
         mDynPolicyLogger.log((new AudioEventLogger.StringEvent("registerAudioPolicy for "
                 + pcb.asBinder() + " with config:" + policyConfig)).printLog(TAG));
+
+        String regId = null;
         synchronized (mAudioPolicies) {
             try {
                 if (mAudioPolicies.containsKey(pcb.asBinder())) {
@@ -6223,6 +6227,76 @@
         return regId;
     }
 
+    /**
+     * Apps with MODIFY_AUDIO_ROUTING can register any policy.
+     * Apps with an audio capable MediaProjection are allowed to register a RENDER|LOOPBACK policy
+     * as those policy do not modify the audio routing.
+     */
+    private boolean isPolicyRegisterAllowed(AudioPolicyConfig policyConfig,
+             IMediaProjection projection) {
+
+        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;
+        }
+
+        boolean hasPermissionModifyAudioRouting =
+                (PackageManager.PERMISSION_GRANTED == mContext.checkCallingPermission(
+                        android.Manifest.permission.MODIFY_AUDIO_ROUTING));
+        if (hasPermissionModifyAudioRouting) {
+            return true;
+        }
+        return false;
+    }
+
+    /** @return true if projection is a valid MediaProjection that can project audio. */
+    private boolean canProjectAudio(IMediaProjection projection) {
+        if (projection == null) {
+            return false;
+        }
+
+        IMediaProjectionManager projectionService = getProjectionService();
+        if (projectionService == null) {
+            Log.e(TAG, "Can't get service IMediaProjectionManager");
+            return false;
+        }
+
+        try {
+            if (!projectionService.isValidMediaProjection(projection)) {
+                Log.w(TAG, "App passed invalid MediaProjection token");
+                return false;
+            }
+        } catch (RemoteException e) {
+            Log.e(TAG, "Can't call .isValidMediaProjection() on IMediaProjectionManager"
+                    + projectionService.asBinder(), e);
+            return false;
+        }
+
+        try {
+            if (!projection.canProjectAudio()) {
+                Log.w(TAG, "App passed MediaProjection that can not project audio");
+                return false;
+            }
+        } catch (RemoteException e) {
+            Log.e(TAG, "Can't call .canProjectAudio() on valid IMediaProjection"
+                    + projection.asBinder(), e);
+            return false;
+        }
+
+        return true;
+    }
+
+    private IMediaProjectionManager getProjectionService() {
+        if (mProjectionService == null) {
+            IBinder b = ServiceManager.getService(Context.MEDIA_PROJECTION_SERVICE);
+            mProjectionService = IMediaProjectionManager.Stub.asInterface(b);
+        }
+        return mProjectionService;
+    }
+
     public void unregisterAudioPolicyAsync(IAudioPolicyCallback pcb) {
         mDynPolicyLogger.log((new AudioEventLogger.StringEvent("unregisterAudioPolicyAsync for "
                 + pcb.asBinder()).printLog(TAG)));