| /* |
| * 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.AudioAttributes.AttributeUsage; |
| 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:allowAudioPlaybackCapture="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 |
| * AudioPlaybackCaptureConfiguration config = |
| * new AudioPlaybackCaptureConfiguration.Builder(mediaProjection) |
| * .addMatchingUsage(AudioAttributes.USAGE_MEDIA) |
| * .build(); |
| * AudioRecord record = new AudioRecord.Builder() |
| * .setAudioPlaybackCaptureConfig(config) |
| * .build(); |
| * </pre> |
| * |
| * @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, |
| MediaProjection projection) { |
| mAudioMixingRule = audioMixingRule; |
| mProjection = projection; |
| } |
| |
| /** |
| * @return the {@code MediaProjection} used to build this object. |
| * @see {@code Builder.Builder} |
| */ |
| public @NonNull MediaProjection getMediaProjection() { |
| return mProjection; |
| } |
| |
| |
| /** |
| * 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. |
| */ |
| @NonNull AudioMix createAudioMix(@NonNull 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 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; |
| |
| /** @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(); |
| } |
| |
| /** |
| * 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(int)}. |
| */ |
| public @NonNull Builder addMatchingUsage(@AttributeUsage int usage) { |
| Preconditions.checkState( |
| mUsageMatchType != MATCH_TYPE_EXCLUSIVE, ERROR_MESSAGE_MISMATCHED_RULES); |
| mAudioMixingRuleBuilder.addRule(new AudioAttributes.Builder().setUsage(usage).build(), |
| 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 @NonNull 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(int)}. |
| */ |
| public @NonNull Builder excludeUsage(@AttributeUsage int usage) { |
| Preconditions.checkState( |
| mUsageMatchType != MATCH_TYPE_INCLUSIVE, ERROR_MESSAGE_MISMATCHED_RULES); |
| mAudioMixingRuleBuilder.excludeRule(new AudioAttributes.Builder() |
| .setUsage(usage) |
| .build(), |
| 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 @NonNull 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 @NonNull AudioPlaybackCaptureConfiguration build() { |
| return new AudioPlaybackCaptureConfiguration(mAudioMixingRuleBuilder.build(), |
| mProjection); |
| } |
| } |
| } |