| /* |
| * 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); |
| } |
| } |
| } |