Loading media/java/android/media/AudioAttributes.java +5 −5 Original line number Diff line number Diff line Loading @@ -413,11 +413,11 @@ public final class AudioAttributes implements Parcelable { /** * Indicates that the audio may only be captured by system apps. * * System apps can capture for many purposes like accessibility, user guidance... * System apps can capture for many purposes like accessibility, live captions, user guidance... * but abide to the following restrictions: * - the audio cannot leave the device * - the audio cannot be passed to a third party app * - the audio can not be recorded at a higher quality then 16kHz 16bit mono * - the audio cannot be recorded at a higher quality than 16kHz 16bit mono * * See {@link Builder#setAllowedCapturePolicy}. */ Loading media/java/android/media/audiopolicy/AudioMix.java +56 −1 Original line number Diff line number Diff line Loading @@ -91,6 +91,20 @@ public class AudioMix { */ public static final int ROUTE_FLAG_LOOP_BACK = 0x1 << 1; /** * An audio mix behavior where the targeted audio is played unaffected but a copy is * accessible for capture through {@link AudioRecord}. * * Only capture of playback is supported, not capture of capture. * Use concurrent capture instead to capture what is captured by other apps. * * The captured audio is an approximation of the played audio. * Effects and volume are not applied, and track are mixed with different delay then in the HAL. * As a result, this API is not suitable for echo cancelling. * @hide */ public static final int ROUTE_FLAG_LOOP_BACK_RENDER = ROUTE_FLAG_LOOP_BACK | ROUTE_FLAG_RENDER; private static final int ROUTE_FLAG_SUPPORTED = ROUTE_FLAG_RENDER | ROUTE_FLAG_LOOP_BACK; // MIX_TYPE_* values to keep in sync with frameworks/av/include/media/AudioPolicy.h Loading Loading @@ -125,6 +139,15 @@ public class AudioMix { */ public static final int MIX_STATE_MIXING = 1; /** Maximum sampling rate for privileged playback capture*/ private static final int PRIVILEDGED_CAPTURE_MAX_SAMPLE_RATE = 16000; /** Maximum channel number for privileged playback capture*/ private static final int PRIVILEDGED_CAPTURE_MAX_CHANNEL_NUMBER = 1; /** Maximum channel number for privileged playback capture*/ private static final int PRIVILEDGED_CAPTURE_MAX_BYTES_PER_SAMPLE = 2; /** * The current mixing state. * @return one of {@link #MIX_STATE_DISABLED}, {@link #MIX_STATE_IDLE}, Loading @@ -140,7 +163,8 @@ public class AudioMix { return mRouteFlags; } AudioFormat getFormat() { /** @hide */ public AudioFormat getFormat() { return mFormat; } Loading Loading @@ -182,6 +206,31 @@ public class AudioMix { return true; } /** @return an error string if the format would not allow Privileged playbackCapture * null otherwise * @hide */ public static String canBeUsedForPrivilegedCapture(AudioFormat format) { int sampleRate = format.getSampleRate(); if (sampleRate > PRIVILEDGED_CAPTURE_MAX_SAMPLE_RATE || sampleRate <= 0) { return "Privileged audio capture sample rate " + sampleRate + " can not be over " + PRIVILEDGED_CAPTURE_MAX_SAMPLE_RATE + "kHz"; } int channelCount = format.getChannelCount(); if (channelCount > PRIVILEDGED_CAPTURE_MAX_CHANNEL_NUMBER || channelCount <= 0) { return "Privileged audio capture channel count " + channelCount + " can not be over " + PRIVILEDGED_CAPTURE_MAX_CHANNEL_NUMBER; } int encoding = format.getEncoding(); if (!format.isPublicEncoding(encoding) || !format.isEncodingLinearPcm(encoding)) { return "Privileged audio capture encoding " + encoding + "is not linear"; } if (format.getBytesPerSample(encoding) > PRIVILEDGED_CAPTURE_MAX_BYTES_PER_SAMPLE) { return "Privileged audio capture encoding " + encoding + " can not be over " + PRIVILEDGED_CAPTURE_MAX_BYTES_PER_SAMPLE + " bytes per sample"; } return null; } /** @hide */ @Override public boolean equals(Object o) { Loading Loading @@ -390,6 +439,12 @@ public class AudioMix { } } } if (mRule.allowPrivilegedPlaybackCapture()) { String error = AudioMix.canBeUsedForPrivilegedCapture(mFormat); if (error != null) { throw new IllegalArgumentException(error); } } return new AudioMix(mRule, mFormat, mRouteFlags, mCallbackFlags, mDeviceSystemType, mDeviceAddress); } Loading media/java/android/media/audiopolicy/AudioMixingRule.java +4 −0 Original line number Diff line number Diff line Loading @@ -365,6 +365,10 @@ public class AudioMixingRule { /** * Set if the audio of app that opted out of audio playback capture should be captured. * * Caller of this method with <code>true</code>, MUST abide to the restriction listed in * {@link ALLOW_CAPTURE_BY_SYSTEM}, including but not limited to the captured audio * can not leave the capturing app, and the quality is limited to 16k mono. * * The permission {@link CAPTURE_AUDIO_OUTPUT} or {@link CAPTURE_MEDIA_OUTPUT} is needed * to ignore the opt-out. * Loading services/core/java/com/android/server/audio/AudioService.java +57 −25 Original line number Diff line number Diff line Loading @@ -6698,7 +6698,10 @@ public class AudioService extends IAudioService.Stub boolean isVolumeController, IMediaProjection projection) { AudioSystem.setDynamicPolicyCallback(mDynPolicyCallback); if (!isPolicyRegisterAllowed(policyConfig, projection)) { if (!isPolicyRegisterAllowed(policyConfig, isFocusPolicy || isTestFocusPolicy || hasFocusListener, isVolumeController, 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"); Loading Loading @@ -6739,42 +6742,71 @@ public class AudioService extends IAudioService.Stub * as those policy do not modify the audio routing. */ private boolean isPolicyRegisterAllowed(AudioPolicyConfig policyConfig, boolean hasFocusAccess, boolean isVolumeController, IMediaProjection projection) { boolean isLoopbackRenderPolicy = policyConfig.getMixes().stream().allMatch( mix -> mix.getRouteFlags() == (mix.ROUTE_FLAG_RENDER | mix.ROUTE_FLAG_LOOP_BACK)); 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 boolean requireValidProjection = false; boolean requireCaptureAudioOrMediaOutputPerm = false; boolean requireModifyRouting = false; if (hasFocusAccess || isVolumeController) { requireModifyRouting |= true; } else if (policyConfig.getMixes().isEmpty()) { // An empty policy could be used to lock the focus or add mixes later requireModifyRouting |= true; } for (AudioMix mix : policyConfig.getMixes()) { // If mix is requesting a privileged capture if (mix.getRule().allowPrivilegedPlaybackCapture()) { // then it must have CAPTURE_MEDIA_OUTPUT or CAPTURE_AUDIO_OUTPUT permission requireCaptureAudioOrMediaOutputPerm |= true; // and its format must be low quality enough String error = mix.canBeUsedForPrivilegedCapture(mix.getFormat()); if (error != null) { Log.e(TAG, error); return false; } } if (canProjectAudio(projection)) { // Policy that do not modify the audio routing only need an audio projection return true; // If mix is RENDER|LOOPBACK, then an audio MediaProjection is enough // otherwise MODIFY_AUDIO_ROUTING permission is required if (mix.getRouteFlags() == mix.ROUTE_FLAG_LOOP_BACK_RENDER && projection != null) { requireValidProjection |= true; } else { requireModifyRouting |= true; } } boolean hasPermissionModifyAudioRouting = (PackageManager.PERMISSION_GRANTED == mContext.checkCallingPermission( android.Manifest.permission.MODIFY_AUDIO_ROUTING)); if (hasPermissionModifyAudioRouting) { return true; if (requireCaptureAudioOrMediaOutputPerm && !callerHasPermission(android.Manifest.permission.CAPTURE_MEDIA_OUTPUT) && !callerHasPermission(android.Manifest.permission.CAPTURE_AUDIO_OUTPUT)) { Log.e(TAG, "Privileged audio capture requires CAPTURE_MEDIA_OUTPUT or " + "CAPTURE_AUDIO_OUTPUT system permission"); return false; } if (requireValidProjection && !canProjectAudio(projection)) { return false; } if (requireModifyRouting && !callerHasPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)) { Log.e(TAG, "Can not capture audio without MODIFY_AUDIO_ROUTING"); return false; } private boolean hasPermission(String permission) { return PackageManager.PERMISSION_GRANTED == mContext.checkCallingPermission(permission); return true; } private boolean callerHasPermission(String permission) { return mContext.checkCallingPermission(permission) == PackageManager.PERMISSION_GRANTED; } /** @return true if projection is a valid MediaProjection that can project audio. */ private boolean canProjectAudio(IMediaProjection projection) { if (projection == null) { Log.e(TAG, "MediaProjection is null"); return false; } Loading Loading
media/java/android/media/AudioAttributes.java +5 −5 Original line number Diff line number Diff line Loading @@ -413,11 +413,11 @@ public final class AudioAttributes implements Parcelable { /** * Indicates that the audio may only be captured by system apps. * * System apps can capture for many purposes like accessibility, user guidance... * System apps can capture for many purposes like accessibility, live captions, user guidance... * but abide to the following restrictions: * - the audio cannot leave the device * - the audio cannot be passed to a third party app * - the audio can not be recorded at a higher quality then 16kHz 16bit mono * - the audio cannot be recorded at a higher quality than 16kHz 16bit mono * * See {@link Builder#setAllowedCapturePolicy}. */ Loading
media/java/android/media/audiopolicy/AudioMix.java +56 −1 Original line number Diff line number Diff line Loading @@ -91,6 +91,20 @@ public class AudioMix { */ public static final int ROUTE_FLAG_LOOP_BACK = 0x1 << 1; /** * An audio mix behavior where the targeted audio is played unaffected but a copy is * accessible for capture through {@link AudioRecord}. * * Only capture of playback is supported, not capture of capture. * Use concurrent capture instead to capture what is captured by other apps. * * The captured audio is an approximation of the played audio. * Effects and volume are not applied, and track are mixed with different delay then in the HAL. * As a result, this API is not suitable for echo cancelling. * @hide */ public static final int ROUTE_FLAG_LOOP_BACK_RENDER = ROUTE_FLAG_LOOP_BACK | ROUTE_FLAG_RENDER; private static final int ROUTE_FLAG_SUPPORTED = ROUTE_FLAG_RENDER | ROUTE_FLAG_LOOP_BACK; // MIX_TYPE_* values to keep in sync with frameworks/av/include/media/AudioPolicy.h Loading Loading @@ -125,6 +139,15 @@ public class AudioMix { */ public static final int MIX_STATE_MIXING = 1; /** Maximum sampling rate for privileged playback capture*/ private static final int PRIVILEDGED_CAPTURE_MAX_SAMPLE_RATE = 16000; /** Maximum channel number for privileged playback capture*/ private static final int PRIVILEDGED_CAPTURE_MAX_CHANNEL_NUMBER = 1; /** Maximum channel number for privileged playback capture*/ private static final int PRIVILEDGED_CAPTURE_MAX_BYTES_PER_SAMPLE = 2; /** * The current mixing state. * @return one of {@link #MIX_STATE_DISABLED}, {@link #MIX_STATE_IDLE}, Loading @@ -140,7 +163,8 @@ public class AudioMix { return mRouteFlags; } AudioFormat getFormat() { /** @hide */ public AudioFormat getFormat() { return mFormat; } Loading Loading @@ -182,6 +206,31 @@ public class AudioMix { return true; } /** @return an error string if the format would not allow Privileged playbackCapture * null otherwise * @hide */ public static String canBeUsedForPrivilegedCapture(AudioFormat format) { int sampleRate = format.getSampleRate(); if (sampleRate > PRIVILEDGED_CAPTURE_MAX_SAMPLE_RATE || sampleRate <= 0) { return "Privileged audio capture sample rate " + sampleRate + " can not be over " + PRIVILEDGED_CAPTURE_MAX_SAMPLE_RATE + "kHz"; } int channelCount = format.getChannelCount(); if (channelCount > PRIVILEDGED_CAPTURE_MAX_CHANNEL_NUMBER || channelCount <= 0) { return "Privileged audio capture channel count " + channelCount + " can not be over " + PRIVILEDGED_CAPTURE_MAX_CHANNEL_NUMBER; } int encoding = format.getEncoding(); if (!format.isPublicEncoding(encoding) || !format.isEncodingLinearPcm(encoding)) { return "Privileged audio capture encoding " + encoding + "is not linear"; } if (format.getBytesPerSample(encoding) > PRIVILEDGED_CAPTURE_MAX_BYTES_PER_SAMPLE) { return "Privileged audio capture encoding " + encoding + " can not be over " + PRIVILEDGED_CAPTURE_MAX_BYTES_PER_SAMPLE + " bytes per sample"; } return null; } /** @hide */ @Override public boolean equals(Object o) { Loading Loading @@ -390,6 +439,12 @@ public class AudioMix { } } } if (mRule.allowPrivilegedPlaybackCapture()) { String error = AudioMix.canBeUsedForPrivilegedCapture(mFormat); if (error != null) { throw new IllegalArgumentException(error); } } return new AudioMix(mRule, mFormat, mRouteFlags, mCallbackFlags, mDeviceSystemType, mDeviceAddress); } Loading
media/java/android/media/audiopolicy/AudioMixingRule.java +4 −0 Original line number Diff line number Diff line Loading @@ -365,6 +365,10 @@ public class AudioMixingRule { /** * Set if the audio of app that opted out of audio playback capture should be captured. * * Caller of this method with <code>true</code>, MUST abide to the restriction listed in * {@link ALLOW_CAPTURE_BY_SYSTEM}, including but not limited to the captured audio * can not leave the capturing app, and the quality is limited to 16k mono. * * The permission {@link CAPTURE_AUDIO_OUTPUT} or {@link CAPTURE_MEDIA_OUTPUT} is needed * to ignore the opt-out. * Loading
services/core/java/com/android/server/audio/AudioService.java +57 −25 Original line number Diff line number Diff line Loading @@ -6698,7 +6698,10 @@ public class AudioService extends IAudioService.Stub boolean isVolumeController, IMediaProjection projection) { AudioSystem.setDynamicPolicyCallback(mDynPolicyCallback); if (!isPolicyRegisterAllowed(policyConfig, projection)) { if (!isPolicyRegisterAllowed(policyConfig, isFocusPolicy || isTestFocusPolicy || hasFocusListener, isVolumeController, 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"); Loading Loading @@ -6739,42 +6742,71 @@ public class AudioService extends IAudioService.Stub * as those policy do not modify the audio routing. */ private boolean isPolicyRegisterAllowed(AudioPolicyConfig policyConfig, boolean hasFocusAccess, boolean isVolumeController, IMediaProjection projection) { boolean isLoopbackRenderPolicy = policyConfig.getMixes().stream().allMatch( mix -> mix.getRouteFlags() == (mix.ROUTE_FLAG_RENDER | mix.ROUTE_FLAG_LOOP_BACK)); 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 boolean requireValidProjection = false; boolean requireCaptureAudioOrMediaOutputPerm = false; boolean requireModifyRouting = false; if (hasFocusAccess || isVolumeController) { requireModifyRouting |= true; } else if (policyConfig.getMixes().isEmpty()) { // An empty policy could be used to lock the focus or add mixes later requireModifyRouting |= true; } for (AudioMix mix : policyConfig.getMixes()) { // If mix is requesting a privileged capture if (mix.getRule().allowPrivilegedPlaybackCapture()) { // then it must have CAPTURE_MEDIA_OUTPUT or CAPTURE_AUDIO_OUTPUT permission requireCaptureAudioOrMediaOutputPerm |= true; // and its format must be low quality enough String error = mix.canBeUsedForPrivilegedCapture(mix.getFormat()); if (error != null) { Log.e(TAG, error); return false; } } if (canProjectAudio(projection)) { // Policy that do not modify the audio routing only need an audio projection return true; // If mix is RENDER|LOOPBACK, then an audio MediaProjection is enough // otherwise MODIFY_AUDIO_ROUTING permission is required if (mix.getRouteFlags() == mix.ROUTE_FLAG_LOOP_BACK_RENDER && projection != null) { requireValidProjection |= true; } else { requireModifyRouting |= true; } } boolean hasPermissionModifyAudioRouting = (PackageManager.PERMISSION_GRANTED == mContext.checkCallingPermission( android.Manifest.permission.MODIFY_AUDIO_ROUTING)); if (hasPermissionModifyAudioRouting) { return true; if (requireCaptureAudioOrMediaOutputPerm && !callerHasPermission(android.Manifest.permission.CAPTURE_MEDIA_OUTPUT) && !callerHasPermission(android.Manifest.permission.CAPTURE_AUDIO_OUTPUT)) { Log.e(TAG, "Privileged audio capture requires CAPTURE_MEDIA_OUTPUT or " + "CAPTURE_AUDIO_OUTPUT system permission"); return false; } if (requireValidProjection && !canProjectAudio(projection)) { return false; } if (requireModifyRouting && !callerHasPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)) { Log.e(TAG, "Can not capture audio without MODIFY_AUDIO_ROUTING"); return false; } private boolean hasPermission(String permission) { return PackageManager.PERMISSION_GRANTED == mContext.checkCallingPermission(permission); return true; } private boolean callerHasPermission(String permission) { return mContext.checkCallingPermission(permission) == PackageManager.PERMISSION_GRANTED; } /** @return true if projection is a valid MediaProjection that can project audio. */ private boolean canProjectAudio(IMediaProjection projection) { if (projection == null) { Log.e(TAG, "MediaProjection is null"); return false; } Loading