| /* | |
| * 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.audiopolicy.AudioMixingRule.AudioMixMatchCriterion; | |
| import android.media.projection.MediaProjection; | |
| import android.os.RemoteException; | |
| import com.android.internal.util.Preconditions; | |
| import java.util.function.ToIntFunction; | |
| /** | |
| * Configuration for capturing audio played by other apps. | |
| * | |
| * When capturing audio signals played by other apps (and yours), | |
| * you will only capture a mix of the audio signals played by players | |
| * (such as AudioTrack or MediaPlayer) which present the following characteristics: | |
| * <ul> | |
| * <li> the usage value MUST be {@link AudioAttributes#USAGE_UNKNOWN} or | |
| * {@link AudioAttributes#USAGE_GAME} | |
| * or {@link AudioAttributes#USAGE_MEDIA}. All other usages CAN NOT be captured. </li> | |
| * <li> AND the capture policy set by their app (with {@link AudioManager#setAllowedCapturePolicy}) | |
| * or on each player (with {@link AudioAttributes.Builder#setAllowedCapturePolicy}) is | |
| * {@link AudioAttributes#ALLOW_CAPTURE_BY_ALL}, whichever is the most strict. </li> | |
| * <li> AND their app attribute allowAudioPlaybackCapture in their manifest | |
| * MUST either be: <ul> | |
| * <li> set to "true" </li> | |
| * <li> not set, and their {@code targetSdkVersion} MUST be equal to or greater than | |
| * {@link android.os.Build.VERSION_CODES#Q}. | |
| * Ie. Apps that do not target at least Android Q must explicitly opt-in to be captured | |
| * by a MediaProjection. </li></ul> | |
| * <li> AND their apps MUST be in the same user profile as your app | |
| * (eg work profile cannot capture user profile apps and vice-versa). </li> | |
| * </ul> | |
| * | |
| * <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 Builder | |
| * @see android.media.projection.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 Builder#Builder(MediaProjection) | |
| */ | |
| public @NonNull MediaProjection getMediaProjection() { | |
| return mProjection; | |
| } | |
| /** @return the usages passed to {@link Builder#addMatchingUsage(int)}. */ | |
| @AttributeUsage | |
| public @NonNull int[] getMatchingUsages() { | |
| return getIntPredicates(AudioMixingRule.RULE_MATCH_ATTRIBUTE_USAGE, | |
| criterion -> criterion.getAudioAttributes().getUsage()); | |
| } | |
| /** @return the UIDs passed to {@link Builder#addMatchingUid(int)}. */ | |
| public @NonNull int[] getMatchingUids() { | |
| return getIntPredicates(AudioMixingRule.RULE_MATCH_UID, | |
| criterion -> criterion.getIntProp()); | |
| } | |
| /** @return the usages passed to {@link Builder#excludeUsage(int)}. */ | |
| @AttributeUsage | |
| public @NonNull int[] getExcludeUsages() { | |
| return getIntPredicates(AudioMixingRule.RULE_EXCLUDE_ATTRIBUTE_USAGE, | |
| criterion -> criterion.getAudioAttributes().getUsage()); | |
| } | |
| /** @return the UIDs passed to {@link Builder#excludeUid(int)}. */ | |
| public @NonNull int[] getExcludeUids() { | |
| return getIntPredicates(AudioMixingRule.RULE_EXCLUDE_UID, | |
| criterion -> criterion.getIntProp()); | |
| } | |
| private int[] getIntPredicates(int rule, | |
| ToIntFunction<AudioMixMatchCriterion> getPredicate) { | |
| return mAudioMixingRule.getCriteria().stream() | |
| .filter(criterion -> criterion.getRule() == rule) | |
| .mapToInt(getPredicate) | |
| .toArray(); | |
| } | |
| /** | |
| * 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); | |
| } | |
| } | |
| } |