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